@letoribo/mcp-graphql-enhanced 2.3.1 → 2.3.2
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 +324 -312
- 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,361 @@
|
|
|
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
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
if (!typeNames || typeNames.length === 0) {
|
|
129
|
-
return `--- Full Schema Introspection ---\nEndpoint: ${endpoint}\n\n${(0, graphql_1.printSchema)(schema)}`;
|
|
130
|
-
}
|
|
131
|
-
const requestedTypeSet = new Set(typeNames.map(name => name.trim()));
|
|
132
|
-
let filteredSchema = `--- Annotated Schema Introspection ---\nEndpoint: ${endpoint}\nRequested Types: ${typeNames.join(', ')}\n\n`;
|
|
133
|
-
// 1. Prune the Query type fields to only include those that resolve to requested types
|
|
134
|
-
const queryType = schema.getQueryType();
|
|
135
|
-
if (queryType) {
|
|
136
|
-
const fields = queryType.getFields();
|
|
137
|
-
const keptQueryFields = [];
|
|
138
|
-
// Flag: If the user requested the "Query" type itself, include all root fields for utility.
|
|
139
|
-
const includeAllQueryFields = requestedTypeSet.has(queryType.name);
|
|
140
|
-
Object.keys(fields).forEach(fieldName => {
|
|
141
|
-
const field = fields[fieldName];
|
|
142
|
-
const returnType = (0, graphql_1.getNamedType)(field.type);
|
|
143
|
-
// Keep the field if:
|
|
144
|
-
// 1. Its return type is one of the requested types, OR
|
|
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()}`);
|
|
73
|
+
});
|
|
74
|
+
const toolHandlers = new Map();
|
|
75
|
+
const introspectSchemaHandler = async ({ typeNames, descriptions = true, directives = true }) => {
|
|
76
|
+
try {
|
|
77
|
+
if (typeNames && typeNames.length > 0) {
|
|
78
|
+
const filtered = await introspectTypes(env.ENDPOINT, env.HEADERS, typeNames);
|
|
79
|
+
return { content: [{ type: "text", text: filtered }] };
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
let schema;
|
|
83
|
+
if (env.SCHEMA) {
|
|
84
|
+
if (env.SCHEMA.startsWith("http://") || env.SCHEMA.startsWith("https://")) {
|
|
85
|
+
schema = await introspectSchemaFromUrl(env.SCHEMA);
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
schema = await introspectLocalSchema(env.SCHEMA);
|
|
89
|
+
}
|
|
150
90
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
91
|
+
else {
|
|
92
|
+
schema = await introspectEndpoint(env.ENDPOINT, env.HEADERS);
|
|
93
|
+
}
|
|
94
|
+
return { content: [{ type: "text", text: schema }] };
|
|
155
95
|
}
|
|
156
96
|
}
|
|
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}`);
|
|
97
|
+
catch (error) {
|
|
98
|
+
return {
|
|
99
|
+
isError: true,
|
|
100
|
+
content: [{ type: "text", text: `Introspection failed: ${error}` }],
|
|
101
|
+
};
|
|
170
102
|
}
|
|
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
103
|
};
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
104
|
+
toolHandlers.set("introspect-schema", introspectSchemaHandler);
|
|
105
|
+
server.tool("introspect-schema", "Introspect the GraphQL schema. Optionally filter to specific types.", {
|
|
106
|
+
typeNames: z.array(z.string()).optional().describe("A list of specific type names to filter the introspection."),
|
|
107
|
+
descriptions: z.boolean().optional().default(true),
|
|
108
|
+
directives: z.boolean().optional().default(true),
|
|
109
|
+
}, introspectSchemaHandler);
|
|
110
|
+
const queryGraphqlHandler = async ({ query, variables, headers }) => {
|
|
111
|
+
try {
|
|
112
|
+
const parsedQuery = parse(query);
|
|
113
|
+
const isMutation = parsedQuery.definitions.some((def) => def.kind === "OperationDefinition" && def.operation === "mutation");
|
|
114
|
+
if (isMutation && !env.ALLOW_MUTATIONS) {
|
|
115
|
+
return {
|
|
116
|
+
isError: true,
|
|
117
|
+
content: [
|
|
118
|
+
{
|
|
119
|
+
type: "text",
|
|
120
|
+
text: "Mutations are not allowed unless you enable them in the configuration. Please use a query operation instead.",
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
188
127
|
return {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
128
|
+
isError: true,
|
|
129
|
+
content: [
|
|
130
|
+
{
|
|
131
|
+
type: "text",
|
|
132
|
+
text: `Invalid GraphQL query: ${error}`,
|
|
133
|
+
},
|
|
134
|
+
],
|
|
195
135
|
};
|
|
196
136
|
}
|
|
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
|
-
};
|
|
137
|
+
try {
|
|
138
|
+
const toolHeaders = headers
|
|
139
|
+
? JSON.parse(headers)
|
|
140
|
+
: {};
|
|
141
|
+
const allHeaders = {
|
|
142
|
+
"Content-Type": "application/json",
|
|
143
|
+
...env.HEADERS,
|
|
144
|
+
...toolHeaders,
|
|
145
|
+
};
|
|
146
|
+
let parsedVariables = null;
|
|
147
|
+
if (variables) {
|
|
148
|
+
if (typeof variables === 'string') {
|
|
149
|
+
parsedVariables = JSON.parse(variables);
|
|
217
150
|
}
|
|
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
|
-
};
|
|
151
|
+
else {
|
|
152
|
+
parsedVariables = variables;
|
|
231
153
|
}
|
|
232
|
-
|
|
154
|
+
}
|
|
155
|
+
const response = await fetch(env.ENDPOINT, {
|
|
156
|
+
method: "POST",
|
|
157
|
+
headers: allHeaders,
|
|
158
|
+
body: JSON.stringify({
|
|
159
|
+
query,
|
|
160
|
+
variables: parsedVariables,
|
|
161
|
+
}),
|
|
162
|
+
});
|
|
163
|
+
if (!response.ok) {
|
|
164
|
+
const responseText = await response.text();
|
|
233
165
|
return {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
166
|
+
isError: true,
|
|
167
|
+
content: [
|
|
168
|
+
{
|
|
169
|
+
type: "text",
|
|
170
|
+
text: `GraphQL request failed: ${response.statusText}\n${responseText}`,
|
|
171
|
+
},
|
|
172
|
+
],
|
|
240
173
|
};
|
|
241
174
|
}
|
|
242
|
-
|
|
243
|
-
|
|
175
|
+
const rawData = await response.json();
|
|
176
|
+
const data = rawData;
|
|
177
|
+
if (data.errors && data.errors.length > 0) {
|
|
244
178
|
return {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
179
|
+
isError: true,
|
|
180
|
+
content: [
|
|
181
|
+
{
|
|
182
|
+
type: "text",
|
|
183
|
+
text: `GraphQL errors: ${JSON.stringify(data.errors, null, 2)}`,
|
|
184
|
+
},
|
|
185
|
+
],
|
|
252
186
|
};
|
|
253
187
|
}
|
|
188
|
+
return {
|
|
189
|
+
content: [
|
|
190
|
+
{
|
|
191
|
+
type: "text",
|
|
192
|
+
text: JSON.stringify(data, null, 2),
|
|
193
|
+
},
|
|
194
|
+
],
|
|
195
|
+
};
|
|
254
196
|
}
|
|
255
|
-
|
|
256
|
-
|
|
197
|
+
catch (error) {
|
|
198
|
+
return {
|
|
199
|
+
isError: true,
|
|
200
|
+
content: [
|
|
201
|
+
{
|
|
202
|
+
type: "text",
|
|
203
|
+
text: `Failed to execute GraphQL query: ${error}`,
|
|
204
|
+
},
|
|
205
|
+
],
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
toolHandlers.set("query-graphql", queryGraphqlHandler);
|
|
210
|
+
server.tool("query-graphql", "Query a GraphQL endpoint with the given query and variables. Optionally pass headers (e.g., for Authorization).", {
|
|
211
|
+
query: z.string(),
|
|
212
|
+
variables: z.string().optional(),
|
|
213
|
+
headers: z
|
|
214
|
+
.string()
|
|
215
|
+
.optional()
|
|
216
|
+
.describe("Optional JSON string of headers to include, e.g., {\"Authorization\": \"Bearer ...\"}"),
|
|
217
|
+
}, queryGraphqlHandler);
|
|
218
|
+
function readBody(req) {
|
|
219
|
+
return new Promise((resolve, reject) => {
|
|
220
|
+
let body = '';
|
|
221
|
+
req.on('data', (chunk) => {
|
|
222
|
+
body += chunk.toString();
|
|
223
|
+
});
|
|
224
|
+
req.on('end', () => {
|
|
225
|
+
resolve(body);
|
|
226
|
+
});
|
|
227
|
+
req.on('error', reject);
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
async function handleHttpRequest(req, res) {
|
|
231
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
232
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
233
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
234
|
+
if (req.method === 'OPTIONS') {
|
|
235
|
+
res.writeHead(204);
|
|
236
|
+
res.end();
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
240
|
+
if (url.pathname === '/health' && req.method === 'GET') {
|
|
241
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
242
|
+
res.end(JSON.stringify({ status: 'ok', server: env.NAME }));
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
if (url.pathname === '/mcp' && req.method === 'POST') {
|
|
246
|
+
let rawBody;
|
|
247
|
+
let request;
|
|
257
248
|
try {
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
249
|
+
rawBody = await readBody(req);
|
|
250
|
+
request = JSON.parse(rawBody);
|
|
251
|
+
}
|
|
252
|
+
catch (e) {
|
|
253
|
+
console.error("HTTP MCP Parse Error:", e);
|
|
254
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
255
|
+
res.end(JSON.stringify({
|
|
256
|
+
jsonrpc: '2.0',
|
|
257
|
+
id: null,
|
|
258
|
+
error: { code: -32700, message: 'Parse Error: Invalid JSON received in request body.' }
|
|
259
|
+
}));
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
try {
|
|
263
|
+
const { method, params, id } = request;
|
|
264
|
+
if (!method || typeof id === 'undefined') {
|
|
265
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
266
|
+
res.end(JSON.stringify({
|
|
267
|
+
jsonrpc: '2.0',
|
|
268
|
+
id: id || null,
|
|
269
|
+
error: { code: -32600, message: 'Invalid JSON-RPC Request structure (missing method or id).' }
|
|
270
|
+
}));
|
|
271
|
+
return;
|
|
269
272
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
+
const handler = toolHandlers.get(method);
|
|
274
|
+
if (!handler) {
|
|
275
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
276
|
+
res.end(JSON.stringify({
|
|
277
|
+
jsonrpc: '2.0',
|
|
278
|
+
id: id,
|
|
279
|
+
error: { code: -32601, message: `Method not found: ${method}` }
|
|
280
|
+
}));
|
|
281
|
+
return;
|
|
273
282
|
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
283
|
+
const result = await handler(params);
|
|
284
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
285
|
+
res.end(JSON.stringify({
|
|
277
286
|
jsonrpc: '2.0',
|
|
278
287
|
id: id,
|
|
279
|
-
result:
|
|
280
|
-
|
|
281
|
-
{
|
|
282
|
-
type: 'text',
|
|
283
|
-
text: resultText,
|
|
284
|
-
},
|
|
285
|
-
],
|
|
286
|
-
},
|
|
287
|
-
};
|
|
288
|
+
result: result
|
|
289
|
+
}));
|
|
288
290
|
}
|
|
289
291
|
catch (error) {
|
|
290
|
-
console.error(
|
|
291
|
-
|
|
292
|
+
console.error("HTTP MCP Execution Error:", error);
|
|
293
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
294
|
+
res.end(JSON.stringify({
|
|
292
295
|
jsonrpc: '2.0',
|
|
293
|
-
id: id,
|
|
294
|
-
error: {
|
|
295
|
-
|
|
296
|
-
message: 'Error during schema introspection',
|
|
297
|
-
data: error.message,
|
|
298
|
-
},
|
|
299
|
-
};
|
|
296
|
+
id: request?.id || null,
|
|
297
|
+
error: { code: -32603, message: 'Internal server error during tool execution.' }
|
|
298
|
+
}));
|
|
300
299
|
}
|
|
300
|
+
return;
|
|
301
301
|
}
|
|
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
|
-
};
|
|
302
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
303
|
+
res.end('Not Found. Use POST /mcp for JSON-RPC or GET /health.');
|
|
311
304
|
}
|
|
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
|
-
|
|
305
|
+
/**
|
|
306
|
+
* Tries to listen on a given port, automatically retrying on the next port if EADDRINUSE occurs.
|
|
307
|
+
* @param server - The HTTP server instance.
|
|
308
|
+
* @param port - The port to attempt binding to.
|
|
309
|
+
* @param maxRetries - Maximum number of retries.
|
|
310
|
+
* @param attempt - Current attempt number.
|
|
311
|
+
* @returns Resolves with the bound server instance.
|
|
312
|
+
*/
|
|
313
|
+
function tryListen(server, port, maxRetries = 5, attempt = 0) {
|
|
314
|
+
return new Promise((resolve, reject) => {
|
|
315
|
+
if (attempt >= maxRetries) {
|
|
316
|
+
reject(new Error(`Failed to bind HTTP server after ${maxRetries} attempts, starting from port ${env.MCP_PORT}.`));
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
if (port > 65535) {
|
|
320
|
+
reject(new Error(`Exceeded maximum port number (65535) during retry.`));
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
const errorHandler = (err) => {
|
|
324
|
+
server.removeListener('error', errorHandler); // Remove listener to prevent memory leak
|
|
325
|
+
if (err.code === 'EADDRINUSE') {
|
|
326
|
+
const nextPort = port + 1;
|
|
327
|
+
// Use console.error so it appears in the Inspector log
|
|
328
|
+
console.error(`[HTTP] Port ${port} is in use (EADDRINUSE). Retrying on ${nextPort}...`);
|
|
329
|
+
server.close(() => {
|
|
330
|
+
// Recursively call tryListen with the next port
|
|
331
|
+
resolve(tryListen(server, nextPort, maxRetries, attempt + 1));
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
else {
|
|
335
|
+
reject(err);
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
server.on('error', errorHandler);
|
|
339
|
+
server.listen(port, () => {
|
|
340
|
+
server.removeListener('error', errorHandler); // success, remove the error listener
|
|
341
|
+
console.error(`[HTTP] Started server on http://localhost:${port}. Listening for POST /mcp requests.`);
|
|
342
|
+
resolve(server);
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
function startHttpTransport() {
|
|
347
|
+
const serverInstance = node_http_1.default.createServer(handleHttpRequest);
|
|
348
|
+
tryListen(serverInstance, env.MCP_PORT).catch((error) => {
|
|
349
|
+
console.error(`[HTTP] Failed to start HTTP transport: ${error.message}`);
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
async function main() {
|
|
353
|
+
const stdioTransport = new StdioServerTransport();
|
|
354
|
+
await server.connect(stdioTransport);
|
|
355
|
+
startHttpTransport();
|
|
356
|
+
console.error(`[STDIO] Started graphql mcp server ${env.NAME} for endpoint: ${env.ENDPOINT}`);
|
|
357
|
+
}
|
|
358
|
+
main().catch((error) => {
|
|
359
|
+
console.error(`Fatal error in main(): ${error}`);
|
|
360
|
+
process.exit(1);
|
|
347
361
|
});
|
|
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.2"
|
|
56
53
|
}
|