@letoribo/mcp-graphql-enhanced 3.9.0 → 3.9.2

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