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

[![smithery badge](https://smithery.ai/badge/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}`,
	)}`,
);

```