@letoribo/mcp-graphql-enhanced 2.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +106 -0
- package/dist/helpers/deprecation.d.ts +8 -0
- package/dist/helpers/deprecation.d.ts.map +1 -0
- package/dist/helpers/deprecation.js +27 -0
- package/dist/helpers/headers.d.ts +8 -0
- package/dist/helpers/headers.d.ts.map +1 -0
- package/dist/helpers/headers.js +26 -0
- package/dist/helpers/introspection.d.ts +20 -0
- package/dist/helpers/introspection.d.ts.map +1 -0
- package/dist/helpers/introspection.js +55 -0
- package/dist/helpers/package.d.ts +2 -0
- package/dist/helpers/package.d.ts.map +1 -0
- package/dist/helpers/package.js +9 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +222 -0
- package/package.json +50 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Boris Besemer
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# mcp-graphql-enhanced
|
|
2
|
+
|
|
3
|
+
[](https://smithery.ai/server/mcp-graphql-enhanced)
|
|
4
|
+
[](https://glama.ai/mcp/servers/@letoribo/mcp-graphql-enhanced)
|
|
5
|
+
|
|
6
|
+
An **enhanced MCP (Model Context Protocol) server for GraphQL** that fixes real-world interoperability issues between LLMs and GraphQL APIs.
|
|
7
|
+
|
|
8
|
+
> Drop-in replacement for `mcp-graphql` — with dynamic headers, robust variables parsing, and zero breaking changes.
|
|
9
|
+
|
|
10
|
+
## ✨ Key Enhancements
|
|
11
|
+
|
|
12
|
+
- ✅ **Dynamic headers** — pass `Authorization`, `X-API-Key`, etc., via tool arguments (no config restarts)
|
|
13
|
+
- ✅ **Robust variables parsing** — fixes `“Query variables must be a null or an object”` error
|
|
14
|
+
- ✅ **Full MCP compatibility** — works with **Claude Desktop**, **Cursor**, **Glama**, and **Smithery**
|
|
15
|
+
- ✅ **Secure by default** — mutations disabled unless explicitly enabled
|
|
16
|
+
|
|
17
|
+
## 🔍 Debug & Inspect
|
|
18
|
+
|
|
19
|
+
Use the official MCP Inspector to test your server live:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npx @modelcontextprotocol/inspector \
|
|
23
|
+
-e ENDPOINT=https://api.example.com/graphql \
|
|
24
|
+
npx mcp-graphql-enhanced --debug
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Environment Variables (Breaking change in 1.0.0)
|
|
28
|
+
|
|
29
|
+
> **Note:** As of version 1.0.0, command line arguments have been replaced with environment variables.
|
|
30
|
+
|
|
31
|
+
| Environment Variable | Description | Default |
|
|
32
|
+
|----------|-------------|---------|
|
|
33
|
+
| `ENDPOINT` | GraphQL endpoint URL | `http://localhost:4000/graphql` |
|
|
34
|
+
| `HEADERS` | JSON string containing headers for requests | `{}` |
|
|
35
|
+
| `ALLOW_MUTATIONS` | Enable mutation operations (disabled by default) | `false` |
|
|
36
|
+
| `NAME` | Name of the MCP server | `mcp-graphql-enhanced` |
|
|
37
|
+
| `SCHEMA` | Path to a local GraphQL schema file or URL (optional) | - |
|
|
38
|
+
|
|
39
|
+
### Examples
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
# Basic usage
|
|
43
|
+
ENDPOINT=http://localhost:3000/graphql npx mcp-graphql-enhanced
|
|
44
|
+
|
|
45
|
+
# With auth header
|
|
46
|
+
ENDPOINT=https://api.example.com/graphql \
|
|
47
|
+
HEADERS='{"Authorization":"Bearer xyz"}' \
|
|
48
|
+
npx mcp-graphql-enhanced
|
|
49
|
+
|
|
50
|
+
# Enable mutations
|
|
51
|
+
ENDPOINT=http://localhost:3000/graphql \
|
|
52
|
+
ALLOW_MUTATIONS=true \
|
|
53
|
+
npx mcp-graphql-enhanced
|
|
54
|
+
|
|
55
|
+
# Use local schema file
|
|
56
|
+
ENDPOINT=http://localhost:3000/graphql \
|
|
57
|
+
SCHEMA=./schema.graphql \
|
|
58
|
+
npx mcp-graphql-enhanced
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Resources
|
|
62
|
+
|
|
63
|
+
- **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.
|
|
64
|
+
|
|
65
|
+
## Available Tools
|
|
66
|
+
|
|
67
|
+
The server provides two main tools:
|
|
68
|
+
|
|
69
|
+
1. **introspect-schema**: This tool retrieves the GraphQL schema. Use this first if you don't have access to the schema as a resource.
|
|
70
|
+
This uses either the local schema file, a schema file hosted at a URL, or an introspection query.
|
|
71
|
+
|
|
72
|
+
2. **query-graphql**: Execute GraphQL queries against the endpoint. By default, mutations are disabled unless `ALLOW_MUTATIONS` is set to `true`.
|
|
73
|
+
|
|
74
|
+
## Installation
|
|
75
|
+
|
|
76
|
+
### Installing via Smithery
|
|
77
|
+
|
|
78
|
+
To install GraphQL MCP Server for Claude Desktop automatically via [Smithery](https://smithery.ai/server/mcp-graphql-enhanced):
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
npx -y @smithery/cli install mcp-graphql-enhanced --client claude
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Installing Manually
|
|
85
|
+
|
|
86
|
+
It can be manually installed to Claude:
|
|
87
|
+
```json
|
|
88
|
+
{
|
|
89
|
+
"mcpServers": {
|
|
90
|
+
"mcp-graphql": {
|
|
91
|
+
"command": "npx",
|
|
92
|
+
"args": ["mcp-graphql-enhanced"],
|
|
93
|
+
"env": {
|
|
94
|
+
"ENDPOINT": "https://your-api.com/graphql"
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Security Considerations
|
|
102
|
+
|
|
103
|
+
Mutations are disabled by default to prevent unintended data changes. Always validate HEADERS and SCHEMA inputs in production. Use HTTPS endpoints and short-lived tokens where possible.
|
|
104
|
+
## Customize for your own server
|
|
105
|
+
|
|
106
|
+
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.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deprecation.d.ts","sourceRoot":"","sources":["../../src/helpers/deprecation.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;GAEG;AACH,wBAAgB,wBAAwB,IAAI,IAAI,CA0B/C"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Helper module for handling deprecation warnings
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.checkDeprecatedArguments = checkDeprecatedArguments;
|
|
7
|
+
/**
|
|
8
|
+
* Check for deprecated command line arguments and output warnings
|
|
9
|
+
*/
|
|
10
|
+
function checkDeprecatedArguments() {
|
|
11
|
+
const deprecatedArgs = [
|
|
12
|
+
"--endpoint",
|
|
13
|
+
"--headers",
|
|
14
|
+
"--enable-mutations",
|
|
15
|
+
"--name",
|
|
16
|
+
"--schema",
|
|
17
|
+
];
|
|
18
|
+
const usedDeprecatedArgs = deprecatedArgs.filter((arg) => process.argv.includes(arg));
|
|
19
|
+
if (usedDeprecatedArgs.length > 0) {
|
|
20
|
+
console.error(`WARNING: Deprecated command line arguments detected: ${usedDeprecatedArgs.join(", ")}`);
|
|
21
|
+
console.error("As of version 1.0.0, command line arguments have been replaced with environment variables.");
|
|
22
|
+
console.error("Please use environment variables instead. For example:");
|
|
23
|
+
console.error(" Instead of: npx mcp-graphql --endpoint http://example.com/graphql");
|
|
24
|
+
console.error(" Use: ENDPOINT=http://example.com/graphql npx mcp-graphql");
|
|
25
|
+
console.error("");
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
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 declare function parseAndMergeHeaders(configHeaders: Record<string, string>, inputHeaders?: string | Record<string, string>): Record<string, string>;
|
|
8
|
+
//# sourceMappingURL=headers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"headers.d.ts","sourceRoot":"","sources":["../../src/helpers/headers.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,wBAAgB,oBAAoB,CACnC,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EACrC,YAAY,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC5C,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAgBxB"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseAndMergeHeaders = parseAndMergeHeaders;
|
|
4
|
+
/**
|
|
5
|
+
* Parse and merge headers from various sources
|
|
6
|
+
* @param configHeaders - Default headers from configuration
|
|
7
|
+
* @param inputHeaders - Headers provided by the user (string or object)
|
|
8
|
+
* @returns Merged headers object
|
|
9
|
+
*/
|
|
10
|
+
function parseAndMergeHeaders(configHeaders, inputHeaders) {
|
|
11
|
+
// Parse headers if they're provided as a string
|
|
12
|
+
let parsedHeaders = {};
|
|
13
|
+
if (typeof inputHeaders === "string") {
|
|
14
|
+
try {
|
|
15
|
+
parsedHeaders = JSON.parse(inputHeaders);
|
|
16
|
+
}
|
|
17
|
+
catch (e) {
|
|
18
|
+
throw new Error(`Invalid headers JSON: ${e}`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
else if (inputHeaders) {
|
|
22
|
+
parsedHeaders = inputHeaders;
|
|
23
|
+
}
|
|
24
|
+
// Merge with config headers (config headers are overridden by input headers)
|
|
25
|
+
return { ...configHeaders, ...parsedHeaders };
|
|
26
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Introspect a GraphQL endpoint and return the schema as the GraphQL SDL
|
|
3
|
+
* @param endpoint - The endpoint to introspect
|
|
4
|
+
* @param headers - Optional headers to include in the request
|
|
5
|
+
* @returns The schema
|
|
6
|
+
*/
|
|
7
|
+
export declare function introspectEndpoint(endpoint: string, headers?: Record<string, string>): Promise<string>;
|
|
8
|
+
/**
|
|
9
|
+
* Introspect a GraphQL schema file hosted at a URL and return the schema as the GraphQL SDL
|
|
10
|
+
* @param url - The URL to the schema file
|
|
11
|
+
* @returns The schema
|
|
12
|
+
*/
|
|
13
|
+
export declare function introspectSchemaFromUrl(url: string): Promise<string>;
|
|
14
|
+
/**
|
|
15
|
+
* Introspect a local GraphQL schema file and return the schema as the GraphQL SDL
|
|
16
|
+
* @param path - The path to the local schema file
|
|
17
|
+
* @returns The schema
|
|
18
|
+
*/
|
|
19
|
+
export declare function introspectLocalSchema(path: string): Promise<string>;
|
|
20
|
+
//# sourceMappingURL=introspection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"introspection.d.ts","sourceRoot":"","sources":["../../src/helpers/introspection.ts"],"names":[],"mappings":"AAGA;;;;;GAKG;AACH,wBAAsB,kBAAkB,CACvC,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,mBAuBhC;AAED;;;;GAIG;AACH,wBAAsB,uBAAuB,CAAC,GAAG,EAAE,MAAM,mBASxD;AAED;;;;GAIG;AACH,wBAAsB,qBAAqB,CAAC,IAAI,EAAE,MAAM,mBAGvD"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.introspectEndpoint = introspectEndpoint;
|
|
4
|
+
exports.introspectSchemaFromUrl = introspectSchemaFromUrl;
|
|
5
|
+
exports.introspectLocalSchema = introspectLocalSchema;
|
|
6
|
+
const graphql_1 = require("graphql");
|
|
7
|
+
const promises_1 = require("node:fs/promises");
|
|
8
|
+
/**
|
|
9
|
+
* Introspect a GraphQL endpoint and return the schema as the GraphQL SDL
|
|
10
|
+
* @param endpoint - The endpoint to introspect
|
|
11
|
+
* @param headers - Optional headers to include in the request
|
|
12
|
+
* @returns The schema
|
|
13
|
+
*/
|
|
14
|
+
async function introspectEndpoint(endpoint, headers) {
|
|
15
|
+
const response = await fetch(endpoint, {
|
|
16
|
+
method: "POST",
|
|
17
|
+
headers: {
|
|
18
|
+
"Content-Type": "application/json",
|
|
19
|
+
...headers,
|
|
20
|
+
},
|
|
21
|
+
body: JSON.stringify({
|
|
22
|
+
query: (0, graphql_1.getIntrospectionQuery)(),
|
|
23
|
+
}),
|
|
24
|
+
});
|
|
25
|
+
if (!response.ok) {
|
|
26
|
+
throw new Error(`GraphQL request failed: ${response.statusText}`);
|
|
27
|
+
}
|
|
28
|
+
const responseJson = await response.json();
|
|
29
|
+
// Transform to a schema object
|
|
30
|
+
const schema = (0, graphql_1.buildClientSchema)(responseJson.data);
|
|
31
|
+
// Print the schema SDL
|
|
32
|
+
return (0, graphql_1.printSchema)(schema);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Introspect a GraphQL schema file hosted at a URL and return the schema as the GraphQL SDL
|
|
36
|
+
* @param url - The URL to the schema file
|
|
37
|
+
* @returns The schema
|
|
38
|
+
*/
|
|
39
|
+
async function introspectSchemaFromUrl(url) {
|
|
40
|
+
const response = await fetch(url);
|
|
41
|
+
if (!response.ok) {
|
|
42
|
+
throw new Error(`Failed to fetch schema from URL: ${response.statusText}`);
|
|
43
|
+
}
|
|
44
|
+
const schema = await response.text();
|
|
45
|
+
return schema;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Introspect a local GraphQL schema file and return the schema as the GraphQL SDL
|
|
49
|
+
* @param path - The path to the local schema file
|
|
50
|
+
* @returns The schema
|
|
51
|
+
*/
|
|
52
|
+
async function introspectLocalSchema(path) {
|
|
53
|
+
const schema = await (0, promises_1.readFile)(path, "utf8");
|
|
54
|
+
return schema;
|
|
55
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"package.d.ts","sourceRoot":"","sources":["../../src/helpers/package.ts"],"names":[],"mappings":"AAOA,wBAAgB,UAAU,QAEzB"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getVersion = getVersion;
|
|
4
|
+
const fs_1 = require("fs");
|
|
5
|
+
const path_1 = require("path");
|
|
6
|
+
const packageJson = JSON.parse((0, fs_1.readFileSync)((0, path_1.join)(__dirname, "../../package.json"), "utf-8"));
|
|
7
|
+
function getVersion() {
|
|
8
|
+
return packageJson.version;
|
|
9
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
declare const McpServer: any;
|
|
3
|
+
declare const StdioServerTransport: any;
|
|
4
|
+
declare const parse: any;
|
|
5
|
+
declare const z: any;
|
|
6
|
+
declare const checkDeprecatedArguments: any;
|
|
7
|
+
declare const introspectEndpoint: any, introspectLocalSchema: any, introspectSchemaFromUrl: any;
|
|
8
|
+
declare const getVersion: () => any;
|
|
9
|
+
declare const EnvSchema: any;
|
|
10
|
+
declare const env: any;
|
|
11
|
+
declare const server: any;
|
|
12
|
+
declare function main(): Promise<void>;
|
|
13
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA,QAAA,MAAQ,SAAS,KAAuD,CAAC;AACzE,QAAA,MAAQ,oBAAoB,KAAyD,CAAC;AACtF,QAAA,MAAQ,KAAK,KAAgC,CAAC;AAC9C,QAAA,MAAM,CAAC,KAAyB,CAAC;AACjC,QAAA,MAAQ,wBAAwB,KAAwC,CAAC;AACzE,QAAA,MACC,kBAAkB,OAClB,qBAAqB,OACrB,uBAAuB,KACiB,CAAC;AAG1C,QAAA,MAAM,UAAU,WAIf,CAAC;AAKF,QAAA,MAAM,SAAS,KAkBb,CAAC;AAEH,QAAA,MAAM,GAAG,KAA+B,CAAC;AAEzC,QAAA,MAAM,MAAM,KAIV,CAAC;AAoMH,iBAAe,IAAI,kBAOlB"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
const { McpServer } = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
4
|
+
const { StdioServerTransport } = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
5
|
+
const { parse } = require("graphql/language");
|
|
6
|
+
const z = require("zod").default;
|
|
7
|
+
const { checkDeprecatedArguments } = require("./helpers/deprecation.js");
|
|
8
|
+
const { introspectEndpoint, introspectLocalSchema, introspectSchemaFromUrl, } = require("./helpers/introspection.js");
|
|
9
|
+
// Simulate macro import — since "with { type: 'macro' }" is not CommonJS compatible
|
|
10
|
+
const getVersion = () => {
|
|
11
|
+
// Replace with your actual version or read from package.json
|
|
12
|
+
const pkg = require("../package.json");
|
|
13
|
+
return pkg.version;
|
|
14
|
+
};
|
|
15
|
+
// Check for deprecated command line arguments
|
|
16
|
+
checkDeprecatedArguments();
|
|
17
|
+
const EnvSchema = z.object({
|
|
18
|
+
NAME: z.string().default("mcp-graphql"),
|
|
19
|
+
ENDPOINT: z.string().url().default("http://localhost:4000/graphql"),
|
|
20
|
+
ALLOW_MUTATIONS: z
|
|
21
|
+
.enum(["true", "false"])
|
|
22
|
+
.transform((value) => value === "true")
|
|
23
|
+
.default("false"),
|
|
24
|
+
HEADERS: z
|
|
25
|
+
.string()
|
|
26
|
+
.default("{}")
|
|
27
|
+
.transform((val) => {
|
|
28
|
+
try {
|
|
29
|
+
return JSON.parse(val);
|
|
30
|
+
}
|
|
31
|
+
catch (e) {
|
|
32
|
+
throw new Error("HEADERS must be a valid JSON string");
|
|
33
|
+
}
|
|
34
|
+
}),
|
|
35
|
+
SCHEMA: z.string().optional(),
|
|
36
|
+
});
|
|
37
|
+
const env = EnvSchema.parse(process.env);
|
|
38
|
+
const server = new McpServer({
|
|
39
|
+
name: env.NAME,
|
|
40
|
+
version: getVersion(),
|
|
41
|
+
description: `GraphQL MCP server for ${env.ENDPOINT}`,
|
|
42
|
+
});
|
|
43
|
+
server.resource("graphql-schema", new URL(env.ENDPOINT).href, async (uri) => {
|
|
44
|
+
try {
|
|
45
|
+
let schema;
|
|
46
|
+
if (env.SCHEMA) {
|
|
47
|
+
if (env.SCHEMA.startsWith("http://") ||
|
|
48
|
+
env.SCHEMA.startsWith("https://")) {
|
|
49
|
+
schema = await introspectSchemaFromUrl(env.SCHEMA);
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
schema = await introspectLocalSchema(env.SCHEMA);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
schema = await introspectEndpoint(env.ENDPOINT, env.HEADERS);
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
contents: [
|
|
60
|
+
{
|
|
61
|
+
uri: uri.href,
|
|
62
|
+
text: schema,
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
throw new Error(`Failed to get GraphQL schema: ${error}`);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
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.", {
|
|
72
|
+
__ignore__: z
|
|
73
|
+
.boolean()
|
|
74
|
+
.default(false)
|
|
75
|
+
.describe("This does not do anything"),
|
|
76
|
+
}, async () => {
|
|
77
|
+
try {
|
|
78
|
+
let schema;
|
|
79
|
+
if (env.SCHEMA) {
|
|
80
|
+
schema = await introspectLocalSchema(env.SCHEMA);
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
schema = await introspectEndpoint(env.ENDPOINT, env.HEADERS);
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
content: [
|
|
87
|
+
{
|
|
88
|
+
type: "text",
|
|
89
|
+
text: schema,
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
return {
|
|
96
|
+
isError: true,
|
|
97
|
+
content: [
|
|
98
|
+
{
|
|
99
|
+
type: "text",
|
|
100
|
+
text: `Failed to introspect schema: ${error}`,
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
server.tool("query-graphql", "Query a GraphQL endpoint with the given query and variables. Optionally pass headers (e.g., for Authorization).", {
|
|
107
|
+
query: z.string(),
|
|
108
|
+
variables: z.string().optional(),
|
|
109
|
+
headers: z
|
|
110
|
+
.string()
|
|
111
|
+
.optional()
|
|
112
|
+
.describe("Optional JSON string of headers to include, e.g., {\"Authorization\": \"Bearer ...\"}"),
|
|
113
|
+
}, async ({ query, variables, headers }) => {
|
|
114
|
+
try {
|
|
115
|
+
const parsedQuery = parse(query);
|
|
116
|
+
const isMutation = parsedQuery.definitions.some((def) => def.kind === "OperationDefinition" && def.operation === "mutation");
|
|
117
|
+
if (isMutation && !env.ALLOW_MUTATIONS) {
|
|
118
|
+
return {
|
|
119
|
+
isError: true,
|
|
120
|
+
content: [
|
|
121
|
+
{
|
|
122
|
+
type: "text",
|
|
123
|
+
text: "Mutations are not allowed unless you enable them in the configuration. Please use a query operation instead.",
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
return {
|
|
131
|
+
isError: true,
|
|
132
|
+
content: [
|
|
133
|
+
{
|
|
134
|
+
type: "text",
|
|
135
|
+
text: `Invalid GraphQL query: ${error}`,
|
|
136
|
+
},
|
|
137
|
+
],
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
const toolHeaders = headers
|
|
142
|
+
? JSON.parse(headers)
|
|
143
|
+
: {};
|
|
144
|
+
const allHeaders = {
|
|
145
|
+
"Content-Type": "application/json",
|
|
146
|
+
...env.HEADERS,
|
|
147
|
+
...toolHeaders,
|
|
148
|
+
};
|
|
149
|
+
// Parse variables if it's a string
|
|
150
|
+
let parsedVariables = null;
|
|
151
|
+
if (variables) {
|
|
152
|
+
if (typeof variables === 'string') {
|
|
153
|
+
parsedVariables = JSON.parse(variables);
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
parsedVariables = variables;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const response = await fetch(env.ENDPOINT, {
|
|
160
|
+
method: "POST",
|
|
161
|
+
headers: allHeaders,
|
|
162
|
+
body: JSON.stringify({
|
|
163
|
+
query,
|
|
164
|
+
variables: parsedVariables,
|
|
165
|
+
}),
|
|
166
|
+
});
|
|
167
|
+
if (!response.ok) {
|
|
168
|
+
const responseText = await response.text();
|
|
169
|
+
return {
|
|
170
|
+
isError: true,
|
|
171
|
+
content: [
|
|
172
|
+
{
|
|
173
|
+
type: "text",
|
|
174
|
+
text: `GraphQL request failed: ${response.statusText}\n${responseText}`,
|
|
175
|
+
},
|
|
176
|
+
],
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
const rawData = await response.json();
|
|
180
|
+
// Type assertion for quick dev (replace with zod validation later)
|
|
181
|
+
const data = rawData;
|
|
182
|
+
if (data.errors && data.errors.length > 0) {
|
|
183
|
+
return {
|
|
184
|
+
isError: true,
|
|
185
|
+
content: [
|
|
186
|
+
{
|
|
187
|
+
type: "text",
|
|
188
|
+
text: `GraphQL errors: ${JSON.stringify(data.errors, null, 2)}`,
|
|
189
|
+
},
|
|
190
|
+
],
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
return {
|
|
194
|
+
content: [
|
|
195
|
+
{
|
|
196
|
+
type: "text",
|
|
197
|
+
text: JSON.stringify(data, null, 2),
|
|
198
|
+
},
|
|
199
|
+
],
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
return {
|
|
204
|
+
isError: true,
|
|
205
|
+
content: [
|
|
206
|
+
{
|
|
207
|
+
type: "text",
|
|
208
|
+
text: `Failed to execute GraphQL query: ${error}`,
|
|
209
|
+
},
|
|
210
|
+
],
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
async function main() {
|
|
215
|
+
const transport = new StdioServerTransport();
|
|
216
|
+
await server.connect(transport);
|
|
217
|
+
console.error(`Started graphql mcp server ${env.NAME} for endpoint: ${env.ENDPOINT}`);
|
|
218
|
+
}
|
|
219
|
+
main().catch((error) => {
|
|
220
|
+
console.error(`Fatal error in main(): ${error}`);
|
|
221
|
+
process.exit(1);
|
|
222
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@letoribo/mcp-graphql-enhanced",
|
|
3
|
+
"description": "Enhanced MCP server for GraphQL with dynamic headers and robust variables parsing — a drop-in replacement for mcp-graphql.",
|
|
4
|
+
"keywords": [
|
|
5
|
+
"mcp",
|
|
6
|
+
"graphql",
|
|
7
|
+
"llm",
|
|
8
|
+
"claude-desktop",
|
|
9
|
+
"cursor",
|
|
10
|
+
"enhanced",
|
|
11
|
+
"tool-calling"
|
|
12
|
+
],
|
|
13
|
+
"author": "letoribo",
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"repository": "github:letoribo/mcp-graphql-enhanced",
|
|
16
|
+
"main": "dist/index.js",
|
|
17
|
+
"types": "dist/index.d.ts",
|
|
18
|
+
"bin": {
|
|
19
|
+
"mcp-graphql-enhanced": "./dist/index.js"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist"
|
|
23
|
+
],
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=18"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"dev": "ts-node src/index.ts",
|
|
29
|
+
"build": "tsc && chmod +x dist/index.js",
|
|
30
|
+
"start": "node dist/index.js",
|
|
31
|
+
"format": "prettier --write ."
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@modelcontextprotocol/sdk": "1.12.0",
|
|
35
|
+
"graphql": "^16.11.0",
|
|
36
|
+
"yargs": "17.7.2",
|
|
37
|
+
"zod": "3.25.30",
|
|
38
|
+
"zod-to-json-schema": "3.24.5"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@graphql-tools/schema": "^10.0.23",
|
|
42
|
+
"@types/node": "^24.7.2",
|
|
43
|
+
"@types/yargs": "17.0.33",
|
|
44
|
+
"graphql-yoga": "^5.13.5",
|
|
45
|
+
"prettier": "^3.6.2",
|
|
46
|
+
"ts-node": "^10.9.2",
|
|
47
|
+
"typescript": "5.8.3"
|
|
48
|
+
},
|
|
49
|
+
"version": "2.0.5"
|
|
50
|
+
}
|