@letoribo/mcp-graphql-enhanced 3.8.1 → 3.9.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 (3) hide show
  1. package/README.md +25 -0
  2. package/dist/index.js +146 -123
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -23,6 +23,31 @@ This is the best place to share your feedback, report issues, or suggest new "en
23
23
  * ✅ **Dynamic Schema Evolution** — Smart diagnostics and gap analysis for servers that regenerate GraphQL types on-the-fly (like Neo4j).
24
24
  * ✅ **Deep Observability** — Automatic Cypher extraction and cleaning from GraphQL extensions.
25
25
 
26
+ ## 🚀 Multi-Endpoint Broadcast (Experimental in v3.9.0+)
27
+ Starting from **v3.9.0**, the server supports querying multiple GraphQL endpoints simultaneously. This was originally designed to synchronize mutations across different environments (e.g., Node.js and Python backends), but it opens up powerful possibilities for data aggregation.
28
+
29
+ * **Zero Breaking Changes**: If you provide a single URL in `ENDPOINT`, the server behaves exactly as before.
30
+ * **Smart Aggregation**: When multiple comma-separated URLs are provided, the server broadcasts the query to all of them and **merges the resulting arrays**.
31
+ * **Bypass Free Tier Limits**: Perfect for users of "Free Tier" cloud databases (like Neo4j Aura). You can split your data across multiple free instances and use this bridge to query them as a **single unified graph**, effectively bypassing entity count limitations.
32
+ * **Deduplication**: The bridge automatically removes duplicate objects based on their unique fields to keep the AI's context window clean.
33
+
34
+ > **⚠️ Use at your own risk:** This feature assumes all endpoints share the same (or very similar) GraphQL schema. The introspection is performed against the **first** endpoint in the list.
35
+
36
+ #### 💡 Use Case: Bridging WSL and Windows (PowerShell)
37
+ A common challenge for Windows developers is the network isolation between the Windows Subsystem for Linux (WSL) and the host OS. This feature allows you to bridge these two worlds into a "Unified Nervous System".
38
+
39
+ Example configuration for Claude Desktop:
40
+ ```bash
41
+ {
42
+ "ENDPOINT": "http://DESKTOP-NAME.local:2311/graphql,http://127.0.0.1:4000/graphql"
43
+ }
44
+ ```
45
+ * **Hybrid Ecosystem**: Seamlessly query and aggregate data across Windows-native processes (PowerShell) and Linux-based environments (WSL).
46
+
47
+ * **mDNS Support**: By using .local addresses, the bridge automatically resolves the host machine's IP from within the WSL environment.
48
+
49
+ * **Transparent Aggregation**: The AI assistant interacts with a single unified schema, unaware that the data is being fetched from different operating systems simultaneously.
50
+
26
51
  ## 🔍 Advanced Observability & Cypher
27
52
  The bridge provides deep insights into how the LLM interacts with your graph database.
28
53
 
package/dist/index.js CHANGED
@@ -16,19 +16,31 @@ const deprecation_js_1 = require("./helpers/deprecation.js");
16
16
  const introspection_js_1 = require("./helpers/introspection.js");
17
17
  const tool_registry_js_1 = require("./helpers/tool-registry.js");
18
18
  const prompt_registry_js_1 = require("./helpers/prompt-registry.js");
19
+ /**
20
+ * Retrieves the current version from package.json
21
+ */
19
22
  const getVersion = () => {
20
23
  try {
21
24
  const pkg = require("../package.json");
22
25
  return pkg.version;
23
26
  }
24
27
  catch {
25
- return "3.6.0";
28
+ return "3.9.0";
26
29
  }
27
30
  };
28
31
  (0, deprecation_js_1.checkDeprecatedArguments)();
32
+ /**
33
+ * Environment configuration schema
34
+ */
29
35
  const EnvSchema = zod_1.default.object({
30
36
  NAME: zod_1.default.string().default("mcp-graphql-enhanced"),
31
- 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"),
37
+ ENDPOINT: zod_1.default.preprocess((val) => {
38
+ if (typeof val === 'string') {
39
+ // Support for multiple endpoints via comma-separated string
40
+ return val.trim();
41
+ }
42
+ return val;
43
+ }, zod_1.default.string().min(1)).default("https://mcp-neo4j-discord.vercel.app/api/graphiql"),
32
44
  ALLOW_MUTATIONS: zod_1.default
33
45
  .enum(["true", "false"])
34
46
  .transform((value) => value === "true")
@@ -60,7 +72,7 @@ const env = EnvSchema.parse(process.env);
60
72
  const server = new mcp_js_1.McpServer({
61
73
  name: env.NAME,
62
74
  version: getVersion(),
63
- description: "Start of the #mcp-graphql-enhanced channel on GraphQL server. Join here: https://discord.com/channels/622115132221685760/1348633379555184640"
75
+ description: "Unified GraphQL-to-MCP bridge with dynamic schema support."
64
76
  }, {
65
77
  capabilities: {
66
78
  prompts: {},
@@ -75,7 +87,7 @@ let isUpdating = false;
75
87
  let updatePromise = null;
76
88
  let lastKnownTypeCount = 0;
77
89
  /**
78
- * Smart Hybrid Schema Fetcher (Zero-Error Version)
90
+ * Smart Hybrid Schema Fetcher
79
91
  * @param force If true, blocks and waits for the new schema evolution.
80
92
  * If false, returns cache immediately and updates in background.
81
93
  */
@@ -88,12 +100,12 @@ async function getSchema(force = false, requestedTypes) {
88
100
  }
89
101
  // 2. Return cache if valid and not forcing
90
102
  if (cachedSDL && !force) {
91
- // Validation check: If user wants specific types but they aren't in the cache
103
+ // Validation check: ensure requested types exist in current cache
92
104
  if (requestedTypes && cachedSchemaObject) {
93
105
  const typeMap = cachedSchemaObject.getTypeMap();
94
106
  const missing = requestedTypes.filter(t => !typeMap[t]);
95
107
  if (missing.length > 0) {
96
- // Force a refresh if requested types are missing from current cache
108
+ // Force a refresh if requested types are missing
97
109
  return await (updatePromise = performUpdate(true));
98
110
  }
99
111
  }
@@ -118,8 +130,7 @@ async function getSchema(force = false, requestedTypes) {
118
130
  }
119
131
  /**
120
132
  * Internal logic for schema introspection and building.
121
- * This version uses universal business-type tracking and provides
122
- * detailed diagnostic reports instead of generic error messages.
133
+ * Optimized for Multi-Endpoint Broadcast: Uses the first available URL for discovery.
123
134
  */
124
135
  async function performUpdate(force) {
125
136
  isUpdating = true;
@@ -127,41 +138,43 @@ async function performUpdate(force) {
127
138
  try {
128
139
  const { buildClientSchema, getIntrospectionQuery, printSchema, buildASTSchema, parse: gqlParse, isObjectType } = require("graphql");
129
140
  let tempSchema;
130
- // --- FETCHING LOGIC (Unified Source) ---
141
+ // --- FETCHING LOGIC ---
131
142
  if (env.SCHEMA) {
132
143
  let sdl;
133
144
  if (env.SCHEMA.startsWith("http")) {
134
- // Remote SDL File: Fetch via HTTP
145
+ // Remote SDL File
135
146
  const response = await fetch(env.SCHEMA);
136
147
  if (!response.ok)
137
148
  throw new Error(`Remote_SDL_Fetch_Failed: ${response.statusText}`);
138
149
  sdl = await response.text();
139
150
  }
140
151
  else {
141
- // Local SDL File: Use your custom helper (readFile inside)
152
+ // Local SDL File via helper
142
153
  sdl = await (0, introspection_js_1.introspectLocalSchema)(env.SCHEMA);
143
154
  }
144
- // Direct path: Convert raw SDL string to GraphQLSchema object
145
155
  tempSchema = buildASTSchema(gqlParse(sdl));
146
156
  }
147
157
  else {
148
- // Standard Path: Execute Introspection Query against live ENDPOINT
149
- const response = await fetch(env.ENDPOINT, {
158
+ // --- BROADCAST ADAPTATION ---
159
+ // Extract the primary endpoint for introspection
160
+ const endpoints = env.ENDPOINT.split(',').map(url => url.trim());
161
+ const primaryEndpoint = endpoints[0];
162
+ // Execute Introspection against the primary target
163
+ const response = await fetch(primaryEndpoint, {
150
164
  method: "POST",
151
165
  headers: { "Content-Type": "application/json", ...env.HEADERS },
152
166
  body: JSON.stringify({ query: getIntrospectionQuery() }),
153
167
  });
154
168
  if (!response.ok)
155
- throw new Error(`HTTP_${response.status}: ${response.statusText}`);
169
+ throw new Error(`HTTP_${response.status} at ${primaryEndpoint}: ${response.statusText}`);
156
170
  const result = await response.json();
157
171
  if (!result.data)
158
172
  throw new Error("Invalid GraphQL response: Missing 'data' field.");
159
- // Build Schema object from introspection JSON
160
173
  tempSchema = buildClientSchema(result.data);
161
174
  }
162
- // --- UNIFIED STRUCTURAL ANALYSIS (For AI Report) ---
175
+ // --- UNIFIED STRUCTURAL ANALYSIS ---
163
176
  const typeMap = tempSchema.getTypeMap();
164
- // Filter "Business Labels" (Nodes) while ignoring internal scalars/system types
177
+ // Filter domain types while ignoring internal system scalars and types
165
178
  const businessTypes = Object.keys(typeMap).filter(typeName => {
166
179
  const type = typeMap[typeName];
167
180
  return (!typeName.startsWith('__') &&
@@ -169,34 +182,31 @@ async function performUpdate(force) {
169
182
  !['String', 'Int', 'Float', 'Boolean', 'ID', 'BigInt', 'DateTime'].includes(typeName) &&
170
183
  isObjectType(type));
171
184
  });
172
- // Maintain state for "Gap Analysis"
173
185
  lastKnownTypeCount = businessTypes.length;
174
186
  const currentSDL = printSchema(tempSchema);
175
- // --- CACHE & NOTIFICATION ---
176
- // Always update the live object if we successfully built it
187
+ // Always update the live object upon successful build
177
188
  cachedSchemaObject = tempSchema;
178
189
  if (currentSDL !== cachedSDL) {
179
190
  cachedSDL = currentSDL;
180
191
  const duration = ((Date.now() - startTime) / 1000).toFixed(2);
192
+ const endpointLabel = env.SCHEMA ? 'Local/Remote SDL' : `Live Broadcast Node (${env.ENDPOINT.split(',').length} targets)`;
181
193
  return [
182
194
  `✨ SCHEMA EVOLVED (${duration}s)`,
183
- `📊 Source: ${env.SCHEMA ? 'Local/Remote SDL' : 'Live Endpoint'}`,
195
+ `📊 Source: ${endpointLabel}`,
184
196
  `🧬 Labels: ${businessTypes.join(', ') || 'None'}`,
185
197
  `---`,
186
198
  `The bridge has updated the graph model. New types are now queryable.`
187
199
  ].join('\n');
188
200
  }
189
201
  else {
190
- // Even if SDL string is the same, we've ensured cachedSchemaObject is set above
191
202
  return `✅ Status: Schema stable (${lastKnownTypeCount} labels).`;
192
203
  }
193
204
  }
194
205
  catch (error) {
195
- // Informative error report to prevent "AI confusion"
196
206
  return [
197
207
  `❌ SCHEMA SYNC FAILED`,
198
208
  `🔍 Reason: ${error.message}`,
199
- `🛠️ Action: Verify your ${env.SCHEMA ? 'file path' : 'endpoint connection'} and retry.`
209
+ `🛠️ Action: Verify your connection and retry.`
200
210
  ].join('\n');
201
211
  }
202
212
  finally {
@@ -206,50 +216,97 @@ async function performUpdate(force) {
206
216
  }
207
217
  // --- TOOL REGISTRY ---
208
218
  const toolHandlers = new Map();
209
- // This will store schemas for our dynamic HTTP 'list-tools' response
210
219
  const registeredToolsMetadata = [];
211
- /** * executionLogs stores the last 5 GraphQL operations.
212
- * This allows the AI to "inspect" its own generated queries and the raw data
213
- * for debugging or bridging to 3D visualization tools.
220
+ /**
221
+ * History buffer for the last 5 operations to support debugging and visualization.
214
222
  */
215
223
  let executionLogs = [];
216
- // Tool: query-graphql
224
+ /**
225
+ * Tool: query-graphql
226
+ * Handles query broadcast and execution across multiple endpoints.
227
+ */
217
228
  const queryGraphqlHandler = async ({ query, variables, headers }) => {
218
229
  try {
219
230
  const parsedQuery = (0, language_1.parse)(query);
220
231
  const isMutation = parsedQuery.definitions.some((def) => def.kind === "OperationDefinition" && def.operation === "mutation");
221
- if (isMutation && !env.ALLOW_MUTATIONS)
232
+ if (isMutation && !env.ALLOW_MUTATIONS) {
222
233
  throw new Error("Mutations are not allowed.");
234
+ }
223
235
  const toolHeaders = headers ? JSON.parse(headers) : {};
224
236
  const allHeaders = { "Content-Type": "application/json", ...env.HEADERS, ...toolHeaders };
225
237
  let parsedVariables = variables;
226
238
  if (typeof variables === 'string')
227
239
  parsedVariables = JSON.parse(variables);
228
- const response = await fetch(env.ENDPOINT, {
229
- method: "POST",
230
- headers: allHeaders,
231
- body: JSON.stringify({ query, variables: parsedVariables }),
232
- });
233
- const data = (await response.json());
234
- // 1. Extract and sanitize Cypher if present in extensions
235
- const rawCypher = data.extensions?.cypher || [];
236
- const cleanCypher = rawCypher.map((c) => c.replace(/^CYPHER: /, '')
237
- .replace(/^CYPHER 5\n/, '')
238
- .replace(/\nPARAMS: \{\}$/, ''));
239
- // 2. Update execution history for internal server state
240
- executionLogs.push({
241
- query,
242
- variables: parsedVariables,
243
- response: data,
244
- timestamp: new Date().toISOString()
240
+ // Split multiple endpoints for broadcast
241
+ const endpoints = env.ENDPOINT.split(',').map(url => url.trim());
242
+ // Execute parallel requests to all targets
243
+ const settleResults = await Promise.allSettled(endpoints.map(async (url) => {
244
+ console.error(`[QUERY] Sending to ${url}`);
245
+ const response = await fetch(url, {
246
+ method: "POST",
247
+ headers: allHeaders,
248
+ body: JSON.stringify({ query, variables: parsedVariables }),
249
+ signal: AbortSignal.timeout(5000) // 5s timeout protection
250
+ });
251
+ if (!response.ok) {
252
+ throw new Error(`HTTP ${response.status} from ${url}`);
253
+ }
254
+ return {
255
+ url,
256
+ data: await response.json()
257
+ };
258
+ }));
259
+ const successfulResponses = settleResults
260
+ .filter((r) => r.status === 'fulfilled')
261
+ .map(r => r.value);
262
+ const failedResponses = settleResults
263
+ .filter((r) => r.status === 'rejected')
264
+ .map(r => r.reason.message || r.reason);
265
+ if (successfulResponses.length === 0) {
266
+ throw new Error(`All endpoints failed: ${failedResponses.join('; ')}`);
267
+ }
268
+ const aggregatedResult = {};
269
+ successfulResponses.forEach((response) => {
270
+ const data = response.data.data;
271
+ if (data && data.patterns) {
272
+ console.error(`[DEBUG] Received ${data.patterns.length} patterns from ${response.url}`);
273
+ }
274
+ if (!data)
275
+ return;
276
+ Object.keys(data).forEach(key => {
277
+ if (Array.isArray(data[key])) {
278
+ // Merge arrays from different sources
279
+ aggregatedResult[key] = [...(aggregatedResult[key] || []), ...data[key]];
280
+ }
281
+ else if (typeof data[key] === 'object' && data[key] !== null) {
282
+ // Merge objects (e.g. metadata)
283
+ aggregatedResult[key] = { ...(aggregatedResult[key] || {}), ...data[key] };
284
+ }
285
+ else {
286
+ // Use last value for primitives
287
+ aggregatedResult[key] = data[key];
288
+ }
289
+ });
245
290
  });
246
- if (executionLogs.length > 5)
247
- executionLogs.shift();
248
- // 3. Prepare optimized response for Claude
291
+ // 2. Deduplication (Removes duplicates by 'sticking' field)
292
+ if (aggregatedResult.patterns) {
293
+ const unique = new Map();
294
+ aggregatedResult.patterns.forEach((p) => unique.set(p.sticking, p));
295
+ aggregatedResult.patterns = Array.from(unique.values());
296
+ }
297
+ // 3. Extract Cypher queries from extensions
298
+ const allCypher = successfulResponses.flatMap(r => r.data.extensions?.cypher || []);
299
+ const cleanCypher = allCypher.map((c) => c.replace(/^CYPHER: /, '').replace(/^CYPHER 5\n/, '').replace(/\nPARAMS: \{\}$/, ''));
300
+ // 4. Final response for the AI agent
249
301
  const responseForClaude = {
250
- result: data.data,
251
- // Only add the cypher field if there's actual data to show
252
- ...(cleanCypher.length > 0 ? { cypher: cleanCypher } : {})
302
+ status: {
303
+ total: endpoints.length,
304
+ success: successfulResponses.length,
305
+ failed: failedResponses.length
306
+ },
307
+ result: aggregatedResult,
308
+ ...(cleanCypher.length > 0 ? { cypher: Array.from(new Set(cleanCypher)) } : {}),
309
+ ...(failedResponses.length > 0 ? { warnings: failedResponses } : {})
253
310
  };
254
311
  return {
255
312
  content: [{
@@ -270,13 +327,10 @@ toolHandlers.set("query-graphql", queryGraphqlHandler);
270
327
  }, queryGraphqlHandler);
271
328
  /**
272
329
  * Tool: introspect-schema
273
- * Main handler for the introspection tool.
274
- * Implements "Agent Recovery" logic to guide the LLM when entities are missing.
330
+ * Provides schema exploration with built-in agent recovery logic.
275
331
  */
276
332
  const introspectHandler = async ({ typeNames }) => {
277
- // 1. Fetch the schema directly from the source
278
333
  const result = await getSchema(true);
279
- // Explicitly check if the result is a valid GraphQLSchema object
280
334
  if (!result || typeof result === 'string') {
281
335
  return {
282
336
  content: [{
@@ -286,16 +340,13 @@ const introspectHandler = async ({ typeNames }) => {
286
340
  }]
287
341
  };
288
342
  }
289
- // --- 1. INITIALIZE MAPPINGS ---
290
343
  const schema = result;
291
344
  const typeMap = schema.getTypeMap();
292
- // Cache Root Type fields for rapid gap analysis
293
345
  const queryType = schema.getQueryType();
294
346
  const queryFields = queryType ? queryType.getFields() : {};
295
347
  const mutationType = schema.getMutationType();
296
348
  const mutationFields = mutationType ? mutationType.getFields() : {};
297
- // --- 2. GAP ANALYSIS & SELF-HEALING LOOP ---
298
- // If specific types were requested, verify their existence in the current schema
349
+ // Gap analysis for requested types
299
350
  if (typeNames && typeNames.length > 0) {
300
351
  const missing = typeNames.filter(name => {
301
352
  const existsAsType = !!typeMap[name];
@@ -303,31 +354,26 @@ const introspectHandler = async ({ typeNames }) => {
303
354
  const existsAsQueryField = !!queryFields[name];
304
355
  return !existsAsType && !existsAsMutation && !existsAsQueryField;
305
356
  });
306
- // If some requested entities are missing, provide the agent with a recovery map
307
357
  if (missing.length > 0) {
308
- // Filter out internal GraphQL types to reduce noise for the agent
309
358
  const internalTypes = ['Query', 'Mutation', 'Subscription'];
310
359
  const availableEntities = Object.keys(typeMap).filter(t => !t.startsWith('__') && !internalTypes.includes(t));
311
- // Generate a pseudo-version ID based on schema state and time
312
360
  const schemaVersion = `v${availableEntities.length}.${Math.floor(Date.now() / 10000) % 1000}`;
313
361
  return {
314
362
  content: [{
315
363
  type: "text",
316
364
  text: `❌ PARTIAL RESULTS [Schema ID: ${schemaVersion}]\n\n` +
317
365
  `MISSING ENTITIES: ${missing.join(", ")}\n` +
318
- `REASON: These specific types or fields were not found in the current schema.\n` +
319
- `ACTION: Re-examine the available entities below and correct your query intent.\n\n` +
366
+ `REASON: Requested entities not found.\n` +
367
+ `ACTION: Re-examine available types below.\n\n` +
320
368
  `AVAILABLE_ENTITIES: ${availableEntities.join(", ")}`
321
369
  }]
322
370
  };
323
371
  }
324
372
  }
325
- // --- 3. GENERAL MANIFEST GENERATION ---
326
- // If no typeNames provided, return a high-level overview of the entry points
373
+ // Return general manifest if no specific types requested
327
374
  if (!typeNames || typeNames.length === 0) {
328
375
  const discoveredEntities = new Set();
329
376
  if (queryType) {
330
- // Map Query fields to their underlying Object Types
331
377
  const fields = queryType.getFields();
332
378
  Object.values(fields).forEach((field) => {
333
379
  const namedType = (0, graphql_1.getNamedType)(field.type);
@@ -349,8 +395,6 @@ const introspectHandler = async ({ typeNames }) => {
349
395
  }]
350
396
  };
351
397
  }
352
- // --- 4. DETAILED INTROSPECTION ---
353
- // Return filtered schema metadata for the requested types or root fields
354
398
  const filtered = (0, introspection_js_1.introspectSpecificTypes)(schema, typeNames);
355
399
  return {
356
400
  content: [{ type: "text", text: JSON.stringify(filtered, null, 2) }]
@@ -360,63 +404,54 @@ toolHandlers.set("introspect-schema", introspectHandler);
360
404
  (0, tool_registry_js_1.registerTool)(server, toolHandlers, registeredToolsMetadata, "introspect-schema", "Introspect the GraphQL schema with optional type filtering", {
361
405
  typeNames: zod_1.default.array(zod_1.default.string()).optional(),
362
406
  }, introspectHandler);
363
- // --- PROMPTS (The "Add from ..." buttons in Claude UI) ---
364
- // 1. Connection check
407
+ // --- PROMPTS ---
365
408
  (0, prompt_registry_js_1.registerPrompt)(server, "health-check", "Check if the GraphQL endpoint is alive", "Run 'query-graphql' with query '{ __typename }' to verify connection.");
366
- // 2. High-level overview
367
409
  (0, prompt_registry_js_1.registerPrompt)(server, "schema-overview", "List all available types", "Run 'introspect-schema' to see all types and entry points.");
368
- // 3. Data types analysis
369
410
  (0, prompt_registry_js_1.registerPrompt)(server, "list-scalars", "List all scalar types", "Run 'introspect-schema' and identify all scalars in the schema.");
370
411
  // --- HTTP SERVER LOGIC ---
412
+ /**
413
+ * Local HTTP server to support GraphiQL UI and SSE transport
414
+ */
371
415
  async function handleHttpRequest(req, res) {
372
- // Standard CORS headers for cross-origin compatibility
416
+ // Standard CORS headers
373
417
  res.setHeader('Access-Control-Allow-Origin', '*');
374
- res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');
375
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
376
- // Handle preflight requests
418
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
419
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, x-mcp-protocol-version, x-mcp-sdk-version');
377
420
  if (req.method === 'OPTIONS') {
378
421
  res.writeHead(204);
379
422
  res.end();
380
423
  return;
381
424
  }
382
425
  const url = new URL(req.url || '', `http://${req.headers.host}`);
383
- // Serve Web GUI (GraphiQL) - ONLY if explicitly enabled
426
+ // Render GraphiQL UI
384
427
  if (req.method === 'GET' && (url.pathname === '/' || url.pathname === '/graphiql')) {
385
- // Check our explicit flag
386
- if (process.env.ENABLE_HTTP === 'true') {
387
- res.writeHead(200, { 'Content-Type': 'text/html' });
388
- return res.end((0, graphiql_js_1.renderGraphiQL)(env.ENDPOINT, env.HEADERS));
389
- }
390
- else {
391
- // Forbidden if not explicitly enabled
392
- res.writeHead(403, { 'Content-Type': 'text/plain' });
393
- return res.end("Forbidden: GraphiQL UI is disabled. Start with ENABLE_HTTP=true to use it.");
394
- }
428
+ res.writeHead(200, { 'Content-Type': 'text/html' });
429
+ return res.end((0, graphiql_js_1.renderGraphiQL)(`http://localhost:6274/mcp`, env.HEADERS));
395
430
  }
396
- // Process MCP JSON-RPC Endpoint
431
+ // Process MCP/GraphQL requests
397
432
  if (url.pathname === '/mcp' && req.method === 'POST') {
398
433
  let body = '';
399
434
  req.on('data', chunk => { body += chunk; });
400
435
  req.on('end', async () => {
401
- let requestId = null; // Defined early to be accessible in catch block
436
+ let requestId = null;
402
437
  try {
403
- const request = JSON.parse(body);
404
- const { method, id, params } = request;
405
- requestId = id;
406
- console.error(`[HTTP-RPC] Method: ${method} | ID: ${id}`);
407
- // --- DYNAMIC DISCOVERY ---
408
- if (method === "list-tools" || method === "tools/list") {
409
- res.writeHead(200, { 'Content-Type': 'application/json' });
410
- return res.end(JSON.stringify({
411
- jsonrpc: '2.0',
412
- id: requestId,
413
- result: { tools: registeredToolsMetadata }
414
- }));
438
+ const payload = JSON.parse(body);
439
+ // Handle raw GraphQL queries (e.g., from Docs or Playground)
440
+ if (!payload.method && payload.query) {
441
+ const handler = toolHandlers.get("query-graphql");
442
+ if (handler) {
443
+ const mcpResult = await handler({ query: payload.query, variables: payload.variables });
444
+ const parsed = JSON.parse(mcpResult.content[0].text);
445
+ res.writeHead(200, { 'Content-Type': 'application/json' });
446
+ const graphQLResponse = parsed.result.data ? parsed.result : { data: parsed.result };
447
+ return res.end(JSON.stringify(graphQLResponse));
448
+ }
415
449
  }
416
- // --- TOOL EXECUTION (CALL) ---
450
+ // Standard MCP JSON-RPC handling
451
+ const { method, id, params } = payload;
452
+ requestId = id;
417
453
  let targetMethod = method;
418
454
  let toolArgs = params;
419
- // Support both direct calls and standard MCP "call-tool" structure
420
455
  if (method === "call-tool" || method === "tools/call") {
421
456
  targetMethod = params.name;
422
457
  toolArgs = params.arguments;
@@ -430,14 +465,9 @@ async function handleHttpRequest(req, res) {
430
465
  error: { code: -32601, message: `Tool ${targetMethod} not found` }
431
466
  }));
432
467
  }
433
- // Execute the actual business logic for the tool
434
468
  const result = await handler(toolArgs);
435
469
  res.writeHead(200, { 'Content-Type': 'application/json' });
436
- res.end(JSON.stringify({
437
- jsonrpc: '2.0',
438
- id: requestId,
439
- result
440
- }));
470
+ res.end(JSON.stringify({ jsonrpc: '2.0', id: requestId, result }));
441
471
  }
442
472
  catch (e) {
443
473
  console.error(`[HTTP-ERROR] ${e.message}`);
@@ -445,18 +475,12 @@ async function handleHttpRequest(req, res) {
445
475
  res.end(JSON.stringify({
446
476
  jsonrpc: '2.0',
447
477
  id: requestId,
448
- error: { code: -32603, message: e.message || "Internal Server Error" }
478
+ error: { code: -32603, message: e.message }
449
479
  }));
450
480
  }
451
481
  });
452
482
  return;
453
483
  }
454
- // Health check endpoint for monitoring
455
- if (url.pathname === '/health') {
456
- res.writeHead(200, { 'Content-Type': 'application/json' });
457
- return res.end(JSON.stringify({ status: "ok", server: env.NAME }));
458
- }
459
- // Default 404 for unknown paths
460
484
  res.writeHead(404);
461
485
  res.end("Not Found");
462
486
  }
@@ -464,7 +488,7 @@ async function handleHttpRequest(req, res) {
464
488
  async function main() {
465
489
  const isInspector = !!(process.env.MCP_INSPECTOR || process.env.INSPECTOR_PORT);
466
490
  const isHttpExplicitlyEnabled = process.env.ENABLE_HTTP === "true";
467
- // Open HTTP port by default for MCP SSE, unless explicitly disabled
491
+ // Enable HTTP port by default unless explicitly disabled or in inspector mode
468
492
  if (process.env.ENABLE_HTTP !== "false" && !isInspector) {
469
493
  const serverHttp = node_http_1.default.createServer(handleHttpRequest);
470
494
  serverHttp.on('error', (e) => {
@@ -473,10 +497,8 @@ async function main() {
473
497
  }
474
498
  });
475
499
  serverHttp.listen(env.MCP_PORT, () => {
476
- // All-in-one status report
477
500
  console.error(`[SYSTEM] Server "${env.NAME}" v${getVersion()} active`);
478
501
  console.error(`🤖 MCP SSE: http://localhost:${env.MCP_PORT}/mcp`);
479
- // Show UI only if explicitly requested
480
502
  if (isHttpExplicitlyEnabled) {
481
503
  console.error(`🎨 GraphiQL UI: http://localhost:${env.MCP_PORT}/graphiql`);
482
504
  }
@@ -484,6 +506,7 @@ async function main() {
484
506
  }
485
507
  const transport = new stdio_js_1.StdioServerTransport();
486
508
  await server.connect(transport);
509
+ // Background schema initialization
487
510
  getSchema().catch(() => { });
488
511
  }
489
512
  process.on('SIGINT', () => process.exit(0));
package/package.json CHANGED
@@ -50,5 +50,5 @@
50
50
  "tsx": "^4.21.0",
51
51
  "typescript": "5.8.3"
52
52
  },
53
- "version": "3.8.1"
53
+ "version": "3.9.0"
54
54
  }