@letoribo/mcp-graphql-enhanced 3.8.0 → 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.
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
 
@@ -8,8 +8,9 @@ export declare function introspectEndpoint(endpoint: string, headers?: Record<st
8
8
  */
9
9
  export declare function introspectLocalSchema(path: string): Promise<string>;
10
10
  /**
11
- * Extract and filter specific types from a schema object.
12
- * Prevents "No result received" errors by only sending requested parts of the graph.
11
+ * Extract and filter specific types or root fields from a GraphQL schema.
12
+ * This prevents "No result received" errors by only sending the requested
13
+ * parts of the graph to the agent, maintaining a stable context window.
13
14
  */
14
15
  export declare function introspectSpecificTypes(schema: GraphQLSchema, typeNames: string[]): Record<string, any>;
15
16
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"introspection.d.ts","sourceRoot":"","sources":["../../src/helpers/introspection.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,aAAa,EAQd,MAAM,SAAS,CAAC;AAGjB;;GAEG;AACH,wBAAsB,kBAAkB,CACtC,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,mBAiBjC;AAED;;GAEG;AACH,wBAAsB,qBAAqB,CAAC,IAAI,EAAE,MAAM,mBAEvD;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,EAAE,uBAmEjF;AAED;;GAEG;AACH,wBAAsB,eAAe,CACnC,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,YAAK,EACpC,SAAS,EAAE,MAAM,EAAE,mBAYpB"}
1
+ {"version":3,"file":"introspection.d.ts","sourceRoot":"","sources":["../../src/helpers/introspection.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,aAAa,EAQd,MAAM,SAAS,CAAC;AAGjB;;GAEG;AACH,wBAAsB,kBAAkB,CACtC,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,mBAiBjC;AAED;;GAEG;AACH,wBAAsB,qBAAqB,CAAC,IAAI,EAAE,MAAM,mBAEvD;AAED;;;;GAIG;AAEH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,EAAE,uBAgGjF;AAED;;GAEG;AACH,wBAAsB,eAAe,CACnC,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,YAAK,EACpC,SAAS,EAAE,MAAM,EAAE,mBAYpB"}
@@ -31,12 +31,39 @@ async function introspectLocalSchema(path) {
31
31
  return await (0, promises_1.readFile)(path, "utf8");
32
32
  }
33
33
  /**
34
- * Extract and filter specific types from a schema object.
35
- * Prevents "No result received" errors by only sending requested parts of the graph.
34
+ * Extract and filter specific types or root fields from a GraphQL schema.
35
+ * This prevents "No result received" errors by only sending the requested
36
+ * parts of the graph to the agent, maintaining a stable context window.
36
37
  */
37
38
  function introspectSpecificTypes(schema, typeNames) {
38
39
  const result = {};
40
+ // Cache root field maps to avoid repeated lookups during the loop
41
+ const queryType = schema.getQueryType();
42
+ const queryFields = queryType ? queryType.getFields() : {};
43
+ const mutationType = schema.getMutationType();
44
+ const mutationFields = mutationType ? mutationType.getFields() : {};
39
45
  for (const name of typeNames) {
46
+ // --- ROOT FIELD RESOLUTION ---
47
+ // Check if the name refers to a root field (Query or Mutation)
48
+ // rather than a named Type.
49
+ const rootField = queryFields[name] || mutationFields[name];
50
+ if (rootField) {
51
+ result[name] = {
52
+ kind: queryFields[name] ? "QUERY_FIELD" : "MUTATION_FIELD",
53
+ description: rootField.description,
54
+ type: rootField.type.toString(),
55
+ args: rootField.args
56
+ .filter(arg => !arg.deprecationReason)
57
+ .map(arg => ({
58
+ name: arg.name,
59
+ type: arg.type.toString(),
60
+ description: arg.description,
61
+ }))
62
+ };
63
+ continue; // Field found, move to next requested name
64
+ }
65
+ // --- NAMED TYPE RESOLUTION ---
66
+ // Fallback to standard type introspection if no root field matches
40
67
  const type = schema.getType(name);
41
68
  if (!type)
42
69
  continue;
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: [{
@@ -268,76 +325,76 @@ toolHandlers.set("query-graphql", queryGraphqlHandler);
268
325
  variables: zod_1.default.string().optional(),
269
326
  headers: zod_1.default.string().optional(),
270
327
  }, queryGraphqlHandler);
271
- // Tool: introspect-schema
272
- // --- TOOL: introspect-schema ---
328
+ /**
329
+ * Tool: introspect-schema
330
+ * Provides schema exploration with built-in agent recovery logic.
331
+ */
273
332
  const introspectHandler = async ({ typeNames }) => {
274
- // 1. Fetch the schema directly
275
333
  const result = await getSchema(true);
276
- // Explicitly check if the result is an error string or null
277
334
  if (!result || typeof result === 'string') {
278
335
  return {
279
336
  content: [{
280
337
  type: "text",
281
338
  text: `❌ SCHEMA_ERROR: ${typeof result === 'string' ? result : 'GraphQL schema is not initialized yet.'}\n` +
282
- `ACTION: Please wait 5-10 seconds for the Neo4j endpoint to respond.`
339
+ `ACTION: Please wait 5-10 seconds for the backend endpoint to respond.`
283
340
  }]
284
341
  };
285
342
  }
286
- // TYPE FIX: Cast to GraphQLSchema so TS knows methods like getTypeMap exist
287
343
  const schema = result;
288
344
  const typeMap = schema.getTypeMap();
345
+ const queryType = schema.getQueryType();
346
+ const queryFields = queryType ? queryType.getFields() : {};
289
347
  const mutationType = schema.getMutationType();
290
348
  const mutationFields = mutationType ? mutationType.getFields() : {};
291
- // --- GAP ANALYSIS ---
349
+ // Gap analysis for requested types
292
350
  if (typeNames && typeNames.length > 0) {
293
351
  const missing = typeNames.filter(name => {
294
352
  const existsAsType = !!typeMap[name];
295
353
  const existsAsMutation = !!mutationFields[name];
296
- return !existsAsType && !existsAsMutation;
354
+ const existsAsQueryField = !!queryFields[name];
355
+ return !existsAsType && !existsAsMutation && !existsAsQueryField;
297
356
  });
298
357
  if (missing.length > 0) {
299
- const typeKeys = Object.keys(typeMap).filter(t => !t.startsWith('__'));
300
- const schemaVersion = `v${typeKeys.length}.${Math.floor(Date.now() / 10000) % 1000}`;
358
+ const internalTypes = ['Query', 'Mutation', 'Subscription'];
359
+ const availableEntities = Object.keys(typeMap).filter(t => !t.startsWith('__') && !internalTypes.includes(t));
360
+ const schemaVersion = `v${availableEntities.length}.${Math.floor(Date.now() / 10000) % 1000}`;
301
361
  return {
302
362
  content: [{
303
363
  type: "text",
304
- text: `❌ PARTIAL RESULTS [ID: ${schemaVersion}]\n\n` +
364
+ text: `❌ PARTIAL RESULTS [Schema ID: ${schemaVersion}]\n\n` +
305
365
  `MISSING ENTITIES: ${missing.join(", ")}\n` +
306
- `REASON: These specific types or mutations were not found in the current schema map.\n` +
307
- `ACTION: Ensure the names match your Neo4j labels exactly.\n\n` +
308
- `AVAILABLE_ENTITIES: ${typeKeys.filter(t => !['Query', 'Mutation', 'Report'].includes(t)).join(", ")}`
366
+ `REASON: Requested entities not found.\n` +
367
+ `ACTION: Re-examine available types below.\n\n` +
368
+ `AVAILABLE_ENTITIES: ${availableEntities.join(", ")}`
309
369
  }]
310
370
  };
311
371
  }
312
372
  }
313
- // 2. Generate General Manifest
373
+ // Return general manifest if no specific types requested
314
374
  if (!typeNames || typeNames.length === 0) {
315
- const queryType = schema.getQueryType();
316
375
  const discoveredEntities = new Set();
317
376
  if (queryType) {
318
- // TYPE FIX: Cast fields to Record<string, GraphQLField<any, any>>
319
- const queryFields = queryType.getFields();
320
- Object.values(queryFields).forEach((field) => {
377
+ const fields = queryType.getFields();
378
+ Object.values(fields).forEach((field) => {
321
379
  const namedType = (0, graphql_1.getNamedType)(field.type);
322
380
  if ((0, graphql_1.isObjectType)(namedType) && !namedType.name.startsWith('__')) {
323
381
  discoveredEntities.add(namedType.name);
324
382
  }
325
383
  });
326
384
  }
327
- const coreEntities = Array.from(discoveredEntities).sort();
328
- const typeKeys = Object.keys(typeMap).filter(t => !t.startsWith('__'));
329
- const schemaVersion = `v${typeKeys.length}.${Math.floor(Date.now() / 10000) % 1000}`;
385
+ const entryPoints = Array.from(discoveredEntities).sort();
386
+ const allTypes = Object.keys(typeMap).filter(t => !t.startsWith('__'));
387
+ const schemaVersion = `v${allTypes.length}.${Math.floor(Date.now() / 10000) % 1000}`;
330
388
  return {
331
389
  content: [{
332
390
  type: "text",
333
391
  text: `GraphQL Schema Manifest [ID: ${schemaVersion}]\n\n` +
334
- `ENTRY_POINT_ENTITIES: ${coreEntities.join(", ") || "None"}\n` +
335
- `TOTAL_SCHEMA_TYPES: ${typeKeys.length}\n\n` +
336
- `ALL_AVAILABLE_TYPES: ${typeKeys.join(", ")}`
392
+ `ENTRY_POINT_ENTITIES: ${entryPoints.join(", ") || "None"}\n` +
393
+ `TOTAL_SCHEMA_TYPES: ${allTypes.length}\n\n` +
394
+ `ALL_AVAILABLE_TYPES: ${allTypes.join(", ")}`
337
395
  }]
338
396
  };
339
397
  }
340
- // 3. Detailed introspection
341
398
  const filtered = (0, introspection_js_1.introspectSpecificTypes)(schema, typeNames);
342
399
  return {
343
400
  content: [{ type: "text", text: JSON.stringify(filtered, null, 2) }]
@@ -347,63 +404,54 @@ toolHandlers.set("introspect-schema", introspectHandler);
347
404
  (0, tool_registry_js_1.registerTool)(server, toolHandlers, registeredToolsMetadata, "introspect-schema", "Introspect the GraphQL schema with optional type filtering", {
348
405
  typeNames: zod_1.default.array(zod_1.default.string()).optional(),
349
406
  }, introspectHandler);
350
- // --- PROMPTS (The "Add from ..." buttons in Claude UI) ---
351
- // 1. Connection check
407
+ // --- PROMPTS ---
352
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.");
353
- // 2. High-level overview
354
409
  (0, prompt_registry_js_1.registerPrompt)(server, "schema-overview", "List all available types", "Run 'introspect-schema' to see all types and entry points.");
355
- // 3. Data types analysis
356
410
  (0, prompt_registry_js_1.registerPrompt)(server, "list-scalars", "List all scalar types", "Run 'introspect-schema' and identify all scalars in the schema.");
357
411
  // --- HTTP SERVER LOGIC ---
412
+ /**
413
+ * Local HTTP server to support GraphiQL UI and SSE transport
414
+ */
358
415
  async function handleHttpRequest(req, res) {
359
- // Standard CORS headers for cross-origin compatibility
416
+ // Standard CORS headers
360
417
  res.setHeader('Access-Control-Allow-Origin', '*');
361
- res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');
362
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
363
- // 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');
364
420
  if (req.method === 'OPTIONS') {
365
421
  res.writeHead(204);
366
422
  res.end();
367
423
  return;
368
424
  }
369
425
  const url = new URL(req.url || '', `http://${req.headers.host}`);
370
- // Serve Web GUI (GraphiQL) - ONLY if explicitly enabled
426
+ // Render GraphiQL UI
371
427
  if (req.method === 'GET' && (url.pathname === '/' || url.pathname === '/graphiql')) {
372
- // Check our explicit flag
373
- if (process.env.ENABLE_HTTP === 'true') {
374
- res.writeHead(200, { 'Content-Type': 'text/html' });
375
- return res.end((0, graphiql_js_1.renderGraphiQL)(env.ENDPOINT, env.HEADERS));
376
- }
377
- else {
378
- // Forbidden if not explicitly enabled
379
- res.writeHead(403, { 'Content-Type': 'text/plain' });
380
- return res.end("Forbidden: GraphiQL UI is disabled. Start with ENABLE_HTTP=true to use it.");
381
- }
428
+ res.writeHead(200, { 'Content-Type': 'text/html' });
429
+ return res.end((0, graphiql_js_1.renderGraphiQL)(`http://localhost:6274/mcp`, env.HEADERS));
382
430
  }
383
- // Process MCP JSON-RPC Endpoint
431
+ // Process MCP/GraphQL requests
384
432
  if (url.pathname === '/mcp' && req.method === 'POST') {
385
433
  let body = '';
386
434
  req.on('data', chunk => { body += chunk; });
387
435
  req.on('end', async () => {
388
- let requestId = null; // Defined early to be accessible in catch block
436
+ let requestId = null;
389
437
  try {
390
- const request = JSON.parse(body);
391
- const { method, id, params } = request;
392
- requestId = id;
393
- console.error(`[HTTP-RPC] Method: ${method} | ID: ${id}`);
394
- // --- DYNAMIC DISCOVERY ---
395
- if (method === "list-tools" || method === "tools/list") {
396
- res.writeHead(200, { 'Content-Type': 'application/json' });
397
- return res.end(JSON.stringify({
398
- jsonrpc: '2.0',
399
- id: requestId,
400
- result: { tools: registeredToolsMetadata }
401
- }));
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
+ }
402
449
  }
403
- // --- TOOL EXECUTION (CALL) ---
450
+ // Standard MCP JSON-RPC handling
451
+ const { method, id, params } = payload;
452
+ requestId = id;
404
453
  let targetMethod = method;
405
454
  let toolArgs = params;
406
- // Support both direct calls and standard MCP "call-tool" structure
407
455
  if (method === "call-tool" || method === "tools/call") {
408
456
  targetMethod = params.name;
409
457
  toolArgs = params.arguments;
@@ -417,14 +465,9 @@ async function handleHttpRequest(req, res) {
417
465
  error: { code: -32601, message: `Tool ${targetMethod} not found` }
418
466
  }));
419
467
  }
420
- // Execute the actual business logic for the tool
421
468
  const result = await handler(toolArgs);
422
469
  res.writeHead(200, { 'Content-Type': 'application/json' });
423
- res.end(JSON.stringify({
424
- jsonrpc: '2.0',
425
- id: requestId,
426
- result
427
- }));
470
+ res.end(JSON.stringify({ jsonrpc: '2.0', id: requestId, result }));
428
471
  }
429
472
  catch (e) {
430
473
  console.error(`[HTTP-ERROR] ${e.message}`);
@@ -432,18 +475,12 @@ async function handleHttpRequest(req, res) {
432
475
  res.end(JSON.stringify({
433
476
  jsonrpc: '2.0',
434
477
  id: requestId,
435
- error: { code: -32603, message: e.message || "Internal Server Error" }
478
+ error: { code: -32603, message: e.message }
436
479
  }));
437
480
  }
438
481
  });
439
482
  return;
440
483
  }
441
- // Health check endpoint for monitoring
442
- if (url.pathname === '/health') {
443
- res.writeHead(200, { 'Content-Type': 'application/json' });
444
- return res.end(JSON.stringify({ status: "ok", server: env.NAME }));
445
- }
446
- // Default 404 for unknown paths
447
484
  res.writeHead(404);
448
485
  res.end("Not Found");
449
486
  }
@@ -451,7 +488,7 @@ async function handleHttpRequest(req, res) {
451
488
  async function main() {
452
489
  const isInspector = !!(process.env.MCP_INSPECTOR || process.env.INSPECTOR_PORT);
453
490
  const isHttpExplicitlyEnabled = process.env.ENABLE_HTTP === "true";
454
- // Open HTTP port by default for MCP SSE, unless explicitly disabled
491
+ // Enable HTTP port by default unless explicitly disabled or in inspector mode
455
492
  if (process.env.ENABLE_HTTP !== "false" && !isInspector) {
456
493
  const serverHttp = node_http_1.default.createServer(handleHttpRequest);
457
494
  serverHttp.on('error', (e) => {
@@ -460,10 +497,8 @@ async function main() {
460
497
  }
461
498
  });
462
499
  serverHttp.listen(env.MCP_PORT, () => {
463
- // All-in-one status report
464
500
  console.error(`[SYSTEM] Server "${env.NAME}" v${getVersion()} active`);
465
501
  console.error(`🤖 MCP SSE: http://localhost:${env.MCP_PORT}/mcp`);
466
- // Show UI only if explicitly requested
467
502
  if (isHttpExplicitlyEnabled) {
468
503
  console.error(`🎨 GraphiQL UI: http://localhost:${env.MCP_PORT}/graphiql`);
469
504
  }
@@ -471,6 +506,7 @@ async function main() {
471
506
  }
472
507
  const transport = new stdio_js_1.StdioServerTransport();
473
508
  await server.connect(transport);
509
+ // Background schema initialization
474
510
  getSchema().catch(() => { });
475
511
  }
476
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.0"
53
+ "version": "3.9.0"
54
54
  }