@letoribo/mcp-graphql-enhanced 3.9.0 → 3.9.1

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 +5 -6
  2. package/dist/index.js +178 -295
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -23,15 +23,14 @@ 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.
26
+ ## 🚀 Federated Multi-Node Architecture (v3.9.1+)
27
+ The server operates as a Federated GraphQL Gateway, merging independent nodes into a unified system.
28
28
 
29
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**.
30
+ * **Federated Introspection**: Scans all endpoints simultaneously to build a global capability map.
31
+ * **Smart Aggregation**: When multiple comma-separated URLs are provided, the server broadcasts queries and merges results using universal deep deduplication (object-level).
32
+ * **Conflict Handling**: Identifies structural differences in identical Type names across nodes and exposes them uniquely.
31
33
  * **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
34
 
36
35
  #### 💡 Use Case: Bridging WSL and Windows (PowerShell)
37
36
  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".
package/dist/index.js CHANGED
@@ -30,15 +30,13 @@ const getVersion = () => {
30
30
  };
31
31
  (0, deprecation_js_1.checkDeprecatedArguments)();
32
32
  /**
33
- * Environment configuration schema
33
+ * Environment configuration schema - Strict validation
34
34
  */
35
35
  const EnvSchema = zod_1.default.object({
36
36
  NAME: zod_1.default.string().default("mcp-graphql-enhanced"),
37
37
  ENDPOINT: zod_1.default.preprocess((val) => {
38
- if (typeof val === 'string') {
39
- // Support for multiple endpoints via comma-separated string
38
+ if (typeof val === 'string')
40
39
  return val.trim();
41
- }
42
40
  return val;
43
41
  }, zod_1.default.string().min(1)).default("https://mcp-neo4j-discord.vercel.app/api/graphiql"),
44
42
  ALLOW_MUTATIONS: zod_1.default
@@ -69,57 +67,52 @@ const EnvSchema = zod_1.default.object({
69
67
  .default("auto"),
70
68
  });
71
69
  const env = EnvSchema.parse(process.env);
70
+ /**
71
+ * Initialize MCP Server with full capabilities
72
+ */
72
73
  const server = new mcp_js_1.McpServer({
73
74
  name: env.NAME,
74
75
  version: getVersion(),
75
- description: "Unified GraphQL-to-MCP bridge with dynamic schema support."
76
+ description: "Federated GraphQL-to-MCP bridge with broadcast introspection and full type visibility."
76
77
  }, {
77
78
  capabilities: {
78
79
  prompts: {},
79
80
  tools: {}
80
81
  }
81
82
  });
82
- // --- CACHE STATE ---
83
+ // --- GLOBAL STATE MANAGEMENT ---
83
84
  let cachedSDL = null;
84
85
  let cachedSchemaObject = null;
86
+ let cachedSchemas = [];
85
87
  let schemaLoadError = null;
86
88
  let isUpdating = false;
87
89
  let updatePromise = null;
88
- let lastKnownTypeCount = 0;
89
90
  /**
90
- * Smart Hybrid Schema Fetcher
91
- * @param force If true, blocks and waits for the new schema evolution.
92
- * If false, returns cache immediately and updates in background.
91
+ * Schema Fetcher with dependency tracking
93
92
  */
94
93
  async function getSchema(force = false, requestedTypes) {
95
- // 1. Hook into existing update if in progress
96
94
  if (isUpdating && updatePromise) {
97
95
  if (force || !cachedSDL)
98
96
  return await updatePromise;
99
97
  return cachedSDL;
100
98
  }
101
- // 2. Return cache if valid and not forcing
102
99
  if (cachedSDL && !force) {
103
- // Validation check: ensure requested types exist in current cache
104
- if (requestedTypes && cachedSchemaObject) {
105
- const typeMap = cachedSchemaObject.getTypeMap();
106
- const missing = requestedTypes.filter(t => !typeMap[t]);
107
- if (missing.length > 0) {
108
- // Force a refresh if requested types are missing
100
+ if (requestedTypes && cachedSchemas.length > 0) {
101
+ const allTypes = new Set(cachedSchemas.flatMap(s => Object.keys(s.getTypeMap())));
102
+ const missing = requestedTypes.filter(t => !allTypes.has(t));
103
+ if (missing.length > 0)
109
104
  return await (updatePromise = performUpdate(true));
110
- }
111
105
  }
112
- return cachedSDL;
106
+ return cachedSchemaObject;
113
107
  }
114
108
  if (force)
115
109
  schemaLoadError = null;
116
110
  if (schemaLoadError)
117
111
  throw schemaLoadError;
118
- // 3. Trigger update
119
112
  updatePromise = performUpdate(force);
120
113
  try {
121
114
  if (force || !cachedSDL) {
122
- await updatePromise; // Wait for update to complete
115
+ await updatePromise;
123
116
  return cachedSchemaObject;
124
117
  }
125
118
  return cachedSchemaObject;
@@ -129,354 +122,246 @@ async function getSchema(force = false, requestedTypes) {
129
122
  }
130
123
  }
131
124
  /**
132
- * Internal logic for schema introspection and building.
133
- * Optimized for Multi-Endpoint Broadcast: Uses the first available URL for discovery.
125
+ * Federated Update: Orchestrates introspection across all endpoints
134
126
  */
135
127
  async function performUpdate(force) {
136
128
  isUpdating = true;
137
129
  const startTime = Date.now();
138
130
  try {
139
- const { buildClientSchema, getIntrospectionQuery, printSchema, buildASTSchema, parse: gqlParse, isObjectType } = require("graphql");
140
- let tempSchema;
141
- // --- FETCHING LOGIC ---
142
- if (env.SCHEMA) {
143
- let sdl;
144
- if (env.SCHEMA.startsWith("http")) {
145
- // Remote SDL File
146
- const response = await fetch(env.SCHEMA);
147
- if (!response.ok)
148
- throw new Error(`Remote_SDL_Fetch_Failed: ${response.statusText}`);
149
- sdl = await response.text();
150
- }
151
- else {
152
- // Local SDL File via helper
153
- sdl = await (0, introspection_js_1.introspectLocalSchema)(env.SCHEMA);
154
- }
155
- tempSchema = buildASTSchema(gqlParse(sdl));
156
- }
157
- else {
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, {
131
+ const endpoints = env.ENDPOINT.split(',').map(url => url.trim());
132
+ console.error(`[SYNC] Initializing broadcast to ${endpoints.length} nodes...`);
133
+ const results = await Promise.allSettled(endpoints.map(async (url) => {
134
+ const response = await fetch(url, {
164
135
  method: "POST",
165
136
  headers: { "Content-Type": "application/json", ...env.HEADERS },
166
- body: JSON.stringify({ query: getIntrospectionQuery() }),
137
+ body: JSON.stringify({ query: (0, graphql_1.getIntrospectionQuery)() }),
138
+ signal: AbortSignal.timeout(10000)
167
139
  });
168
140
  if (!response.ok)
169
- throw new Error(`HTTP_${response.status} at ${primaryEndpoint}: ${response.statusText}`);
141
+ throw new Error(`HTTP Error ${response.status}`);
170
142
  const result = await response.json();
171
143
  if (!result.data)
172
- throw new Error("Invalid GraphQL response: Missing 'data' field.");
173
- tempSchema = buildClientSchema(result.data);
144
+ throw new Error("Empty introspection data");
145
+ const schema = (0, graphql_1.buildClientSchema)(result.data);
146
+ schema._originUrl = url;
147
+ return { url, schema };
148
+ }));
149
+ const successful = results
150
+ .filter((r) => r.status === 'fulfilled')
151
+ .map(r => r.value);
152
+ const failures = results
153
+ .filter((r) => r.status === 'rejected')
154
+ .map(r => r.reason.message);
155
+ if (successful.length === 0) {
156
+ throw new Error(`Federation failed. All nodes unreachable: ${failures.join(', ')}`);
174
157
  }
175
- // --- UNIFIED STRUCTURAL ANALYSIS ---
176
- const typeMap = tempSchema.getTypeMap();
177
- // Filter domain types while ignoring internal system scalars and types
178
- const businessTypes = Object.keys(typeMap).filter(typeName => {
179
- const type = typeMap[typeName];
180
- return (!typeName.startsWith('__') &&
181
- !['Query', 'Mutation', 'Subscription'].includes(typeName) &&
182
- !['String', 'Int', 'Float', 'Boolean', 'ID', 'BigInt', 'DateTime'].includes(typeName) &&
183
- isObjectType(type));
158
+ // Update cache with all discovered schemas
159
+ cachedSchemas = successful.map(s => s.schema);
160
+ cachedSchemaObject = cachedSchemas[0]; // Baseline schema
161
+ cachedSDL = (0, graphql_1.printSchema)(cachedSchemaObject);
162
+ // Generate Node Manifest for AI reasoning (Full data, no slices)
163
+ global.nodeManifest = successful.map(node => {
164
+ const typeMap = node.schema.getTypeMap();
165
+ const domainTypes = Object.keys(typeMap).filter(name => {
166
+ const type = typeMap[name];
167
+ return !name.startsWith('__') && !(0, graphql_1.isScalarType)(type);
168
+ });
169
+ return {
170
+ endpoint: node.url,
171
+ availableMutations: Object.keys(node.schema.getMutationType()?.getFields() || {}),
172
+ domainEntities: domainTypes
173
+ };
184
174
  });
185
- lastKnownTypeCount = businessTypes.length;
186
- const currentSDL = printSchema(tempSchema);
187
- // Always update the live object upon successful build
188
- cachedSchemaObject = tempSchema;
189
- if (currentSDL !== cachedSDL) {
190
- cachedSDL = currentSDL;
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)`;
193
- return [
194
- `✨ SCHEMA EVOLVED (${duration}s)`,
195
- `📊 Source: ${endpointLabel}`,
196
- `🧬 Labels: ${businessTypes.join(', ') || 'None'}`,
197
- `---`,
198
- `The bridge has updated the graph model. New types are now queryable.`
199
- ].join('\n');
200
- }
201
- else {
202
- return `✅ Status: Schema stable (${lastKnownTypeCount} labels).`;
203
- }
175
+ const uniqueTypeCount = new Set(cachedSchemas.flatMap(s => Object.keys(s.getTypeMap()))).size;
176
+ const duration = ((Date.now() - startTime) / 1000).toFixed(2);
177
+ return `✅ FEDERATION SYNCED: ${successful.length} active nodes, ${uniqueTypeCount} unique types discovered in ${duration}s.`;
204
178
  }
205
179
  catch (error) {
206
- return [
207
- `❌ SCHEMA SYNC FAILED`,
208
- `🔍 Reason: ${error.message}`,
209
- `🛠️ Action: Verify your connection and retry.`
210
- ].join('\n');
180
+ console.error(`[CRITICAL] Sync failure: ${error.message}`);
181
+ schemaLoadError = error;
182
+ return `❌ SYNC ERROR: ${error.message}`;
211
183
  }
212
184
  finally {
213
185
  isUpdating = false;
214
186
  updatePromise = null;
215
187
  }
216
188
  }
217
- // --- TOOL REGISTRY ---
189
+ // --- TOOLS IMPLEMENTATION ---
218
190
  const toolHandlers = new Map();
219
191
  const registeredToolsMetadata = [];
220
- /**
221
- * History buffer for the last 5 operations to support debugging and visualization.
222
- */
223
- let executionLogs = [];
224
192
  /**
225
193
  * Tool: query-graphql
226
- * Handles query broadcast and execution across multiple endpoints.
194
+ * Broadcasts queries to all nodes and merges results with universal deduplication.
227
195
  */
228
196
  const queryGraphqlHandler = async ({ query, variables, headers }) => {
229
197
  try {
230
198
  const parsedQuery = (0, language_1.parse)(query);
231
- const isMutation = parsedQuery.definitions.some((def) => def.kind === "OperationDefinition" && def.operation === "mutation");
232
- if (isMutation && !env.ALLOW_MUTATIONS) {
233
- throw new Error("Mutations are not allowed.");
199
+ const hasMutation = parsedQuery.definitions.some((def) => def.kind === "OperationDefinition" && def.operation === "mutation");
200
+ if (hasMutation && !env.ALLOW_MUTATIONS) {
201
+ throw new Error("Mutation execution is blocked by ALLOW_MUTATIONS=false.");
234
202
  }
235
- const toolHeaders = headers ? JSON.parse(headers) : {};
236
- const allHeaders = { "Content-Type": "application/json", ...env.HEADERS, ...toolHeaders };
237
- let parsedVariables = variables;
238
- if (typeof variables === 'string')
239
- parsedVariables = JSON.parse(variables);
240
- // Split multiple endpoints for broadcast
203
+ const runtimeHeaders = headers ? JSON.parse(headers) : {};
204
+ const fetchHeaders = { "Content-Type": "application/json", ...env.HEADERS, ...runtimeHeaders };
205
+ const fetchVariables = variables ? (typeof variables === 'string' ? JSON.parse(variables) : variables) : undefined;
241
206
  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}`);
207
+ const executeResults = await Promise.allSettled(endpoints.map(async (url) => {
245
208
  const response = await fetch(url, {
246
209
  method: "POST",
247
- headers: allHeaders,
248
- body: JSON.stringify({ query, variables: parsedVariables }),
249
- signal: AbortSignal.timeout(5000) // 5s timeout protection
210
+ headers: fetchHeaders,
211
+ body: JSON.stringify({ query, variables: fetchVariables }),
212
+ signal: AbortSignal.timeout(15000)
250
213
  });
251
- if (!response.ok) {
252
- throw new Error(`HTTP ${response.status} from ${url}`);
253
- }
254
- return {
255
- url,
256
- data: await response.json()
257
- };
214
+ if (!response.ok)
215
+ throw new Error(`Node ${url} returned ${response.status}`);
216
+ return { url, data: await response.json() };
258
217
  }));
259
- const successfulResponses = settleResults
218
+ const successes = executeResults
260
219
  .filter((r) => r.status === 'fulfilled')
261
220
  .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)
221
+ if (successes.length === 0)
222
+ throw new Error("Execution failed on all available nodes.");
223
+ // Aggregate and Deduplicate
224
+ const mergedData = {};
225
+ successes.forEach((resp) => {
226
+ const nodeData = resp.data.data;
227
+ if (!nodeData)
275
228
  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]];
229
+ Object.keys(nodeData).forEach(key => {
230
+ if (Array.isArray(nodeData[key])) {
231
+ const existing = mergedData[key] || [];
232
+ const combined = [...existing, ...nodeData[key]];
233
+ // Universal object-level deduplication
234
+ mergedData[key] = Array.from(new Set(combined.map(v => JSON.stringify(v))))
235
+ .map(s => JSON.parse(s));
280
236
  }
281
- else if (typeof data[key] === 'object' && data[key] !== null) {
282
- // Merge objects (e.g. metadata)
283
- aggregatedResult[key] = { ...(aggregatedResult[key] || {}), ...data[key] };
237
+ else if (typeof nodeData[key] === 'object' && nodeData[key] !== null) {
238
+ mergedData[key] = { ...(mergedData[key] || {}), ...nodeData[key] };
284
239
  }
285
240
  else {
286
- // Use last value for primitives
287
- aggregatedResult[key] = data[key];
241
+ mergedData[key] = nodeData[key];
288
242
  }
289
243
  });
290
244
  });
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
301
- const responseForClaude = {
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 } : {})
310
- };
245
+ // Restore Cypher extraction logic
246
+ const cypherLogs = successes.flatMap(r => r.data.extensions?.cypher || []);
247
+ const cleanCypher = cypherLogs.map((c) => c.replace(/^CYPHER: /, '').replace(/^CYPHER 5\n/, '').replace(/\nPARAMS: \{\}$/, ''));
311
248
  return {
312
249
  content: [{
313
250
  type: "text",
314
- text: JSON.stringify(responseForClaude, null, 2)
251
+ text: JSON.stringify({
252
+ meta: { nodes_queried: endpoints.length, nodes_responding: successes.length },
253
+ data: mergedData,
254
+ ...(cleanCypher.length > 0 ? { cypher_execution_plan: cleanCypher } : {})
255
+ }, null, 2)
315
256
  }]
316
257
  };
317
258
  }
318
259
  catch (error) {
319
- throw new Error(`Execution failed: ${error.message}`);
260
+ return { content: [{ type: "text", text: `❌ Execution error: ${error.message}` }], isError: true };
320
261
  }
321
262
  };
322
263
  toolHandlers.set("query-graphql", queryGraphqlHandler);
323
- (0, tool_registry_js_1.registerTool)(server, toolHandlers, registeredToolsMetadata, "query-graphql", "Execute a GraphQL query against the endpoint", {
324
- query: zod_1.default.string(),
325
- variables: zod_1.default.string().optional(),
326
- headers: zod_1.default.string().optional(),
264
+ (0, tool_registry_js_1.registerTool)(server, toolHandlers, registeredToolsMetadata, "query-graphql", "Execute a GraphQL query across all nodes in the federated system", {
265
+ query: zod_1.default.string().describe("The GraphQL query or mutation string"),
266
+ variables: zod_1.default.string().optional().describe("JSON string of variables"),
267
+ headers: zod_1.default.string().optional().describe("JSON string of extra headers"),
327
268
  }, queryGraphqlHandler);
328
269
  /**
329
270
  * Tool: introspect-schema
330
- * Provides schema exploration with built-in agent recovery logic.
271
+ * Provides a global view of all nodes and resolves type conflicts.
331
272
  */
332
273
  const introspectHandler = async ({ typeNames }) => {
333
- const result = await getSchema(true);
334
- if (!result || typeof result === 'string') {
335
- return {
336
- content: [{
337
- type: "text",
338
- text: `❌ SCHEMA_ERROR: ${typeof result === 'string' ? result : 'GraphQL schema is not initialized yet.'}\n` +
339
- `ACTION: Please wait 5-10 seconds for the backend endpoint to respond.`
340
- }]
341
- };
342
- }
343
- const schema = result;
344
- const typeMap = schema.getTypeMap();
345
- const queryType = schema.getQueryType();
346
- const queryFields = queryType ? queryType.getFields() : {};
347
- const mutationType = schema.getMutationType();
348
- const mutationFields = mutationType ? mutationType.getFields() : {};
349
- // Gap analysis for requested types
350
- if (typeNames && typeNames.length > 0) {
351
- const missing = typeNames.filter(name => {
352
- const existsAsType = !!typeMap[name];
353
- const existsAsMutation = !!mutationFields[name];
354
- const existsAsQueryField = !!queryFields[name];
355
- return !existsAsType && !existsAsMutation && !existsAsQueryField;
356
- });
357
- if (missing.length > 0) {
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}`;
361
- return {
362
- content: [{
363
- type: "text",
364
- text: `❌ PARTIAL RESULTS [Schema ID: ${schemaVersion}]\n\n` +
365
- `MISSING ENTITIES: ${missing.join(", ")}\n` +
366
- `REASON: Requested entities not found.\n` +
367
- `ACTION: Re-examine available types below.\n\n` +
368
- `AVAILABLE_ENTITIES: ${availableEntities.join(", ")}`
369
- }]
370
- };
371
- }
274
+ await getSchema(true);
275
+ if (cachedSchemas.length === 0) {
276
+ return { content: [{ type: "text", text: "❌ System is not initialized." }] };
372
277
  }
373
- // Return general manifest if no specific types requested
278
+ // Default: Return the Manifest of all nodes
374
279
  if (!typeNames || typeNames.length === 0) {
375
- const discoveredEntities = new Set();
376
- if (queryType) {
377
- const fields = queryType.getFields();
378
- Object.values(fields).forEach((field) => {
379
- const namedType = (0, graphql_1.getNamedType)(field.type);
380
- if ((0, graphql_1.isObjectType)(namedType) && !namedType.name.startsWith('__')) {
381
- discoveredEntities.add(namedType.name);
382
- }
383
- });
280
+ const manifest = global.nodeManifest || [];
281
+ const body = manifest.map((m) => `🌐 NODE: ${m.endpoint}\n ACTIONS: ${m.availableMutations.join(', ') || 'none'}\n ENTITIES: ${m.domainEntities.join(', ')}`).join('\n\n');
282
+ return { content: [{ type: "text", text: `FEDERATED SCHEMA OVERVIEW\n\n${body}` }] };
283
+ }
284
+ const resolution = {};
285
+ for (const name of typeNames) {
286
+ const variants = [];
287
+ for (const schema of cachedSchemas) {
288
+ const found = (0, introspection_js_1.introspectSpecificTypes)(schema, [name]);
289
+ if (found && found[name]) {
290
+ variants.push({ origin: schema._originUrl, data: found[name] });
291
+ }
292
+ }
293
+ if (variants.length === 0)
294
+ continue;
295
+ if (variants.length === 1) {
296
+ resolution[name] = variants[0].data;
297
+ }
298
+ else {
299
+ // Check for structural equality
300
+ const baseline = JSON.stringify(variants[0].data);
301
+ const allMatch = variants.every(v => JSON.stringify(v.data) === baseline);
302
+ if (allMatch) {
303
+ resolution[name] = variants[0].data;
304
+ }
305
+ else {
306
+ // Conflict: Expose all variants with node origin
307
+ variants.forEach((v, idx) => {
308
+ resolution[`${name}_from_node_${idx + 1}`] = {
309
+ ...v.data,
310
+ _meta: { origin_node: v.origin, conflict: "Structural difference detected across schemas" }
311
+ };
312
+ });
313
+ }
384
314
  }
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}`;
388
- return {
389
- content: [{
390
- type: "text",
391
- text: `GraphQL Schema Manifest [ID: ${schemaVersion}]\n\n` +
392
- `ENTRY_POINT_ENTITIES: ${entryPoints.join(", ") || "None"}\n` +
393
- `TOTAL_SCHEMA_TYPES: ${allTypes.length}\n\n` +
394
- `ALL_AVAILABLE_TYPES: ${allTypes.join(", ")}`
395
- }]
396
- };
397
315
  }
398
- const filtered = (0, introspection_js_1.introspectSpecificTypes)(schema, typeNames);
399
316
  return {
400
- content: [{ type: "text", text: JSON.stringify(filtered, null, 2) }]
317
+ content: [{
318
+ type: "text",
319
+ text: JSON.stringify(resolution, null, 2)
320
+ }]
401
321
  };
402
322
  };
403
323
  toolHandlers.set("introspect-schema", introspectHandler);
404
- (0, tool_registry_js_1.registerTool)(server, toolHandlers, registeredToolsMetadata, "introspect-schema", "Introspect the GraphQL schema with optional type filtering", {
405
- typeNames: zod_1.default.array(zod_1.default.string()).optional(),
324
+ (0, tool_registry_js_1.registerTool)(server, toolHandlers, registeredToolsMetadata, "introspect-schema", "Retrieve full type definitions and resolve conflicts across all system nodes", {
325
+ typeNames: zod_1.default.array(zod_1.default.string()).optional().describe("List of type names to introspect"),
406
326
  }, introspectHandler);
407
- // --- PROMPTS ---
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.");
409
- (0, prompt_registry_js_1.registerPrompt)(server, "schema-overview", "List all available types", "Run 'introspect-schema' to see all types and entry points.");
410
- (0, prompt_registry_js_1.registerPrompt)(server, "list-scalars", "List all scalar types", "Run 'introspect-schema' and identify all scalars in the schema.");
411
- // --- HTTP SERVER LOGIC ---
412
- /**
413
- * Local HTTP server to support GraphiQL UI and SSE transport
414
- */
327
+ // --- PROMPT REGISTRY ---
328
+ (0, prompt_registry_js_1.registerPrompt)(server, "system-health", "Check status of all nodes", "Perform a simple __typename query on all endpoints.");
329
+ // --- HTTP ADAPTER FOR GRAPHIQL & SSE ---
415
330
  async function handleHttpRequest(req, res) {
416
- // Standard CORS headers
417
331
  res.setHeader('Access-Control-Allow-Origin', '*');
418
332
  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');
333
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
420
334
  if (req.method === 'OPTIONS') {
421
335
  res.writeHead(204);
422
336
  res.end();
423
337
  return;
424
338
  }
425
339
  const url = new URL(req.url || '', `http://${req.headers.host}`);
426
- // Render GraphiQL UI
427
340
  if (req.method === 'GET' && (url.pathname === '/' || url.pathname === '/graphiql')) {
428
341
  res.writeHead(200, { 'Content-Type': 'text/html' });
429
- return res.end((0, graphiql_js_1.renderGraphiQL)(`http://localhost:6274/mcp`, env.HEADERS));
342
+ return res.end((0, graphiql_js_1.renderGraphiQL)(`http://localhost:${env.MCP_PORT}/mcp`, env.HEADERS));
430
343
  }
431
- // Process MCP/GraphQL requests
432
344
  if (url.pathname === '/mcp' && req.method === 'POST') {
433
345
  let body = '';
434
346
  req.on('data', chunk => { body += chunk; });
435
347
  req.on('end', async () => {
436
- let requestId = null;
437
348
  try {
438
349
  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
- }
449
- }
450
- // Standard MCP JSON-RPC handling
451
350
  const { method, id, params } = payload;
452
- requestId = id;
453
- let targetMethod = method;
454
- let toolArgs = params;
455
- if (method === "call-tool" || method === "tools/call") {
456
- targetMethod = params.name;
457
- toolArgs = params.arguments;
458
- }
459
- const handler = toolHandlers.get(targetMethod);
351
+ const target = (method === "call-tool" || method === "tools/call") ? params.name : method;
352
+ const args = (method === "call-tool" || method === "tools/call") ? params.arguments : params;
353
+ const handler = toolHandlers.get(target);
460
354
  if (!handler) {
461
355
  res.writeHead(404);
462
- return res.end(JSON.stringify({
463
- jsonrpc: '2.0',
464
- id: requestId,
465
- error: { code: -32601, message: `Tool ${targetMethod} not found` }
466
- }));
356
+ return res.end(JSON.stringify({ error: { code: -32601, message: "Method not found" } }));
467
357
  }
468
- const result = await handler(toolArgs);
358
+ const result = await handler(args);
469
359
  res.writeHead(200, { 'Content-Type': 'application/json' });
470
- res.end(JSON.stringify({ jsonrpc: '2.0', id: requestId, result }));
360
+ res.end(JSON.stringify({ jsonrpc: '2.0', id, result }));
471
361
  }
472
362
  catch (e) {
473
- console.error(`[HTTP-ERROR] ${e.message}`);
474
363
  res.writeHead(500);
475
- res.end(JSON.stringify({
476
- jsonrpc: '2.0',
477
- id: requestId,
478
- error: { code: -32603, message: e.message }
479
- }));
364
+ res.end(JSON.stringify({ error: { message: e.message } }));
480
365
  }
481
366
  });
482
367
  return;
@@ -484,34 +369,32 @@ async function handleHttpRequest(req, res) {
484
369
  res.writeHead(404);
485
370
  res.end("Not Found");
486
371
  }
487
- // --- STARTUP ---
372
+ // --- SERVER LIFECYCLE ---
488
373
  async function main() {
489
374
  const isInspector = !!(process.env.MCP_INSPECTOR || process.env.INSPECTOR_PORT);
490
- const isHttpExplicitlyEnabled = process.env.ENABLE_HTTP === "true";
491
- // Enable HTTP port by default unless explicitly disabled or in inspector mode
492
375
  if (process.env.ENABLE_HTTP !== "false" && !isInspector) {
493
- const serverHttp = node_http_1.default.createServer(handleHttpRequest);
494
- serverHttp.on('error', (e) => {
495
- if (e.code === 'EADDRINUSE') {
496
- console.error(`[HTTP-ERROR] Port ${env.MCP_PORT} is busy.`);
497
- }
376
+ const httpSrv = node_http_1.default.createServer(handleHttpRequest);
377
+ httpSrv.on('error', (e) => {
378
+ if (e.code === 'EADDRINUSE')
379
+ console.error(`[ERROR] Port ${env.MCP_PORT} already in use.`);
498
380
  });
499
- serverHttp.listen(env.MCP_PORT, () => {
500
- console.error(`[SYSTEM] Server "${env.NAME}" v${getVersion()} active`);
501
- console.error(`🤖 MCP SSE: http://localhost:${env.MCP_PORT}/mcp`);
502
- if (isHttpExplicitlyEnabled) {
503
- console.error(`🎨 GraphiQL UI: http://localhost:${env.MCP_PORT}/graphiql`);
381
+ httpSrv.listen(env.MCP_PORT, () => {
382
+ console.error(`[SYSTEM] Federated Bridge v${getVersion()} active on port ${env.MCP_PORT}`);
383
+ console.error(`📡 SSE Endpoint: http://localhost:${env.MCP_PORT}/mcp`);
384
+ if (process.env.ENABLE_HTTP === "true") {
385
+ console.error(`🎨 GraphiQL: http://localhost:${env.MCP_PORT}/graphiql`);
504
386
  }
505
387
  });
506
388
  }
507
389
  const transport = new stdio_js_1.StdioServerTransport();
508
390
  await server.connect(transport);
509
- // Background schema initialization
510
- getSchema().catch(() => { });
391
+ // Initial background sync
392
+ getSchema(true).catch(err => console.error(`[BOOT-WARN] Initial sync failed: ${err.message}`));
511
393
  }
512
- process.on('SIGINT', () => process.exit(0));
513
- process.on('SIGTERM', () => process.exit(0));
514
- main().catch(error => {
515
- console.error(`[FATAL] ${error}`);
394
+ // Global process management
395
+ process.on('SIGINT', () => { console.error('[SYSTEM] Shutting down...'); process.exit(0); });
396
+ process.on('SIGTERM', () => { process.exit(0); });
397
+ main().catch(err => {
398
+ console.error(`[FATAL] Startup failed: ${err.message}`);
516
399
  process.exit(1);
517
400
  });
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.9.0"
53
+ "version": "3.9.1"
54
54
  }