@letoribo/mcp-graphql-enhanced 3.5.0 → 3.6.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/dist/index.js +108 -17
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -198,8 +198,41 @@ async function performUpdate(force) {
|
|
|
198
198
|
updatePromise = null;
|
|
199
199
|
}
|
|
200
200
|
}
|
|
201
|
-
// --- TOOL
|
|
201
|
+
// --- TOOL REGISTRY ---
|
|
202
202
|
const toolHandlers = new Map();
|
|
203
|
+
// This will store schemas for our dynamic HTTP 'list-tools' response
|
|
204
|
+
const registeredToolsMetadata = [];
|
|
205
|
+
/**
|
|
206
|
+
* Helper to register tools in both MCP Server and our local registry
|
|
207
|
+
* Handles both plain objects and Zod schemas
|
|
208
|
+
*/
|
|
209
|
+
function registerTool(name, description, schema, handler) {
|
|
210
|
+
// 1. Register in official MCP Server
|
|
211
|
+
server.tool(name, description, schema, handler);
|
|
212
|
+
// 2. Save handler for our HTTP routing
|
|
213
|
+
toolHandlers.set(name, handler);
|
|
214
|
+
// 3. Store metadata for dynamic Discovery
|
|
215
|
+
// We treat 'schema' as a plain object for the JSON output
|
|
216
|
+
registeredToolsMetadata.push({
|
|
217
|
+
name,
|
|
218
|
+
description,
|
|
219
|
+
inputSchema: {
|
|
220
|
+
type: "object",
|
|
221
|
+
properties: Object.fromEntries(Object.entries(schema).map(([key, value]) => {
|
|
222
|
+
// Detect type from Zod or default to string
|
|
223
|
+
const typeName = value?._def?.typeName
|
|
224
|
+
? value._def.typeName.replace('Zod', '').toLowerCase()
|
|
225
|
+
: "string";
|
|
226
|
+
return [key, { type: typeName }];
|
|
227
|
+
})),
|
|
228
|
+
// Assume all fields in the object are required unless specified
|
|
229
|
+
required: Object.keys(schema).filter(key => {
|
|
230
|
+
const val = schema[key];
|
|
231
|
+
return val?._def?.typeName !== 'ZodOptional' && !key.includes('?');
|
|
232
|
+
})
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
}
|
|
203
236
|
/** * executionLogs stores the last 5 GraphQL operations.
|
|
204
237
|
* This allows the AI to "inspect" its own generated queries and the raw data
|
|
205
238
|
* for debugging or bridging to 3D visualization tools.
|
|
@@ -255,7 +288,7 @@ const queryGraphqlHandler = async ({ query, variables, headers }) => {
|
|
|
255
288
|
}
|
|
256
289
|
};
|
|
257
290
|
toolHandlers.set("query-graphql", queryGraphqlHandler);
|
|
258
|
-
|
|
291
|
+
registerTool("query-graphql", "Execute a GraphQL query against the endpoint", {
|
|
259
292
|
query: zod_1.default.string(),
|
|
260
293
|
variables: zod_1.default.string().optional(),
|
|
261
294
|
headers: zod_1.default.string().optional(),
|
|
@@ -322,71 +355,129 @@ const introspectHandler = async ({ typeNames }) => {
|
|
|
322
355
|
};
|
|
323
356
|
};
|
|
324
357
|
toolHandlers.set("introspect-schema", introspectHandler);
|
|
325
|
-
|
|
358
|
+
registerTool("introspect-schema", "Introspect the GraphQL schema with optional type filtering", {
|
|
326
359
|
typeNames: zod_1.default.array(zod_1.default.string()).optional(),
|
|
327
360
|
}, introspectHandler);
|
|
328
361
|
// --- HTTP SERVER LOGIC ---
|
|
329
362
|
async function handleHttpRequest(req, res) {
|
|
363
|
+
// Standard CORS headers for cross-origin compatibility
|
|
330
364
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
331
365
|
res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');
|
|
332
366
|
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
367
|
+
// Handle preflight requests
|
|
333
368
|
if (req.method === 'OPTIONS') {
|
|
334
369
|
res.writeHead(204);
|
|
335
370
|
res.end();
|
|
336
371
|
return;
|
|
337
372
|
}
|
|
338
373
|
const url = new URL(req.url || '', `http://${req.headers.host}`);
|
|
339
|
-
//
|
|
374
|
+
// Serve Web GUI (GraphiQL)
|
|
340
375
|
if (req.method === 'GET' && (url.pathname === '/' || url.pathname === '/graphiql')) {
|
|
341
376
|
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
342
|
-
// Pass env.HEADERS to the explorer
|
|
343
377
|
return res.end((0, graphiql_js_1.renderGraphiQL)(env.ENDPOINT, env.HEADERS));
|
|
344
378
|
}
|
|
379
|
+
// Process MCP JSON-RPC Endpoint
|
|
345
380
|
if (url.pathname === '/mcp' && req.method === 'POST') {
|
|
346
381
|
let body = '';
|
|
347
382
|
req.on('data', chunk => { body += chunk; });
|
|
348
383
|
req.on('end', async () => {
|
|
384
|
+
let requestId = null; // Defined early to be accessible in catch block
|
|
349
385
|
try {
|
|
350
|
-
const
|
|
386
|
+
const request = JSON.parse(body);
|
|
387
|
+
const { method, id, params } = request;
|
|
388
|
+
requestId = id;
|
|
351
389
|
console.error(`[HTTP-RPC] Method: ${method} | ID: ${id}`);
|
|
352
|
-
|
|
390
|
+
// --- DYNAMIC DISCOVERY ---
|
|
391
|
+
if (method === "list-tools" || method === "tools/list") {
|
|
392
|
+
// Просто отдаем то, что накопили в нашем реестре при регистрации
|
|
393
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
394
|
+
return res.end(JSON.stringify({
|
|
395
|
+
jsonrpc: '2.0',
|
|
396
|
+
id: requestId,
|
|
397
|
+
result: { tools: registeredToolsMetadata }
|
|
398
|
+
}));
|
|
399
|
+
}
|
|
400
|
+
// --- TOOL EXECUTION (CALL) ---
|
|
401
|
+
let targetMethod = method;
|
|
402
|
+
let toolArgs = params;
|
|
403
|
+
// Support both direct calls and standard MCP "call-tool" structure
|
|
404
|
+
if (method === "call-tool" || method === "tools/call") {
|
|
405
|
+
targetMethod = params.name;
|
|
406
|
+
toolArgs = params.arguments;
|
|
407
|
+
}
|
|
408
|
+
const handler = toolHandlers.get(targetMethod);
|
|
353
409
|
if (!handler) {
|
|
354
410
|
res.writeHead(404);
|
|
355
|
-
return res.end(JSON.stringify({
|
|
411
|
+
return res.end(JSON.stringify({
|
|
412
|
+
jsonrpc: '2.0',
|
|
413
|
+
id: requestId,
|
|
414
|
+
error: { code: -32601, message: `Tool ${targetMethod} not found` }
|
|
415
|
+
}));
|
|
356
416
|
}
|
|
357
|
-
|
|
417
|
+
// Execute the actual business logic for the tool
|
|
418
|
+
const result = await handler(toolArgs);
|
|
358
419
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
359
|
-
res.end(JSON.stringify({
|
|
420
|
+
res.end(JSON.stringify({
|
|
421
|
+
jsonrpc: '2.0',
|
|
422
|
+
id: requestId,
|
|
423
|
+
result
|
|
424
|
+
}));
|
|
360
425
|
}
|
|
361
426
|
catch (e) {
|
|
362
427
|
console.error(`[HTTP-ERROR] ${e.message}`);
|
|
363
428
|
res.writeHead(500);
|
|
364
|
-
res.end(JSON.stringify({
|
|
429
|
+
res.end(JSON.stringify({
|
|
430
|
+
jsonrpc: '2.0',
|
|
431
|
+
id: requestId,
|
|
432
|
+
error: { code: -32603, message: e.message || "Internal Server Error" }
|
|
433
|
+
}));
|
|
365
434
|
}
|
|
366
435
|
});
|
|
367
436
|
return;
|
|
368
437
|
}
|
|
438
|
+
// Health check endpoint for monitoring
|
|
369
439
|
if (url.pathname === '/health') {
|
|
370
440
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
371
441
|
return res.end(JSON.stringify({ status: "ok", server: env.NAME }));
|
|
372
442
|
}
|
|
443
|
+
// Default 404 for unknown paths
|
|
373
444
|
res.writeHead(404);
|
|
374
445
|
res.end("Not Found");
|
|
375
446
|
}
|
|
376
447
|
// --- STARTUP ---
|
|
377
448
|
async function main() {
|
|
378
|
-
|
|
449
|
+
const rawEnableHttp = process.env.ENABLE_HTTP;
|
|
450
|
+
// Determine if we should open the HTTP port.
|
|
451
|
+
// 1. Check if we're not running inside the MCP Inspector.
|
|
452
|
+
// 2. Ensure the user hasn't explicitly disabled HTTP (ENABLE_HTTP="false").
|
|
453
|
+
const isInspector = !!(process.env.MCP_INSPECTOR || process.env.INSPECTOR_PORT);
|
|
454
|
+
const shouldOpenPort = rawEnableHttp !== "false" && !isInspector;
|
|
455
|
+
if (shouldOpenPort) {
|
|
379
456
|
const serverHttp = node_http_1.default.createServer(handleHttpRequest);
|
|
457
|
+
// Error handling to prevent crashes if the port is already in use
|
|
458
|
+
serverHttp.on('error', (e) => {
|
|
459
|
+
if (e.code !== 'EADDRINUSE')
|
|
460
|
+
console.error(`[HTTP-ERROR] ${e.message}`);
|
|
461
|
+
});
|
|
380
462
|
serverHttp.listen(env.MCP_PORT, () => {
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
463
|
+
// Log URLs only if HTTP is explicitly enabled
|
|
464
|
+
if (env.ENABLE_HTTP === true) {
|
|
465
|
+
console.error(`[HTTP] Server started on http://localhost:${env.MCP_PORT}`);
|
|
466
|
+
console.error(`🎨 GraphiQL IDE: http://localhost:${env.MCP_PORT}/graphiql`);
|
|
467
|
+
}
|
|
468
|
+
console.error(`🤖 MCP Endpoint: http://localhost:${env.MCP_PORT}/mcp`);
|
|
384
469
|
});
|
|
385
470
|
}
|
|
386
471
|
const transport = new stdio_js_1.StdioServerTransport();
|
|
387
472
|
await server.connect(transport);
|
|
388
|
-
|
|
389
|
-
|
|
473
|
+
// Maintain quiet mode when running inside the Inspector to avoid protocol interference
|
|
474
|
+
if (!isInspector) {
|
|
475
|
+
console.error(`[STDIO] MCP Server "${env.NAME}" v${getVersion()} started`);
|
|
476
|
+
}
|
|
477
|
+
getSchema().catch(e => {
|
|
478
|
+
if (!isInspector)
|
|
479
|
+
console.error(`[SCHEMA] Warning: ${e.message}`);
|
|
480
|
+
});
|
|
390
481
|
}
|
|
391
482
|
process.on('SIGINT', () => process.exit(0));
|
|
392
483
|
process.on('SIGTERM', () => process.exit(0));
|
package/package.json
CHANGED
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"format": "prettier --write ."
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
38
|
+
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
39
39
|
"graphql": "^16.11.0",
|
|
40
40
|
"yargs": "17.7.2",
|
|
41
41
|
"zod": "3.25.30",
|
|
@@ -50,5 +50,5 @@
|
|
|
50
50
|
"tsx": "^4.21.0",
|
|
51
51
|
"typescript": "5.8.3"
|
|
52
52
|
},
|
|
53
|
-
"version": "3.
|
|
53
|
+
"version": "3.6.0"
|
|
54
54
|
}
|