@letoribo/mcp-graphql-enhanced 3.2.0 → 3.3.0
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/README.md +25 -7
- package/dist/helpers/graphiql.d.ts +7 -0
- package/dist/helpers/graphiql.d.ts.map +1 -0
- package/dist/helpers/graphiql.js +76 -0
- package/dist/index.js +103 -197
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -4,6 +4,7 @@ An **enhanced MCP (Model Context Protocol) server for GraphQL** that fixes real-
|
|
|
4
4
|
> Drop-in replacement for `mcp-graphql` — with dynamic headers, robust variables parsing, and zero breaking changes.
|
|
5
5
|
|
|
6
6
|
## ✨ Key Enhancements
|
|
7
|
+
* ✅ **Built-in GraphiQL IDE** — Visual playground at http://localhost:MCP_PORT/ (or /graphiql) with pre-configured headers.
|
|
7
8
|
* ✅ **Dual Transport** — Supports both **STDIO** (for local CLI/client tools) and **HTTP/JSON-RPC** (for external/browser clients).
|
|
8
9
|
* ✅ **Dynamic headers** — pass `Authorization`, `X-API-Key`, etc., via tool arguments (no config restarts)
|
|
9
10
|
* ✅ **Robust variables parsing** — fixes `“Query variables must be a null or an object”` error
|
|
@@ -12,18 +13,27 @@ An **enhanced MCP (Model Context Protocol) server for GraphQL** that fixes real-
|
|
|
12
13
|
* ✅ **Secure by default** — mutations disabled unless explicitly enabled
|
|
13
14
|
|
|
14
15
|
---
|
|
16
|
+
## 🎨 Visual Command Center (GraphiQL)
|
|
17
|
+
Unlike standard MCP servers, this one provides a visual interface for humans. When running with `ENABLE_HTTP=true`, you can open a full-featured **GraphiQL IDE** in your browser.
|
|
18
|
+
|
|
19
|
+
* **Endpoint:** `http://localhost:6274/` (or `/graphiql`)
|
|
20
|
+
* **Header Sync:** Any headers set in your environment (like GitHub tokens) are automatically injected into the GraphiQL "Headers" tab for immediate testing.
|
|
15
21
|
|
|
16
22
|
## 💻 HTTP / Dual Transport
|
|
17
23
|
|
|
18
24
|
This server now runs in **dual transport mode**, supporting both the standard **STDIO** communication (used by most MCP clients) and a new **HTTP JSON-RPC** endpoint on port `6274`.
|
|
19
25
|
|
|
20
|
-
This allows external systems, web applications, and direct `curl` commands to access the server's tools.
|
|
26
|
+
This allows external systems, web applications, and direct `curl` commands to access the server's tools with **live request logging** in your terminal (`[HTTP-RPC]` logs).
|
|
21
27
|
|
|
22
28
|
| **Endpoint** | **Method** | **Description** |
|
|
23
29
|
| :--- | :--- | :--- |
|
|
24
|
-
| `/
|
|
30
|
+
| `/graphiql` | `GET` | Human Interface: The visual GraphQL IDE. |
|
|
31
|
+
| `/mcp` | `POST` | The main JSON-RPC 2.0 endpoint for tool execution. |
|
|
25
32
|
| `/health` | `GET` | Simple health check, returns `{ status: 'ok' }`. |
|
|
26
33
|
|
|
34
|
+
### Automatic Port Selection
|
|
35
|
+
The server defaults to port `6274`. If you encounter an `EADDRINUSE` error, the server will automatically find the next available port. **Check the server logs for the final bound port** (e.g., `[HTTP] Started server on http://localhost:6275`).
|
|
36
|
+
|
|
27
37
|
### Resolving Port Conflicts (EADDRINUSE) and Automatic Port Selection
|
|
28
38
|
|
|
29
39
|
The server defaults to port `6274`. If you encounter an `EADDRINUSE: address already in use :::6274` error (common in local development due to stale processes), the server will automatically find the next available port (up to 10 attempts, not spawning multiple servers).
|
|
@@ -43,28 +53,29 @@ curl http://localhost:6274/health
|
|
|
43
53
|
# Example: Test the query tool via JSON-RPC (using port 6275 if 6274 was busy)
|
|
44
54
|
curl -X POST http://localhost:6275/mcp -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","method":"query-graphql","params":{"query":"query { __typename }"},"id":1}'
|
|
45
55
|
|
|
46
|
-
## 🔍 Filtered Introspection
|
|
56
|
+
## 🔍 Filtered Introspection
|
|
47
57
|
Avoid 50k-line schema dumps. Ask for only what you need:
|
|
48
|
-
|
|
58
|
+
`@introspect-schema typeNames ["Query", "User"]`
|
|
49
59
|
## 🔍 Debug & Inspect
|
|
50
60
|
Use the official MCP Inspector to test your server live:
|
|
51
61
|
```bash
|
|
52
62
|
npx @modelcontextprotocol/inspector \
|
|
53
63
|
-e ENDPOINT=https://api.example.com/graphql \
|
|
54
|
-
npx @letoribo/mcp-graphql-enhanced
|
|
64
|
+
npx @letoribo/mcp-graphql-enhanced
|
|
55
65
|
```
|
|
56
66
|
### Environment Variables (Breaking change in 1.0.0)
|
|
57
67
|
> **Note:** As of version 1.0.0, command line arguments have been replaced with environment variables.
|
|
58
68
|
|
|
59
69
|
| Environment Variable | Description | Default |
|
|
60
|
-
|
|
70
|
+
| :--- | :--- | :--- |
|
|
61
71
|
| `ENDPOINT` | GraphQL endpoint URL | `https://mcp-neo4j-discord.vercel.app/api/graphiql` |
|
|
62
72
|
| `HEADERS` | JSON string containing headers for requests | `{}` |
|
|
63
73
|
| `ALLOW_MUTATIONS` | Enable mutation operations (disabled by default) | `false` |
|
|
64
74
|
| `NAME` | Name of the MCP server | `mcp-graphql-enhanced` |
|
|
65
|
-
| `SCHEMA` | Path to a local GraphQL schema file or URL
|
|
75
|
+
| `SCHEMA` | Path to a local GraphQL schema file or URL | - |
|
|
66
76
|
| `MCP_PORT` | Port for the HTTP/JSON-RPC server. | `6274` |
|
|
67
77
|
| `ENABLE_HTTP` | Enable HTTP transport: `auto` (default), `true`, or `false` | `auto` |
|
|
78
|
+
| `DEBUG` | Set to `mcp:*` for detailed SDK logs | - |
|
|
68
79
|
**Note on `ENABLE_HTTP`:**
|
|
69
80
|
- `auto` (default): Automatically enables HTTP only when running in MCP Inspector...
|
|
70
81
|
- `true`: Always enable HTTP server
|
|
@@ -89,6 +100,13 @@ npx @letoribo/mcp-graphql-enhanced
|
|
|
89
100
|
MCP_PORT=8080 npx @letoribo/mcp-graphql-enhanced
|
|
90
101
|
# Disable HTTP transport (fastest, recommended for Claude Desktop)
|
|
91
102
|
ENABLE_HTTP=false npx @letoribo/mcp-graphql-enhanced
|
|
103
|
+
# Test the surgical precision and the IDE immediately:
|
|
104
|
+
ENDPOINT=https://api.github.com/graphql \
|
|
105
|
+
HEADERS='{"Authorization":"Bearer YOUR_GITHUB_TOKEN"}' \
|
|
106
|
+
ENABLE_HTTP=true \
|
|
107
|
+
npx @letoribo/mcp-graphql-enhanced
|
|
108
|
+
|
|
109
|
+
# Then visit http://localhost:6274/graphiql
|
|
92
110
|
```
|
|
93
111
|
### 🖥️ Claude Desktop Configuration Examples
|
|
94
112
|
You can connect Claude Desktop to your GraphQL API using either the npx package (recommended for simplicity) or the Docker image (ideal for reproducibility and isolation).
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Renders the GraphiQL IDE template with visible headers
|
|
3
|
+
* @param endpoint The GraphQL API endpoint to connect to
|
|
4
|
+
* @param headers Optional default headers to inject into the editor
|
|
5
|
+
*/
|
|
6
|
+
export declare function renderGraphiQL(endpoint: string, headers?: object): string;
|
|
7
|
+
//# sourceMappingURL=graphiql.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"graphiql.d.ts","sourceRoot":"","sources":["../../src/helpers/graphiql.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,GAAE,MAAW,GAAG,MAAM,CAoE7E"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.renderGraphiQL = renderGraphiQL;
|
|
4
|
+
/**
|
|
5
|
+
* Renders the GraphiQL IDE template with visible headers
|
|
6
|
+
* @param endpoint The GraphQL API endpoint to connect to
|
|
7
|
+
* @param headers Optional default headers to inject into the editor
|
|
8
|
+
*/
|
|
9
|
+
function renderGraphiQL(endpoint, headers = {}) {
|
|
10
|
+
// Stringify headers for the injection into the script
|
|
11
|
+
const stringifiedHeaders = JSON.stringify(headers, null, 2);
|
|
12
|
+
return `
|
|
13
|
+
<!DOCTYPE html>
|
|
14
|
+
<html lang="en">
|
|
15
|
+
<head>
|
|
16
|
+
<meta charset="utf-8" />
|
|
17
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
18
|
+
<title>GraphiQL Explorer | Surgical MCP</title>
|
|
19
|
+
<link href="https://unpkg.com/graphiql@3.8.2/graphiql.min.css" rel="stylesheet" />
|
|
20
|
+
<style>
|
|
21
|
+
body {
|
|
22
|
+
margin: 0;
|
|
23
|
+
height: 100vh;
|
|
24
|
+
width: 100vw;
|
|
25
|
+
overflow: hidden;
|
|
26
|
+
background: #0b1016;
|
|
27
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
28
|
+
}
|
|
29
|
+
#graphiql { height: 100vh; }
|
|
30
|
+
.loading-screen {
|
|
31
|
+
color: white;
|
|
32
|
+
display: flex;
|
|
33
|
+
justify-content: center;
|
|
34
|
+
align-items: center;
|
|
35
|
+
height: 100%;
|
|
36
|
+
font-size: 1.2rem;
|
|
37
|
+
}
|
|
38
|
+
</style>
|
|
39
|
+
</head>
|
|
40
|
+
<body>
|
|
41
|
+
<div id="graphiql">
|
|
42
|
+
<div class="loading-screen">Initializing Surgical GraphiQL...</div>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<script src="https://unpkg.com/react@18.2.0/umd/react.production.min.js" crossorigin referrerpolicy="no-referrer"></script>
|
|
46
|
+
<script src="https://unpkg.com/react-dom@18.2.0/umd/react-dom.production.min.js" crossorigin referrerpolicy="no-referrer"></script>
|
|
47
|
+
<script src="https://unpkg.com/graphiql@3.8.2/graphiql.min.js" crossorigin referrerpolicy="no-referrer"></script>
|
|
48
|
+
|
|
49
|
+
<script>
|
|
50
|
+
window.addEventListener('load', function() {
|
|
51
|
+
if (typeof GraphiQL !== 'undefined') {
|
|
52
|
+
const fetcher = GraphiQL.createFetcher({
|
|
53
|
+
url: '${endpoint}'
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const root = ReactDOM.createRoot(document.getElementById('graphiql'));
|
|
57
|
+
root.render(
|
|
58
|
+
React.createElement(GraphiQL, {
|
|
59
|
+
fetcher: fetcher,
|
|
60
|
+
headerEditorEnabled: true,
|
|
61
|
+
shouldPersistHeaders: true,
|
|
62
|
+
// This makes the headers visible in the UI tab
|
|
63
|
+
defaultHeaders: \`${stringifiedHeaders}\`,
|
|
64
|
+
theme: 'dark',
|
|
65
|
+
defaultEditorToolsVisibility: true
|
|
66
|
+
})
|
|
67
|
+
);
|
|
68
|
+
} else {
|
|
69
|
+
document.getElementById('graphiql').innerHTML =
|
|
70
|
+
'<div class="loading-screen" style="color: #ff4d4d">Error: GraphiQL SDK failed to load.</div>';
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
</script>
|
|
74
|
+
</body>
|
|
75
|
+
</html>`;
|
|
76
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -5,25 +5,32 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
};
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
7
|
const node_http_1 = __importDefault(require("node:http"));
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
|
|
8
|
+
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
9
|
+
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
10
|
+
const language_1 = require("graphql/language");
|
|
11
|
+
const zod_1 = __importDefault(require("zod"));
|
|
12
|
+
const graphiql_js_1 = require("./helpers/graphiql.js");
|
|
13
|
+
// Helper imports
|
|
14
|
+
const deprecation_js_1 = require("./helpers/deprecation.js");
|
|
15
|
+
const introspection_js_1 = require("./helpers/introspection.js");
|
|
14
16
|
const getVersion = () => {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
+
try {
|
|
18
|
+
const pkg = require("../package.json");
|
|
19
|
+
return pkg.version;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return "3.2.1";
|
|
23
|
+
}
|
|
17
24
|
};
|
|
18
|
-
checkDeprecatedArguments();
|
|
19
|
-
const EnvSchema =
|
|
20
|
-
NAME:
|
|
21
|
-
ENDPOINT:
|
|
22
|
-
ALLOW_MUTATIONS:
|
|
25
|
+
(0, deprecation_js_1.checkDeprecatedArguments)();
|
|
26
|
+
const EnvSchema = zod_1.default.object({
|
|
27
|
+
NAME: zod_1.default.string().default("mcp-graphql-enhanced"),
|
|
28
|
+
ENDPOINT: zod_1.default.preprocess((val) => (typeof val === 'string' ? val.trim() : val), zod_1.default.string().url()).default("https://mcp-neo4j-discord.vercel.app/api/graphiql"),
|
|
29
|
+
ALLOW_MUTATIONS: zod_1.default
|
|
23
30
|
.enum(["true", "false"])
|
|
24
31
|
.transform((value) => value === "true")
|
|
25
32
|
.default("false"),
|
|
26
|
-
HEADERS:
|
|
33
|
+
HEADERS: zod_1.default
|
|
27
34
|
.string()
|
|
28
35
|
.default("{}")
|
|
29
36
|
.transform((val) => {
|
|
@@ -34,32 +41,27 @@ const EnvSchema = z.object({
|
|
|
34
41
|
throw new Error("HEADERS must be a valid JSON string");
|
|
35
42
|
}
|
|
36
43
|
}),
|
|
37
|
-
SCHEMA:
|
|
38
|
-
MCP_PORT:
|
|
39
|
-
ENABLE_HTTP:
|
|
44
|
+
SCHEMA: zod_1.default.string().optional(),
|
|
45
|
+
MCP_PORT: zod_1.default.preprocess((val) => (val ? parseInt(val) : 6274), zod_1.default.number().int().min(1024).max(65535)).default(6274),
|
|
46
|
+
ENABLE_HTTP: zod_1.default
|
|
40
47
|
.enum(["true", "false", "auto"])
|
|
41
48
|
.transform((value) => {
|
|
42
49
|
if (value === "auto") {
|
|
43
|
-
return !!(process.env.MCP_INSPECTOR || process.env.INSPECTOR_PORT);
|
|
50
|
+
return !!(process.env.MCP_INSPECTOR || process.env.INSPECTOR_PORT || process.env.MCP_PORT);
|
|
44
51
|
}
|
|
45
52
|
return value === "true";
|
|
46
53
|
})
|
|
47
54
|
.default("auto"),
|
|
48
55
|
});
|
|
49
56
|
const env = EnvSchema.parse(process.env);
|
|
50
|
-
const server = new McpServer({
|
|
57
|
+
const server = new mcp_js_1.McpServer({
|
|
51
58
|
name: env.NAME,
|
|
52
59
|
version: getVersion(),
|
|
53
|
-
description: `GraphQL MCP server for ${env.ENDPOINT}`,
|
|
54
60
|
});
|
|
55
61
|
// --- CACHE STATE ---
|
|
56
62
|
let cachedSDL = null;
|
|
57
63
|
let cachedSchemaObject = null;
|
|
58
64
|
let schemaLoadError = null;
|
|
59
|
-
/**
|
|
60
|
-
* Loads the schema into memory.
|
|
61
|
-
* Populates both cachedSDL (string) and cachedSchemaObject (GraphQLSchema object).
|
|
62
|
-
*/
|
|
63
65
|
async function getSchema() {
|
|
64
66
|
if (cachedSDL)
|
|
65
67
|
return cachedSDL;
|
|
@@ -69,17 +71,18 @@ async function getSchema() {
|
|
|
69
71
|
const { buildClientSchema, getIntrospectionQuery, printSchema, buildASTSchema, parse: gqlParse } = require("graphql");
|
|
70
72
|
let sdl;
|
|
71
73
|
if (env.SCHEMA) {
|
|
74
|
+
// Check if it's a URL or local path
|
|
72
75
|
if (env.SCHEMA.startsWith("http")) {
|
|
73
|
-
|
|
76
|
+
const response = await fetch(env.SCHEMA);
|
|
77
|
+
sdl = await response.text();
|
|
74
78
|
}
|
|
75
79
|
else {
|
|
76
|
-
sdl = await introspectLocalSchema(env.SCHEMA);
|
|
80
|
+
sdl = await (0, introspection_js_1.introspectLocalSchema)(env.SCHEMA);
|
|
77
81
|
}
|
|
78
82
|
cachedSchemaObject = buildASTSchema(gqlParse(sdl));
|
|
79
83
|
cachedSDL = sdl;
|
|
80
84
|
}
|
|
81
85
|
else {
|
|
82
|
-
// Live Introspection
|
|
83
86
|
const response = await fetch(env.ENDPOINT, {
|
|
84
87
|
method: "POST",
|
|
85
88
|
headers: { "Content-Type": "application/json", ...env.HEADERS },
|
|
@@ -99,93 +102,15 @@ async function getSchema() {
|
|
|
99
102
|
throw schemaLoadError;
|
|
100
103
|
}
|
|
101
104
|
}
|
|
102
|
-
// --- RESOURCES ---
|
|
103
|
-
server.resource("graphql-schema", new URL(env.ENDPOINT).href, async (uri) => {
|
|
104
|
-
try {
|
|
105
|
-
const sdl = await getSchema();
|
|
106
|
-
return { contents: [{ uri: uri.href, text: sdl }] };
|
|
107
|
-
}
|
|
108
|
-
catch (error) {
|
|
109
|
-
throw error;
|
|
110
|
-
}
|
|
111
|
-
});
|
|
112
105
|
// --- TOOL HANDLERS ---
|
|
113
106
|
const toolHandlers = new Map();
|
|
114
|
-
|
|
115
|
-
try {
|
|
116
|
-
// Ensure cache is populated
|
|
117
|
-
await getSchema();
|
|
118
|
-
// Safety: If no specific types requested, return the 'Map' of types to prevent bridge crash
|
|
119
|
-
if (!typeNames || typeNames.length === 0) {
|
|
120
|
-
const allTypeNames = Object.keys(cachedSchemaObject.getTypeMap())
|
|
121
|
-
.filter(t => !t.startsWith('__'));
|
|
122
|
-
return {
|
|
123
|
-
content: [{
|
|
124
|
-
type: "text",
|
|
125
|
-
text: `Schema is large. Please request specific types for full details.\n\nAvailable types: ${allTypeNames.join(", ")}`
|
|
126
|
-
}]
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
// Use the new filtering logic from helpers/introspection.js
|
|
130
|
-
const filteredResult = introspectSpecificTypes(cachedSchemaObject, typeNames);
|
|
131
|
-
return {
|
|
132
|
-
content: [{
|
|
133
|
-
type: "text",
|
|
134
|
-
text: JSON.stringify(filteredResult, null, 2)
|
|
135
|
-
}]
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
catch (error) {
|
|
139
|
-
throw new Error(`Introspection failed: ${error.message}`);
|
|
140
|
-
}
|
|
141
|
-
};
|
|
142
|
-
toolHandlers.set("introspect-schema", introspectSchemaHandler);
|
|
143
|
-
server.tool("introspect-schema", "Introspect the GraphQL schema. Optionally filter to specific types.", {
|
|
144
|
-
typeNames: z.array(z.string()).optional().describe("A list of specific type names to filter (e.g. ['User', 'Post'])."),
|
|
145
|
-
}, async ({ typeNames }) => {
|
|
146
|
-
try {
|
|
147
|
-
console.error(`[TOOL] Introspect called with types: ${JSON.stringify(typeNames || "NONE")}`);
|
|
148
|
-
// 1. Ensure the schema is loaded into the cache
|
|
149
|
-
await getSchema();
|
|
150
|
-
// 2. THE GATEKEEPER: If no types requested, send ONLY names.
|
|
151
|
-
if (!typeNames || typeNames.length === 0) {
|
|
152
|
-
const allTypeNames = Object.keys(cachedSchemaObject.getTypeMap())
|
|
153
|
-
.filter(t => !t.startsWith('__'));
|
|
154
|
-
console.error(`[TOOL] Sending summary list of ${allTypeNames.length} types.`);
|
|
155
|
-
return {
|
|
156
|
-
content: [{
|
|
157
|
-
type: "text",
|
|
158
|
-
text: `Schema is large. Please request specific types for details.\n\nAvailable types: ${allTypeNames.join(", ")}`
|
|
159
|
-
}]
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
// 3. DRILL DOWN: If Claude asks for specific types, use the helper.
|
|
163
|
-
console.error(`[TOOL] Filtering for: ${typeNames.join(", ")}`);
|
|
164
|
-
const filteredResult = introspectSpecificTypes(cachedSchemaObject, typeNames);
|
|
165
|
-
return {
|
|
166
|
-
content: [{
|
|
167
|
-
type: "text",
|
|
168
|
-
text: JSON.stringify(filteredResult, null, 2)
|
|
169
|
-
}]
|
|
170
|
-
};
|
|
171
|
-
}
|
|
172
|
-
catch (error) {
|
|
173
|
-
console.error(`[TOOL ERROR] ${error.message}`);
|
|
174
|
-
throw new Error(`Introspection failed: ${error.message}`);
|
|
175
|
-
}
|
|
176
|
-
});
|
|
107
|
+
// Tool: query-graphql
|
|
177
108
|
const queryGraphqlHandler = async ({ query, variables, headers }) => {
|
|
178
109
|
try {
|
|
179
|
-
const parsedQuery = parse(query);
|
|
110
|
+
const parsedQuery = (0, language_1.parse)(query);
|
|
180
111
|
const isMutation = parsedQuery.definitions.some((def) => def.kind === "OperationDefinition" && def.operation === "mutation");
|
|
181
|
-
if (isMutation && !env.ALLOW_MUTATIONS)
|
|
182
|
-
throw new Error("Mutations are not allowed.
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
catch (error) {
|
|
186
|
-
throw new Error(`Invalid GraphQL query: ${error}`);
|
|
187
|
-
}
|
|
188
|
-
try {
|
|
112
|
+
if (isMutation && !env.ALLOW_MUTATIONS)
|
|
113
|
+
throw new Error("Mutations are not allowed.");
|
|
189
114
|
const toolHeaders = headers ? JSON.parse(headers) : {};
|
|
190
115
|
const allHeaders = { "Content-Type": "application/json", ...env.HEADERS, ...toolHeaders };
|
|
191
116
|
let parsedVariables = variables;
|
|
@@ -196,124 +121,105 @@ const queryGraphqlHandler = async ({ query, variables, headers }) => {
|
|
|
196
121
|
headers: allHeaders,
|
|
197
122
|
body: JSON.stringify({ query, variables: parsedVariables }),
|
|
198
123
|
});
|
|
199
|
-
if (!response.ok) {
|
|
200
|
-
const responseText = await response.text();
|
|
201
|
-
throw new Error(`GraphQL request failed: ${response.statusText}\n${responseText}`);
|
|
202
|
-
}
|
|
203
124
|
const data = await response.json();
|
|
204
|
-
if (data.errors && data.errors.length > 0) {
|
|
205
|
-
throw new Error(`GraphQL errors: ${JSON.stringify(data.errors, null, 2)}`);
|
|
206
|
-
}
|
|
207
125
|
return {
|
|
208
|
-
content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
|
|
126
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
|
|
209
127
|
};
|
|
210
128
|
}
|
|
211
129
|
catch (error) {
|
|
212
|
-
throw new Error(`
|
|
130
|
+
throw new Error(`Execution failed: ${error.message}`);
|
|
213
131
|
}
|
|
214
132
|
};
|
|
215
133
|
toolHandlers.set("query-graphql", queryGraphqlHandler);
|
|
216
|
-
server.tool("query-graphql", "
|
|
217
|
-
query:
|
|
218
|
-
variables:
|
|
219
|
-
headers:
|
|
134
|
+
server.tool("query-graphql", "Execute a GraphQL query against the endpoint", {
|
|
135
|
+
query: zod_1.default.string(),
|
|
136
|
+
variables: zod_1.default.string().optional(),
|
|
137
|
+
headers: zod_1.default.string().optional(),
|
|
220
138
|
}, queryGraphqlHandler);
|
|
139
|
+
// Tool: introspect-schema
|
|
140
|
+
const introspectHandler = async ({ typeNames }) => {
|
|
141
|
+
await getSchema();
|
|
142
|
+
if (!typeNames || typeNames.length === 0) {
|
|
143
|
+
const allTypeNames = Object.keys(cachedSchemaObject.getTypeMap()).filter(t => !t.startsWith('__'));
|
|
144
|
+
return {
|
|
145
|
+
content: [{ type: "text", text: `Schema is large. Available types: ${allTypeNames.join(", ")}` }]
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
const filtered = (0, introspection_js_1.introspectSpecificTypes)(cachedSchemaObject, typeNames);
|
|
149
|
+
return {
|
|
150
|
+
content: [{ type: "text", text: JSON.stringify(filtered, null, 2) }]
|
|
151
|
+
};
|
|
152
|
+
};
|
|
153
|
+
toolHandlers.set("introspect-schema", introspectHandler);
|
|
154
|
+
server.tool("introspect-schema", "Introspect the GraphQL schema with optional type filtering", {
|
|
155
|
+
typeNames: zod_1.default.array(zod_1.default.string()).optional(),
|
|
156
|
+
}, introspectHandler);
|
|
221
157
|
// --- HTTP SERVER LOGIC ---
|
|
222
|
-
function readBody(req) {
|
|
223
|
-
return new Promise((resolve, reject) => {
|
|
224
|
-
let body = '';
|
|
225
|
-
req.on('data', chunk => body += chunk.toString());
|
|
226
|
-
req.on('end', () => resolve(body));
|
|
227
|
-
req.on('error', reject);
|
|
228
|
-
});
|
|
229
|
-
}
|
|
230
158
|
async function handleHttpRequest(req, res) {
|
|
231
159
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
232
|
-
res.setHeader('Access-Control-Allow-Methods', '
|
|
160
|
+
res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');
|
|
233
161
|
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
234
162
|
if (req.method === 'OPTIONS') {
|
|
235
163
|
res.writeHead(204);
|
|
236
164
|
res.end();
|
|
237
165
|
return;
|
|
238
166
|
}
|
|
239
|
-
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
res.
|
|
243
|
-
|
|
167
|
+
const url = new URL(req.url || '', `http://${req.headers.host}`);
|
|
168
|
+
// NEW FEATURE: Web GUI for Humans
|
|
169
|
+
if (req.method === 'GET' && (url.pathname === '/' || url.pathname === '/graphiql')) {
|
|
170
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
171
|
+
// Pass env.HEADERS to the explorer
|
|
172
|
+
return res.end((0, graphiql_js_1.renderGraphiQL)(env.ENDPOINT, env.HEADERS));
|
|
244
173
|
}
|
|
245
174
|
if (url.pathname === '/mcp' && req.method === 'POST') {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
175
|
+
let body = '';
|
|
176
|
+
req.on('data', chunk => { body += chunk; });
|
|
177
|
+
req.on('end', async () => {
|
|
178
|
+
try {
|
|
179
|
+
const { method, params, id } = JSON.parse(body);
|
|
180
|
+
console.error(`[HTTP-RPC] Method: ${method} | ID: ${id}`);
|
|
181
|
+
const handler = toolHandlers.get(method);
|
|
182
|
+
if (!handler) {
|
|
183
|
+
res.writeHead(404);
|
|
184
|
+
return res.end(JSON.stringify({ jsonrpc: '2.0', id, error: { code: -32601, message: "Method not found" } }));
|
|
185
|
+
}
|
|
186
|
+
const result = await handler(params);
|
|
187
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
188
|
+
res.end(JSON.stringify({ jsonrpc: '2.0', id, result }));
|
|
254
189
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
res.end(JSON.stringify({ jsonrpc: '2.0', error: { code: -32603, message: 'Internal error' } }));
|
|
262
|
-
}
|
|
190
|
+
catch (e) {
|
|
191
|
+
console.error(`[HTTP-ERROR] ${e.message}`);
|
|
192
|
+
res.writeHead(500);
|
|
193
|
+
res.end(JSON.stringify({ jsonrpc: '2.0', error: { code: -32603, message: "Internal Error" } }));
|
|
194
|
+
}
|
|
195
|
+
});
|
|
263
196
|
return;
|
|
264
197
|
}
|
|
198
|
+
if (url.pathname === '/health') {
|
|
199
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
200
|
+
return res.end(JSON.stringify({ status: "ok", server: env.NAME }));
|
|
201
|
+
}
|
|
265
202
|
res.writeHead(404);
|
|
266
|
-
res.end(
|
|
267
|
-
}
|
|
268
|
-
let httpServer = null;
|
|
269
|
-
async function startHttpServer(initialPort) {
|
|
270
|
-
return new Promise((resolve, reject) => {
|
|
271
|
-
let port = initialPort;
|
|
272
|
-
const server = node_http_1.default.createServer(handleHttpRequest);
|
|
273
|
-
server.once('error', (err) => {
|
|
274
|
-
if (err.code === 'EADDRINUSE') {
|
|
275
|
-
server.close();
|
|
276
|
-
resolve(startHttpServer(port + 1));
|
|
277
|
-
}
|
|
278
|
-
else
|
|
279
|
-
reject(err);
|
|
280
|
-
});
|
|
281
|
-
server.listen(port, () => {
|
|
282
|
-
httpServer = server;
|
|
283
|
-
resolve(port);
|
|
284
|
-
});
|
|
285
|
-
});
|
|
203
|
+
res.end("Not Found");
|
|
286
204
|
}
|
|
287
205
|
// --- STARTUP ---
|
|
288
206
|
async function main() {
|
|
289
|
-
console.error(`[SCHEMA] Pre-loading GraphQL schema...`);
|
|
290
|
-
const schemaPromise = getSchema().catch(error => {
|
|
291
|
-
console.error(`[SCHEMA] Warning: Failed to pre-load schema: ${error}`);
|
|
292
|
-
});
|
|
293
|
-
const stdioTransport = new StdioServerTransport();
|
|
294
|
-
await server.connect(stdioTransport);
|
|
295
|
-
console.error(`[STDIO] Started GraphQL MCP server ${env.NAME}`);
|
|
296
207
|
if (env.ENABLE_HTTP) {
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
console.error(`[HTTP] Server started on http://localhost:${
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
}
|
|
208
|
+
const serverHttp = node_http_1.default.createServer(handleHttpRequest);
|
|
209
|
+
serverHttp.listen(env.MCP_PORT, () => {
|
|
210
|
+
console.error(`[HTTP] Server started on http://localhost:${env.MCP_PORT}`);
|
|
211
|
+
console.error(`🎨 GraphiQL IDE: http://localhost:${env.MCP_PORT}/graphiql`);
|
|
212
|
+
console.error(`🤖 MCP Endpoint: http://localhost:${env.MCP_PORT}/mcp\n`);
|
|
213
|
+
});
|
|
304
214
|
}
|
|
305
|
-
|
|
215
|
+
const transport = new stdio_js_1.StdioServerTransport();
|
|
216
|
+
await server.connect(transport);
|
|
217
|
+
console.error(`[STDIO] MCP Server "${env.NAME}" v${getVersion()} started`);
|
|
218
|
+
getSchema().catch(e => console.error(`[SCHEMA] Warning: Preload failed: ${e.message}`));
|
|
306
219
|
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
if (httpServer)
|
|
310
|
-
httpServer.close(() => process.exit(0));
|
|
311
|
-
else
|
|
312
|
-
process.exit(0);
|
|
313
|
-
};
|
|
314
|
-
process.on('SIGINT', shutdown);
|
|
315
|
-
process.on('SIGTERM', shutdown);
|
|
220
|
+
process.on('SIGINT', () => process.exit(0));
|
|
221
|
+
process.on('SIGTERM', () => process.exit(0));
|
|
316
222
|
main().catch(error => {
|
|
317
|
-
console.error(`
|
|
223
|
+
console.error(`[FATAL] ${error}`);
|
|
318
224
|
process.exit(1);
|
|
319
225
|
});
|
package/package.json
CHANGED
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"node": ">=18"
|
|
30
30
|
},
|
|
31
31
|
"scripts": {
|
|
32
|
-
"dev": "
|
|
32
|
+
"dev": "tsx --watch src/index.ts",
|
|
33
33
|
"build": "tsc && chmod +x dist/index.js",
|
|
34
34
|
"start": "node dist/index.js",
|
|
35
35
|
"format": "prettier --write ."
|
|
@@ -47,7 +47,8 @@
|
|
|
47
47
|
"@types/yargs": "17.0.33",
|
|
48
48
|
"prettier": "^3.6.2",
|
|
49
49
|
"ts-node": "^10.9.2",
|
|
50
|
+
"tsx": "^4.21.0",
|
|
50
51
|
"typescript": "5.8.3"
|
|
51
52
|
},
|
|
52
|
-
"version": "3.
|
|
53
|
+
"version": "3.3.0"
|
|
53
54
|
}
|