@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.
Files changed (2) hide show
  1. package/dist/index.js +108 -17
  2. 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 HANDLERS ---
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
- server.tool("query-graphql", "Execute a GraphQL query against the endpoint", {
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
- server.tool("introspect-schema", "Introspect the GraphQL schema with optional type filtering", {
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
- // NEW FEATURE: Web GUI for Humans
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 { method, params, id } = JSON.parse(body);
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
- const handler = toolHandlers.get(method);
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({ jsonrpc: '2.0', id, error: { code: -32601, message: "Method not found" } }));
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
- const result = await handler(params);
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({ jsonrpc: '2.0', id, result }));
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({ jsonrpc: '2.0', error: { code: -32603, message: "Internal Error" } }));
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
- if (env.ENABLE_HTTP) {
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
- console.error(`[HTTP] Server started on http://localhost:${env.MCP_PORT}`);
382
- console.error(`🎨 GraphiQL IDE: http://localhost:${env.MCP_PORT}/graphiql`);
383
- console.error(`🤖 MCP Endpoint: http://localhost:${env.MCP_PORT}/mcp\n`);
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
- console.error(`[STDIO] MCP Server "${env.NAME}" v${getVersion()} started`);
389
- getSchema().catch(e => console.error(`[SCHEMA] Warning: Preload failed: ${e.message}`));
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.20.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.5.0"
53
+ "version": "3.6.0"
54
54
  }