@mcp-layer/graphql
v0.2.2
Published
Fastify GraphQL plugin and schema builder for MCP catalogs with shared gateway runtime primitives.
Maintainers
Readme
@mcp-layer/graphql
@mcp-layer/graphql exposes MCP catalogs through GraphQL using shared runtime primitives from @mcp-layer/gateway. It supports generated per-item operations and generic fallback operations in the same schema.
Table of Contents
Installation
pnpm add @mcp-layer/graphql
# or
npm install @mcp-layer/graphql
# or
yarn add @mcp-layer/graphqlWhat this package provides
- Fastify plugin for GraphQL endpoint registration on MCP sessions.
- Framework-agnostic schema builder export.
- Deterministic MCP-to-GraphQL field mapping.
- Shared validation/resilience/telemetry/session-manager behavior through
@mcp-layer/gateway.
Contract defaults
- Endpoint:
/{version}/graphql - IDE route: disabled by default
- Operation surface: generated operations + generic fallback operations
- Subscription root: intentionally omitted in v1
Quick Start
This example demonstrates the plugin surface when you need a production GraphQL endpoint with minimal setup. This matters for teams that already run Fastify and want GraphQL over MCP without writing adapter boilerplate.
Expected behavior: the plugin mounts POST /v0/graphql (or other derived version), supports generic and generated operations, and reuses gateway runtime validation and breaker behavior.
import Fastify from 'fastify';
import { attach } from '@mcp-layer/attach';
import mcpGraphql from '@mcp-layer/graphql';
import { build } from '@mcp-layer/test-server';
const server = build();
const session = await attach(server, 'primary');
const app = Fastify({ logger: true });
await app.register(mcpGraphql, { session });
await app.listen({ port: 3000 });This example demonstrates framework-agnostic schema generation. This matters when you need custom hosting, custom execution pipelines, or prebuilt schema inspection in tooling.
Expected behavior: schema(...) returns executable schema + typeDefs + resolvers + deterministic mapping metadata.
import { schema } from '@mcp-layer/graphql';
const built = schema(catalog, {
operations: {
generated: true,
generic: true
}
});
console.log(built.typeDefs);
console.log(built.mapping.findField('tool', 'echo'));API Reference
Default export: Fastify plugin
Registers GraphQL routes from MCP sessions.
fastify.register(mcpGraphql, options)Options:
| Option | Type | Default | Description |
| --- | --- | --- | --- |
| session | Session or Session[] | required unless using manager + catalog | Bootstrap session(s) used for catalog extraction and static mode. |
| catalog | { server?, items? } | optional | Precomputed bootstrap catalog used when the live session should only be resolved lazily. |
| manager | { get(request), close?() } | optional | Per-request session manager (requires bootstrap session or catalog). |
| prefix | string or (version, info, name) => string | derived | Per-session route prefix. |
| endpoint | string | "/graphql" | GraphQL POST endpoint path under prefix. |
| ide.enabled | boolean | false | Enables GraphiQL route. |
| ide.path | string | "/graphiql" | IDE alias route path when IDE is enabled (redirect stays within the session prefix). |
| operations.generated | boolean | true | Include generated per-item operations. |
| operations.generic | boolean | true | Include generic fallback operations. |
| validation.* | object | gateway defaults | Validation trust/safety options. |
| resilience.* | object | gateway defaults | Breaker options. |
| telemetry.* | object | gateway defaults | OpenTelemetry API options. |
| errors.exposeDetails | boolean | false | Include raw upstream error message in GraphQL error text. |
schema(catalog, options?)
Builds executable GraphQL schema primitives from an extracted catalog.
schema(catalog, {
operations?: {
generated?: boolean;
generic?: boolean;
}
}) => {
schema: GraphQLSchema;
typeDefs: string;
resolvers: Record<string, unknown>;
mapping: ReturnType<typeof map>;
}map(catalog, options?)
Returns deterministic mapping metadata used by generated operations.
Returns:
entries[]with{ type, root, name, field, item }find(type, name)findField(type, name)- typed lists (
tools,prompts,resources,templates)
GraphQL operation surface
Generic operations
Mutation.callTool(name: String!, input: JSON): ToolResult!Mutation.getPrompt(name: String!, input: JSON): PromptResult!Query.readResource(uri: String!): ResourceResult!Query.readTemplate(uriTemplate: String!, params: JSON): ResourceResult!
readTemplate uses RFC6570 expansion semantics through uri-template, so operator/modifier forms like {+name} and {name*} are expanded using the provided params.
Generated operations
Generated fields mirror MCP catalog items and are deterministic/sanitized.
Examples:
- Tool
echo->Mutation.echo(input: JSON): ToolResult! - Tool
fail-gracefully->Mutation.fail_gracefully(input: JSON): ToolResult!
Response types
ToolResult { content, isError, structuredContent }PromptResult { messages, payload }ResourceResult { contents, text, mimeType, payload }Catalog+CatalogEntrymetadata for schema discovery
Error Handling
GraphQL execution errors are returned in errors[] with machine-readable extensions. Successful transport responses may still include resolver failures (HTTP 200 with errors present), so client-side handling must inspect both data and errors.
Response contract highlights:
extensions.codecarries the primary classification (BAD_USER_INPUT,UNAUTHENTICATED,SERVICE_UNAVAILABLE,TOOL_ERROR, etc.).extensions.instanceandextensions.requestIdprovide request correlation metadata.- Validation failures include
extensions.errors[]payloads with path/keyword/message metadata. - Tool failures include
extensions.toolErrorwith preserved MCP payload content.
Operational debugging checklist:
- Capture
extensions.code,extensions.instance, andextensions.requestId. - Distinguish transport success from resolver failure (
HTTP 200pluserrors). - For validation failures, inspect
extensions.errorsbefore retrying. - For startup failures, resolve configuration errors before plugin registration.
Runtime Error Reference
This section is written for high-pressure debugging moments. Entries are split into request-time GraphQL errors, request-time internal LayerError guards, and startup configuration errors.
Every LayerError entry uses a hash anchor (error-xxxxxx) from @mcp-layer/error so incidents can link directly to a deterministic remediation block.
Validation failures (BAD_USER_INPUT)
When it happens: input validation fails against tool/prompt schemas or template parameter limits.
Step-by-step resolution:
- Read
extensions.errors[]and identify failingpath/message. - Compare payload against tool/prompt schema from catalog discovery.
- Re-submit with corrected input shape and types.
- Add client-side validation for common invalid payloads.
This example demonstrates extracting validation details from GraphQL responses. This matters because GraphQL transport can be 200 while operations fail semantically. Expected behavior: client logs structured validation hints and blocks invalid retries.
const response = await gqlClient.request(query, vars);
if (Array.isArray(response.errors) && response.errors.length > 0) {
const first = response.errors[0];
if (first.extensions?.code === 'BAD_USER_INPUT') {
console.error(first.extensions.errors);
}
}Authorization failures (UNAUTHENTICATED)
When it happens: manager mode requires auth header and request is missing/invalid.
Step-by-step resolution:
- Verify
Authorizationheader is present for protected routes. - Confirm
Bearer <token>formatting is correct. - Validate manager auth mode (
requiredvsoptional) for your environment. - Add request middleware tests for missing and malformed auth headers.
This example shows a bearer-authenticated GraphQL request. This matters because manager mode may resolve different sessions based on identity. Expected behavior: request resolves session and executes operation without UNAUTHENTICATED.
await fastify.inject({
method: 'POST',
url: '/v0/graphql',
headers: {
authorization: 'Bearer token-value',
'content-type': 'application/json'
},
payload: { query, variables }
});Circuit open failures (SERVICE_UNAVAILABLE)
When it happens: breaker is open for the selected session and request is failed fast.
Step-by-step resolution:
- Check recent upstream failures and breaker thresholds.
- Reduce request pressure or isolate failing operations.
- Tune
resilience.errorThresholdPercentage,volumeThreshold, andresetTimeout. - Re-run traffic after reset window and monitor for repeat opens.
This example demonstrates conservative breaker tuning for unstable upstreams. This matters because GraphQL adapters often multiplex many clients through one session surface. Expected behavior: breaker opens less aggressively while still protecting upstream.
await fastify.register(mcpGraphql, {
session,
resilience: {
enabled: true,
timeout: 20000,
errorThresholdPercentage: 60,
resetTimeout: 10000,
volumeThreshold: 10
}
});Tool payload errors (TOOL_ERROR)
When it happens: MCP tool returns isError: true.
Step-by-step resolution:
- Read
extensions.toolError.contentfor upstream tool diagnostics. - Inspect input arguments for domain/business constraint failures.
- Retry only after correcting payload or upstream state.
- Surface structured tool error content to API consumers.
This example shows handling tool-level failure payloads in GraphQL clients. This matters because tool failures are semantically different from protocol/runtime faults. Expected behavior: client presents tool-specific corrective guidance.
const first = response.errors?.[0];
if (first?.extensions?.code === 'TOOL_ERROR') {
console.error(first.extensions.toolError?.content);
}Upstream runtime failures (INTERNAL_SERVER_ERROR, TIMEOUT, etc.)
When it happens: MCP/JSON-RPC errors bubble from upstream tool/prompt/resource calls.
Step-by-step resolution:
- Inspect
extensions.mcpErrorCodeand classify retryability. - Correlate with upstream logs via
requestIdandinstance. - Enable
errors.exposeDetailstemporarily in controlled environments. - Add retry/backoff only for transient categories (
TIMEOUT, service pressure).
This example demonstrates temporary detail exposure during incident diagnosis. This matters because opaque runtime failures are hard to triage without short-term enhanced diagnostics. Expected behavior: richer error detail is available for debugging, then disabled again.
await fastify.register(mcpGraphql, {
session,
errors: {
exposeDetails: true
}
});Runtime Guard Errors (LayerError)
These entries are emitted from resolver guard paths before the final GraphQL error mapping. They are still useful for root-cause analysis because they identify the failing internal method and deterministic hash.
Request payload failed schema validation. (callTool)
Thrown from: callTool
This happens when tool input fails schema validation before invoking MCP tools/call.
Step-by-step resolution:
- Inspect the GraphQL
BAD_USER_INPUTresponse and collectextensions.errors. - Compare tool variables against the tool schema exported in catalog metadata.
- Coerce client values to the expected shape/types before dispatch.
- Add tests that submit invalid and valid payloads for the same tool.
const mutation = `
mutation Call($input: JSONObject!) {
callTool(name: "echo", input: $input) {
isError
content
}
}
`;
await gqlClient.request(mutation, {
input: { text: 'hello world' }
});Tool "{tool}" reported an error. (callTool)
Thrown from: callTool
This happens when MCP responds with isError: true for a tool invocation. GraphQL then maps this into TOOL_ERROR.
Step-by-step resolution:
- Inspect
extensions.toolError.contentfor the upstream tool failure details. - Validate tool arguments and domain constraints against server-side expectations.
- Fix upstream state or request payload before retrying.
- Add consumer handling that surfaces tool-level errors distinctly from transport/runtime failures.
const first = response.errors?.[0];
if (first?.extensions?.code === 'TOOL_ERROR') {
const content = first.extensions.toolError?.content ?? [];
console.error(content);
}Request payload failed schema validation. (getPrompt)
Thrown from: getPrompt
This happens when prompt arguments fail schema validation before invoking MCP prompts/get.
Step-by-step resolution:
- Inspect GraphQL
BAD_USER_INPUTmetadata for prompt argument path/message. - Compare client variables with prompt input schema from catalog.
- Normalize optional/required prompt fields in client payload builders.
- Add prompt-specific request validation tests in your GraphQL client or gateway layer.
const mutation = `
mutation Prompt($input: JSONObject!) {
getPrompt(name: "welcome", input: $input) {
messages
payload
}
}
`;
await gqlClient.request(mutation, {
input: { topic: 'launch' }
});Startup Configuration Errors (LayerError)
The following entries are thrown before route registration when plugin options are invalid.
endpoint must start with "/".
Thrown from: validateOptions
This happens when endpoint does not begin with /.
Step-by-step resolution:
- Inspect
endpointoption source. - Normalize path to absolute route format.
- Keep endpoint formatting logic in one config utility.
- Add startup tests for invalid and valid endpoint paths.
await fastify.register(mcpGraphql, {
session,
endpoint: '/graphql'
});ide.path must start with "/".
Thrown from: validateOptions
This happens when ide.path is not absolute.
Step-by-step resolution:
- Ensure
ide.pathstarts with/. - Keep IDE route path separate from endpoint path.
- Validate defaults and env overrides together.
- Add registration tests for custom IDE path.
await fastify.register(mcpGraphql, {
session,
ide: {
enabled: true,
path: '/graphiql'
}
});operations.generated and operations.generic cannot both be false.
Thrown from: validateOptions
This happens when both operation surfaces are disabled.
Step-by-step resolution:
- Enable at least one operation mode.
- Use
generic: trueduring migration phases. - Disable generated operations only when explicitly required.
- Add tests covering each allowed operation combination.
await fastify.register(mcpGraphql, {
session,
operations: {
generated: true,
generic: false
}
});"{option}" must be a positive number.
Thrown from: requirePositiveNumber (via @mcp-layer/gateway validateRuntimeOptions)
This happens when any numeric runtime limit is <= 0, NaN, or non-finite.
Step-by-step resolution:
- Read the failing
{option}name from the thrown message. - Trace that value from env/config parsing into plugin registration.
- Coerce and validate value ranges before calling
fastify.register(...). - Add startup tests for invalid and valid values.
Canonical gateway reference: @mcp-layer/gateway runtime errors.
const timeout = Number(process.env.MCP_TIMEOUT_MS ?? 30000);
if (!Number.isFinite(timeout) || timeout <= 0)
throw new Error('MCP_TIMEOUT_MS must be a positive number');
await fastify.register(mcpGraphql, {
session,
resilience: { timeout }
});validation.trustSchemas must be "auto", true, or false.
Thrown from: trustMode (via @mcp-layer/gateway validateRuntimeOptions)
This happens when validation.trustSchemas is set to a value outside "auto", true, or false.
Step-by-step resolution:
- Inspect runtime plugin options for
validation.trustSchemas. - Replace stringified booleans (for example
"true") with real booleans. - Default to
"auto"unless you need a strict trust override. - Add config tests covering all allowed values.
Canonical gateway reference: @mcp-layer/gateway runtime errors.
await fastify.register(mcpGraphql, {
session,
validation: {
trustSchemas: 'auto'
}
});prefix must be a string or function.
Thrown from: validateRuntimeOptions (via @mcp-layer/gateway)
This happens when prefix is neither a string nor a callback.
Step-by-step resolution:
- Use a static string for fixed mounting.
- Use a callback for version/session-aware prefixes.
- Remove unsupported injected types from config loaders.
- Add tests for both valid prefix modes.
Canonical gateway reference: @mcp-layer/gateway runtime errors.
await fastify.register(mcpGraphql, {
session,
prefix: function prefix(version, info, name) {
return `/${version}/${name}`;
}
});manager does not support multiple sessions. Register multiple plugins instead.
Thrown from: validateRuntimeOptions (via @mcp-layer/gateway)
This happens when manager is provided while session is an array.
Step-by-step resolution:
- In manager mode, pass one bootstrap
sessionor one bootstrapcatalog. - For multi-session static mounts, remove
manager. - Register separate plugin instances per static session surface.
- Add startup tests for both architecture modes.
Canonical gateway reference: @mcp-layer/gateway runtime errors.
await fastify.register(mcpGraphql, {
session: bootstrapSession,
manager
});session or catalog is required when manager is provided (used for catalog bootstrap).
Thrown from: validateRuntimeOptions (via @mcp-layer/gateway)
This happens when manager is configured without a bootstrap session or catalog.
Step-by-step resolution:
- Always provide a bootstrap
sessionorcatalogwithmanager. - Ensure the bootstrap session is connected before registration, or ensure the bootstrap catalog matches the managed session surface.
- Keep bootstrap and managed sessions aligned in capability surface.
- Add tests that fail fast without bootstrap metadata.
Canonical gateway reference: @mcp-layer/gateway runtime errors.
await fastify.register(mcpGraphql, {
session: bootstrapSession,
manager
});await fastify.register(mcpGraphql, {
catalog,
manager
});catalog must be an object.
Thrown from: validateRuntimeOptions (via @mcp-layer/gateway)
This happens when catalog is passed as a primitive, array, or serialized payload instead of the composed catalog object the GraphQL plugin expects at bootstrap.
Step-by-step resolution:
- Pass the bootstrap catalog as a plain object.
- Do not pass stringified JSON or only the
itemsarray. - Ensure the object shape matches the extracted/composed catalog surface.
- Add registration tests that reject malformed catalog bootstrap values.
Canonical gateway reference: @mcp-layer/gateway runtime errors.
await fastify.register(mcpGraphql, {
catalog: {
server: {
info: { name: 'example-server', version: '1.0.0' }
},
items: []
},
manager
});session or manager option is required.
Thrown from: validateRuntimeOptions (via @mcp-layer/gateway)
This happens when plugin options omit both session and manager.
Step-by-step resolution:
- Pass a connected
sessionfor static mode. - Or pass
managerplus bootstrapsessionorcatalogfor dynamic mode. - Assert required options before
register. - Add tests that validate registration failure on missing session source.
Canonical gateway reference: @mcp-layer/gateway runtime errors.
await fastify.register(mcpGraphql, { session });manager must be an object with a get(request) function.
Thrown from: validateRuntimeOptions (via @mcp-layer/gateway)
This happens when manager is not an object implementing get(request).
Step-by-step resolution:
- Ensure
manageris an object. - Implement
async get(request)returning aSession. - Optionally add
close()for lifecycle cleanup. - Add contract tests for malformed manager values.
Canonical gateway reference: @mcp-layer/gateway runtime errors.
const manager = {
async get(request) {
return resolveSession(request);
}
};errors must be an object.
Thrown from: validateRuntimeOptions (via @mcp-layer/gateway)
This happens when errors is provided as a primitive instead of an object.
Step-by-step resolution:
- Ensure
errorsis an object literal. - Keep only supported keys under
errors. - Remove primitive shorthand from environment mappers.
- Add shape-validation tests.
Canonical gateway reference: @mcp-layer/gateway runtime errors.
await fastify.register(mcpGraphql, {
session,
errors: { exposeDetails: false }
});validation must be an object.
Thrown from: validateRuntimeOptions (via @mcp-layer/gateway)
This happens when validation is provided as a non-object value.
Step-by-step resolution:
- Move validation keys under a
validationobject. - Keep values typed correctly (number/boolean/allowed enums).
- Validate env/config coercion before registration.
- Add tests for malformed and valid validation objects.
Canonical gateway reference: @mcp-layer/gateway runtime errors.
await fastify.register(mcpGraphql, {
session,
validation: {
trustSchemas: 'auto',
maxSchemaDepth: 10
}
});normalizeError must be a function.
Thrown from: validateRuntimeOptions (via @mcp-layer/gateway)
This happens when normalizeError is provided with a non-callable value.
Step-by-step resolution:
- Ensure
normalizeErroris a function reference. - Keep function signature aligned with gateway runtime usage.
- Remove object/string placeholders from config loaders.
- Add tests validating custom normalizer behavior.
Canonical gateway reference: @mcp-layer/gateway runtime errors.
await fastify.register(mcpGraphql, {
session,
normalizeError: function normalizeError(error, instance, requestId, options) {
return mapGraphQLError(error, instance, requestId, options);
}
});License
MIT
