@letoribo/mcp-graphql-enhanced 2.3.1 → 2.3.3
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 +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +326 -311
- package/package.json +1 -4
package/README.md
CHANGED
|
@@ -42,7 +42,7 @@ curl http://localhost:6274/health
|
|
|
42
42
|
|
|
43
43
|
# Example: Test the query tool via JSON-RPC (using port 6275 if 6274 was busy)
|
|
44
44
|
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
|
-
|
|
45
|
+
|
|
46
46
|
## 🔍 Filtered Introspection (New!)
|
|
47
47
|
Avoid 50k-line schema dumps. Ask for only what you need:
|
|
48
48
|
```@introspect-schema typeNames ["Query", "User"]```
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
export
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
export {};
|
|
3
3
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
|
package/dist/index.js
CHANGED
|
@@ -1,349 +1,364 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
1
2
|
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
-
var ownKeys = function(o) {
|
|
20
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
-
var ar = [];
|
|
22
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
-
return ar;
|
|
24
|
-
};
|
|
25
|
-
return ownKeys(o);
|
|
26
|
-
};
|
|
27
|
-
return function (mod) {
|
|
28
|
-
if (mod && mod.__esModule) return mod;
|
|
29
|
-
var result = {};
|
|
30
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
-
__setModuleDefault(result, mod);
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
34
|
-
})();
|
|
35
3
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
4
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
5
|
};
|
|
38
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
catch (error) {
|
|
80
|
-
console.error('[Setup] CRITICAL RUNTIME ERROR: Failed to initialize Apollo Client:', error instanceof Error ? error.message : String(error));
|
|
81
|
-
}
|
|
82
|
-
// --- GraphQL Introspection Helpers (Using global fetch) ---
|
|
83
|
-
/**
|
|
84
|
-
* Executes a GraphQL introspection query against a remote endpoint.
|
|
85
|
-
*/
|
|
86
|
-
const executeIntrospection = async (endpoint, headers, query) => {
|
|
87
|
-
let response;
|
|
7
|
+
const node_http_1 = __importDefault(require("node:http"));
|
|
8
|
+
const { McpServer } = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
9
|
+
const { StdioServerTransport } = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
10
|
+
const { parse } = require("graphql/language");
|
|
11
|
+
const z = require("zod").default;
|
|
12
|
+
const { checkDeprecatedArguments } = require("./helpers/deprecation.js");
|
|
13
|
+
const { introspectEndpoint, introspectLocalSchema, introspectSchemaFromUrl, introspectTypes, } = require("./helpers/introspection.js");
|
|
14
|
+
const getVersion = () => {
|
|
15
|
+
const pkg = require("../package.json");
|
|
16
|
+
return pkg.version;
|
|
17
|
+
};
|
|
18
|
+
checkDeprecatedArguments();
|
|
19
|
+
const EnvSchema = z.object({
|
|
20
|
+
NAME: z.string().default("mcp-graphql-enhanced"),
|
|
21
|
+
ENDPOINT: z.preprocess((val) => (typeof val === 'string' ? val.trim() : val), z.string().url("ENDPOINT must be a valid URL (e.g., https://example.com/graphql)")).default("https://mcp-neo4j-discord.vercel.app/api/graphiql"),
|
|
22
|
+
ALLOW_MUTATIONS: z
|
|
23
|
+
.enum(["true", "false"])
|
|
24
|
+
.transform((value) => value === "true")
|
|
25
|
+
.default("false"),
|
|
26
|
+
HEADERS: z
|
|
27
|
+
.string()
|
|
28
|
+
.default("{}")
|
|
29
|
+
.transform((val) => {
|
|
30
|
+
try {
|
|
31
|
+
return JSON.parse(val);
|
|
32
|
+
}
|
|
33
|
+
catch (e) {
|
|
34
|
+
throw new Error("HEADERS must be a valid JSON string");
|
|
35
|
+
}
|
|
36
|
+
}),
|
|
37
|
+
SCHEMA: z.string().optional(),
|
|
38
|
+
MCP_PORT: z.preprocess((val) => (val ? parseInt(val) : 6274), z.number().int().min(1024).max(65535)).default(6274),
|
|
39
|
+
});
|
|
40
|
+
const env = EnvSchema.parse(process.env);
|
|
41
|
+
const server = new McpServer({
|
|
42
|
+
name: env.NAME,
|
|
43
|
+
version: getVersion(),
|
|
44
|
+
description: `GraphQL MCP server for ${env.ENDPOINT}`,
|
|
45
|
+
});
|
|
46
|
+
server.resource("graphql-schema", new URL(env.ENDPOINT).href, async (uri) => {
|
|
88
47
|
try {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
48
|
+
let schema;
|
|
49
|
+
if (env.SCHEMA) {
|
|
50
|
+
if (env.SCHEMA.startsWith("http://") ||
|
|
51
|
+
env.SCHEMA.startsWith("https://")) {
|
|
52
|
+
schema = await introspectSchemaFromUrl(env.SCHEMA);
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
schema = await introspectLocalSchema(env.SCHEMA);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
schema = await introspectEndpoint(env.ENDPOINT, env.HEADERS);
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
contents: [
|
|
63
|
+
{
|
|
64
|
+
uri: uri.href,
|
|
65
|
+
text: schema,
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
};
|
|
106
69
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
throw new Error(`GraphQL errors during introspection: ${JSON.stringify(json.errors)}`);
|
|
70
|
+
catch (error) {
|
|
71
|
+
throw new Error(`Failed to get GraphQL schema: ${error}`);
|
|
110
72
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const introspectEndpoint = async (endpoint, headers) => {
|
|
117
|
-
const data = await executeIntrospection(endpoint, headers, (0, graphql_1.getIntrospectionQuery)());
|
|
118
|
-
const schema = (0, graphql_1.buildClientSchema)(data);
|
|
119
|
-
return (0, graphql_1.printSchema)(schema);
|
|
120
|
-
};
|
|
121
|
-
/**
|
|
122
|
-
* Introspects the endpoint and returns an annotated schema, optionally filtering root fields.
|
|
123
|
-
*/
|
|
124
|
-
const introspectTypes = async (endpoint, headers, typeNames) => {
|
|
125
|
-
const data = await executeIntrospection(endpoint, headers, (0, graphql_1.getIntrospectionQuery)());
|
|
126
|
-
const schema = (0, graphql_1.buildClientSchema)(data);
|
|
127
|
-
// Fallback to full schema if no typeNames are provided or if filtering is not feasible
|
|
128
|
-
if (!typeNames || typeNames.length === 0) {
|
|
129
|
-
return `--- Full Schema Introspection ---\nEndpoint: ${endpoint}\n\n${(0, graphql_1.printSchema)(schema)}`;
|
|
73
|
+
});
|
|
74
|
+
const toolHandlers = new Map();
|
|
75
|
+
const introspectSchemaHandler = async ({ typeNames, descriptions = true, directives = true }) => {
|
|
76
|
+
if (typeNames === null) {
|
|
77
|
+
typeNames = undefined;
|
|
130
78
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
// 2. The user explicitly requested the "Query" type itself.
|
|
146
|
-
if (includeAllQueryFields || requestedTypeSet.has(returnType.name)) {
|
|
147
|
-
// Build a representation of the field including arguments and full type string
|
|
148
|
-
const args = field.args.map(arg => `${arg.name}: ${arg.type.toString()}`).join(', ');
|
|
149
|
-
keptQueryFields.push(` ${fieldName}${args ? `(${args})` : ''}: ${field.type.toString()}`);
|
|
79
|
+
try {
|
|
80
|
+
if (typeNames && typeNames.length > 0) {
|
|
81
|
+
const filtered = await introspectTypes(env.ENDPOINT, env.HEADERS, typeNames);
|
|
82
|
+
return { content: [{ type: "text", text: filtered }] };
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
let schema;
|
|
86
|
+
if (env.SCHEMA) {
|
|
87
|
+
if (env.SCHEMA.startsWith("http://") || env.SCHEMA.startsWith("https://")) {
|
|
88
|
+
schema = await introspectSchemaFromUrl(env.SCHEMA);
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
schema = await introspectLocalSchema(env.SCHEMA);
|
|
92
|
+
}
|
|
150
93
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
94
|
+
else {
|
|
95
|
+
schema = await introspectEndpoint(env.ENDPOINT, env.HEADERS);
|
|
96
|
+
}
|
|
97
|
+
return { content: [{ type: "text", text: schema }] };
|
|
155
98
|
}
|
|
156
99
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
};
|
|
162
|
-
/**
|
|
163
|
-
* Fetches a schema file from a remote URL.
|
|
164
|
-
*/
|
|
165
|
-
const introspectSchemaFromUrl = async (schemaUrl) => {
|
|
166
|
-
// Use the global fetch function
|
|
167
|
-
const response = await fetch(schemaUrl);
|
|
168
|
-
if (!response.ok) {
|
|
169
|
-
throw new Error(`Failed to fetch schema from ${schemaUrl}: ${response.statusText}`);
|
|
100
|
+
catch (error) {
|
|
101
|
+
return {
|
|
102
|
+
isError: true,
|
|
103
|
+
content: [{ type: "text", text: `Introspection failed: ${error}` }],
|
|
104
|
+
};
|
|
170
105
|
}
|
|
171
|
-
return response.text();
|
|
172
|
-
};
|
|
173
|
-
/**
|
|
174
|
-
* Handles local file loading (not supported on Vercel).
|
|
175
|
-
*/
|
|
176
|
-
const introspectLocalSchema = async (filePath) => {
|
|
177
|
-
throw new Error("Local schema file loading is not supported in the Vercel environment.");
|
|
178
106
|
};
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
107
|
+
toolHandlers.set("introspect-schema", introspectSchemaHandler);
|
|
108
|
+
server.tool("introspect-schema", "Introspect the GraphQL schema. Optionally filter to specific types.", {
|
|
109
|
+
typeNames: z.array(z.string()).optional().describe("A list of specific type names to filter the introspection."),
|
|
110
|
+
descriptions: z.boolean().optional().default(true),
|
|
111
|
+
directives: z.boolean().optional().default(true),
|
|
112
|
+
}, introspectSchemaHandler);
|
|
113
|
+
const queryGraphqlHandler = 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) {
|
|
188
130
|
return {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
131
|
+
isError: true,
|
|
132
|
+
content: [
|
|
133
|
+
{
|
|
134
|
+
type: "text",
|
|
135
|
+
text: `Invalid GraphQL query: ${error}`,
|
|
136
|
+
},
|
|
137
|
+
],
|
|
195
138
|
};
|
|
196
139
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
if (result.data) {
|
|
211
|
-
return {
|
|
212
|
-
jsonrpc: '2.0',
|
|
213
|
-
id: id,
|
|
214
|
-
// Map the raw GraphQL data to the JSON-RPC 'result' field
|
|
215
|
-
result: result.data,
|
|
216
|
-
};
|
|
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
|
+
let parsedVariables = null;
|
|
150
|
+
if (variables) {
|
|
151
|
+
if (typeof variables === 'string') {
|
|
152
|
+
parsedVariables = JSON.parse(variables);
|
|
217
153
|
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
console.error('GraphQL Execution Errors Found:', result.errors);
|
|
221
|
-
// Return a JSON-RPC Error object if GraphQL execution failed
|
|
222
|
-
return {
|
|
223
|
-
jsonrpc: "2.0",
|
|
224
|
-
error: {
|
|
225
|
-
code: -32000, // Standard JSON-RPC error code for Server Error
|
|
226
|
-
message: "GraphQL Execution Error",
|
|
227
|
-
data: result.errors
|
|
228
|
-
},
|
|
229
|
-
id: id
|
|
230
|
-
};
|
|
154
|
+
else {
|
|
155
|
+
parsedVariables = variables;
|
|
231
156
|
}
|
|
232
|
-
|
|
157
|
+
}
|
|
158
|
+
const response = await fetch(env.ENDPOINT, {
|
|
159
|
+
method: "POST",
|
|
160
|
+
headers: allHeaders,
|
|
161
|
+
body: JSON.stringify({
|
|
162
|
+
query,
|
|
163
|
+
variables: parsedVariables,
|
|
164
|
+
}),
|
|
165
|
+
});
|
|
166
|
+
if (!response.ok) {
|
|
167
|
+
const responseText = await response.text();
|
|
233
168
|
return {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
169
|
+
isError: true,
|
|
170
|
+
content: [
|
|
171
|
+
{
|
|
172
|
+
type: "text",
|
|
173
|
+
text: `GraphQL request failed: ${response.statusText}\n${responseText}`,
|
|
174
|
+
},
|
|
175
|
+
],
|
|
240
176
|
};
|
|
241
177
|
}
|
|
242
|
-
|
|
243
|
-
|
|
178
|
+
const rawData = await response.json();
|
|
179
|
+
const data = rawData;
|
|
180
|
+
if (data.errors && data.errors.length > 0) {
|
|
244
181
|
return {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
182
|
+
isError: true,
|
|
183
|
+
content: [
|
|
184
|
+
{
|
|
185
|
+
type: "text",
|
|
186
|
+
text: `GraphQL errors: ${JSON.stringify(data.errors, null, 2)}`,
|
|
187
|
+
},
|
|
188
|
+
],
|
|
252
189
|
};
|
|
253
190
|
}
|
|
191
|
+
return {
|
|
192
|
+
content: [
|
|
193
|
+
{
|
|
194
|
+
type: "text",
|
|
195
|
+
text: JSON.stringify(data, null, 2),
|
|
196
|
+
},
|
|
197
|
+
],
|
|
198
|
+
};
|
|
254
199
|
}
|
|
255
|
-
|
|
256
|
-
|
|
200
|
+
catch (error) {
|
|
201
|
+
return {
|
|
202
|
+
isError: true,
|
|
203
|
+
content: [
|
|
204
|
+
{
|
|
205
|
+
type: "text",
|
|
206
|
+
text: `Failed to execute GraphQL query: ${error}`,
|
|
207
|
+
},
|
|
208
|
+
],
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
toolHandlers.set("query-graphql", queryGraphqlHandler);
|
|
213
|
+
server.tool("query-graphql", "Query a GraphQL endpoint with the given query and variables. Optionally pass headers (e.g., for Authorization).", {
|
|
214
|
+
query: z.string(),
|
|
215
|
+
variables: z.string().optional(),
|
|
216
|
+
headers: z
|
|
217
|
+
.string()
|
|
218
|
+
.optional()
|
|
219
|
+
.describe("Optional JSON string of headers to include, e.g., {\"Authorization\": \"Bearer ...\"}"),
|
|
220
|
+
}, queryGraphqlHandler);
|
|
221
|
+
function readBody(req) {
|
|
222
|
+
return new Promise((resolve, reject) => {
|
|
223
|
+
let body = '';
|
|
224
|
+
req.on('data', (chunk) => {
|
|
225
|
+
body += chunk.toString();
|
|
226
|
+
});
|
|
227
|
+
req.on('end', () => {
|
|
228
|
+
resolve(body);
|
|
229
|
+
});
|
|
230
|
+
req.on('error', reject);
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
async function handleHttpRequest(req, res) {
|
|
234
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
235
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
236
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
237
|
+
if (req.method === 'OPTIONS') {
|
|
238
|
+
res.writeHead(204);
|
|
239
|
+
res.end();
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
243
|
+
if (url.pathname === '/health' && req.method === 'GET') {
|
|
244
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
245
|
+
res.end(JSON.stringify({ status: 'ok', server: env.NAME }));
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
if (url.pathname === '/mcp' && req.method === 'POST') {
|
|
249
|
+
let rawBody;
|
|
250
|
+
let request;
|
|
257
251
|
try {
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
252
|
+
rawBody = await readBody(req);
|
|
253
|
+
request = JSON.parse(rawBody);
|
|
254
|
+
}
|
|
255
|
+
catch (e) {
|
|
256
|
+
console.error("HTTP MCP Parse Error:", e);
|
|
257
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
258
|
+
res.end(JSON.stringify({
|
|
259
|
+
jsonrpc: '2.0',
|
|
260
|
+
id: null,
|
|
261
|
+
error: { code: -32700, message: 'Parse Error: Invalid JSON received in request body.' }
|
|
262
|
+
}));
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
try {
|
|
266
|
+
const { method, params, id } = request;
|
|
267
|
+
if (!method || typeof id === 'undefined') {
|
|
268
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
269
|
+
res.end(JSON.stringify({
|
|
270
|
+
jsonrpc: '2.0',
|
|
271
|
+
id: id || null,
|
|
272
|
+
error: { code: -32600, message: 'Invalid JSON-RPC Request structure (missing method or id).' }
|
|
273
|
+
}));
|
|
274
|
+
return;
|
|
269
275
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
276
|
+
const handler = toolHandlers.get(method);
|
|
277
|
+
if (!handler) {
|
|
278
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
279
|
+
res.end(JSON.stringify({
|
|
280
|
+
jsonrpc: '2.0',
|
|
281
|
+
id: id,
|
|
282
|
+
error: { code: -32601, message: `Method not found: ${method}` }
|
|
283
|
+
}));
|
|
284
|
+
return;
|
|
273
285
|
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
286
|
+
const result = await handler(params);
|
|
287
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
288
|
+
res.end(JSON.stringify({
|
|
277
289
|
jsonrpc: '2.0',
|
|
278
290
|
id: id,
|
|
279
|
-
result:
|
|
280
|
-
|
|
281
|
-
{
|
|
282
|
-
type: 'text',
|
|
283
|
-
text: resultText,
|
|
284
|
-
},
|
|
285
|
-
],
|
|
286
|
-
},
|
|
287
|
-
};
|
|
291
|
+
result: result
|
|
292
|
+
}));
|
|
288
293
|
}
|
|
289
294
|
catch (error) {
|
|
290
|
-
console.error(
|
|
291
|
-
|
|
295
|
+
console.error("HTTP MCP Execution Error:", error);
|
|
296
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
297
|
+
res.end(JSON.stringify({
|
|
292
298
|
jsonrpc: '2.0',
|
|
293
|
-
id: id,
|
|
294
|
-
error: {
|
|
295
|
-
|
|
296
|
-
message: 'Error during schema introspection',
|
|
297
|
-
data: error.message,
|
|
298
|
-
},
|
|
299
|
-
};
|
|
299
|
+
id: request?.id || null,
|
|
300
|
+
error: { code: -32603, message: 'Internal server error during tool execution.' }
|
|
301
|
+
}));
|
|
300
302
|
}
|
|
303
|
+
return;
|
|
301
304
|
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
jsonrpc: '2.0',
|
|
305
|
-
id: id || null,
|
|
306
|
-
error: {
|
|
307
|
-
code: -32601, // Method not found
|
|
308
|
-
message: `Method not found: ${method}`,
|
|
309
|
-
},
|
|
310
|
-
};
|
|
305
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
306
|
+
res.end('Not Found. Use POST /mcp for JSON-RPC or GET /health.');
|
|
311
307
|
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
308
|
+
/**
|
|
309
|
+
* Tries to listen on a given port, automatically retrying on the next port if EADDRINUSE occurs.
|
|
310
|
+
* @param server - The HTTP server instance.
|
|
311
|
+
* @param port - The port to attempt binding to.
|
|
312
|
+
* @param maxRetries - Maximum number of retries.
|
|
313
|
+
* @param attempt - Current attempt number.
|
|
314
|
+
* @returns Resolves with the bound server instance.
|
|
315
|
+
*/
|
|
316
|
+
function tryListen(server, port, maxRetries = 5, attempt = 0) {
|
|
317
|
+
return new Promise((resolve, reject) => {
|
|
318
|
+
if (attempt >= maxRetries) {
|
|
319
|
+
reject(new Error(`Failed to bind HTTP server after ${maxRetries} attempts, starting from port ${env.MCP_PORT}.`));
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
if (port > 65535) {
|
|
323
|
+
reject(new Error(`Exceeded maximum port number (65535) during retry.`));
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
const errorHandler = (err) => {
|
|
327
|
+
server.removeListener('error', errorHandler); // Remove listener to prevent memory leak
|
|
328
|
+
if (err.code === 'EADDRINUSE') {
|
|
329
|
+
const nextPort = port + 1;
|
|
330
|
+
// Use console.error so it appears in the Inspector log
|
|
331
|
+
console.error(`[HTTP] Port ${port} is in use (EADDRINUSE). Retrying on ${nextPort}...`);
|
|
332
|
+
server.close(() => {
|
|
333
|
+
// Recursively call tryListen with the next port
|
|
334
|
+
resolve(tryListen(server, nextPort, maxRetries, attempt + 1));
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
else {
|
|
338
|
+
reject(err);
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
server.on('error', errorHandler);
|
|
342
|
+
server.listen(port, () => {
|
|
343
|
+
server.removeListener('error', errorHandler); // success, remove the error listener
|
|
344
|
+
console.error(`[HTTP] Started server on http://localhost:${port}. Listening for POST /mcp requests.`);
|
|
345
|
+
resolve(server);
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
function startHttpTransport() {
|
|
350
|
+
const serverInstance = node_http_1.default.createServer(handleHttpRequest);
|
|
351
|
+
tryListen(serverInstance, env.MCP_PORT).catch((error) => {
|
|
352
|
+
console.error(`[HTTP] Failed to start HTTP transport: ${error.message}`);
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
async function main() {
|
|
356
|
+
const stdioTransport = new StdioServerTransport();
|
|
357
|
+
await server.connect(stdioTransport);
|
|
358
|
+
startHttpTransport();
|
|
359
|
+
console.error(`[STDIO] Started graphql mcp server ${env.NAME} for endpoint: ${env.ENDPOINT}`);
|
|
360
|
+
}
|
|
361
|
+
main().catch((error) => {
|
|
362
|
+
console.error(`Fatal error in main(): ${error}`);
|
|
363
|
+
process.exit(1);
|
|
347
364
|
});
|
|
348
|
-
// Export the app instance for Vercel
|
|
349
|
-
exports.default = app;
|
package/package.json
CHANGED
|
@@ -43,14 +43,11 @@
|
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@graphql-tools/schema": "^10.0.23",
|
|
46
|
-
"@types/express": "^5.0.5",
|
|
47
|
-
"@types/graphql": "^14.2.3",
|
|
48
46
|
"@types/node": "^24.7.2",
|
|
49
47
|
"@types/yargs": "17.0.33",
|
|
50
|
-
"graphql-yoga": "^5.13.5",
|
|
51
48
|
"prettier": "^3.6.2",
|
|
52
49
|
"ts-node": "^10.9.2",
|
|
53
50
|
"typescript": "5.8.3"
|
|
54
51
|
},
|
|
55
|
-
"version": "2.3.
|
|
52
|
+
"version": "2.3.3"
|
|
56
53
|
}
|