# Directory Structure ``` ├── .dockerignore ├── .gitattributes ├── .github │ └── workflows │ ├── ci.yml │ └── docker.yml ├── .gitignore ├── .npmrc ├── .vscode │ └── settings.json ├── biome.json ├── bun.lock ├── CONTRIBUTING.md ├── dev │ ├── debug-client.ts │ ├── debug-manual-client.ts │ └── graphql.ts ├── Dockerfile ├── LICENSE ├── package.json ├── README.md ├── smithery.yaml ├── src │ ├── helpers │ │ ├── deprecation.ts │ │ ├── headers.ts │ │ ├── introspection.ts │ │ └── package.ts │ └── index.ts └── tsconfig.json ``` # Files -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- ``` package-lock=false ``` -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- ``` *.ts linguist-language=TypeScript ``` -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- ``` node_modules Dockerfile* docker-compose* .dockerignore .git .gitignore README.md LICENSE .vscode Makefile helm-charts .env .editorconfig .idea coverage* dist ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` # Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore # Logs logs _.log npm-debug.log_ yarn-debug.log* yarn-error.log* lerna-debug.log* .pnpm-debug.log* # Caches .cache # Diagnostic reports (https://nodejs.org/api/report.html) report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json # Runtime data pids _.pid _.seed *.pid.lock # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage *.lcov # nyc test coverage .nyc_output # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) .grunt # Bower dependency directory (https://bower.io/) bower_components # node-waf configuration .lock-wscript # Compiled binary addons (https://nodejs.org/api/addons.html) build/Release # Dependency directories node_modules/ jspm_packages/ # Snowpack dependency directory (https://snowpack.dev/) web_modules/ # TypeScript cache *.tsbuildinfo # Optional npm cache directory .npm # Optional eslint cache .eslintcache # Optional stylelint cache .stylelintcache # Microbundle cache .rpt2_cache/ .rts2_cache_cjs/ .rts2_cache_es/ .rts2_cache_umd/ # Optional REPL history .node_repl_history # Output of 'npm pack' *.tgz # Yarn Integrity file .yarn-integrity # dotenv environment variable files .env .env.development.local .env.test.local .env.production.local .env.local # parcel-bundler cache (https://parceljs.org/) .parcel-cache # Next.js build output .next out # Nuxt.js build / generate output .nuxt dist # Gatsby files # Comment in the public line in if your project uses Gatsby and not Next.js # https://nextjs.org/blog/next-9-1#public-directory-support # public # vuepress build output .vuepress/dist # vuepress v2.x temp and cache directory .temp # Docusaurus cache and generated files .docusaurus # Serverless directories .serverless/ # FuseBox cache .fusebox/ # DynamoDB Local files .dynamodb/ # TernJS port file .tern-port # Stores VSCode versions used for testing VSCode extensions .vscode-test # yarn v2 .yarn/cache .yarn/unplugged .yarn/build-state.yml .yarn/install-state.gz .pnp.* # IntelliJ based IDEs .idea # Finder (MacOS) folder config .DS_Store dist/ # GraphQL schema for debugging /schema.graphql ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown # mcp-graphql [](https://smithery.ai/server/mcp-graphql) A Model Context Protocol server that enables LLMs to interact with GraphQL APIs. This implementation provides schema introspection and query execution capabilities, allowing models to discover and use GraphQL APIs dynamically. <a href="https://glama.ai/mcp/servers/4zwa4l8utf"><img width="380" height="200" src="https://glama.ai/mcp/servers/4zwa4l8utf/badge" alt="mcp-graphql MCP server" /></a> ## Usage Run `mcp-graphql` with the correct endpoint, it will automatically try to introspect your queries. ### Environment Variables (Breaking change in 1.0.0) > **Note:** As of version 1.0.0, command line arguments have been replaced with environment variables. | Environment Variable | Description | Default | |----------|-------------|---------| | `ENDPOINT` | GraphQL endpoint URL | `http://localhost:4000/graphql` | | `HEADERS` | JSON string containing headers for requests | `{}` | | `ALLOW_MUTATIONS` | Enable mutation operations (disabled by default) | `false` | | `NAME` | Name of the MCP server | `mcp-graphql` | | `SCHEMA` | Path to a local GraphQL schema file or URL (optional) | - | ### Examples ```bash # Basic usage with a local GraphQL server ENDPOINT=http://localhost:3000/graphql npx mcp-graphql # Using with custom headers ENDPOINT=https://api.example.com/graphql HEADERS='{"Authorization":"Bearer token123"}' npx mcp-graphql # Enable mutation operations ENDPOINT=http://localhost:3000/graphql ALLOW_MUTATIONS=true npx mcp-graphql # Using a local schema file instead of introspection ENDPOINT=http://localhost:3000/graphql SCHEMA=./schema.graphql npx mcp-graphql # Using a schema file hosted at a URL ENDPOINT=http://localhost:3000/graphql SCHEMA=https://example.com/schema.graphql npx mcp-graphql ``` ## Resources - **graphql-schema**: The server exposes the GraphQL schema as a resource that clients can access. This is either the local schema file, a schema file hosted at a URL, or based on an introspection query. ## Available Tools The server provides two main tools: 1. **introspect-schema**: This tool retrieves the GraphQL schema. Use this first if you don't have access to the schema as a resource. This uses either the local schema file, a schema file hosted at a URL, or an introspection query. 2. **query-graphql**: Execute GraphQL queries against the endpoint. By default, mutations are disabled unless `ALLOW_MUTATIONS` is set to `true`. ## Installation ### Installing via Smithery To install GraphQL MCP Server for Claude Desktop automatically via [Smithery](https://smithery.ai/server/mcp-graphql): ```bash npx -y @smithery/cli install mcp-graphql --client claude ``` ### Installing Manually It can be manually installed to Claude: ```json { "mcpServers": { "mcp-graphql": { "command": "npx", "args": ["mcp-graphql"], "env": { "ENDPOINT": "http://localhost:3000/graphql" } } } } ``` ## Security Considerations Mutations are disabled by default as a security measure to prevent an LLM from modifying your database or service data. Consider carefully before enabling mutations in production environments. ## Customize for your own server This is a very generic implementation where it allows for complete introspection and for your users to do whatever (including mutations). If you need a more specific implementation I'd suggest to just create your own MCP and lock down tool calling for clients to only input specific query fields and/or variables. You can use this as a reference. ``` -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- ```markdown Yes please! Add issues or pull requests for whatever is missing/should be improved :) ``` -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- ```json { "formatter": { "enabled": true, "indentStyle": "tab", "indentWidth": 2 } } ``` -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- ```json { "editor.defaultFormatter": "biomejs.biome", "[typescript]": { "editor.defaultFormatter": "biomejs.biome" } } ``` -------------------------------------------------------------------------------- /src/helpers/package.ts: -------------------------------------------------------------------------------- ```typescript import { readFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); // Current package version so I only need to update it in one place const { version } = JSON.parse( readFileSync(join(__dirname, "../../package.json"), "utf-8"), ); export function getVersion() { return version; } ``` -------------------------------------------------------------------------------- /dev/debug-client.ts: -------------------------------------------------------------------------------- ```typescript // Small debug client to test a few specific interactions import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; const transport = new StdioClientTransport({ command: "node", args: ["dist/index.js"], }); const client = new Client({ name: "debug-client", version: "1.0.0", }); await client.connect(transport); // Call introspect-schema with undefined argument const result = await client.callTool({ name: "introspect-schema", arguments: {}, }); console.log(result); ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json { "compilerOptions": { // Enable latest features "lib": ["ESNext", "DOM"], "target": "ESNext", "module": "ESNext", "moduleDetection": "force", "jsx": "react-jsx", "allowJs": true, // Bundler mode "moduleResolution": "bundler", "allowImportingTsExtensions": true, "verbatimModuleSyntax": true, "noEmit": true, // Best practices "strict": true, "skipLibCheck": true, "noFallthroughCasesInSwitch": true, // Some stricter flags (disabled by default) "noUnusedLocals": false, "noUnusedParameters": false, "noPropertyAccessFromIndexSignature": false } } ``` -------------------------------------------------------------------------------- /smithery.yaml: -------------------------------------------------------------------------------- ```yaml # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml startCommand: type: stdio configSchema: # JSON Schema defining the configuration options for the MCP. type: object required: - endpoint properties: endpoint: type: string description: The GraphQL server endpoint URL. headers: type: string description: Optional JSON string of headers to send with the GraphQL requests. commandFunction: # A function that produces the CLI command to start the MCP on stdio. |- (config) => ({ command: 'node', args: ['/app/dist/index.js', '--endpoint', config.endpoint].concat(config.headers ? ['--headers', config.headers] : []), env: {} }) ``` -------------------------------------------------------------------------------- /src/helpers/headers.ts: -------------------------------------------------------------------------------- ```typescript /** * Parse and merge headers from various sources * @param configHeaders - Default headers from configuration * @param inputHeaders - Headers provided by the user (string or object) * @returns Merged headers object */ export function parseAndMergeHeaders( configHeaders: Record<string, string>, inputHeaders?: string | Record<string, string>, ): Record<string, string> { // Parse headers if they're provided as a string let parsedHeaders: Record<string, string> = {}; if (typeof inputHeaders === "string") { try { parsedHeaders = JSON.parse(inputHeaders); } catch (e) { throw new Error(`Invalid headers JSON: ${e}`); } } else if (inputHeaders) { parsedHeaders = inputHeaders; } // Merge with config headers (config headers are overridden by input headers) return { ...configHeaders, ...parsedHeaders }; } ``` -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- ```dockerfile # Bun based Dockerfile # Does not build the server, but runs it directly from source using bun FROM oven/bun:1 AS base WORKDIR /usr/src/app # Cached dependency install layer FROM base AS install RUN mkdir -p /temp/dev COPY package.json bun.lock /temp/dev/ RUN cd /temp/dev && bun install --frozen-lockfile # exclude devDependencies RUN mkdir -p /temp/prod COPY package.json bun.lock /temp/prod/ RUN cd /temp/prod && bun install --frozen-lockfile --production FROM base AS prerelease COPY --from=install /temp/dev/node_modules node_modules COPY . . # copy production dependencies and source code into final image FROM base AS release COPY --from=install /temp/prod/node_modules node_modules COPY --from=prerelease /usr/src/app/src/ ./src/ COPY --from=prerelease /usr/src/app/package.json . # run the app USER bun ENTRYPOINT [ "bun", "run", "src/index.ts" ] ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json { "name": "mcp-graphql", "module": "index.ts", "type": "module", "version": "2.0.4", "repository": "github:blurrah/mcp-graphql", "license": "MIT", "bin": { "mcp-graphql": "./dist/index.js" }, "files": [ "dist" ], "devDependencies": { "@graphql-tools/schema": "^10.0.23", "@types/bun": "^1.2.14", "@types/yargs": "17.0.33", "biome": "^0.3.3", "graphql-yoga": "^5.13.5", "typescript": "5.8.3" }, "dependencies": { "@modelcontextprotocol/sdk": "1.12.0", "graphql": "^16.11.0", "yargs": "17.7.2", "zod": "3.25.30", "zod-to-json-schema": "3.24.5" }, "scripts": { "dev": "bun --watch src/index.ts", "build": "bun build src/index.ts --outdir dist --target node && bun -e \"require('fs').chmodSync('dist/index.js', '755')\"", "start": "bun run dist/index.js", "format": "biome format --write .", "check": "biome format ." }, "packageManager": "[email protected]" } ``` -------------------------------------------------------------------------------- /src/helpers/deprecation.ts: -------------------------------------------------------------------------------- ```typescript /** * Helper module for handling deprecation warnings */ /** * Check for deprecated command line arguments and output warnings */ export function checkDeprecatedArguments(): void { const deprecatedArgs = [ "--endpoint", "--headers", "--enable-mutations", "--name", "--schema", ]; const usedDeprecatedArgs = deprecatedArgs.filter((arg) => process.argv.includes(arg), ); if (usedDeprecatedArgs.length > 0) { console.error( `WARNING: Deprecated command line arguments detected: ${usedDeprecatedArgs.join(", ")}`, ); console.error( "As of version 1.0.0, command line arguments have been replaced with environment variables.", ); console.error("Please use environment variables instead. For example:"); console.error( " Instead of: npx mcp-graphql --endpoint http://example.com/graphql", ); console.error(" Use: ENDPOINT=http://example.com/graphql npx mcp-graphql"); console.error(""); } } ``` -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- ```yaml name: docker on: push: branches: ['main'] env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} jobs: build-and-push-image: runs-on: ubuntu-latest permissions: contents: read packages: write attestations: write id-token: write steps: - name: Checkout repository uses: actions/checkout@v4 - name: Log in to the Container registry uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Extract metadata (tags, labels) for Docker id: meta uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - name: Build and push Docker image id: push uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4 with: context: . push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} - name: Generate artifact attestation uses: actions/attest-build-provenance@v2 with: subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}} subject-digest: ${{ steps.push.outputs.digest }} push-to-registry: true ``` -------------------------------------------------------------------------------- /src/helpers/introspection.ts: -------------------------------------------------------------------------------- ```typescript import { buildClientSchema, getIntrospectionQuery, printSchema } from "graphql"; import { readFile } from "node:fs/promises"; /** * Introspect a GraphQL endpoint and return the schema as the GraphQL SDL * @param endpoint - The endpoint to introspect * @param headers - Optional headers to include in the request * @returns The schema */ export async function introspectEndpoint( endpoint: string, headers?: Record<string, string>, ) { const response = await fetch(endpoint, { method: "POST", headers: { "Content-Type": "application/json", ...headers, }, body: JSON.stringify({ query: getIntrospectionQuery(), }), }); if (!response.ok) { throw new Error(`GraphQL request failed: ${response.statusText}`); } const responseJson = await response.json(); // Transform to a schema object const schema = buildClientSchema(responseJson.data); // Print the schema SDL return printSchema(schema); } /** * Introspect a GraphQL schema file hosted at a URL and return the schema as the GraphQL SDL * @param url - The URL to the schema file * @returns The schema */ export async function introspectSchemaFromUrl(url: string) { const response = await fetch(url); if (!response.ok) { throw new Error(`Failed to fetch schema from URL: ${response.statusText}`); } const schema = await response.text(); return schema; } /** * Introspect a local GraphQL schema file and return the schema as the GraphQL SDL * @param path - The path to the local schema file * @returns The schema */ export async function introspectLocalSchema(path: string) { const schema = await readFile(path, "utf8"); return schema; } ``` -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- ```yaml name: ci on: push: branches: - main tags: - v* pull_request: types: [opened, synchronize] release: types: [published] workflow_dispatch: inputs: version: description: 'Version to release' required: true jobs: build: runs-on: ubuntu-latest if: github.event_name != 'release' steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - uses: oven-sh/setup-bun@v2 with: bun-version: latest - run: bun install - run: bun run format - run: bun run check - run: bun run build - name: Get tag annotation id: get-tag-annotation run: | TAG_NAME=${GITHUB_REF#refs/tags/} echo "Processing tag: $TAG_NAME" # Check if tag exists and is annotated if TAG_MESSAGE=$(git tag -l --format='%(contents)' $TAG_NAME 2>/dev/null); then echo "Found annotated tag message" else echo "No tag annotation found, using empty message" TAG_MESSAGE="" fi # Use multiline output syntax for GitHub Actions echo "TAG_MESSAGE<<EOF" >> $GITHUB_OUTPUT echo "$TAG_MESSAGE" >> $GITHUB_OUTPUT echo "EOF" >> $GITHUB_OUTPUT - name: Create GitHub Release uses: softprops/action-gh-release@v2 if: startsWith(github.ref, 'refs/tags/') with: files: dist/* generate_release_notes: true body: ${{ steps.get-tag-annotation.outputs.TAG_MESSAGE }} append_body: true # Use PAT as it sends a release event, built in token doesn't token: ${{ secrets.RELEASE_TOKEN }} publish: runs-on: ubuntu-latest if: github.event_name == 'release' || github.event_name == 'workflow_dispatch' && inputs.version != '' permissions: contents: read id-token: write steps: - uses: actions/checkout@v4 # Falling back to node since I want provenance for the npm package - uses: actions/setup-node@v4 with: node-version: 22 registry-url: 'https://registry.npmjs.org' # Might be a useless optimization but I felt like not reusing the build job - name: Download index.js from release run: | gh release download ${{ github.event.release.tag_name || inputs.version }} --dir dist --pattern "index.js" env: GH_TOKEN: ${{ github.token }} - run: npm publish --provenance --access public env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} ``` -------------------------------------------------------------------------------- /dev/debug-manual-client.ts: -------------------------------------------------------------------------------- ```typescript // Manual MCP client using stdio directly (no SDK) // This demonstrates the raw JSON-RPC protocol communication import { type ChildProcess, spawn } from "node:child_process"; import { createInterface } from "node:readline"; interface JsonRpcMessage { jsonrpc: "2.0"; id?: string | number; method?: string; params?: unknown; result?: unknown; error?: { code: number; message: string; data?: unknown; }; } class ManualMcpClient { private serverProcess: ChildProcess; private messageId = 1; private pendingRequests = new Map< string | number, (response: JsonRpcMessage) => void >(); constructor() { // Start the MCP server process this.serverProcess = spawn("node", ["dist/index.js"], { stdio: ["pipe", "pipe", "pipe"], }); // Set up readline to read server responses line by line if (this.serverProcess.stdout) { const rl = createInterface({ input: this.serverProcess.stdout, }); rl.on("line", (line) => { try { const message: JsonRpcMessage = JSON.parse(line); this.handleServerMessage(message); } catch (error) { console.error("Failed to parse server message:", line, error); } }); } // Handle server errors this.serverProcess.stderr?.on("data", (data: Buffer) => { console.error("Server stderr:", data.toString()); }); this.serverProcess.on("exit", (code: number | null) => { console.log(`Server process exited with code ${code}`); }); } private handleServerMessage(message: JsonRpcMessage) { console.log("← Received from server:", JSON.stringify(message, null, 2)); // Handle responses to our requests if (message.id !== undefined && this.pendingRequests.has(message.id)) { const resolver = this.pendingRequests.get(message.id); if (resolver) { this.pendingRequests.delete(message.id); resolver(message); } } } private sendMessage(message: JsonRpcMessage): Promise<JsonRpcMessage> { const messageStr = JSON.stringify(message); console.log("→ Sending to server:", messageStr); this.serverProcess.stdin?.write(`${messageStr}\n`); // If this is a request (has an id), wait for response if (message.id !== undefined) { return new Promise((resolve) => { if (message.id !== undefined) { this.pendingRequests.set(message.id, resolve); } }); } return Promise.resolve(message); } private getNextId(): number { return this.messageId++; } async initialize(): Promise<JsonRpcMessage> { const initMessage: JsonRpcMessage = { jsonrpc: "2.0", method: "initialize", params: { protocolVersion: "2025-03-26", capabilities: {}, clientInfo: { name: "manual-debug-client", version: "1.0.0", }, }, id: this.getNextId(), }; const response = await this.sendMessage(initMessage); // Send initialized notification const initializedNotification: JsonRpcMessage = { jsonrpc: "2.0", method: "notifications/initialized", }; await this.sendMessage(initializedNotification); return response; } async ping(): Promise<JsonRpcMessage> { const pingMessage: JsonRpcMessage = { jsonrpc: "2.0", method: "ping", id: this.getNextId(), }; return this.sendMessage(pingMessage); } async introspectSchema(): Promise<JsonRpcMessage> { const introspectMessage: JsonRpcMessage = { jsonrpc: "2.0", method: "tools/call", params: { name: "introspect-schema", arguments: {}, }, id: this.getNextId(), }; return this.sendMessage(introspectMessage); } async listTools(): Promise<JsonRpcMessage> { const listToolsMessage: JsonRpcMessage = { jsonrpc: "2.0", method: "tools/list", params: {}, id: this.getNextId(), }; return this.sendMessage(listToolsMessage); } async close() { this.serverProcess.kill(); } } // Main execution async function main() { console.log("🚀 Starting manual MCP client..."); const client = new ManualMcpClient(); try { // Wait a bit for the server to start await new Promise((resolve) => setTimeout(resolve, 1000)); console.log("\n📋 Step 1: Initialize connection"); const initResponse = await client.initialize(); console.log("✅ Initialization complete"); console.log("\n📋 Step 2: Ping server"); const pingResponse = await client.ping(); console.log("✅ Ping successful"); console.log("\n📋 Step 3: List available tools"); const toolsResponse = await client.listTools(); console.log("✅ Tools listed"); console.log("\n📋 Step 4: Call introspect-schema tool"); const schemaResponse = await client.introspectSchema(); console.log("✅ Schema introspection complete"); console.log("\n🎉 All operations completed successfully!"); } catch (error) { console.error("❌ Error:", error); } finally { console.log("\n🔚 Closing client..."); client.close(); } } main().catch(console.error); ``` -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- ```typescript #!/usr/bin/env node import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { parse } from "graphql/language"; import { z } from "zod"; import { checkDeprecatedArguments } from "./helpers/deprecation.js"; import { introspectEndpoint, introspectLocalSchema, introspectSchemaFromUrl, } from "./helpers/introspection.js"; import { getVersion } from "./helpers/package.js" with { type: "macro" }; // Check for deprecated command line arguments checkDeprecatedArguments(); const EnvSchema = z.object({ NAME: z.string().default("mcp-graphql"), ENDPOINT: z.string().url().default("http://localhost:4000/graphql"), ALLOW_MUTATIONS: z .enum(["true", "false"]) .transform((value) => value === "true") .default("false"), HEADERS: z .string() .default("{}") .transform((val) => { try { return JSON.parse(val); } catch (e) { throw new Error("HEADERS must be a valid JSON string"); } }), SCHEMA: z.string().optional(), }); const env = EnvSchema.parse(process.env); const server = new McpServer({ name: env.NAME, version: getVersion(), description: `GraphQL MCP server for ${env.ENDPOINT}`, }); server.resource("graphql-schema", new URL(env.ENDPOINT).href, async (uri) => { try { let schema: string; if (env.SCHEMA) { if ( env.SCHEMA.startsWith("http://") || env.SCHEMA.startsWith("https://") ) { schema = await introspectSchemaFromUrl(env.SCHEMA); } else { schema = await introspectLocalSchema(env.SCHEMA); } } else { schema = await introspectEndpoint(env.ENDPOINT, env.HEADERS); } return { contents: [ { uri: uri.href, text: schema, }, ], }; } catch (error) { throw new Error(`Failed to get GraphQL schema: ${error}`); } }); server.tool( "introspect-schema", "Introspect the GraphQL schema, use this tool before doing a query to get the schema information if you do not have it available as a resource already.", { // This is a workaround to help clients that can't handle an empty object as an argument // They will often send undefined instead of an empty object which is not allowed by the schema __ignore__: z .boolean() .default(false) .describe("This does not do anything"), }, async () => { try { let schema: string; if (env.SCHEMA) { schema = await introspectLocalSchema(env.SCHEMA); } else { schema = await introspectEndpoint(env.ENDPOINT, env.HEADERS); } return { content: [ { type: "text", text: schema, }, ], }; } catch (error) { return { isError: true, content: [ { type: "text", text: `Failed to introspect schema: ${error}`, }, ], }; } }, ); server.tool( "query-graphql", "Query a GraphQL endpoint with the given query and variables", { query: z.string(), variables: z.string().optional(), }, async ({ query, variables }) => { try { const parsedQuery = parse(query); // Check if the query is a mutation const isMutation = parsedQuery.definitions.some( (def) => def.kind === "OperationDefinition" && def.operation === "mutation", ); if (isMutation && !env.ALLOW_MUTATIONS) { return { isError: true, content: [ { type: "text", text: "Mutations are not allowed unless you enable them in the configuration. Please use a query operation instead.", }, ], }; } } catch (error) { return { isError: true, content: [ { type: "text", text: `Invalid GraphQL query: ${error}`, }, ], }; } try { const response = await fetch(env.ENDPOINT, { method: "POST", headers: { "Content-Type": "application/json", ...env.HEADERS, }, body: JSON.stringify({ query, variables, }), }); if (!response.ok) { const responseText = await response.text(); return { isError: true, content: [ { type: "text", text: `GraphQL request failed: ${response.statusText}\n${responseText}`, }, ], }; } const data = await response.json(); if (data.errors && data.errors.length > 0) { // Contains GraphQL errors return { isError: true, content: [ { type: "text", text: `The GraphQL response has errors, please fix the query: ${JSON.stringify( data, null, 2, )}`, }, ], }; } return { content: [ { type: "text", text: JSON.stringify(data, null, 2), }, ], }; } catch (error) { throw new Error(`Failed to execute GraphQL query: ${error}`); } }, ); async function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.error( `Started graphql mcp server ${env.NAME} for endpoint: ${env.ENDPOINT}`, ); } main().catch((error) => { console.error(`Fatal error in main(): ${error}`); process.exit(1); }); ``` -------------------------------------------------------------------------------- /dev/graphql.ts: -------------------------------------------------------------------------------- ```typescript import { makeExecutableSchema } from "@graphql-tools/schema"; import { createYoga } from "graphql-yoga"; import fs from "node:fs"; /** * Simple GraphQL server implementation for testing purposes * * This is a simple GraphQL server implementation for testing purposes. * It is not intended to be used in production. * * It is used to test the GraphQL schema and resolvers. * */ // Define types interface User { id: string; name: string; email: string; createdAt: string; updatedAt: string | null; } interface Post { id: string; title: string; content: string; published: boolean; authorId: string; createdAt: string; updatedAt: string | null; } interface Comment { id: string; text: string; postId: string; authorId: string; createdAt: string; } interface CreateUserInput { name: string; email: string; } interface UpdateUserInput { name?: string; email?: string; } interface CreatePostInput { title: string; content: string; published?: boolean; authorId: string; } interface AddCommentInput { text: string; postId: string; authorId: string; } // Define resolver context type type ResolverContext = Record<string, never>; // Read schema from file const typeDefs = fs.readFileSync("./schema-simple.graphql", "utf-8"); // Create mock data const users: User[] = [ { id: "1", name: "John Doe", email: "[email protected]", createdAt: new Date().toISOString(), updatedAt: null, }, { id: "2", name: "Jane Smith", email: "[email protected]", createdAt: new Date().toISOString(), updatedAt: null, }, { id: "3", name: "Bob Johnson", email: "[email protected]", createdAt: new Date().toISOString(), updatedAt: null, }, ]; const posts: Post[] = [ { id: "1", title: "First Post", content: "This is my first post", published: true, authorId: "1", createdAt: new Date().toISOString(), updatedAt: null, }, { id: "2", title: "GraphQL is Awesome", content: "Here is why GraphQL is better than REST", published: true, authorId: "1", createdAt: new Date().toISOString(), updatedAt: null, }, { id: "3", title: "Yoga Tutorial", content: "Learn how to use GraphQL Yoga", published: false, authorId: "2", createdAt: new Date().toISOString(), updatedAt: null, }, ]; const comments: Comment[] = [ { id: "1", text: "Great post!", postId: "1", authorId: "2", createdAt: new Date().toISOString(), }, { id: "2", text: "I learned a lot", postId: "1", authorId: "3", createdAt: new Date().toISOString(), }, { id: "3", text: "Looking forward to more content", postId: "2", authorId: "2", createdAt: new Date().toISOString(), }, ]; // Define resolvers const resolvers = { Query: { user: ( _parent: unknown, { id }: { id: string }, _context: ResolverContext, ) => users.find((user) => user.id === id), users: () => users, post: ( _parent: unknown, { id }: { id: string }, _context: ResolverContext, ) => posts.find((post) => post.id === id), posts: () => posts, commentsByPost: ( _parent: unknown, { postId }: { postId: string }, _context: ResolverContext, ) => comments.filter((comment) => comment.postId === postId), }, Mutation: { createUser: ( _parent: unknown, { input }: { input: CreateUserInput }, _context: ResolverContext, ) => { const newUser: User = { id: String(users.length + 1), name: input.name, email: input.email, createdAt: new Date().toISOString(), updatedAt: null, }; users.push(newUser); return newUser; }, updateUser: ( _parent: unknown, { id, input }: { id: string; input: UpdateUserInput }, _context: ResolverContext, ) => { const userIndex = users.findIndex((user) => user.id === id); if (userIndex === -1) throw new Error(`User with ID ${id} not found`); users[userIndex] = { ...users[userIndex], ...input, updatedAt: new Date().toISOString(), }; return users[userIndex]; }, deleteUser: ( _parent: unknown, { id }: { id: string }, _context: ResolverContext, ) => { const userIndex = users.findIndex((user) => user.id === id); if (userIndex === -1) return false; users.splice(userIndex, 1); return true; }, createPost: ( _parent: unknown, { input }: { input: CreatePostInput }, _context: ResolverContext, ) => { const newPost: Post = { id: String(posts.length + 1), title: input.title, content: input.content, published: input.published ?? false, authorId: input.authorId, createdAt: new Date().toISOString(), updatedAt: null, }; posts.push(newPost); return newPost; }, addComment: ( _parent: unknown, { input }: { input: AddCommentInput }, _context: ResolverContext, ) => { const newComment: Comment = { id: String(comments.length + 1), text: input.text, postId: input.postId, authorId: input.authorId, createdAt: new Date().toISOString(), }; comments.push(newComment); return newComment; }, }, User: { posts: (parent: User) => posts.filter((post) => post.authorId === parent.id), comments: (parent: User) => comments.filter((comment) => comment.authorId === parent.id), }, Post: { author: (parent: Post) => users.find((user) => user.id === parent.authorId), comments: (parent: Post) => comments.filter((comment) => comment.postId === parent.id), }, Comment: { post: (parent: Comment) => posts.find((post) => post.id === parent.postId), author: (parent: Comment) => users.find((user) => user.id === parent.authorId), }, }; // Create executable schema const schema = makeExecutableSchema({ typeDefs, resolvers, }); // Create Yoga instance const yoga = createYoga({ schema }); // Start server with proper request handler const server = Bun.serve({ port: 4000, fetch: (request) => { // Add dev logger for incoming requests console.log( `[${new Date().toISOString()}] Incoming request: ${request.method} ${ request.url }`, ); return yoga.fetch(request); }, }); console.info( `GraphQL server is running on ${new URL( yoga.graphqlEndpoint, `http://${server.hostname}:${server.port}`, )}`, ); ```