# 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}`,
)}`,
);
```