Generate MCP tool definitions from your GraphQL operations. Annotate queries and mutations with @mcpTool, run codegen, get type-safe tools with JSON Schema inputs derived from your GraphQL schema.
This repo is the companion code for the blog post Generating MCP tools from your GraphQL schema. The article walks through the architecture, codegen pipeline, and safety model in detail. The code here is everything you need to try it yourself.
- Mark GraphQL operations with
@mcpTooland@mcpToolVariabledirectives - Run GraphQL Code Generator — the included document transform filters to
@mcpTooloperations and generates persisted documents - A codegen plugin reads the persisted documents, converts GraphQL types to JSON Schema, and outputs typed tool definitions
- At runtime, a thin wrapper turns each generated tool into an executable MCP tool handler
Clone the repo and install dependencies:
git clone https://github.com/labd/graphql-codegen-mcp-tools.git
cd graphql-mcp-tools
pnpm installRun the example codegen to see it in action:
pnpm codegenThis generates example/generated/mcp-tools.generated.ts from the example schema and operations in example/.
To try the MCP server (uses mock data, no backend required):
pnpm exampleCopy or adapt the files from this repo into your project. The key pieces are:
| What | Where |
|---|---|
| Directives | src/directives.graphql |
| Document transform | src/codegen/document-transform.ts |
| Tools codegen plugin | src/codegen/tools-plugin.ts |
| Directive stripping | src/codegen/strip-directives.ts |
| Runtime tool wrapper | src/runtime/create-tools.ts |
You'll also need these dependencies:
pnpm add @modelcontextprotocol/sdk change-case graphql
pnpm add -D @graphql-codegen/cli @graphql-codegen/client-preset @graphql-codegen/plugin-helpersCopy src/directives.graphql into your project:
directive @mcpTool(
description: String!
exclude: Boolean = false
) on QUERY | MUTATION
directive @mcpToolVariable(description: String) on VARIABLE_DEFINITIONquery GetProducts(
$searchTerm: String @mcpToolVariable(description: "Free-text search query")
$pageSize: Int! @mcpToolVariable(description: "Number of products per page")
$page: Int! @mcpToolVariable(description: "Page number, starting at 1")
) @mcpTool(description: "Search products in the catalog") {
productSearch(searchTerm: $searchTerm, pageSize: $pageSize, page: $page) {
total
results {
name
variant {
sku
price {
gross {
centAmount
currency
}
}
}
}
}
}See example/codegen.ts for a working config. The two key pieces are:
- Use
mcpToolTransformas a document transform to filter operations - Use
src/codegen/tools-plugin.tsas a plugin to generate tool definitions from persisted documents
// codegen.ts
import type { CodegenConfig } from "@graphql-codegen/cli";
import { mcpToolTransform } from "./src/codegen/index.js";
const config: CodegenConfig = {
schema: [
"./schema.graphql", // your schema
"./src/directives.graphql", // MCP directives
],
generates: {
"./generated/": {
documents: ["./operations.graphql"],
preset: "client",
documentTransforms: [mcpToolTransform],
presetConfig: {
persistedDocuments: {
mode: "embedHashInDocument",
hashPropertyName: "documentId",
},
},
},
"./generated/mcp-tools.generated.ts": {
plugins: ["graphql-mcp-tools/codegen/tools"],
config: {
persistedDocumentsPath: "./generated/persisted-documents.json",
},
},
},
};
export default config;pnpm graphql-codegen --config codegen.tsThis generates mcp-tools.generated.ts with typed tool definitions and persisted-documents.json for use with your GraphQL server's operation registry.
See example/server.ts for a full working example with mock data. The key parts:
import { createServer } from "node:http";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { createToolFromGenerated } from "./src/runtime/index.js";
import type { GraphQLExecutor } from "./src/runtime/index.js";
import { generatedMcpTools } from "./generated/mcp-tools.generated.js";
// Replace with a real fetch to your GraphQL server
const executor: GraphQLExecutor = async (query, variables, documentId) => {
const res = await fetch("http://localhost:4000/graphql", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ query, variables }),
});
const { data, errors } = await res.json();
if (errors?.length) throw new Error(JSON.stringify(errors));
return data;
};
function createMcpServer(): McpServer {
const server = new McpServer({ name: "my-graphql-mcp", version: "0.1.0" });
const tools = generatedMcpTools.map((t) =>
createToolFromGenerated(t, executor),
);
for (const tool of tools) {
server.tool(
tool.name,
tool.description,
tool.inputSchema,
async (args) => ({
content: [
{
type: "text",
text: JSON.stringify(await tool.handler(args), null, 2),
},
],
}),
);
}
return server;
}
// Stateless HTTP — each request gets a fresh server + transport
const httpServer = createServer(async (req, res) => {
const server = createMcpServer();
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
await server.connect(transport);
await transport.handleRequest(req, res);
res.on("close", () => {
transport.close();
server.close();
});
});
httpServer.listen(3001);The example server runs locally on HTTP (http://localhost:3001), but Claude Desktop needs a public HTTPS endpoint.
- Start the MCP server:
pnpm example- In another terminal, expose it with ngrok:
ngrok http 3001-
Copy the
https://...forwarding URL from ngrok and use that in your Claude Desktop config. -
In Claude Desktop, add that ngrok URL as a Remote MCP server.
Or test locally with:
mcp-inspector --transport http http://localhost:3001The port defaults to 3001 and can be changed via the PORT environment variable.
The codegen outputs persisted-documents.json mapping content hashes to query strings. You can register these with your GraphQL server's operation registry (GraphQL Hive, Apollo's persisted queries, or a custom solution) to reject any query not in the allowlist. This ensures the AI can only execute operations you've explicitly approved.
mcpToolTransform— Document transform that filters to@mcpTooloperations and strips MCP directives from the outputplugin(intools-plugin.ts) — Codegen plugin that generates tool definitions from persisted documentsstripMcpDirectives(query)— Strips@mcpTool/@mcpToolVariablefrom a query string
createToolFromGenerated(tool, executor)— Wraps a generated tool with an executor functionGeneratedMCPTool— Type for generated tool definitionsExecutableMCPTool— Type for tools with a handlerGraphQLExecutor— Type for the executor function(query, variables, documentId) => Promise<unknown>
MIT