@cubicecho/graphql-mcp 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/executor.d.ts +47 -0
- package/dist/executor.js +58 -0
- package/dist/http.d.ts +47 -0
- package/dist/http.js +50 -0
- package/dist/index.d.ts +47 -0
- package/dist/index.js +41 -0
- package/dist/operation.d.ts +33 -0
- package/dist/operation.js +34 -0
- package/dist/selection.d.ts +25 -0
- package/dist/selection.js +70 -0
- package/dist/server.d.ts +88 -0
- package/dist/server.js +116 -0
- package/dist/tools.d.ts +55 -0
- package/dist/tools.js +98 -0
- package/dist/types.d.ts +56 -0
- package/dist/types.js +10 -0
- package/dist/zodSchema.d.ts +27 -0
- package/dist/zodSchema.js +80 -0
- package/package.json +8 -2
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Benjamin Van Treese
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two built-in {@link GraphqlExecutor}s — the seam that decides *where* a
|
|
3
|
+
* tool's GraphQL operation runs.
|
|
4
|
+
*
|
|
5
|
+
* - {@link createLocalExecutor} runs it in-process against the schema you already
|
|
6
|
+
* have (the simplest "side-by-side" setup: mount the MCP handler on a route in
|
|
7
|
+
* the same app as your GraphQL endpoint).
|
|
8
|
+
* - {@link createHttpExecutor} forwards it to a separate GraphQL HTTP endpoint —
|
|
9
|
+
* for when the MCP server runs as its own process next to the GraphQL server.
|
|
10
|
+
*/
|
|
11
|
+
import { type GraphQLSchema } from 'graphql';
|
|
12
|
+
import type { GraphqlExecutor } from './types.ts';
|
|
13
|
+
/** Options for {@link createLocalExecutor}. */
|
|
14
|
+
export interface LocalExecutorOptions {
|
|
15
|
+
/** Default `rootValue` for execution (e.g. a resolver root for `buildSchema` schemas). */
|
|
16
|
+
rootValue?: unknown;
|
|
17
|
+
/** Fallback `contextValue` used when a call provides no per-request `context`. */
|
|
18
|
+
contextValue?: unknown;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* An executor that runs operations in-process against `schema` via graphql-js's
|
|
22
|
+
* `execute`. The per-call `context` (from the `context` server option) is passed
|
|
23
|
+
* as the GraphQL `contextValue`, falling back to `options.contextValue`.
|
|
24
|
+
*
|
|
25
|
+
* @param schema - The executable schema to run against.
|
|
26
|
+
* @param options - Default root/context values.
|
|
27
|
+
*/
|
|
28
|
+
export declare function createLocalExecutor(schema: GraphQLSchema, options?: LocalExecutorOptions): GraphqlExecutor;
|
|
29
|
+
/** Options for {@link createHttpExecutor}. */
|
|
30
|
+
export interface HttpExecutorOptions {
|
|
31
|
+
/**
|
|
32
|
+
* Extra request headers. Either a static record or a function of the per-call
|
|
33
|
+
* `context` — use the function form to forward auth derived from the MCP
|
|
34
|
+
* request (e.g. `(ctx) => ({ authorization: ctx.token })`).
|
|
35
|
+
*/
|
|
36
|
+
headers?: Record<string, string> | ((context: unknown) => Record<string, string>);
|
|
37
|
+
/** Override the `fetch` implementation (defaults to the global `fetch`). */
|
|
38
|
+
fetch?: typeof fetch;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* An executor that POSTs operations to a GraphQL HTTP `endpoint` (the standard
|
|
42
|
+
* `{ query, variables, operationName }` body) and returns the parsed JSON.
|
|
43
|
+
*
|
|
44
|
+
* @param endpoint - The GraphQL endpoint URL.
|
|
45
|
+
* @param options - Header and `fetch` overrides.
|
|
46
|
+
*/
|
|
47
|
+
export declare function createHttpExecutor(endpoint: string, options?: HttpExecutorOptions): GraphqlExecutor;
|
package/dist/executor.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two built-in {@link GraphqlExecutor}s — the seam that decides *where* a
|
|
3
|
+
* tool's GraphQL operation runs.
|
|
4
|
+
*
|
|
5
|
+
* - {@link createLocalExecutor} runs it in-process against the schema you already
|
|
6
|
+
* have (the simplest "side-by-side" setup: mount the MCP handler on a route in
|
|
7
|
+
* the same app as your GraphQL endpoint).
|
|
8
|
+
* - {@link createHttpExecutor} forwards it to a separate GraphQL HTTP endpoint —
|
|
9
|
+
* for when the MCP server runs as its own process next to the GraphQL server.
|
|
10
|
+
*/
|
|
11
|
+
import { execute, parse } from 'graphql';
|
|
12
|
+
/**
|
|
13
|
+
* An executor that runs operations in-process against `schema` via graphql-js's
|
|
14
|
+
* `execute`. The per-call `context` (from the `context` server option) is passed
|
|
15
|
+
* as the GraphQL `contextValue`, falling back to `options.contextValue`.
|
|
16
|
+
*
|
|
17
|
+
* @param schema - The executable schema to run against.
|
|
18
|
+
* @param options - Default root/context values.
|
|
19
|
+
*/
|
|
20
|
+
export function createLocalExecutor(schema, options = {}) {
|
|
21
|
+
return async ({ query, variables, operationName, context }) => {
|
|
22
|
+
const result = await execute({
|
|
23
|
+
schema,
|
|
24
|
+
document: parse(query),
|
|
25
|
+
variableValues: variables,
|
|
26
|
+
operationName,
|
|
27
|
+
rootValue: options.rootValue,
|
|
28
|
+
contextValue: context ?? options.contextValue,
|
|
29
|
+
});
|
|
30
|
+
return result;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* An executor that POSTs operations to a GraphQL HTTP `endpoint` (the standard
|
|
35
|
+
* `{ query, variables, operationName }` body) and returns the parsed JSON.
|
|
36
|
+
*
|
|
37
|
+
* @param endpoint - The GraphQL endpoint URL.
|
|
38
|
+
* @param options - Header and `fetch` overrides.
|
|
39
|
+
*/
|
|
40
|
+
export function createHttpExecutor(endpoint, options = {}) {
|
|
41
|
+
const doFetch = options.fetch ?? globalThis.fetch;
|
|
42
|
+
return async ({ query, variables, operationName, context }) => {
|
|
43
|
+
const extra = typeof options.headers === 'function' ? options.headers(context) : options.headers;
|
|
44
|
+
const response = await doFetch(endpoint, {
|
|
45
|
+
method: 'POST',
|
|
46
|
+
headers: { 'content-type': 'application/json', accept: 'application/json', ...extra },
|
|
47
|
+
body: JSON.stringify({ query, variables, operationName }),
|
|
48
|
+
});
|
|
49
|
+
if (!response.ok) {
|
|
50
|
+
return {
|
|
51
|
+
errors: [
|
|
52
|
+
{ message: `GraphQL endpoint responded ${response.status} ${response.statusText}` },
|
|
53
|
+
],
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
return (await response.json());
|
|
57
|
+
};
|
|
58
|
+
}
|
package/dist/http.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The HTTP glue for running the MCP server "side-by-side" with your GraphQL
|
|
3
|
+
* server: {@link createHttpHandler} returns a plain `(req, res)` handler you
|
|
4
|
+
* mount on a route (e.g. `app.post('/mcp', handler)` in Express).
|
|
5
|
+
*
|
|
6
|
+
* It uses the MCP SDK's Streamable HTTP transport in stateless JSON mode and
|
|
7
|
+
* creates a fresh `McpServer` + transport per request — the transport owns a
|
|
8
|
+
* single connection, so per-request isolation is what keeps concurrent calls
|
|
9
|
+
* from clobbering each other.
|
|
10
|
+
*
|
|
11
|
+
* Express is assumed for the MVP, but nothing here imports it: any framework
|
|
12
|
+
* works as long as it hands the handler a Node `IncomingMessage` whose parsed
|
|
13
|
+
* JSON body is on `req.body` (Express's `express.json()` does this) and a Node
|
|
14
|
+
* `ServerResponse`. Adapting other servers is tracked in TODO.md.
|
|
15
|
+
*/
|
|
16
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
17
|
+
import { type CreateMcpServerOptions } from './server.ts';
|
|
18
|
+
/** A request with its parsed JSON body attached (as `express.json()` provides). */
|
|
19
|
+
export type McpHttpRequest = IncomingMessage & {
|
|
20
|
+
body?: unknown;
|
|
21
|
+
};
|
|
22
|
+
/** An Express/Node-compatible request handler for MCP-over-HTTP. */
|
|
23
|
+
export type McpHttpHandler = (req: McpHttpRequest, res: ServerResponse) => Promise<void>;
|
|
24
|
+
/** Options for {@link createHttpHandler}. */
|
|
25
|
+
export interface HttpHandlerOptions extends CreateMcpServerOptions {
|
|
26
|
+
/**
|
|
27
|
+
* Derive per-request GraphQL context from the HTTP request (e.g. read an auth
|
|
28
|
+
* header). Takes precedence over the `context` option for HTTP calls and lets
|
|
29
|
+
* you key context off the real request rather than the MCP `extra`.
|
|
30
|
+
*/
|
|
31
|
+
contextFromRequest?: (req: McpHttpRequest) => unknown | Promise<unknown>;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Creates an HTTP handler that serves the schema's tools over the MCP Streamable
|
|
35
|
+
* HTTP transport. Tool descriptors are built once; each request gets a fresh
|
|
36
|
+
* server and transport.
|
|
37
|
+
*
|
|
38
|
+
* @param options - The same options as {@link createMcpServer}, plus
|
|
39
|
+
* `contextFromRequest` for request-derived GraphQL context.
|
|
40
|
+
* @returns A `(req, res)` handler to mount on a route.
|
|
41
|
+
* @example
|
|
42
|
+
* ```ts
|
|
43
|
+
* const handler = createHttpHandler({ schema });
|
|
44
|
+
* app.post('/mcp', handler); // run beside app.post('/graphql', ...)
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export declare function createHttpHandler(options: HttpHandlerOptions): McpHttpHandler;
|
package/dist/http.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The HTTP glue for running the MCP server "side-by-side" with your GraphQL
|
|
3
|
+
* server: {@link createHttpHandler} returns a plain `(req, res)` handler you
|
|
4
|
+
* mount on a route (e.g. `app.post('/mcp', handler)` in Express).
|
|
5
|
+
*
|
|
6
|
+
* It uses the MCP SDK's Streamable HTTP transport in stateless JSON mode and
|
|
7
|
+
* creates a fresh `McpServer` + transport per request — the transport owns a
|
|
8
|
+
* single connection, so per-request isolation is what keeps concurrent calls
|
|
9
|
+
* from clobbering each other.
|
|
10
|
+
*
|
|
11
|
+
* Express is assumed for the MVP, but nothing here imports it: any framework
|
|
12
|
+
* works as long as it hands the handler a Node `IncomingMessage` whose parsed
|
|
13
|
+
* JSON body is on `req.body` (Express's `express.json()` does this) and a Node
|
|
14
|
+
* `ServerResponse`. Adapting other servers is tracked in TODO.md.
|
|
15
|
+
*/
|
|
16
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
17
|
+
import { createServerFactory } from "./server.js";
|
|
18
|
+
/**
|
|
19
|
+
* Creates an HTTP handler that serves the schema's tools over the MCP Streamable
|
|
20
|
+
* HTTP transport. Tool descriptors are built once; each request gets a fresh
|
|
21
|
+
* server and transport.
|
|
22
|
+
*
|
|
23
|
+
* @param options - The same options as {@link createMcpServer}, plus
|
|
24
|
+
* `contextFromRequest` for request-derived GraphQL context.
|
|
25
|
+
* @returns A `(req, res)` handler to mount on a route.
|
|
26
|
+
* @example
|
|
27
|
+
* ```ts
|
|
28
|
+
* const handler = createHttpHandler({ schema });
|
|
29
|
+
* app.post('/mcp', handler); // run beside app.post('/graphql', ...)
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export function createHttpHandler(options) {
|
|
33
|
+
const { contextFromRequest, ...serverOptions } = options;
|
|
34
|
+
const makeServer = createServerFactory(serverOptions);
|
|
35
|
+
return async (req, res) => {
|
|
36
|
+
// Per-request context derived from the real HTTP request wins over a static
|
|
37
|
+
// `context`; otherwise fall back to whatever `serverOptions.context` holds.
|
|
38
|
+
const server = makeServer(contextFromRequest ? () => contextFromRequest(req) : undefined);
|
|
39
|
+
const transport = new StreamableHTTPServerTransport({
|
|
40
|
+
sessionIdGenerator: undefined,
|
|
41
|
+
enableJsonResponse: true,
|
|
42
|
+
});
|
|
43
|
+
res.on('close', () => {
|
|
44
|
+
transport.close();
|
|
45
|
+
server.close();
|
|
46
|
+
});
|
|
47
|
+
await server.connect(transport);
|
|
48
|
+
await transport.handleRequest(req, res, req.body);
|
|
49
|
+
};
|
|
50
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* graphql-mcp — turn a GraphQL schema into an MCP server.
|
|
3
|
+
*
|
|
4
|
+
* Point it at a `GraphQLSchema` and every `Query`/`Mutation` root field becomes
|
|
5
|
+
* a Model Context Protocol tool, described from the SDL (field and argument
|
|
6
|
+
* descriptions, types) so an AI can discover and call your API. It's a thin
|
|
7
|
+
* wrapper meant to run *beside* your GraphQL server: mount the returned HTTP
|
|
8
|
+
* handler on a route in the same app, or run it as its own process and forward
|
|
9
|
+
* to a remote endpoint.
|
|
10
|
+
*
|
|
11
|
+
* Quick start:
|
|
12
|
+
* ```ts
|
|
13
|
+
* import express from 'express';
|
|
14
|
+
* import { createHttpHandler } from '@cubicecho/graphql-mcp';
|
|
15
|
+
* import { schema } from './schema.js';
|
|
16
|
+
*
|
|
17
|
+
* const app = express();
|
|
18
|
+
* app.use(express.json());
|
|
19
|
+
* app.post('/mcp', createHttpHandler({ schema })); // beside app.post('/graphql', …)
|
|
20
|
+
* app.listen(4000);
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* Modules:
|
|
24
|
+
* - `types` — `GraphqlExecutor`/`GraphqlRequest`/`GraphqlResult`, the execution seam
|
|
25
|
+
* - `zodSchema` — GraphQL args → Zod input schema (`argsToZodShape`)
|
|
26
|
+
* - `selection` — auto-built selection sets (`buildSelectionSet`)
|
|
27
|
+
* - `operation` — per-field operation documents (`buildOperation`)
|
|
28
|
+
* - `tools` — schema → `ToolDescriptor`s (`buildTools`)
|
|
29
|
+
* - `executor` — `createLocalExecutor` (in-process) / `createHttpExecutor` (forwarding)
|
|
30
|
+
* - `server` — `createMcpServer` / `createServerFactory` / `registerGraphqlTools` (+ custom tools)
|
|
31
|
+
* - `http` — `createHttpHandler` for the Streamable HTTP transport
|
|
32
|
+
*
|
|
33
|
+
* @packageDocumentation
|
|
34
|
+
*/
|
|
35
|
+
export type { HttpExecutorOptions, LocalExecutorOptions } from './executor.ts';
|
|
36
|
+
export { createHttpExecutor, createLocalExecutor } from './executor.ts';
|
|
37
|
+
export type { HttpHandlerOptions, McpHttpHandler, McpHttpRequest } from './http.ts';
|
|
38
|
+
export { createHttpHandler } from './http.ts';
|
|
39
|
+
export type { BuiltOperation } from './operation.ts';
|
|
40
|
+
export { buildOperation } from './operation.ts';
|
|
41
|
+
export { buildSelectionSet } from './selection.ts';
|
|
42
|
+
export type { ContextFactory, CreateMcpServerOptions, CustomTool, ServerFactory, ToolHandler, } from './server.ts';
|
|
43
|
+
export { createMcpServer, createServerFactory, registerGraphqlTools, } from './server.ts';
|
|
44
|
+
export type { BuildToolsOptions, ToolDescriptor } from './tools.ts';
|
|
45
|
+
export { buildTools } from './tools.ts';
|
|
46
|
+
export type { GraphqlError, GraphqlExecutor, GraphqlRequest, GraphqlResult, OperationKind, ToolAnnotations, } from './types.ts';
|
|
47
|
+
export { argsToZodShape } from './zodSchema.ts';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* graphql-mcp — turn a GraphQL schema into an MCP server.
|
|
3
|
+
*
|
|
4
|
+
* Point it at a `GraphQLSchema` and every `Query`/`Mutation` root field becomes
|
|
5
|
+
* a Model Context Protocol tool, described from the SDL (field and argument
|
|
6
|
+
* descriptions, types) so an AI can discover and call your API. It's a thin
|
|
7
|
+
* wrapper meant to run *beside* your GraphQL server: mount the returned HTTP
|
|
8
|
+
* handler on a route in the same app, or run it as its own process and forward
|
|
9
|
+
* to a remote endpoint.
|
|
10
|
+
*
|
|
11
|
+
* Quick start:
|
|
12
|
+
* ```ts
|
|
13
|
+
* import express from 'express';
|
|
14
|
+
* import { createHttpHandler } from '@cubicecho/graphql-mcp';
|
|
15
|
+
* import { schema } from './schema.js';
|
|
16
|
+
*
|
|
17
|
+
* const app = express();
|
|
18
|
+
* app.use(express.json());
|
|
19
|
+
* app.post('/mcp', createHttpHandler({ schema })); // beside app.post('/graphql', …)
|
|
20
|
+
* app.listen(4000);
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* Modules:
|
|
24
|
+
* - `types` — `GraphqlExecutor`/`GraphqlRequest`/`GraphqlResult`, the execution seam
|
|
25
|
+
* - `zodSchema` — GraphQL args → Zod input schema (`argsToZodShape`)
|
|
26
|
+
* - `selection` — auto-built selection sets (`buildSelectionSet`)
|
|
27
|
+
* - `operation` — per-field operation documents (`buildOperation`)
|
|
28
|
+
* - `tools` — schema → `ToolDescriptor`s (`buildTools`)
|
|
29
|
+
* - `executor` — `createLocalExecutor` (in-process) / `createHttpExecutor` (forwarding)
|
|
30
|
+
* - `server` — `createMcpServer` / `createServerFactory` / `registerGraphqlTools` (+ custom tools)
|
|
31
|
+
* - `http` — `createHttpHandler` for the Streamable HTTP transport
|
|
32
|
+
*
|
|
33
|
+
* @packageDocumentation
|
|
34
|
+
*/
|
|
35
|
+
export { createHttpExecutor, createLocalExecutor } from "./executor.js";
|
|
36
|
+
export { createHttpHandler } from "./http.js";
|
|
37
|
+
export { buildOperation } from "./operation.js";
|
|
38
|
+
export { buildSelectionSet } from "./selection.js";
|
|
39
|
+
export { createMcpServer, createServerFactory, registerGraphqlTools, } from "./server.js";
|
|
40
|
+
export { buildTools } from "./tools.js";
|
|
41
|
+
export { argsToZodShape } from "./zodSchema.js";
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Assembles a complete, named GraphQL operation document for a single root
|
|
3
|
+
* field, passing every argument as a variable (so values are never inlined into
|
|
4
|
+
* the query string — the executor's variable layer handles escaping/coercion).
|
|
5
|
+
*
|
|
6
|
+
* Example output for a `Mutation.createTodo(input: CreateTodoInput!)` field:
|
|
7
|
+
*
|
|
8
|
+
* ```graphql
|
|
9
|
+
* mutation createTodo($input: CreateTodoInput!) {
|
|
10
|
+
* createTodo(input: $input) { id completed __typename }
|
|
11
|
+
* }
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
import type { GraphQLField } from 'graphql';
|
|
15
|
+
import type { OperationKind } from './types.ts';
|
|
16
|
+
/** A built operation: the document plus the metadata needed to invoke it. */
|
|
17
|
+
export interface BuiltOperation {
|
|
18
|
+
/** The operation document string. */
|
|
19
|
+
query: string;
|
|
20
|
+
/** The operation name (equal to the field name). */
|
|
21
|
+
operationName: string;
|
|
22
|
+
/** The field's argument names, in declared order. */
|
|
23
|
+
argNames: string[];
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Builds the operation document for a root `field` of the given `kind`.
|
|
27
|
+
*
|
|
28
|
+
* @param kind - `'query'` or `'mutation'` — becomes the operation keyword.
|
|
29
|
+
* @param field - The root field to wrap.
|
|
30
|
+
* @param selectionDepth - Passed through to {@link buildSelectionSet} (default `2`).
|
|
31
|
+
* @returns The {@link BuiltOperation}.
|
|
32
|
+
*/
|
|
33
|
+
export declare function buildOperation(kind: OperationKind, field: GraphQLField<any, any>, selectionDepth?: number): BuiltOperation;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Assembles a complete, named GraphQL operation document for a single root
|
|
3
|
+
* field, passing every argument as a variable (so values are never inlined into
|
|
4
|
+
* the query string — the executor's variable layer handles escaping/coercion).
|
|
5
|
+
*
|
|
6
|
+
* Example output for a `Mutation.createTodo(input: CreateTodoInput!)` field:
|
|
7
|
+
*
|
|
8
|
+
* ```graphql
|
|
9
|
+
* mutation createTodo($input: CreateTodoInput!) {
|
|
10
|
+
* createTodo(input: $input) { id completed __typename }
|
|
11
|
+
* }
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
import { buildSelectionSet } from "./selection.js";
|
|
15
|
+
/**
|
|
16
|
+
* Builds the operation document for a root `field` of the given `kind`.
|
|
17
|
+
*
|
|
18
|
+
* @param kind - `'query'` or `'mutation'` — becomes the operation keyword.
|
|
19
|
+
* @param field - The root field to wrap.
|
|
20
|
+
* @param selectionDepth - Passed through to {@link buildSelectionSet} (default `2`).
|
|
21
|
+
* @returns The {@link BuiltOperation}.
|
|
22
|
+
*/
|
|
23
|
+
export function buildOperation(kind,
|
|
24
|
+
// biome-ignore lint/suspicious/noExplicitAny: a root field's source/context types are irrelevant here
|
|
25
|
+
field, selectionDepth = 2) {
|
|
26
|
+
const variableDefs = field.args.map((arg) => `$${arg.name}: ${arg.type.toString()}`);
|
|
27
|
+
const argPassings = field.args.map((arg) => `${arg.name}: $${arg.name}`);
|
|
28
|
+
const varBlock = variableDefs.length ? `(${variableDefs.join(', ')})` : '';
|
|
29
|
+
const argBlock = argPassings.length ? `(${argPassings.join(', ')})` : '';
|
|
30
|
+
const selection = buildSelectionSet(field.type, selectionDepth);
|
|
31
|
+
const selectionBlock = selection ? ` ${selection}` : '';
|
|
32
|
+
const query = `${kind} ${field.name}${varBlock} {\n ${field.name}${argBlock}${selectionBlock}\n}`;
|
|
33
|
+
return { query, operationName: field.name, argNames: field.args.map((arg) => arg.name) };
|
|
34
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-generates a GraphQL selection set for a field's return type.
|
|
3
|
+
*
|
|
4
|
+
* A tool call has no way to ask the AI "which fields do you want back?", so we
|
|
5
|
+
* select a sensible default: every scalar/enum leaf at each level, descending
|
|
6
|
+
* into nested object/interface/union types up to `maxDepth`. `__typename` is
|
|
7
|
+
* always included so the result is self-describing (and never an empty, invalid
|
|
8
|
+
* selection set).
|
|
9
|
+
*
|
|
10
|
+
* Two things are deliberately skipped (see TODO.md): fields that require
|
|
11
|
+
* arguments (we can't invent argument values) and types already on the current
|
|
12
|
+
* path (cycle guard).
|
|
13
|
+
*/
|
|
14
|
+
import { type GraphQLOutputType } from 'graphql';
|
|
15
|
+
/**
|
|
16
|
+
* Builds a selection set string (e.g. `{ id name author { id __typename } }`)
|
|
17
|
+
* for a field's return `type`. Returns `''` when the type is a scalar/enum leaf
|
|
18
|
+
* (such a field takes no selection set).
|
|
19
|
+
*
|
|
20
|
+
* @param type - The field's return type (wrappers are unwrapped automatically).
|
|
21
|
+
* @param maxDepth - How many object levels deep to select. `1` = leaf fields of
|
|
22
|
+
* the return type only; `2` (default) also expands one level of nested objects.
|
|
23
|
+
* @returns The selection set string, or `''` for a leaf return type.
|
|
24
|
+
*/
|
|
25
|
+
export declare function buildSelectionSet(type: GraphQLOutputType, maxDepth?: number): string;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-generates a GraphQL selection set for a field's return type.
|
|
3
|
+
*
|
|
4
|
+
* A tool call has no way to ask the AI "which fields do you want back?", so we
|
|
5
|
+
* select a sensible default: every scalar/enum leaf at each level, descending
|
|
6
|
+
* into nested object/interface/union types up to `maxDepth`. `__typename` is
|
|
7
|
+
* always included so the result is self-describing (and never an empty, invalid
|
|
8
|
+
* selection set).
|
|
9
|
+
*
|
|
10
|
+
* Two things are deliberately skipped (see TODO.md): fields that require
|
|
11
|
+
* arguments (we can't invent argument values) and types already on the current
|
|
12
|
+
* path (cycle guard).
|
|
13
|
+
*/
|
|
14
|
+
import { getNamedType, isEnumType, isInterfaceType, isNonNullType, isObjectType, isScalarType, isUnionType, } from 'graphql';
|
|
15
|
+
/**
|
|
16
|
+
* Builds a selection set string (e.g. `{ id name author { id __typename } }`)
|
|
17
|
+
* for a field's return `type`. Returns `''` when the type is a scalar/enum leaf
|
|
18
|
+
* (such a field takes no selection set).
|
|
19
|
+
*
|
|
20
|
+
* @param type - The field's return type (wrappers are unwrapped automatically).
|
|
21
|
+
* @param maxDepth - How many object levels deep to select. `1` = leaf fields of
|
|
22
|
+
* the return type only; `2` (default) also expands one level of nested objects.
|
|
23
|
+
* @returns The selection set string, or `''` for a leaf return type.
|
|
24
|
+
*/
|
|
25
|
+
export function buildSelectionSet(type, maxDepth = 2) {
|
|
26
|
+
return selectionFor(getNamedType(type), maxDepth, new Set());
|
|
27
|
+
}
|
|
28
|
+
/** Returns a `{ ... }` block for a composite type, or `''` for a leaf. */
|
|
29
|
+
function selectionFor(named, depth, path) {
|
|
30
|
+
if (isScalarType(named) || isEnumType(named)) {
|
|
31
|
+
return '';
|
|
32
|
+
}
|
|
33
|
+
if (isUnionType(named)) {
|
|
34
|
+
const parts = ['__typename'];
|
|
35
|
+
for (const member of named.getTypes()) {
|
|
36
|
+
parts.push(`... on ${member.name} { ${compositeFields(member, depth, path)} }`);
|
|
37
|
+
}
|
|
38
|
+
return `{ ${parts.join(' ')} }`;
|
|
39
|
+
}
|
|
40
|
+
if (isObjectType(named) || isInterfaceType(named)) {
|
|
41
|
+
return `{ ${compositeFields(named, depth, path)} }`;
|
|
42
|
+
}
|
|
43
|
+
return '';
|
|
44
|
+
}
|
|
45
|
+
/** Joins the selectable fields of an object/interface type, always ending with `__typename`. */
|
|
46
|
+
function compositeFields(type, depth, path) {
|
|
47
|
+
const selected = [];
|
|
48
|
+
const nextPath = new Set(path).add(type.name);
|
|
49
|
+
for (const [name, field] of Object.entries(type.getFields())) {
|
|
50
|
+
// Can't auto-select a field that requires arguments we don't have.
|
|
51
|
+
if (field.args.some((arg) => isNonNullType(arg.type) && arg.defaultValue === undefined)) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const named = getNamedType(field.type);
|
|
55
|
+
if (isScalarType(named) || isEnumType(named)) {
|
|
56
|
+
selected.push(name);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
// A composite field: only descend if we have depth left and aren't cycling.
|
|
60
|
+
if (depth <= 1 || path.has(named.name)) {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const sub = selectionFor(named, depth - 1, nextPath);
|
|
64
|
+
if (sub) {
|
|
65
|
+
selected.push(`${name} ${sub}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
selected.push('__typename');
|
|
69
|
+
return selected.join(' ');
|
|
70
|
+
}
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wires schema-derived {@link ToolDescriptor}s onto an `McpServer`, binding each
|
|
3
|
+
* to a {@link GraphqlExecutor}, and lets callers register custom tools that add
|
|
4
|
+
* to — or override, by name — the generated ones.
|
|
5
|
+
*
|
|
6
|
+
* Two entry points:
|
|
7
|
+
* - {@link createMcpServer} — a ready `McpServer` (use directly for stdio or a
|
|
8
|
+
* single long-lived connection).
|
|
9
|
+
* - {@link createServerFactory} — builds the (pure) descriptors once and returns
|
|
10
|
+
* a `() => McpServer` that mints a fresh server per call. The HTTP layer uses
|
|
11
|
+
* this so each stateless request gets its own server+transport.
|
|
12
|
+
*/
|
|
13
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
14
|
+
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
|
15
|
+
import type { GraphQLSchema } from 'graphql';
|
|
16
|
+
import type { ZodRawShape } from 'zod';
|
|
17
|
+
import { type BuildToolsOptions, type ToolDescriptor } from './tools.ts';
|
|
18
|
+
import type { GraphqlExecutor, ToolAnnotations } from './types.ts';
|
|
19
|
+
/** The handler signature for a custom tool: validated args plus the MCP `extra`. */
|
|
20
|
+
export type ToolHandler = (args: Record<string, unknown>, extra: unknown) => CallToolResult | Promise<CallToolResult>;
|
|
21
|
+
/**
|
|
22
|
+
* A user-supplied tool. If its `name` matches a generated tool, it replaces that
|
|
23
|
+
* tool; otherwise it's added. Omit `inputSchema` for a no-argument tool.
|
|
24
|
+
*/
|
|
25
|
+
export interface CustomTool {
|
|
26
|
+
name: string;
|
|
27
|
+
title?: string;
|
|
28
|
+
description: string;
|
|
29
|
+
inputSchema?: ZodRawShape;
|
|
30
|
+
annotations?: ToolAnnotations;
|
|
31
|
+
handler: ToolHandler;
|
|
32
|
+
}
|
|
33
|
+
/** Derives the per-call GraphQL context from the MCP request's `extra`. */
|
|
34
|
+
export type ContextFactory = (extra: unknown) => unknown | Promise<unknown>;
|
|
35
|
+
/** Options for {@link createMcpServer} / {@link createServerFactory}. */
|
|
36
|
+
export interface CreateMcpServerOptions extends BuildToolsOptions {
|
|
37
|
+
/** The GraphQL schema to expose. */
|
|
38
|
+
schema: GraphQLSchema;
|
|
39
|
+
/** MCP server name advertised to clients. Default `'graphql-mcp-server'`. */
|
|
40
|
+
name?: string;
|
|
41
|
+
/** MCP server version. Default `'0.1.0'`. */
|
|
42
|
+
version?: string;
|
|
43
|
+
/**
|
|
44
|
+
* Where tool operations run. Default: {@link createLocalExecutor} against
|
|
45
|
+
* `schema`. Swap in {@link createHttpExecutor} to forward to a separate server.
|
|
46
|
+
*/
|
|
47
|
+
executor?: GraphqlExecutor;
|
|
48
|
+
/**
|
|
49
|
+
* Per-call GraphQL context. A static value, or a factory of the MCP `extra`
|
|
50
|
+
* (which carries request/auth info under HTTP transport) — use the factory to
|
|
51
|
+
* derive auth context per request.
|
|
52
|
+
*/
|
|
53
|
+
context?: unknown | ContextFactory;
|
|
54
|
+
/** Custom tools to add or override generated ones by name. */
|
|
55
|
+
tools?: CustomTool[];
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Builds a single `McpServer` with all generated and custom tools registered.
|
|
59
|
+
*
|
|
60
|
+
* For stateless HTTP, prefer {@link createHttpHandler} (which gives each request
|
|
61
|
+
* its own server). Use this directly for stdio or a single persistent session.
|
|
62
|
+
*
|
|
63
|
+
* @param options - Schema, executor, context, and tool options.
|
|
64
|
+
*/
|
|
65
|
+
export declare function createMcpServer(options: CreateMcpServerOptions): McpServer;
|
|
66
|
+
/** A factory minting fresh `McpServer`s; an optional arg overrides the call context. */
|
|
67
|
+
export type ServerFactory = (contextOverride?: unknown | ContextFactory) => McpServer;
|
|
68
|
+
/**
|
|
69
|
+
* Builds the tool descriptors once and returns a factory that mints a fresh
|
|
70
|
+
* `McpServer` (with those tools registered) on each call. The factory accepts an
|
|
71
|
+
* optional context override so per-request callers (e.g. the HTTP handler) can
|
|
72
|
+
* supply request-derived context without rebuilding the descriptors.
|
|
73
|
+
*
|
|
74
|
+
* @param options - Schema, executor, context, and tool options.
|
|
75
|
+
* @returns A {@link ServerFactory}.
|
|
76
|
+
*/
|
|
77
|
+
export declare function createServerFactory(options: CreateMcpServerOptions): ServerFactory;
|
|
78
|
+
/**
|
|
79
|
+
* Registers schema-derived `descriptors` onto an existing `server`, binding each
|
|
80
|
+
* to `executor`. The lower-level building block behind {@link createMcpServer};
|
|
81
|
+
* use it when you manage the `McpServer` lifecycle yourself.
|
|
82
|
+
*
|
|
83
|
+
* @param server - The MCP server to register tools on.
|
|
84
|
+
* @param descriptors - Tool descriptors (from `buildTools`).
|
|
85
|
+
* @param executor - Where the tools' operations run.
|
|
86
|
+
* @param context - Per-call GraphQL context (value or factory of MCP `extra`).
|
|
87
|
+
*/
|
|
88
|
+
export declare function registerGraphqlTools(server: McpServer, descriptors: ToolDescriptor[], executor: GraphqlExecutor, context?: unknown | ContextFactory): void;
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wires schema-derived {@link ToolDescriptor}s onto an `McpServer`, binding each
|
|
3
|
+
* to a {@link GraphqlExecutor}, and lets callers register custom tools that add
|
|
4
|
+
* to — or override, by name — the generated ones.
|
|
5
|
+
*
|
|
6
|
+
* Two entry points:
|
|
7
|
+
* - {@link createMcpServer} — a ready `McpServer` (use directly for stdio or a
|
|
8
|
+
* single long-lived connection).
|
|
9
|
+
* - {@link createServerFactory} — builds the (pure) descriptors once and returns
|
|
10
|
+
* a `() => McpServer` that mints a fresh server per call. The HTTP layer uses
|
|
11
|
+
* this so each stateless request gets its own server+transport.
|
|
12
|
+
*/
|
|
13
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
14
|
+
import { createLocalExecutor } from "./executor.js";
|
|
15
|
+
import { buildTools } from "./tools.js";
|
|
16
|
+
/**
|
|
17
|
+
* Builds a single `McpServer` with all generated and custom tools registered.
|
|
18
|
+
*
|
|
19
|
+
* For stateless HTTP, prefer {@link createHttpHandler} (which gives each request
|
|
20
|
+
* its own server). Use this directly for stdio or a single persistent session.
|
|
21
|
+
*
|
|
22
|
+
* @param options - Schema, executor, context, and tool options.
|
|
23
|
+
*/
|
|
24
|
+
export function createMcpServer(options) {
|
|
25
|
+
return createServerFactory(options)();
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Builds the tool descriptors once and returns a factory that mints a fresh
|
|
29
|
+
* `McpServer` (with those tools registered) on each call. The factory accepts an
|
|
30
|
+
* optional context override so per-request callers (e.g. the HTTP handler) can
|
|
31
|
+
* supply request-derived context without rebuilding the descriptors.
|
|
32
|
+
*
|
|
33
|
+
* @param options - Schema, executor, context, and tool options.
|
|
34
|
+
* @returns A {@link ServerFactory}.
|
|
35
|
+
*/
|
|
36
|
+
export function createServerFactory(options) {
|
|
37
|
+
const descriptors = buildTools(options.schema, options);
|
|
38
|
+
const executor = options.executor ?? createLocalExecutor(options.schema);
|
|
39
|
+
const customTools = options.tools ?? [];
|
|
40
|
+
const overridden = new Set(customTools.map((tool) => tool.name));
|
|
41
|
+
return (contextOverride) => {
|
|
42
|
+
const context = contextOverride ?? options.context;
|
|
43
|
+
const server = new McpServer({
|
|
44
|
+
name: options.name ?? 'graphql-mcp-server',
|
|
45
|
+
version: options.version ?? '0.1.0',
|
|
46
|
+
});
|
|
47
|
+
for (const descriptor of descriptors) {
|
|
48
|
+
if (overridden.has(descriptor.name))
|
|
49
|
+
continue;
|
|
50
|
+
registerGeneratedTool(server, descriptor, executor, context);
|
|
51
|
+
}
|
|
52
|
+
for (const tool of customTools) {
|
|
53
|
+
registerCustomTool(server, tool);
|
|
54
|
+
}
|
|
55
|
+
return server;
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Registers schema-derived `descriptors` onto an existing `server`, binding each
|
|
60
|
+
* to `executor`. The lower-level building block behind {@link createMcpServer};
|
|
61
|
+
* use it when you manage the `McpServer` lifecycle yourself.
|
|
62
|
+
*
|
|
63
|
+
* @param server - The MCP server to register tools on.
|
|
64
|
+
* @param descriptors - Tool descriptors (from `buildTools`).
|
|
65
|
+
* @param executor - Where the tools' operations run.
|
|
66
|
+
* @param context - Per-call GraphQL context (value or factory of MCP `extra`).
|
|
67
|
+
*/
|
|
68
|
+
export function registerGraphqlTools(server, descriptors, executor, context) {
|
|
69
|
+
for (const descriptor of descriptors) {
|
|
70
|
+
registerGeneratedTool(server, descriptor, executor, context);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function registerGeneratedTool(server, descriptor, executor, context) {
|
|
74
|
+
server.registerTool(descriptor.name, {
|
|
75
|
+
title: descriptor.title,
|
|
76
|
+
description: descriptor.description,
|
|
77
|
+
inputSchema: descriptor.inputSchema,
|
|
78
|
+
annotations: descriptor.annotations,
|
|
79
|
+
}, async (args, extra) => {
|
|
80
|
+
const variables = {};
|
|
81
|
+
for (const argName of descriptor.argNames) {
|
|
82
|
+
if (args[argName] !== undefined)
|
|
83
|
+
variables[argName] = args[argName];
|
|
84
|
+
}
|
|
85
|
+
const resolvedContext = await resolveContext(context, extra);
|
|
86
|
+
const result = await executor({
|
|
87
|
+
query: descriptor.query,
|
|
88
|
+
variables,
|
|
89
|
+
operationName: descriptor.name,
|
|
90
|
+
context: resolvedContext,
|
|
91
|
+
});
|
|
92
|
+
return toCallToolResult(result);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
function registerCustomTool(server, tool) {
|
|
96
|
+
// The SDK's overloads differ by whether `inputSchema` is present; cast the
|
|
97
|
+
// config/handler at this boundary so callers get a single clean `CustomTool`.
|
|
98
|
+
const config = {
|
|
99
|
+
title: tool.title,
|
|
100
|
+
description: tool.description,
|
|
101
|
+
annotations: tool.annotations,
|
|
102
|
+
...(tool.inputSchema ? { inputSchema: tool.inputSchema } : {}),
|
|
103
|
+
};
|
|
104
|
+
// biome-ignore lint/suspicious/noExplicitAny: bridging our uniform CustomTool to the SDK's split overloads
|
|
105
|
+
server.registerTool(tool.name, config, tool.handler);
|
|
106
|
+
}
|
|
107
|
+
async function resolveContext(context, extra) {
|
|
108
|
+
return typeof context === 'function' ? await context(extra) : context;
|
|
109
|
+
}
|
|
110
|
+
/** Wraps a GraphQL result as an MCP tool result; flags GraphQL errors as `isError`. */
|
|
111
|
+
function toCallToolResult(result) {
|
|
112
|
+
return {
|
|
113
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
114
|
+
isError: Boolean(result.errors?.length),
|
|
115
|
+
};
|
|
116
|
+
}
|
package/dist/tools.d.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turns a `GraphQLSchema` into a flat list of {@link ToolDescriptor}s — one per
|
|
3
|
+
* `Query`/`Mutation` root field. This is the heart of the wrapper: it reads the
|
|
4
|
+
* SDL (field + argument descriptions, types) and projects each operation into an
|
|
5
|
+
* MCP tool whose name, description, and input schema mirror the GraphQL surface
|
|
6
|
+
* one-to-one.
|
|
7
|
+
*
|
|
8
|
+
* Descriptors are pure data (no SDK, no executor). `registerGraphqlTools` binds
|
|
9
|
+
* them to an executor and an `McpServer`.
|
|
10
|
+
*/
|
|
11
|
+
import type { GraphQLField, GraphQLSchema } from 'graphql';
|
|
12
|
+
import type { ZodRawShape } from 'zod';
|
|
13
|
+
import type { OperationKind, ToolAnnotations } from './types.ts';
|
|
14
|
+
/** A schema-derived MCP tool, prior to being bound to an executor/server. */
|
|
15
|
+
export interface ToolDescriptor {
|
|
16
|
+
/** Tool name (the GraphQL field name, unless remapped via `toolName`). */
|
|
17
|
+
name: string;
|
|
18
|
+
/** Whether this came from `Query` or `Mutation`. */
|
|
19
|
+
kind: OperationKind;
|
|
20
|
+
/** Human-friendly title (e.g. `Create Todo`). */
|
|
21
|
+
title: string;
|
|
22
|
+
/** Full tool description, derived from the SDL. */
|
|
23
|
+
description: string;
|
|
24
|
+
/** Zod raw shape for the field's arguments (the tool `inputSchema`). */
|
|
25
|
+
inputSchema: ZodRawShape;
|
|
26
|
+
/** MCP behaviour hints, defaulted from the operation kind. */
|
|
27
|
+
annotations: ToolAnnotations;
|
|
28
|
+
/** The pre-built operation document this tool runs. */
|
|
29
|
+
query: string;
|
|
30
|
+
/** The field's argument names (used to pluck variables from validated input). */
|
|
31
|
+
argNames: string[];
|
|
32
|
+
}
|
|
33
|
+
/** Options controlling which fields become tools and how they're named. */
|
|
34
|
+
export interface BuildToolsOptions {
|
|
35
|
+
/** Wrap `Query` fields as tools. Default `true`. */
|
|
36
|
+
includeQueries?: boolean;
|
|
37
|
+
/** Wrap `Mutation` fields as tools. Default `true`. */
|
|
38
|
+
includeMutations?: boolean;
|
|
39
|
+
/** Selection-set depth for return types (see `buildSelectionSet`). Default `2`. */
|
|
40
|
+
selectionDepth?: number;
|
|
41
|
+
/** Keep a field only when this returns `true`. Receives the field and its kind. */
|
|
42
|
+
filter?: (field: GraphQLField<any, any>, kind: OperationKind) => boolean;
|
|
43
|
+
/** Map a field to a custom tool name. Default: the field name verbatim. */
|
|
44
|
+
toolName?: (field: GraphQLField<any, any>, kind: OperationKind) => string;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Builds the {@link ToolDescriptor}s for a schema's root fields.
|
|
48
|
+
*
|
|
49
|
+
* @param schema - The GraphQL schema to wrap.
|
|
50
|
+
* @param options - Inclusion, depth, filtering, and naming options.
|
|
51
|
+
* @returns One descriptor per included root field.
|
|
52
|
+
* @throws If two included fields map to the same tool name (e.g. a query and a
|
|
53
|
+
* mutation share a name) — resolve the clash with `toolName` or `filter`.
|
|
54
|
+
*/
|
|
55
|
+
export declare function buildTools(schema: GraphQLSchema, options?: BuildToolsOptions): ToolDescriptor[];
|
package/dist/tools.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turns a `GraphQLSchema` into a flat list of {@link ToolDescriptor}s — one per
|
|
3
|
+
* `Query`/`Mutation` root field. This is the heart of the wrapper: it reads the
|
|
4
|
+
* SDL (field + argument descriptions, types) and projects each operation into an
|
|
5
|
+
* MCP tool whose name, description, and input schema mirror the GraphQL surface
|
|
6
|
+
* one-to-one.
|
|
7
|
+
*
|
|
8
|
+
* Descriptors are pure data (no SDK, no executor). `registerGraphqlTools` binds
|
|
9
|
+
* them to an executor and an `McpServer`.
|
|
10
|
+
*/
|
|
11
|
+
import { buildOperation } from "./operation.js";
|
|
12
|
+
import { argsToZodShape } from "./zodSchema.js";
|
|
13
|
+
/**
|
|
14
|
+
* Builds the {@link ToolDescriptor}s for a schema's root fields.
|
|
15
|
+
*
|
|
16
|
+
* @param schema - The GraphQL schema to wrap.
|
|
17
|
+
* @param options - Inclusion, depth, filtering, and naming options.
|
|
18
|
+
* @returns One descriptor per included root field.
|
|
19
|
+
* @throws If two included fields map to the same tool name (e.g. a query and a
|
|
20
|
+
* mutation share a name) — resolve the clash with `toolName` or `filter`.
|
|
21
|
+
*/
|
|
22
|
+
export function buildTools(schema, options = {}) {
|
|
23
|
+
const { includeQueries = true, includeMutations = true } = options;
|
|
24
|
+
const descriptors = [];
|
|
25
|
+
const seen = new Set();
|
|
26
|
+
const collect = (root, kind) => {
|
|
27
|
+
if (!root)
|
|
28
|
+
return;
|
|
29
|
+
for (const field of Object.values(root.getFields())) {
|
|
30
|
+
if (options.filter && !options.filter(field, kind))
|
|
31
|
+
continue;
|
|
32
|
+
const name = options.toolName ? options.toolName(field, kind) : field.name;
|
|
33
|
+
if (seen.has(name)) {
|
|
34
|
+
throw new Error(`graphql-mcp: duplicate tool name '${name}'. A query and mutation field likely ` +
|
|
35
|
+
'collide — disambiguate with the `toolName` or `filter` option.');
|
|
36
|
+
}
|
|
37
|
+
seen.add(name);
|
|
38
|
+
descriptors.push(toDescriptor(name, field, kind, options.selectionDepth));
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
if (includeQueries)
|
|
42
|
+
collect(schema.getQueryType(), 'query');
|
|
43
|
+
if (includeMutations)
|
|
44
|
+
collect(schema.getMutationType(), 'mutation');
|
|
45
|
+
return descriptors;
|
|
46
|
+
}
|
|
47
|
+
function toDescriptor(name,
|
|
48
|
+
// biome-ignore lint/suspicious/noExplicitAny: a root field's source/context types are irrelevant here
|
|
49
|
+
field, kind, selectionDepth) {
|
|
50
|
+
const { query, argNames } = buildOperation(kind, field, selectionDepth);
|
|
51
|
+
return {
|
|
52
|
+
name,
|
|
53
|
+
kind,
|
|
54
|
+
title: humanize(field.name),
|
|
55
|
+
description: buildDescription(field, kind),
|
|
56
|
+
inputSchema: argsToZodShape(field.args),
|
|
57
|
+
annotations: annotationsFor(kind, humanize(field.name)),
|
|
58
|
+
query,
|
|
59
|
+
argNames,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/** Composes a tool description from the field's SDL: docstring, signature, and args. */
|
|
63
|
+
// biome-ignore lint/suspicious/noExplicitAny: a root field's source/context types are irrelevant here
|
|
64
|
+
function buildDescription(field, kind) {
|
|
65
|
+
const lines = [];
|
|
66
|
+
lines.push(field.description?.trim() || `The \`${field.name}\` ${kind}.`);
|
|
67
|
+
lines.push('');
|
|
68
|
+
lines.push(`GraphQL ${kind}: \`${field.name}\` → \`${field.type.toString()}\``);
|
|
69
|
+
if (field.args.length) {
|
|
70
|
+
lines.push('');
|
|
71
|
+
lines.push('Arguments:');
|
|
72
|
+
for (const arg of field.args) {
|
|
73
|
+
const desc = arg.description ? ` — ${arg.description.trim()}` : '';
|
|
74
|
+
lines.push(`- \`${arg.name}\`: \`${arg.type.toString()}\`${desc}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return lines.join('\n');
|
|
78
|
+
}
|
|
79
|
+
/** Default MCP annotations: queries are read-only/idempotent, mutations are writes. */
|
|
80
|
+
function annotationsFor(kind, title) {
|
|
81
|
+
const isQuery = kind === 'query';
|
|
82
|
+
return {
|
|
83
|
+
title,
|
|
84
|
+
readOnlyHint: isQuery,
|
|
85
|
+
destructiveHint: !isQuery,
|
|
86
|
+
idempotentHint: isQuery,
|
|
87
|
+
// Tools reach a GraphQL backend, whose data lives outside this server.
|
|
88
|
+
openWorldHint: true,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/** `createTodo` → `Create Todo`; `me` → `Me`. */
|
|
92
|
+
function humanize(fieldName) {
|
|
93
|
+
const spaced = fieldName
|
|
94
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
95
|
+
.replace(/[_-]+/g, ' ')
|
|
96
|
+
.trim();
|
|
97
|
+
return spaced.replace(/\b\w/g, (char) => char.toUpperCase());
|
|
98
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core shared types. Pure types — no runtime, no internal dependencies.
|
|
3
|
+
*
|
|
4
|
+
* The central abstraction is {@link GraphqlExecutor}: the single seam between a
|
|
5
|
+
* generated MCP tool and "where GraphQL actually runs". The default executor
|
|
6
|
+
* (`createLocalExecutor`) runs the operation against the in-process schema, but
|
|
7
|
+
* a tool never knows the difference — swap in `createHttpExecutor` to forward to
|
|
8
|
+
* a separate GraphQL server running side-by-side instead.
|
|
9
|
+
*/
|
|
10
|
+
/** Whether a root field originates from the schema's `Query` or `Mutation` type. */
|
|
11
|
+
export type OperationKind = 'query' | 'mutation';
|
|
12
|
+
/** A single GraphQL error, mirroring the spec's error shape. */
|
|
13
|
+
export interface GraphqlError {
|
|
14
|
+
message: string;
|
|
15
|
+
path?: ReadonlyArray<string | number>;
|
|
16
|
+
[key: string]: any;
|
|
17
|
+
}
|
|
18
|
+
/** A GraphQL execution result, mirroring the spec's `{ data, errors }` envelope. */
|
|
19
|
+
export interface GraphqlResult {
|
|
20
|
+
data?: Record<string, unknown> | null;
|
|
21
|
+
errors?: ReadonlyArray<GraphqlError>;
|
|
22
|
+
}
|
|
23
|
+
/** A GraphQL request as produced from a tool call: a document plus its variables. */
|
|
24
|
+
export interface GraphqlRequest {
|
|
25
|
+
/** The operation document — a `query`/`mutation` string. */
|
|
26
|
+
query: string;
|
|
27
|
+
/** Variables keyed by the root field's argument names. */
|
|
28
|
+
variables: Record<string, unknown>;
|
|
29
|
+
/** The operation name (equal to the tool / root-field name). */
|
|
30
|
+
operationName: string;
|
|
31
|
+
/**
|
|
32
|
+
* Per-call context, opaque to the tool layer. Forwarded as the GraphQL
|
|
33
|
+
* `contextValue` (local executor) or used to derive request headers (HTTP
|
|
34
|
+
* executor). Typically derived from the MCP request via the `context` option.
|
|
35
|
+
*/
|
|
36
|
+
context?: unknown;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Runs a GraphQL operation and returns its `{ data, errors }` result.
|
|
40
|
+
*
|
|
41
|
+
* This is the one place the library is decoupled from execution: implement it
|
|
42
|
+
* (or use {@link createLocalExecutor} / {@link createHttpExecutor}) to run tools
|
|
43
|
+
* against an in-process schema, a remote endpoint, or anything else.
|
|
44
|
+
*/
|
|
45
|
+
export type GraphqlExecutor = (request: GraphqlRequest) => Promise<GraphqlResult>;
|
|
46
|
+
/**
|
|
47
|
+
* MCP tool behaviour hints. Mirrors the SDK's `ToolAnnotations` (kept local so
|
|
48
|
+
* the type helpers don't depend on SDK internals); all fields are optional.
|
|
49
|
+
*/
|
|
50
|
+
export interface ToolAnnotations {
|
|
51
|
+
title?: string;
|
|
52
|
+
readOnlyHint?: boolean;
|
|
53
|
+
destructiveHint?: boolean;
|
|
54
|
+
idempotentHint?: boolean;
|
|
55
|
+
openWorldHint?: boolean;
|
|
56
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core shared types. Pure types — no runtime, no internal dependencies.
|
|
3
|
+
*
|
|
4
|
+
* The central abstraction is {@link GraphqlExecutor}: the single seam between a
|
|
5
|
+
* generated MCP tool and "where GraphQL actually runs". The default executor
|
|
6
|
+
* (`createLocalExecutor`) runs the operation against the in-process schema, but
|
|
7
|
+
* a tool never knows the difference — swap in `createHttpExecutor` to forward to
|
|
8
|
+
* a separate GraphQL server running side-by-side instead.
|
|
9
|
+
*/
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts a GraphQL field's arguments into a Zod "raw shape" — the input-schema
|
|
3
|
+
* form the MCP SDK's `registerTool` expects. Written by hand (rather than pulling
|
|
4
|
+
* in a graphql-to-zod dependency) because the mapping is small and we want full
|
|
5
|
+
* control over nullability, descriptions, and custom-scalar fallbacks.
|
|
6
|
+
*
|
|
7
|
+
* Mapping rules:
|
|
8
|
+
* - `NonNull` → required (no `.nullish()`); a nullable arg/field becomes `.nullish()`
|
|
9
|
+
* - `List` → `z.array(element)`
|
|
10
|
+
* - built-in scalars → `Int`/`Float` ⇒ number, `String`/`ID` ⇒ string, `Boolean` ⇒ boolean
|
|
11
|
+
* - custom scalars → `z.any()` (the server still validates them) tagged with the scalar name
|
|
12
|
+
* - enums → `z.enum([...names])` (enum *names*, the form passed as GraphQL variables)
|
|
13
|
+
* - input objects → `z.object({...})`, recursively; a recursive input type falls back
|
|
14
|
+
* to `z.any()` once revisited (a pragmatic MVP guard — see TODO.md)
|
|
15
|
+
*/
|
|
16
|
+
import { type GraphQLArgument } from 'graphql';
|
|
17
|
+
import { type ZodRawShape } from 'zod';
|
|
18
|
+
/**
|
|
19
|
+
* Builds a Zod raw shape (`{ argName: ZodType }`) from a GraphQL field's
|
|
20
|
+
* arguments, ready to pass as a tool's `inputSchema`. Non-null args are required;
|
|
21
|
+
* nullable args are optional. Each arg's GraphQL description is carried onto its
|
|
22
|
+
* Zod type so it shows up in the tool's generated JSON Schema.
|
|
23
|
+
*
|
|
24
|
+
* @param args - The field's arguments (`field.args`).
|
|
25
|
+
* @returns A Zod raw shape; empty (`{}`) for a field with no arguments.
|
|
26
|
+
*/
|
|
27
|
+
export declare function argsToZodShape(args: ReadonlyArray<GraphQLArgument>): ZodRawShape;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts a GraphQL field's arguments into a Zod "raw shape" — the input-schema
|
|
3
|
+
* form the MCP SDK's `registerTool` expects. Written by hand (rather than pulling
|
|
4
|
+
* in a graphql-to-zod dependency) because the mapping is small and we want full
|
|
5
|
+
* control over nullability, descriptions, and custom-scalar fallbacks.
|
|
6
|
+
*
|
|
7
|
+
* Mapping rules:
|
|
8
|
+
* - `NonNull` → required (no `.nullish()`); a nullable arg/field becomes `.nullish()`
|
|
9
|
+
* - `List` → `z.array(element)`
|
|
10
|
+
* - built-in scalars → `Int`/`Float` ⇒ number, `String`/`ID` ⇒ string, `Boolean` ⇒ boolean
|
|
11
|
+
* - custom scalars → `z.any()` (the server still validates them) tagged with the scalar name
|
|
12
|
+
* - enums → `z.enum([...names])` (enum *names*, the form passed as GraphQL variables)
|
|
13
|
+
* - input objects → `z.object({...})`, recursively; a recursive input type falls back
|
|
14
|
+
* to `z.any()` once revisited (a pragmatic MVP guard — see TODO.md)
|
|
15
|
+
*/
|
|
16
|
+
import { isEnumType, isInputObjectType, isListType, isNonNullType, isScalarType, } from 'graphql';
|
|
17
|
+
import { z } from 'zod';
|
|
18
|
+
const SCALAR_BUILDERS = {
|
|
19
|
+
Int: () => z.number().int(),
|
|
20
|
+
Float: () => z.number(),
|
|
21
|
+
String: () => z.string(),
|
|
22
|
+
Boolean: () => z.boolean(),
|
|
23
|
+
ID: () => z.string(),
|
|
24
|
+
};
|
|
25
|
+
/** Applies an element/field type's nullability: required for `NonNull`, else `.nullish()`. */
|
|
26
|
+
function fieldToZod(type, seen) {
|
|
27
|
+
if (isNonNullType(type)) {
|
|
28
|
+
return baseToZod(type.ofType, seen);
|
|
29
|
+
}
|
|
30
|
+
return baseToZod(type, seen).nullish();
|
|
31
|
+
}
|
|
32
|
+
/** Builds the Zod type for a (already nullability-stripped) list/named GraphQL type. */
|
|
33
|
+
function baseToZod(type, seen) {
|
|
34
|
+
if (isListType(type)) {
|
|
35
|
+
return z.array(fieldToZod(type.ofType, seen));
|
|
36
|
+
}
|
|
37
|
+
if (isScalarType(type)) {
|
|
38
|
+
const builder = SCALAR_BUILDERS[type.name];
|
|
39
|
+
return builder ? builder() : z.any().describe(`Custom scalar ${type.name}`);
|
|
40
|
+
}
|
|
41
|
+
if (isEnumType(type)) {
|
|
42
|
+
const names = type.getValues().map((value) => value.name);
|
|
43
|
+
// An enum with no values can't happen in a valid schema, but guard the cast.
|
|
44
|
+
return names.length ? z.enum(names) : z.string();
|
|
45
|
+
}
|
|
46
|
+
if (isInputObjectType(type)) {
|
|
47
|
+
// Recursive input types (e.g. nested filter inputs) would recurse forever;
|
|
48
|
+
// once a type reappears on the current path, fall back to an opaque value.
|
|
49
|
+
if (seen.has(type.name)) {
|
|
50
|
+
return z.any().describe(`Recursive input ${type.name}`);
|
|
51
|
+
}
|
|
52
|
+
const next = new Set(seen).add(type.name);
|
|
53
|
+
const shape = {};
|
|
54
|
+
for (const [name, field] of Object.entries(type.getFields())) {
|
|
55
|
+
shape[name] = describe(fieldToZod(field.type, next), field.description);
|
|
56
|
+
}
|
|
57
|
+
return z.object(shape);
|
|
58
|
+
}
|
|
59
|
+
// Unreachable for valid input types; keep type-checking happy and fail soft.
|
|
60
|
+
return z.any();
|
|
61
|
+
}
|
|
62
|
+
function describe(schema, description) {
|
|
63
|
+
return description ? schema.describe(description) : schema;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Builds a Zod raw shape (`{ argName: ZodType }`) from a GraphQL field's
|
|
67
|
+
* arguments, ready to pass as a tool's `inputSchema`. Non-null args are required;
|
|
68
|
+
* nullable args are optional. Each arg's GraphQL description is carried onto its
|
|
69
|
+
* Zod type so it shows up in the tool's generated JSON Schema.
|
|
70
|
+
*
|
|
71
|
+
* @param args - The field's arguments (`field.args`).
|
|
72
|
+
* @returns A Zod raw shape; empty (`{}`) for a field with no arguments.
|
|
73
|
+
*/
|
|
74
|
+
export function argsToZodShape(args) {
|
|
75
|
+
const shape = {};
|
|
76
|
+
for (const arg of args) {
|
|
77
|
+
shape[arg.name] = describe(fieldToZod(arg.type, new Set()), arg.description);
|
|
78
|
+
}
|
|
79
|
+
return shape;
|
|
80
|
+
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cubicecho/graphql-mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Turn a GraphQL schema into a Model Context Protocol (MCP) server: each query/mutation becomes a tool, runnable side-by-side with your GraphQL server.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Benjamin Van Treese",
|
|
5
7
|
"type": "module",
|
|
8
|
+
"engines": {
|
|
9
|
+
"node": ">=22"
|
|
10
|
+
},
|
|
6
11
|
"repository": {
|
|
7
12
|
"type": "git",
|
|
8
13
|
"url": "git+https://github.com/cubicecho/graphql-mcp.git"
|
|
@@ -24,7 +29,8 @@
|
|
|
24
29
|
"access": "public"
|
|
25
30
|
},
|
|
26
31
|
"scripts": {
|
|
27
|
-
"build": "tsc",
|
|
32
|
+
"build": "tsc -p tsconfig.build.json",
|
|
33
|
+
"prepack": "npm run build",
|
|
28
34
|
"typecheck": "tsc --noEmit",
|
|
29
35
|
"typecheck:tests": "tsc -p tsconfig.tests.json",
|
|
30
36
|
"test": "node --test --experimental-strip-types \"src/**/*.test.ts\"",
|