#
tokens: 13376/50000 22/22 files
lines: on (toggle) GitHub
raw markdown copy reset
# 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:
--------------------------------------------------------------------------------

```
1 | package-lock=false
2 | 
```

--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------

```
1 | *.ts linguist-language=TypeScript
2 | 
```

--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------

```
 1 | node_modules
 2 | Dockerfile*
 3 | docker-compose*
 4 | .dockerignore
 5 | .git
 6 | .gitignore
 7 | README.md
 8 | LICENSE
 9 | .vscode
10 | Makefile
11 | helm-charts
12 | .env
13 | .editorconfig
14 | .idea
15 | coverage*
16 | dist
17 | 
```

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
  1 | # Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
  2 | 
  3 | # Logs
  4 | 
  5 | logs
  6 | _.log
  7 | npm-debug.log_
  8 | yarn-debug.log*
  9 | yarn-error.log*
 10 | lerna-debug.log*
 11 | .pnpm-debug.log*
 12 | 
 13 | # Caches
 14 | 
 15 | .cache
 16 | 
 17 | # Diagnostic reports (https://nodejs.org/api/report.html)
 18 | 
 19 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
 20 | 
 21 | # Runtime data
 22 | 
 23 | pids
 24 | _.pid
 25 | _.seed
 26 | *.pid.lock
 27 | 
 28 | # Directory for instrumented libs generated by jscoverage/JSCover
 29 | 
 30 | lib-cov
 31 | 
 32 | # Coverage directory used by tools like istanbul
 33 | 
 34 | coverage
 35 | *.lcov
 36 | 
 37 | # nyc test coverage
 38 | 
 39 | .nyc_output
 40 | 
 41 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
 42 | 
 43 | .grunt
 44 | 
 45 | # Bower dependency directory (https://bower.io/)
 46 | 
 47 | bower_components
 48 | 
 49 | # node-waf configuration
 50 | 
 51 | .lock-wscript
 52 | 
 53 | # Compiled binary addons (https://nodejs.org/api/addons.html)
 54 | 
 55 | build/Release
 56 | 
 57 | # Dependency directories
 58 | 
 59 | node_modules/
 60 | jspm_packages/
 61 | 
 62 | # Snowpack dependency directory (https://snowpack.dev/)
 63 | 
 64 | web_modules/
 65 | 
 66 | # TypeScript cache
 67 | 
 68 | *.tsbuildinfo
 69 | 
 70 | # Optional npm cache directory
 71 | 
 72 | .npm
 73 | 
 74 | # Optional eslint cache
 75 | 
 76 | .eslintcache
 77 | 
 78 | # Optional stylelint cache
 79 | 
 80 | .stylelintcache
 81 | 
 82 | # Microbundle cache
 83 | 
 84 | .rpt2_cache/
 85 | .rts2_cache_cjs/
 86 | .rts2_cache_es/
 87 | .rts2_cache_umd/
 88 | 
 89 | # Optional REPL history
 90 | 
 91 | .node_repl_history
 92 | 
 93 | # Output of 'npm pack'
 94 | 
 95 | *.tgz
 96 | 
 97 | # Yarn Integrity file
 98 | 
 99 | .yarn-integrity
100 | 
101 | # dotenv environment variable files
102 | 
103 | .env
104 | .env.development.local
105 | .env.test.local
106 | .env.production.local
107 | .env.local
108 | 
109 | # parcel-bundler cache (https://parceljs.org/)
110 | 
111 | .parcel-cache
112 | 
113 | # Next.js build output
114 | 
115 | .next
116 | out
117 | 
118 | # Nuxt.js build / generate output
119 | 
120 | .nuxt
121 | dist
122 | 
123 | # Gatsby files
124 | 
125 | # Comment in the public line in if your project uses Gatsby and not Next.js
126 | 
127 | # https://nextjs.org/blog/next-9-1#public-directory-support
128 | 
129 | # public
130 | 
131 | # vuepress build output
132 | 
133 | .vuepress/dist
134 | 
135 | # vuepress v2.x temp and cache directory
136 | 
137 | .temp
138 | 
139 | # Docusaurus cache and generated files
140 | 
141 | .docusaurus
142 | 
143 | # Serverless directories
144 | 
145 | .serverless/
146 | 
147 | # FuseBox cache
148 | 
149 | .fusebox/
150 | 
151 | # DynamoDB Local files
152 | 
153 | .dynamodb/
154 | 
155 | # TernJS port file
156 | 
157 | .tern-port
158 | 
159 | # Stores VSCode versions used for testing VSCode extensions
160 | 
161 | .vscode-test
162 | 
163 | # yarn v2
164 | 
165 | .yarn/cache
166 | .yarn/unplugged
167 | .yarn/build-state.yml
168 | .yarn/install-state.gz
169 | .pnp.*
170 | 
171 | # IntelliJ based IDEs
172 | .idea
173 | 
174 | # Finder (MacOS) folder config
175 | .DS_Store
176 | 
177 | dist/
178 | 
179 | # GraphQL schema for debugging
180 | /schema.graphql
181 | 
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
 1 | # mcp-graphql
 2 | 
 3 | [![smithery badge](https://smithery.ai/badge/mcp-graphql)](https://smithery.ai/server/mcp-graphql)
 4 | 
 5 | 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.
 6 | 
 7 | <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>
 8 | 
 9 | ## Usage
10 | 
11 | Run `mcp-graphql` with the correct endpoint, it will automatically try to introspect your queries.
12 | 
13 | ### Environment Variables (Breaking change in 1.0.0)
14 | 
15 | > **Note:** As of version 1.0.0, command line arguments have been replaced with environment variables.
16 | 
17 | | Environment Variable | Description | Default |
18 | |----------|-------------|---------|
19 | | `ENDPOINT` | GraphQL endpoint URL | `http://localhost:4000/graphql` |
20 | | `HEADERS` | JSON string containing headers for requests | `{}` |
21 | | `ALLOW_MUTATIONS` | Enable mutation operations (disabled by default) | `false` |
22 | | `NAME` | Name of the MCP server | `mcp-graphql` |
23 | | `SCHEMA` | Path to a local GraphQL schema file or URL (optional) | - |
24 | 
25 | ### Examples
26 | 
27 | ```bash
28 | # Basic usage with a local GraphQL server
29 | ENDPOINT=http://localhost:3000/graphql npx mcp-graphql
30 | 
31 | # Using with custom headers
32 | ENDPOINT=https://api.example.com/graphql HEADERS='{"Authorization":"Bearer token123"}' npx mcp-graphql
33 | 
34 | # Enable mutation operations
35 | ENDPOINT=http://localhost:3000/graphql ALLOW_MUTATIONS=true npx mcp-graphql
36 | 
37 | # Using a local schema file instead of introspection
38 | ENDPOINT=http://localhost:3000/graphql SCHEMA=./schema.graphql npx mcp-graphql
39 | 
40 | # Using a schema file hosted at a URL
41 | ENDPOINT=http://localhost:3000/graphql SCHEMA=https://example.com/schema.graphql npx mcp-graphql
42 | ```
43 | 
44 | ## Resources
45 | 
46 | - **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.
47 | 
48 | ## Available Tools
49 | 
50 | The server provides two main tools:
51 | 
52 | 1. **introspect-schema**: This tool retrieves the GraphQL schema. Use this first if you don't have access to the schema as a resource.
53 | This uses either the local schema file, a schema file hosted at a URL, or an introspection query.
54 | 
55 | 2. **query-graphql**: Execute GraphQL queries against the endpoint. By default, mutations are disabled unless `ALLOW_MUTATIONS` is set to `true`.
56 | 
57 | ## Installation
58 | 
59 | ### Installing via Smithery
60 | 
61 | To install GraphQL MCP Server for Claude Desktop automatically via [Smithery](https://smithery.ai/server/mcp-graphql):
62 | 
63 | ```bash
64 | npx -y @smithery/cli install mcp-graphql --client claude
65 | ```
66 | 
67 | ### Installing Manually
68 | 
69 | It can be manually installed to Claude:
70 | ```json
71 | {
72 |     "mcpServers": {
73 |         "mcp-graphql": {
74 |             "command": "npx",
75 |             "args": ["mcp-graphql"],
76 |             "env": {
77 |                 "ENDPOINT": "http://localhost:3000/graphql"
78 |             }
79 |         }
80 |     }
81 | }
82 | ```
83 | 
84 | ## Security Considerations
85 | 
86 | 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.
87 | 
88 | ## Customize for your own server
89 | 
90 | 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.
91 | 
```

--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------

```markdown
1 | Yes please! Add issues or pull requests for whatever is missing/should be improved :)
2 | 
```

--------------------------------------------------------------------------------
/biome.json:
--------------------------------------------------------------------------------

```json
1 | {
2 | 	"formatter": {
3 | 		"enabled": true,
4 | 		"indentStyle": "tab",
5 | 		"indentWidth": 2
6 | 	}
7 | }
8 | 
```

--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------

```json
1 | {
2 | 	"editor.defaultFormatter": "biomejs.biome",
3 | 	"[typescript]": {
4 | 		"editor.defaultFormatter": "biomejs.biome"
5 | 	}
6 | }
7 | 
```

--------------------------------------------------------------------------------
/src/helpers/package.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { readFileSync } from "node:fs";
 2 | import { dirname, join } from "node:path";
 3 | import { fileURLToPath } from "node:url";
 4 | 
 5 | const __filename = fileURLToPath(import.meta.url);
 6 | const __dirname = dirname(__filename);
 7 | 
 8 | // Current package version so I only need to update it in one place
 9 | const { version } = JSON.parse(
10 | 	readFileSync(join(__dirname, "../../package.json"), "utf-8"),
11 | );
12 | 
13 | export function getVersion() {
14 | 	return version;
15 | }
16 | 
```

--------------------------------------------------------------------------------
/dev/debug-client.ts:
--------------------------------------------------------------------------------

```typescript
 1 | // Small debug client to test a few specific interactions
 2 | 
 3 | import { Client } from "@modelcontextprotocol/sdk/client/index.js";
 4 | import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
 5 | 
 6 | const transport = new StdioClientTransport({
 7 | 	command: "node",
 8 | 	args: ["dist/index.js"],
 9 | });
10 | 
11 | const client = new Client({
12 | 	name: "debug-client",
13 | 	version: "1.0.0",
14 | });
15 | 
16 | await client.connect(transport);
17 | 
18 | // Call introspect-schema with undefined argument
19 | const result = await client.callTool({
20 | 	name: "introspect-schema",
21 | 	arguments: {},
22 | });
23 | 
24 | console.log(result);
25 | 
```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 | 	"compilerOptions": {
 3 | 		// Enable latest features
 4 | 		"lib": ["ESNext", "DOM"],
 5 | 		"target": "ESNext",
 6 | 		"module": "ESNext",
 7 | 		"moduleDetection": "force",
 8 | 		"jsx": "react-jsx",
 9 | 		"allowJs": true,
10 | 
11 | 		// Bundler mode
12 | 		"moduleResolution": "bundler",
13 | 		"allowImportingTsExtensions": true,
14 | 		"verbatimModuleSyntax": true,
15 | 		"noEmit": true,
16 | 
17 | 		// Best practices
18 | 		"strict": true,
19 | 		"skipLibCheck": true,
20 | 		"noFallthroughCasesInSwitch": true,
21 | 
22 | 		// Some stricter flags (disabled by default)
23 | 		"noUnusedLocals": false,
24 | 		"noUnusedParameters": false,
25 | 		"noPropertyAccessFromIndexSignature": false
26 | 	}
27 | }
28 | 
```

--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------

```yaml
 1 | # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml
 2 | 
 3 | startCommand:
 4 |   type: stdio
 5 |   configSchema:
 6 |     # JSON Schema defining the configuration options for the MCP.
 7 |     type: object
 8 |     required:
 9 |       - endpoint
10 |     properties:
11 |       endpoint:
12 |         type: string
13 |         description: The GraphQL server endpoint URL.
14 |       headers:
15 |         type: string
16 |         description: Optional JSON string of headers to send with the GraphQL requests.
17 |   commandFunction:
18 |     # A function that produces the CLI command to start the MCP on stdio.
19 |     |-
20 |     (config) => ({ command: 'node', args: ['/app/dist/index.js', '--endpoint', config.endpoint].concat(config.headers ? ['--headers', config.headers] : []), env: {} })
```

--------------------------------------------------------------------------------
/src/helpers/headers.ts:
--------------------------------------------------------------------------------

```typescript
 1 | /**
 2 |  * Parse and merge headers from various sources
 3 |  * @param configHeaders - Default headers from configuration
 4 |  * @param inputHeaders - Headers provided by the user (string or object)
 5 |  * @returns Merged headers object
 6 |  */
 7 | export function parseAndMergeHeaders(
 8 | 	configHeaders: Record<string, string>,
 9 | 	inputHeaders?: string | Record<string, string>,
10 | ): Record<string, string> {
11 | 	// Parse headers if they're provided as a string
12 | 	let parsedHeaders: Record<string, string> = {};
13 | 
14 | 	if (typeof inputHeaders === "string") {
15 | 		try {
16 | 			parsedHeaders = JSON.parse(inputHeaders);
17 | 		} catch (e) {
18 | 			throw new Error(`Invalid headers JSON: ${e}`);
19 | 		}
20 | 	} else if (inputHeaders) {
21 | 		parsedHeaders = inputHeaders;
22 | 	}
23 | 
24 | 	// Merge with config headers (config headers are overridden by input headers)
25 | 	return { ...configHeaders, ...parsedHeaders };
26 | }
27 | 
```

--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
 1 | # Bun based Dockerfile
 2 | # Does not build the server, but runs it directly from source using bun
 3 | 
 4 | FROM oven/bun:1 AS base
 5 | WORKDIR /usr/src/app
 6 | 
 7 | # Cached dependency install layer
 8 | FROM base AS install
 9 | RUN mkdir -p /temp/dev
10 | COPY package.json bun.lock /temp/dev/
11 | RUN cd /temp/dev && bun install --frozen-lockfile
12 | 
13 | # exclude devDependencies
14 | RUN mkdir -p /temp/prod
15 | COPY package.json bun.lock /temp/prod/
16 | RUN cd /temp/prod && bun install --frozen-lockfile --production
17 | 
18 | FROM base AS prerelease
19 | COPY --from=install /temp/dev/node_modules node_modules
20 | COPY . .
21 | 
22 | # copy production dependencies and source code into final image
23 | FROM base AS release
24 | COPY --from=install /temp/prod/node_modules node_modules
25 | COPY --from=prerelease /usr/src/app/src/ ./src/
26 | COPY --from=prerelease /usr/src/app/package.json .
27 | 
28 | # run the app
29 | USER bun
30 | ENTRYPOINT [ "bun", "run", "src/index.ts" ]
31 | 
```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 | 	"name": "mcp-graphql",
 3 | 	"module": "index.ts",
 4 | 	"type": "module",
 5 | 	"version": "2.0.4",
 6 | 	"repository": "github:blurrah/mcp-graphql",
 7 | 	"license": "MIT",
 8 | 	"bin": {
 9 | 		"mcp-graphql": "./dist/index.js"
10 | 	},
11 | 	"files": [
12 | 		"dist"
13 | 	],
14 | 	"devDependencies": {
15 | 		"@graphql-tools/schema": "^10.0.23",
16 | 		"@types/bun": "^1.2.14",
17 | 		"@types/yargs": "17.0.33",
18 | 		"biome": "^0.3.3",
19 | 		"graphql-yoga": "^5.13.5",
20 | 		"typescript": "5.8.3"
21 | 	},
22 | 	"dependencies": {
23 | 		"@modelcontextprotocol/sdk": "1.12.0",
24 | 		"graphql": "^16.11.0",
25 | 		"yargs": "17.7.2",
26 | 		"zod": "3.25.30",
27 | 		"zod-to-json-schema": "3.24.5"
28 | 	},
29 | 	"scripts": {
30 | 		"dev": "bun --watch src/index.ts",
31 | 		"build": "bun build src/index.ts --outdir dist --target node && bun -e \"require('fs').chmodSync('dist/index.js', '755')\"",
32 | 		"start": "bun run dist/index.js",
33 | 		"format": "biome format --write .",
34 | 		"check": "biome format ."
35 | 	},
36 | 	"packageManager": "[email protected]"
37 | }
38 | 
```

--------------------------------------------------------------------------------
/src/helpers/deprecation.ts:
--------------------------------------------------------------------------------

```typescript
 1 | /**
 2 |  * Helper module for handling deprecation warnings
 3 |  */
 4 | 
 5 | /**
 6 |  * Check for deprecated command line arguments and output warnings
 7 |  */
 8 | export function checkDeprecatedArguments(): void {
 9 | 	const deprecatedArgs = [
10 | 		"--endpoint",
11 | 		"--headers",
12 | 		"--enable-mutations",
13 | 		"--name",
14 | 		"--schema",
15 | 	];
16 | 	const usedDeprecatedArgs = deprecatedArgs.filter((arg) =>
17 | 		process.argv.includes(arg),
18 | 	);
19 | 
20 | 	if (usedDeprecatedArgs.length > 0) {
21 | 		console.error(
22 | 			`WARNING: Deprecated command line arguments detected: ${usedDeprecatedArgs.join(", ")}`,
23 | 		);
24 | 		console.error(
25 | 			"As of version 1.0.0, command line arguments have been replaced with environment variables.",
26 | 		);
27 | 		console.error("Please use environment variables instead. For example:");
28 | 		console.error(
29 | 			"  Instead of: npx mcp-graphql --endpoint http://example.com/graphql",
30 | 		);
31 | 		console.error("  Use: ENDPOINT=http://example.com/graphql npx mcp-graphql");
32 | 		console.error("");
33 | 	}
34 | }
35 | 
```

--------------------------------------------------------------------------------
/.github/workflows/docker.yml:
--------------------------------------------------------------------------------

```yaml
 1 | name: docker
 2 | 
 3 | on:
 4 |   push:
 5 |     branches: ['main']
 6 | 
 7 | env:
 8 |   REGISTRY: ghcr.io
 9 |   IMAGE_NAME: ${{ github.repository }}
10 | 
11 | jobs:
12 |   build-and-push-image:
13 |     runs-on: ubuntu-latest
14 |     permissions:
15 |       contents: read
16 |       packages: write
17 |       attestations: write
18 |       id-token: write
19 | 
20 |     steps:
21 |       - name: Checkout repository
22 |         uses: actions/checkout@v4
23 | 
24 |       - name: Log in to the Container registry
25 |         uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1
26 |         with:
27 |           registry: ${{ env.REGISTRY }}
28 |           username: ${{ github.actor }}
29 |           password: ${{ secrets.GITHUB_TOKEN }}
30 | 
31 |       - name: Extract metadata (tags, labels) for Docker
32 |         id: meta
33 |         uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
34 |         with:
35 |           images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
36 | 
37 |       - name: Build and push Docker image
38 |         id: push
39 |         uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4
40 |         with:
41 |           context: .
42 |           push: true
43 |           tags: ${{ steps.meta.outputs.tags }}
44 |           labels: ${{ steps.meta.outputs.labels }}
45 | 
46 | 
47 |       - name: Generate artifact attestation
48 |         uses: actions/attest-build-provenance@v2
49 |         with:
50 |           subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}
51 |           subject-digest: ${{ steps.push.outputs.digest }}
52 |           push-to-registry: true
53 | 
54 | 
```

--------------------------------------------------------------------------------
/src/helpers/introspection.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { buildClientSchema, getIntrospectionQuery, printSchema } from "graphql";
 2 | import { readFile } from "node:fs/promises";
 3 | 
 4 | /**
 5 |  * Introspect a GraphQL endpoint and return the schema as the GraphQL SDL
 6 |  * @param endpoint - The endpoint to introspect
 7 |  * @param headers - Optional headers to include in the request
 8 |  * @returns The schema
 9 |  */
10 | export async function introspectEndpoint(
11 | 	endpoint: string,
12 | 	headers?: Record<string, string>,
13 | ) {
14 | 	const response = await fetch(endpoint, {
15 | 		method: "POST",
16 | 		headers: {
17 | 			"Content-Type": "application/json",
18 | 			...headers,
19 | 		},
20 | 		body: JSON.stringify({
21 | 			query: getIntrospectionQuery(),
22 | 		}),
23 | 	});
24 | 
25 | 	if (!response.ok) {
26 | 		throw new Error(`GraphQL request failed: ${response.statusText}`);
27 | 	}
28 | 
29 | 	const responseJson = await response.json();
30 | 	// Transform to a schema object
31 | 	const schema = buildClientSchema(responseJson.data);
32 | 
33 | 	// Print the schema SDL
34 | 	return printSchema(schema);
35 | }
36 | 
37 | /**
38 |  * Introspect a GraphQL schema file hosted at a URL and return the schema as the GraphQL SDL
39 |  * @param url - The URL to the schema file
40 |  * @returns The schema
41 |  */
42 | export async function introspectSchemaFromUrl(url: string) {
43 | 	const response = await fetch(url);
44 | 
45 | 	if (!response.ok) {
46 | 		throw new Error(`Failed to fetch schema from URL: ${response.statusText}`);
47 | 	}
48 | 
49 | 	const schema = await response.text();
50 | 	return schema;
51 | }
52 | 
53 | /**
54 |  * Introspect a local GraphQL schema file and return the schema as the GraphQL SDL
55 |  * @param path - The path to the local schema file
56 |  * @returns The schema
57 |  */
58 | export async function introspectLocalSchema(path: string) {
59 | 	const schema = await readFile(path, "utf8");
60 | 	return schema;
61 | }
62 | 
```

--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------

```yaml
 1 | name: ci
 2 | 
 3 | on:
 4 |   push:
 5 |     branches:
 6 |       - main
 7 |     tags:
 8 |       - v*
 9 |   pull_request:
10 |     types: [opened, synchronize]
11 |   release:
12 |     types: [published]
13 |   workflow_dispatch:
14 |     inputs:
15 |       version:
16 |         description: 'Version to release'
17 |         required: true
18 | 
19 | jobs:
20 |   build:
21 |     runs-on: ubuntu-latest
22 |     if: github.event_name != 'release'
23 |     steps:
24 |       - uses: actions/checkout@v4
25 |         with:
26 |           fetch-depth: 0
27 |       - uses: oven-sh/setup-bun@v2
28 |         with:
29 |           bun-version: latest
30 |       - run: bun install
31 |       - run: bun run format
32 |       - run: bun run check
33 |       - run: bun run build
34 |       - name: Get tag annotation
35 |         id: get-tag-annotation
36 |         run: |
37 |           TAG_NAME=${GITHUB_REF#refs/tags/}
38 |           echo "Processing tag: $TAG_NAME"
39 |           # Check if tag exists and is annotated
40 |           if TAG_MESSAGE=$(git tag -l --format='%(contents)' $TAG_NAME 2>/dev/null); then
41 |             echo "Found annotated tag message"
42 |           else
43 |             echo "No tag annotation found, using empty message"
44 |             TAG_MESSAGE=""
45 |           fi
46 |           # Use multiline output syntax for GitHub Actions
47 |           echo "TAG_MESSAGE<<EOF" >> $GITHUB_OUTPUT
48 |           echo "$TAG_MESSAGE" >> $GITHUB_OUTPUT
49 |           echo "EOF" >> $GITHUB_OUTPUT
50 | 
51 |       - name: Create GitHub Release
52 |         uses: softprops/action-gh-release@v2
53 |         if: startsWith(github.ref, 'refs/tags/')
54 |         with:
55 |           files: dist/*
56 |           generate_release_notes: true
57 |           body: ${{ steps.get-tag-annotation.outputs.TAG_MESSAGE }}
58 |           append_body: true
59 |           # Use PAT as it sends a release event, built in token doesn't
60 |           token: ${{ secrets.RELEASE_TOKEN }}
61 | 
62 |   publish:
63 |     runs-on: ubuntu-latest
64 |     if: github.event_name == 'release' || github.event_name == 'workflow_dispatch' && inputs.version != ''
65 |     permissions:
66 |       contents: read
67 |       id-token: write
68 |     steps:
69 |       - uses: actions/checkout@v4
70 |       # Falling back to node since I want provenance for the npm package
71 |       - uses: actions/setup-node@v4
72 |         with:
73 |           node-version: 22
74 |           registry-url: 'https://registry.npmjs.org'
75 |       # Might be a useless optimization but I felt like not reusing the build job
76 |       - name: Download index.js from release
77 |         run: |
78 |           gh release download ${{ github.event.release.tag_name || inputs.version }} --dir dist --pattern "index.js"
79 |         env:
80 |           GH_TOKEN: ${{ github.token }}
81 |       - run: npm publish --provenance --access public
82 |         env:
83 |           NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
84 | 
```

--------------------------------------------------------------------------------
/dev/debug-manual-client.ts:
--------------------------------------------------------------------------------

```typescript
  1 | // Manual MCP client using stdio directly (no SDK)
  2 | // This demonstrates the raw JSON-RPC protocol communication
  3 | 
  4 | import { type ChildProcess, spawn } from "node:child_process";
  5 | import { createInterface } from "node:readline";
  6 | 
  7 | interface JsonRpcMessage {
  8 | 	jsonrpc: "2.0";
  9 | 	id?: string | number;
 10 | 	method?: string;
 11 | 	params?: unknown;
 12 | 	result?: unknown;
 13 | 	error?: {
 14 | 		code: number;
 15 | 		message: string;
 16 | 		data?: unknown;
 17 | 	};
 18 | }
 19 | 
 20 | class ManualMcpClient {
 21 | 	private serverProcess: ChildProcess;
 22 | 	private messageId = 1;
 23 | 	private pendingRequests = new Map<
 24 | 		string | number,
 25 | 		(response: JsonRpcMessage) => void
 26 | 	>();
 27 | 
 28 | 	constructor() {
 29 | 		// Start the MCP server process
 30 | 		this.serverProcess = spawn("node", ["dist/index.js"], {
 31 | 			stdio: ["pipe", "pipe", "pipe"],
 32 | 		});
 33 | 
 34 | 		// Set up readline to read server responses line by line
 35 | 		if (this.serverProcess.stdout) {
 36 | 			const rl = createInterface({
 37 | 				input: this.serverProcess.stdout,
 38 | 			});
 39 | 
 40 | 			rl.on("line", (line) => {
 41 | 				try {
 42 | 					const message: JsonRpcMessage = JSON.parse(line);
 43 | 					this.handleServerMessage(message);
 44 | 				} catch (error) {
 45 | 					console.error("Failed to parse server message:", line, error);
 46 | 				}
 47 | 			});
 48 | 		}
 49 | 
 50 | 		// Handle server errors
 51 | 		this.serverProcess.stderr?.on("data", (data: Buffer) => {
 52 | 			console.error("Server stderr:", data.toString());
 53 | 		});
 54 | 
 55 | 		this.serverProcess.on("exit", (code: number | null) => {
 56 | 			console.log(`Server process exited with code ${code}`);
 57 | 		});
 58 | 	}
 59 | 
 60 | 	private handleServerMessage(message: JsonRpcMessage) {
 61 | 		console.log("← Received from server:", JSON.stringify(message, null, 2));
 62 | 
 63 | 		// Handle responses to our requests
 64 | 		if (message.id !== undefined && this.pendingRequests.has(message.id)) {
 65 | 			const resolver = this.pendingRequests.get(message.id);
 66 | 			if (resolver) {
 67 | 				this.pendingRequests.delete(message.id);
 68 | 				resolver(message);
 69 | 			}
 70 | 		}
 71 | 	}
 72 | 
 73 | 	private sendMessage(message: JsonRpcMessage): Promise<JsonRpcMessage> {
 74 | 		const messageStr = JSON.stringify(message);
 75 | 		console.log("→ Sending to server:", messageStr);
 76 | 
 77 | 		this.serverProcess.stdin?.write(`${messageStr}\n`);
 78 | 
 79 | 		// If this is a request (has an id), wait for response
 80 | 		if (message.id !== undefined) {
 81 | 			return new Promise((resolve) => {
 82 | 				if (message.id !== undefined) {
 83 | 					this.pendingRequests.set(message.id, resolve);
 84 | 				}
 85 | 			});
 86 | 		}
 87 | 
 88 | 		return Promise.resolve(message);
 89 | 	}
 90 | 
 91 | 	private getNextId(): number {
 92 | 		return this.messageId++;
 93 | 	}
 94 | 
 95 | 	async initialize(): Promise<JsonRpcMessage> {
 96 | 		const initMessage: JsonRpcMessage = {
 97 | 			jsonrpc: "2.0",
 98 | 			method: "initialize",
 99 | 			params: {
100 | 				protocolVersion: "2025-03-26",
101 | 				capabilities: {},
102 | 				clientInfo: {
103 | 					name: "manual-debug-client",
104 | 					version: "1.0.0",
105 | 				},
106 | 			},
107 | 			id: this.getNextId(),
108 | 		};
109 | 
110 | 		const response = await this.sendMessage(initMessage);
111 | 
112 | 		// Send initialized notification
113 | 		const initializedNotification: JsonRpcMessage = {
114 | 			jsonrpc: "2.0",
115 | 			method: "notifications/initialized",
116 | 		};
117 | 
118 | 		await this.sendMessage(initializedNotification);
119 | 
120 | 		return response;
121 | 	}
122 | 
123 | 	async ping(): Promise<JsonRpcMessage> {
124 | 		const pingMessage: JsonRpcMessage = {
125 | 			jsonrpc: "2.0",
126 | 			method: "ping",
127 | 			id: this.getNextId(),
128 | 		};
129 | 
130 | 		return this.sendMessage(pingMessage);
131 | 	}
132 | 
133 | 	async introspectSchema(): Promise<JsonRpcMessage> {
134 | 		const introspectMessage: JsonRpcMessage = {
135 | 			jsonrpc: "2.0",
136 | 			method: "tools/call",
137 | 			params: {
138 | 				name: "introspect-schema",
139 | 				arguments: {},
140 | 			},
141 | 			id: this.getNextId(),
142 | 		};
143 | 
144 | 		return this.sendMessage(introspectMessage);
145 | 	}
146 | 
147 | 	async listTools(): Promise<JsonRpcMessage> {
148 | 		const listToolsMessage: JsonRpcMessage = {
149 | 			jsonrpc: "2.0",
150 | 			method: "tools/list",
151 | 			params: {},
152 | 			id: this.getNextId(),
153 | 		};
154 | 
155 | 		return this.sendMessage(listToolsMessage);
156 | 	}
157 | 
158 | 	async close() {
159 | 		this.serverProcess.kill();
160 | 	}
161 | }
162 | 
163 | // Main execution
164 | async function main() {
165 | 	console.log("🚀 Starting manual MCP client...");
166 | 
167 | 	const client = new ManualMcpClient();
168 | 
169 | 	try {
170 | 		// Wait a bit for the server to start
171 | 		await new Promise((resolve) => setTimeout(resolve, 1000));
172 | 
173 | 		console.log("\n📋 Step 1: Initialize connection");
174 | 		const initResponse = await client.initialize();
175 | 		console.log("✅ Initialization complete");
176 | 
177 | 		console.log("\n📋 Step 2: Ping server");
178 | 		const pingResponse = await client.ping();
179 | 		console.log("✅ Ping successful");
180 | 
181 | 		console.log("\n📋 Step 3: List available tools");
182 | 		const toolsResponse = await client.listTools();
183 | 		console.log("✅ Tools listed");
184 | 
185 | 		console.log("\n📋 Step 4: Call introspect-schema tool");
186 | 		const schemaResponse = await client.introspectSchema();
187 | 		console.log("✅ Schema introspection complete");
188 | 
189 | 		console.log("\n🎉 All operations completed successfully!");
190 | 	} catch (error) {
191 | 		console.error("❌ Error:", error);
192 | 	} finally {
193 | 		console.log("\n🔚 Closing client...");
194 | 		client.close();
195 | 	}
196 | }
197 | 
198 | main().catch(console.error);
199 | 
```

--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------

```typescript
  1 | #!/usr/bin/env node
  2 | 
  3 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
  4 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
  5 | import { parse } from "graphql/language";
  6 | import { z } from "zod";
  7 | import { checkDeprecatedArguments } from "./helpers/deprecation.js";
  8 | import {
  9 | 	introspectEndpoint,
 10 | 	introspectLocalSchema,
 11 | 	introspectSchemaFromUrl,
 12 | } from "./helpers/introspection.js";
 13 | import { getVersion } from "./helpers/package.js" with { type: "macro" };
 14 | 
 15 | // Check for deprecated command line arguments
 16 | checkDeprecatedArguments();
 17 | 
 18 | const EnvSchema = z.object({
 19 | 	NAME: z.string().default("mcp-graphql"),
 20 | 	ENDPOINT: z.string().url().default("http://localhost:4000/graphql"),
 21 | 	ALLOW_MUTATIONS: z
 22 | 		.enum(["true", "false"])
 23 | 		.transform((value) => value === "true")
 24 | 		.default("false"),
 25 | 	HEADERS: z
 26 | 		.string()
 27 | 		.default("{}")
 28 | 		.transform((val) => {
 29 | 			try {
 30 | 				return JSON.parse(val);
 31 | 			} catch (e) {
 32 | 				throw new Error("HEADERS must be a valid JSON string");
 33 | 			}
 34 | 		}),
 35 | 	SCHEMA: z.string().optional(),
 36 | });
 37 | 
 38 | const env = EnvSchema.parse(process.env);
 39 | 
 40 | const server = new McpServer({
 41 | 	name: env.NAME,
 42 | 	version: getVersion(),
 43 | 	description: `GraphQL MCP server for ${env.ENDPOINT}`,
 44 | });
 45 | 
 46 | server.resource("graphql-schema", new URL(env.ENDPOINT).href, async (uri) => {
 47 | 	try {
 48 | 		let schema: string;
 49 | 		if (env.SCHEMA) {
 50 | 			if (
 51 | 				env.SCHEMA.startsWith("http://") ||
 52 | 				env.SCHEMA.startsWith("https://")
 53 | 			) {
 54 | 				schema = await introspectSchemaFromUrl(env.SCHEMA);
 55 | 			} else {
 56 | 				schema = await introspectLocalSchema(env.SCHEMA);
 57 | 			}
 58 | 		} else {
 59 | 			schema = await introspectEndpoint(env.ENDPOINT, env.HEADERS);
 60 | 		}
 61 | 
 62 | 		return {
 63 | 			contents: [
 64 | 				{
 65 | 					uri: uri.href,
 66 | 					text: schema,
 67 | 				},
 68 | 			],
 69 | 		};
 70 | 	} catch (error) {
 71 | 		throw new Error(`Failed to get GraphQL schema: ${error}`);
 72 | 	}
 73 | });
 74 | 
 75 | server.tool(
 76 | 	"introspect-schema",
 77 | 	"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.",
 78 | 	{
 79 | 		// This is a workaround to help clients that can't handle an empty object as an argument
 80 | 		// They will often send undefined instead of an empty object which is not allowed by the schema
 81 | 		__ignore__: z
 82 | 			.boolean()
 83 | 			.default(false)
 84 | 			.describe("This does not do anything"),
 85 | 	},
 86 | 	async () => {
 87 | 		try {
 88 | 			let schema: string;
 89 | 			if (env.SCHEMA) {
 90 | 				schema = await introspectLocalSchema(env.SCHEMA);
 91 | 			} else {
 92 | 				schema = await introspectEndpoint(env.ENDPOINT, env.HEADERS);
 93 | 			}
 94 | 
 95 | 			return {
 96 | 				content: [
 97 | 					{
 98 | 						type: "text",
 99 | 						text: schema,
100 | 					},
101 | 				],
102 | 			};
103 | 		} catch (error) {
104 | 			return {
105 | 				isError: true,
106 | 				content: [
107 | 					{
108 | 						type: "text",
109 | 						text: `Failed to introspect schema: ${error}`,
110 | 					},
111 | 				],
112 | 			};
113 | 		}
114 | 	},
115 | );
116 | 
117 | server.tool(
118 | 	"query-graphql",
119 | 	"Query a GraphQL endpoint with the given query and variables",
120 | 	{
121 | 		query: z.string(),
122 | 		variables: z.string().optional(),
123 | 	},
124 | 	async ({ query, variables }) => {
125 | 		try {
126 | 			const parsedQuery = parse(query);
127 | 
128 | 			// Check if the query is a mutation
129 | 			const isMutation = parsedQuery.definitions.some(
130 | 				(def) =>
131 | 					def.kind === "OperationDefinition" && def.operation === "mutation",
132 | 			);
133 | 
134 | 			if (isMutation && !env.ALLOW_MUTATIONS) {
135 | 				return {
136 | 					isError: true,
137 | 					content: [
138 | 						{
139 | 							type: "text",
140 | 							text: "Mutations are not allowed unless you enable them in the configuration. Please use a query operation instead.",
141 | 						},
142 | 					],
143 | 				};
144 | 			}
145 | 		} catch (error) {
146 | 			return {
147 | 				isError: true,
148 | 				content: [
149 | 					{
150 | 						type: "text",
151 | 						text: `Invalid GraphQL query: ${error}`,
152 | 					},
153 | 				],
154 | 			};
155 | 		}
156 | 
157 | 		try {
158 | 			const response = await fetch(env.ENDPOINT, {
159 | 				method: "POST",
160 | 				headers: {
161 | 					"Content-Type": "application/json",
162 | 					...env.HEADERS,
163 | 				},
164 | 				body: JSON.stringify({
165 | 					query,
166 | 					variables,
167 | 				}),
168 | 			});
169 | 
170 | 			if (!response.ok) {
171 | 				const responseText = await response.text();
172 | 
173 | 				return {
174 | 					isError: true,
175 | 					content: [
176 | 						{
177 | 							type: "text",
178 | 							text: `GraphQL request failed: ${response.statusText}\n${responseText}`,
179 | 						},
180 | 					],
181 | 				};
182 | 			}
183 | 
184 | 			const data = await response.json();
185 | 
186 | 			if (data.errors && data.errors.length > 0) {
187 | 				// Contains GraphQL errors
188 | 				return {
189 | 					isError: true,
190 | 					content: [
191 | 						{
192 | 							type: "text",
193 | 							text: `The GraphQL response has errors, please fix the query: ${JSON.stringify(
194 | 								data,
195 | 								null,
196 | 								2,
197 | 							)}`,
198 | 						},
199 | 					],
200 | 				};
201 | 			}
202 | 
203 | 			return {
204 | 				content: [
205 | 					{
206 | 						type: "text",
207 | 						text: JSON.stringify(data, null, 2),
208 | 					},
209 | 				],
210 | 			};
211 | 		} catch (error) {
212 | 			throw new Error(`Failed to execute GraphQL query: ${error}`);
213 | 		}
214 | 	},
215 | );
216 | 
217 | async function main() {
218 | 	const transport = new StdioServerTransport();
219 | 	await server.connect(transport);
220 | 
221 | 	console.error(
222 | 		`Started graphql mcp server ${env.NAME} for endpoint: ${env.ENDPOINT}`,
223 | 	);
224 | }
225 | 
226 | main().catch((error) => {
227 | 	console.error(`Fatal error in main(): ${error}`);
228 | 	process.exit(1);
229 | });
230 | 
```

--------------------------------------------------------------------------------
/dev/graphql.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { makeExecutableSchema } from "@graphql-tools/schema";
  2 | import { createYoga } from "graphql-yoga";
  3 | import fs from "node:fs";
  4 | 
  5 | /**
  6 |  * Simple GraphQL server implementation for testing purposes
  7 |  *
  8 |  * This is a simple GraphQL server implementation for testing purposes.
  9 |  * It is not intended to be used in production.
 10 |  *
 11 |  * It is used to test the GraphQL schema and resolvers.
 12 |  *
 13 |  */
 14 | 
 15 | // Define types
 16 | interface User {
 17 | 	id: string;
 18 | 	name: string;
 19 | 	email: string;
 20 | 	createdAt: string;
 21 | 	updatedAt: string | null;
 22 | }
 23 | 
 24 | interface Post {
 25 | 	id: string;
 26 | 	title: string;
 27 | 	content: string;
 28 | 	published: boolean;
 29 | 	authorId: string;
 30 | 	createdAt: string;
 31 | 	updatedAt: string | null;
 32 | }
 33 | 
 34 | interface Comment {
 35 | 	id: string;
 36 | 	text: string;
 37 | 	postId: string;
 38 | 	authorId: string;
 39 | 	createdAt: string;
 40 | }
 41 | 
 42 | interface CreateUserInput {
 43 | 	name: string;
 44 | 	email: string;
 45 | }
 46 | 
 47 | interface UpdateUserInput {
 48 | 	name?: string;
 49 | 	email?: string;
 50 | }
 51 | 
 52 | interface CreatePostInput {
 53 | 	title: string;
 54 | 	content: string;
 55 | 	published?: boolean;
 56 | 	authorId: string;
 57 | }
 58 | 
 59 | interface AddCommentInput {
 60 | 	text: string;
 61 | 	postId: string;
 62 | 	authorId: string;
 63 | }
 64 | 
 65 | // Define resolver context type
 66 | type ResolverContext = Record<string, never>;
 67 | 
 68 | // Read schema from file
 69 | const typeDefs = fs.readFileSync("./schema-simple.graphql", "utf-8");
 70 | 
 71 | // Create mock data
 72 | const users: User[] = [
 73 | 	{
 74 | 		id: "1",
 75 | 		name: "John Doe",
 76 | 		email: "[email protected]",
 77 | 		createdAt: new Date().toISOString(),
 78 | 		updatedAt: null,
 79 | 	},
 80 | 	{
 81 | 		id: "2",
 82 | 		name: "Jane Smith",
 83 | 		email: "[email protected]",
 84 | 		createdAt: new Date().toISOString(),
 85 | 		updatedAt: null,
 86 | 	},
 87 | 	{
 88 | 		id: "3",
 89 | 		name: "Bob Johnson",
 90 | 		email: "[email protected]",
 91 | 		createdAt: new Date().toISOString(),
 92 | 		updatedAt: null,
 93 | 	},
 94 | ];
 95 | 
 96 | const posts: Post[] = [
 97 | 	{
 98 | 		id: "1",
 99 | 		title: "First Post",
100 | 		content: "This is my first post",
101 | 		published: true,
102 | 		authorId: "1",
103 | 		createdAt: new Date().toISOString(),
104 | 		updatedAt: null,
105 | 	},
106 | 	{
107 | 		id: "2",
108 | 		title: "GraphQL is Awesome",
109 | 		content: "Here is why GraphQL is better than REST",
110 | 		published: true,
111 | 		authorId: "1",
112 | 		createdAt: new Date().toISOString(),
113 | 		updatedAt: null,
114 | 	},
115 | 	{
116 | 		id: "3",
117 | 		title: "Yoga Tutorial",
118 | 		content: "Learn how to use GraphQL Yoga",
119 | 		published: false,
120 | 		authorId: "2",
121 | 		createdAt: new Date().toISOString(),
122 | 		updatedAt: null,
123 | 	},
124 | ];
125 | 
126 | const comments: Comment[] = [
127 | 	{
128 | 		id: "1",
129 | 		text: "Great post!",
130 | 		postId: "1",
131 | 		authorId: "2",
132 | 		createdAt: new Date().toISOString(),
133 | 	},
134 | 	{
135 | 		id: "2",
136 | 		text: "I learned a lot",
137 | 		postId: "1",
138 | 		authorId: "3",
139 | 		createdAt: new Date().toISOString(),
140 | 	},
141 | 	{
142 | 		id: "3",
143 | 		text: "Looking forward to more content",
144 | 		postId: "2",
145 | 		authorId: "2",
146 | 		createdAt: new Date().toISOString(),
147 | 	},
148 | ];
149 | 
150 | // Define resolvers
151 | const resolvers = {
152 | 	Query: {
153 | 		user: (
154 | 			_parent: unknown,
155 | 			{ id }: { id: string },
156 | 			_context: ResolverContext,
157 | 		) => users.find((user) => user.id === id),
158 | 		users: () => users,
159 | 		post: (
160 | 			_parent: unknown,
161 | 			{ id }: { id: string },
162 | 			_context: ResolverContext,
163 | 		) => posts.find((post) => post.id === id),
164 | 		posts: () => posts,
165 | 		commentsByPost: (
166 | 			_parent: unknown,
167 | 			{ postId }: { postId: string },
168 | 			_context: ResolverContext,
169 | 		) => comments.filter((comment) => comment.postId === postId),
170 | 	},
171 | 	Mutation: {
172 | 		createUser: (
173 | 			_parent: unknown,
174 | 			{ input }: { input: CreateUserInput },
175 | 			_context: ResolverContext,
176 | 		) => {
177 | 			const newUser: User = {
178 | 				id: String(users.length + 1),
179 | 				name: input.name,
180 | 				email: input.email,
181 | 				createdAt: new Date().toISOString(),
182 | 				updatedAt: null,
183 | 			};
184 | 			users.push(newUser);
185 | 			return newUser;
186 | 		},
187 | 		updateUser: (
188 | 			_parent: unknown,
189 | 			{ id, input }: { id: string; input: UpdateUserInput },
190 | 			_context: ResolverContext,
191 | 		) => {
192 | 			const userIndex = users.findIndex((user) => user.id === id);
193 | 			if (userIndex === -1) throw new Error(`User with ID ${id} not found`);
194 | 
195 | 			users[userIndex] = {
196 | 				...users[userIndex],
197 | 				...input,
198 | 				updatedAt: new Date().toISOString(),
199 | 			};
200 | 
201 | 			return users[userIndex];
202 | 		},
203 | 		deleteUser: (
204 | 			_parent: unknown,
205 | 			{ id }: { id: string },
206 | 			_context: ResolverContext,
207 | 		) => {
208 | 			const userIndex = users.findIndex((user) => user.id === id);
209 | 			if (userIndex === -1) return false;
210 | 
211 | 			users.splice(userIndex, 1);
212 | 			return true;
213 | 		},
214 | 		createPost: (
215 | 			_parent: unknown,
216 | 			{ input }: { input: CreatePostInput },
217 | 			_context: ResolverContext,
218 | 		) => {
219 | 			const newPost: Post = {
220 | 				id: String(posts.length + 1),
221 | 				title: input.title,
222 | 				content: input.content,
223 | 				published: input.published ?? false,
224 | 				authorId: input.authorId,
225 | 				createdAt: new Date().toISOString(),
226 | 				updatedAt: null,
227 | 			};
228 | 			posts.push(newPost);
229 | 			return newPost;
230 | 		},
231 | 		addComment: (
232 | 			_parent: unknown,
233 | 			{ input }: { input: AddCommentInput },
234 | 			_context: ResolverContext,
235 | 		) => {
236 | 			const newComment: Comment = {
237 | 				id: String(comments.length + 1),
238 | 				text: input.text,
239 | 				postId: input.postId,
240 | 				authorId: input.authorId,
241 | 				createdAt: new Date().toISOString(),
242 | 			};
243 | 			comments.push(newComment);
244 | 			return newComment;
245 | 		},
246 | 	},
247 | 	User: {
248 | 		posts: (parent: User) =>
249 | 			posts.filter((post) => post.authorId === parent.id),
250 | 		comments: (parent: User) =>
251 | 			comments.filter((comment) => comment.authorId === parent.id),
252 | 	},
253 | 	Post: {
254 | 		author: (parent: Post) => users.find((user) => user.id === parent.authorId),
255 | 		comments: (parent: Post) =>
256 | 			comments.filter((comment) => comment.postId === parent.id),
257 | 	},
258 | 	Comment: {
259 | 		post: (parent: Comment) => posts.find((post) => post.id === parent.postId),
260 | 		author: (parent: Comment) =>
261 | 			users.find((user) => user.id === parent.authorId),
262 | 	},
263 | };
264 | 
265 | // Create executable schema
266 | const schema = makeExecutableSchema({
267 | 	typeDefs,
268 | 	resolvers,
269 | });
270 | 
271 | // Create Yoga instance
272 | const yoga = createYoga({ schema });
273 | 
274 | // Start server with proper request handler
275 | const server = Bun.serve({
276 | 	port: 4000,
277 | 	fetch: (request) => {
278 | 		// Add dev logger for incoming requests
279 | 		console.log(
280 | 			`[${new Date().toISOString()}] Incoming request: ${request.method} ${
281 | 				request.url
282 | 			}`,
283 | 		);
284 | 		return yoga.fetch(request);
285 | 	},
286 | });
287 | 
288 | console.info(
289 | 	`GraphQL server is running on ${new URL(
290 | 		yoga.graphqlEndpoint,
291 | 		`http://${server.hostname}:${server.port}`,
292 | 	)}`,
293 | );
294 | 
```