@letoribo/mcp-graphql-enhanced 3.8.1 → 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 +24 -0
  2. package/dist/index.js +207 -301
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -23,6 +23,30 @@ 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
+ ## 🚀 Federated Multi-Node Architecture (v3.9.1+)
27
+ The server operates as a Federated GraphQL Gateway, merging independent nodes into a unified system.
28
+
29
+ * **Zero Breaking Changes**: If you provide a single URL in `ENDPOINT`, the server behaves exactly as before.
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.
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.
34
+
35
+ #### 💡 Use Case: Bridging WSL and Windows (PowerShell)
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".
37
+
38
+ Example configuration for Claude Desktop:
39
+ ```bash
40
+ {
41
+ "ENDPOINT": "http://DESKTOP-NAME.local:2311/graphql,http://127.0.0.1:4000/graphql"
42
+ }
43
+ ```
44
+ * **Hybrid Ecosystem**: Seamlessly query and aggregate data across Windows-native processes (PowerShell) and Linux-based environments (WSL).
45
+
46
+ * **mDNS Support**: By using .local addresses, the bridge automatically resolves the host machine's IP from within the WSL environment.
47
+
48
+ * **Transparent Aggregation**: The AI assistant interacts with a single unified schema, unaware that the data is being fetched from different operating systems simultaneously.
49
+
26
50
  ## 🔍 Advanced Observability & Cypher
27
51
  The bridge provides deep insights into how the LLM interacts with your graph database.
28
52
 
package/dist/index.js CHANGED
@@ -16,19 +16,29 @@ 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 - Strict validation
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
+ return val.trim();
40
+ return val;
41
+ }, zod_1.default.string().min(1)).default("https://mcp-neo4j-discord.vercel.app/api/graphiql"),
32
42
  ALLOW_MUTATIONS: zod_1.default
33
43
  .enum(["true", "false"])
34
44
  .transform((value) => value === "true")
@@ -57,57 +67,52 @@ const EnvSchema = zod_1.default.object({
57
67
  .default("auto"),
58
68
  });
59
69
  const env = EnvSchema.parse(process.env);
70
+ /**
71
+ * Initialize MCP Server with full capabilities
72
+ */
60
73
  const server = new mcp_js_1.McpServer({
61
74
  name: env.NAME,
62
75
  version: getVersion(),
63
- description: "Start of the #mcp-graphql-enhanced channel on GraphQL server. Join here: https://discord.com/channels/622115132221685760/1348633379555184640"
76
+ description: "Federated GraphQL-to-MCP bridge with broadcast introspection and full type visibility."
64
77
  }, {
65
78
  capabilities: {
66
79
  prompts: {},
67
80
  tools: {}
68
81
  }
69
82
  });
70
- // --- CACHE STATE ---
83
+ // --- GLOBAL STATE MANAGEMENT ---
71
84
  let cachedSDL = null;
72
85
  let cachedSchemaObject = null;
86
+ let cachedSchemas = [];
73
87
  let schemaLoadError = null;
74
88
  let isUpdating = false;
75
89
  let updatePromise = null;
76
- let lastKnownTypeCount = 0;
77
90
  /**
78
- * Smart Hybrid Schema Fetcher (Zero-Error Version)
79
- * @param force If true, blocks and waits for the new schema evolution.
80
- * If false, returns cache immediately and updates in background.
91
+ * Schema Fetcher with dependency tracking
81
92
  */
82
93
  async function getSchema(force = false, requestedTypes) {
83
- // 1. Hook into existing update if in progress
84
94
  if (isUpdating && updatePromise) {
85
95
  if (force || !cachedSDL)
86
96
  return await updatePromise;
87
97
  return cachedSDL;
88
98
  }
89
- // 2. Return cache if valid and not forcing
90
99
  if (cachedSDL && !force) {
91
- // Validation check: If user wants specific types but they aren't in the cache
92
- if (requestedTypes && cachedSchemaObject) {
93
- const typeMap = cachedSchemaObject.getTypeMap();
94
- const missing = requestedTypes.filter(t => !typeMap[t]);
95
- if (missing.length > 0) {
96
- // Force a refresh if requested types are missing from current cache
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)
97
104
  return await (updatePromise = performUpdate(true));
98
- }
99
105
  }
100
- return cachedSDL;
106
+ return cachedSchemaObject;
101
107
  }
102
108
  if (force)
103
109
  schemaLoadError = null;
104
110
  if (schemaLoadError)
105
111
  throw schemaLoadError;
106
- // 3. Trigger update
107
112
  updatePromise = performUpdate(force);
108
113
  try {
109
114
  if (force || !cachedSDL) {
110
- await updatePromise; // Wait for update to complete
115
+ await updatePromise;
111
116
  return cachedSchemaObject;
112
117
  }
113
118
  return cachedSchemaObject;
@@ -117,378 +122,279 @@ async function getSchema(force = false, requestedTypes) {
117
122
  }
118
123
  }
119
124
  /**
120
- * 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.
125
+ * Federated Update: Orchestrates introspection across all endpoints
123
126
  */
124
127
  async function performUpdate(force) {
125
128
  isUpdating = true;
126
129
  const startTime = Date.now();
127
130
  try {
128
- const { buildClientSchema, getIntrospectionQuery, printSchema, buildASTSchema, parse: gqlParse, isObjectType } = require("graphql");
129
- let tempSchema;
130
- // --- FETCHING LOGIC (Unified Source) ---
131
- if (env.SCHEMA) {
132
- let sdl;
133
- if (env.SCHEMA.startsWith("http")) {
134
- // Remote SDL File: Fetch via HTTP
135
- const response = await fetch(env.SCHEMA);
136
- if (!response.ok)
137
- throw new Error(`Remote_SDL_Fetch_Failed: ${response.statusText}`);
138
- sdl = await response.text();
139
- }
140
- else {
141
- // Local SDL File: Use your custom helper (readFile inside)
142
- sdl = await (0, introspection_js_1.introspectLocalSchema)(env.SCHEMA);
143
- }
144
- // Direct path: Convert raw SDL string to GraphQLSchema object
145
- tempSchema = buildASTSchema(gqlParse(sdl));
146
- }
147
- else {
148
- // Standard Path: Execute Introspection Query against live ENDPOINT
149
- const response = await fetch(env.ENDPOINT, {
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, {
150
135
  method: "POST",
151
136
  headers: { "Content-Type": "application/json", ...env.HEADERS },
152
- body: JSON.stringify({ query: getIntrospectionQuery() }),
137
+ body: JSON.stringify({ query: (0, graphql_1.getIntrospectionQuery)() }),
138
+ signal: AbortSignal.timeout(10000)
153
139
  });
154
140
  if (!response.ok)
155
- throw new Error(`HTTP_${response.status}: ${response.statusText}`);
141
+ throw new Error(`HTTP Error ${response.status}`);
156
142
  const result = await response.json();
157
143
  if (!result.data)
158
- throw new Error("Invalid GraphQL response: Missing 'data' field.");
159
- // Build Schema object from introspection JSON
160
- 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(', ')}`);
161
157
  }
162
- // --- UNIFIED STRUCTURAL ANALYSIS (For AI Report) ---
163
- const typeMap = tempSchema.getTypeMap();
164
- // Filter "Business Labels" (Nodes) while ignoring internal scalars/system types
165
- const businessTypes = Object.keys(typeMap).filter(typeName => {
166
- const type = typeMap[typeName];
167
- return (!typeName.startsWith('__') &&
168
- !['Query', 'Mutation', 'Subscription'].includes(typeName) &&
169
- !['String', 'Int', 'Float', 'Boolean', 'ID', 'BigInt', 'DateTime'].includes(typeName) &&
170
- 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
+ };
171
174
  });
172
- // Maintain state for "Gap Analysis"
173
- lastKnownTypeCount = businessTypes.length;
174
- const currentSDL = printSchema(tempSchema);
175
- // --- CACHE & NOTIFICATION ---
176
- // Always update the live object if we successfully built it
177
- cachedSchemaObject = tempSchema;
178
- if (currentSDL !== cachedSDL) {
179
- cachedSDL = currentSDL;
180
- const duration = ((Date.now() - startTime) / 1000).toFixed(2);
181
- return [
182
- `✨ SCHEMA EVOLVED (${duration}s)`,
183
- `📊 Source: ${env.SCHEMA ? 'Local/Remote SDL' : 'Live Endpoint'}`,
184
- `🧬 Labels: ${businessTypes.join(', ') || 'None'}`,
185
- `---`,
186
- `The bridge has updated the graph model. New types are now queryable.`
187
- ].join('\n');
188
- }
189
- else {
190
- // Even if SDL string is the same, we've ensured cachedSchemaObject is set above
191
- return `✅ Status: Schema stable (${lastKnownTypeCount} labels).`;
192
- }
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.`;
193
178
  }
194
179
  catch (error) {
195
- // Informative error report to prevent "AI confusion"
196
- return [
197
- `❌ SCHEMA SYNC FAILED`,
198
- `🔍 Reason: ${error.message}`,
199
- `🛠️ Action: Verify your ${env.SCHEMA ? 'file path' : 'endpoint connection'} and retry.`
200
- ].join('\n');
180
+ console.error(`[CRITICAL] Sync failure: ${error.message}`);
181
+ schemaLoadError = error;
182
+ return `❌ SYNC ERROR: ${error.message}`;
201
183
  }
202
184
  finally {
203
185
  isUpdating = false;
204
186
  updatePromise = null;
205
187
  }
206
188
  }
207
- // --- TOOL REGISTRY ---
189
+ // --- TOOLS IMPLEMENTATION ---
208
190
  const toolHandlers = new Map();
209
- // This will store schemas for our dynamic HTTP 'list-tools' response
210
191
  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.
192
+ /**
193
+ * Tool: query-graphql
194
+ * Broadcasts queries to all nodes and merges results with universal deduplication.
214
195
  */
215
- let executionLogs = [];
216
- // Tool: query-graphql
217
196
  const queryGraphqlHandler = async ({ query, variables, headers }) => {
218
197
  try {
219
198
  const parsedQuery = (0, language_1.parse)(query);
220
- const isMutation = parsedQuery.definitions.some((def) => def.kind === "OperationDefinition" && def.operation === "mutation");
221
- if (isMutation && !env.ALLOW_MUTATIONS)
222
- throw new Error("Mutations are not allowed.");
223
- const toolHeaders = headers ? JSON.parse(headers) : {};
224
- const allHeaders = { "Content-Type": "application/json", ...env.HEADERS, ...toolHeaders };
225
- let parsedVariables = variables;
226
- if (typeof variables === 'string')
227
- 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()
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.");
202
+ }
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;
206
+ const endpoints = env.ENDPOINT.split(',').map(url => url.trim());
207
+ const executeResults = await Promise.allSettled(endpoints.map(async (url) => {
208
+ const response = await fetch(url, {
209
+ method: "POST",
210
+ headers: fetchHeaders,
211
+ body: JSON.stringify({ query, variables: fetchVariables }),
212
+ signal: AbortSignal.timeout(15000)
213
+ });
214
+ if (!response.ok)
215
+ throw new Error(`Node ${url} returned ${response.status}`);
216
+ return { url, data: await response.json() };
217
+ }));
218
+ const successes = executeResults
219
+ .filter((r) => r.status === 'fulfilled')
220
+ .map(r => r.value);
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)
228
+ return;
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));
236
+ }
237
+ else if (typeof nodeData[key] === 'object' && nodeData[key] !== null) {
238
+ mergedData[key] = { ...(mergedData[key] || {}), ...nodeData[key] };
239
+ }
240
+ else {
241
+ mergedData[key] = nodeData[key];
242
+ }
243
+ });
245
244
  });
246
- if (executionLogs.length > 5)
247
- executionLogs.shift();
248
- // 3. Prepare optimized response for Claude
249
- 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 } : {})
253
- };
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: \{\}$/, ''));
254
248
  return {
255
249
  content: [{
256
250
  type: "text",
257
- 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)
258
256
  }]
259
257
  };
260
258
  }
261
259
  catch (error) {
262
- throw new Error(`Execution failed: ${error.message}`);
260
+ return { content: [{ type: "text", text: `❌ Execution error: ${error.message}` }], isError: true };
263
261
  }
264
262
  };
265
263
  toolHandlers.set("query-graphql", queryGraphqlHandler);
266
- (0, tool_registry_js_1.registerTool)(server, toolHandlers, registeredToolsMetadata, "query-graphql", "Execute a GraphQL query against the endpoint", {
267
- query: zod_1.default.string(),
268
- variables: zod_1.default.string().optional(),
269
- 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"),
270
268
  }, queryGraphqlHandler);
271
269
  /**
272
270
  * Tool: introspect-schema
273
- * Main handler for the introspection tool.
274
- * Implements "Agent Recovery" logic to guide the LLM when entities are missing.
271
+ * Provides a global view of all nodes and resolves type conflicts.
275
272
  */
276
273
  const introspectHandler = async ({ typeNames }) => {
277
- // 1. Fetch the schema directly from the source
278
- const result = await getSchema(true);
279
- // Explicitly check if the result is a valid GraphQLSchema object
280
- if (!result || typeof result === 'string') {
281
- return {
282
- content: [{
283
- type: "text",
284
- text: `❌ SCHEMA_ERROR: ${typeof result === 'string' ? result : 'GraphQL schema is not initialized yet.'}\n` +
285
- `ACTION: Please wait 5-10 seconds for the backend endpoint to respond.`
286
- }]
287
- };
288
- }
289
- // --- 1. INITIALIZE MAPPINGS ---
290
- const schema = result;
291
- const typeMap = schema.getTypeMap();
292
- // Cache Root Type fields for rapid gap analysis
293
- const queryType = schema.getQueryType();
294
- const queryFields = queryType ? queryType.getFields() : {};
295
- const mutationType = schema.getMutationType();
296
- const mutationFields = mutationType ? mutationType.getFields() : {};
297
- // --- 2. GAP ANALYSIS & SELF-HEALING LOOP ---
298
- // If specific types were requested, verify their existence in the current schema
299
- if (typeNames && typeNames.length > 0) {
300
- const missing = typeNames.filter(name => {
301
- const existsAsType = !!typeMap[name];
302
- const existsAsMutation = !!mutationFields[name];
303
- const existsAsQueryField = !!queryFields[name];
304
- return !existsAsType && !existsAsMutation && !existsAsQueryField;
305
- });
306
- // If some requested entities are missing, provide the agent with a recovery map
307
- if (missing.length > 0) {
308
- // Filter out internal GraphQL types to reduce noise for the agent
309
- const internalTypes = ['Query', 'Mutation', 'Subscription'];
310
- const availableEntities = Object.keys(typeMap).filter(t => !t.startsWith('__') && !internalTypes.includes(t));
311
- // Generate a pseudo-version ID based on schema state and time
312
- const schemaVersion = `v${availableEntities.length}.${Math.floor(Date.now() / 10000) % 1000}`;
313
- return {
314
- content: [{
315
- type: "text",
316
- text: `❌ PARTIAL RESULTS [Schema ID: ${schemaVersion}]\n\n` +
317
- `MISSING ENTITIES: ${missing.join(", ")}\n` +
318
- `REASON: These specific types or fields were not found in the current schema.\n` +
319
- `ACTION: Re-examine the available entities below and correct your query intent.\n\n` +
320
- `AVAILABLE_ENTITIES: ${availableEntities.join(", ")}`
321
- }]
322
- };
323
- }
274
+ await getSchema(true);
275
+ if (cachedSchemas.length === 0) {
276
+ return { content: [{ type: "text", text: "❌ System is not initialized." }] };
324
277
  }
325
- // --- 3. GENERAL MANIFEST GENERATION ---
326
- // If no typeNames provided, return a high-level overview of the entry points
278
+ // Default: Return the Manifest of all nodes
327
279
  if (!typeNames || typeNames.length === 0) {
328
- const discoveredEntities = new Set();
329
- if (queryType) {
330
- // Map Query fields to their underlying Object Types
331
- const fields = queryType.getFields();
332
- Object.values(fields).forEach((field) => {
333
- const namedType = (0, graphql_1.getNamedType)(field.type);
334
- if ((0, graphql_1.isObjectType)(namedType) && !namedType.name.startsWith('__')) {
335
- discoveredEntities.add(namedType.name);
336
- }
337
- });
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
+ }
338
314
  }
339
- const entryPoints = Array.from(discoveredEntities).sort();
340
- const allTypes = Object.keys(typeMap).filter(t => !t.startsWith('__'));
341
- const schemaVersion = `v${allTypes.length}.${Math.floor(Date.now() / 10000) % 1000}`;
342
- return {
343
- content: [{
344
- type: "text",
345
- text: `GraphQL Schema Manifest [ID: ${schemaVersion}]\n\n` +
346
- `ENTRY_POINT_ENTITIES: ${entryPoints.join(", ") || "None"}\n` +
347
- `TOTAL_SCHEMA_TYPES: ${allTypes.length}\n\n` +
348
- `ALL_AVAILABLE_TYPES: ${allTypes.join(", ")}`
349
- }]
350
- };
351
315
  }
352
- // --- 4. DETAILED INTROSPECTION ---
353
- // Return filtered schema metadata for the requested types or root fields
354
- const filtered = (0, introspection_js_1.introspectSpecificTypes)(schema, typeNames);
355
316
  return {
356
- content: [{ type: "text", text: JSON.stringify(filtered, null, 2) }]
317
+ content: [{
318
+ type: "text",
319
+ text: JSON.stringify(resolution, null, 2)
320
+ }]
357
321
  };
358
322
  };
359
323
  toolHandlers.set("introspect-schema", introspectHandler);
360
- (0, tool_registry_js_1.registerTool)(server, toolHandlers, registeredToolsMetadata, "introspect-schema", "Introspect the GraphQL schema with optional type filtering", {
361
- 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"),
362
326
  }, introspectHandler);
363
- // --- PROMPTS (The "Add from ..." buttons in Claude UI) ---
364
- // 1. Connection check
365
- (0, prompt_registry_js_1.registerPrompt)(server, "health-check", "Check if the GraphQL endpoint is alive", "Run 'query-graphql' with query '{ __typename }' to verify connection.");
366
- // 2. High-level overview
367
- (0, prompt_registry_js_1.registerPrompt)(server, "schema-overview", "List all available types", "Run 'introspect-schema' to see all types and entry points.");
368
- // 3. Data types analysis
369
- (0, prompt_registry_js_1.registerPrompt)(server, "list-scalars", "List all scalar types", "Run 'introspect-schema' and identify all scalars in the schema.");
370
- // --- HTTP SERVER LOGIC ---
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 ---
371
330
  async function handleHttpRequest(req, res) {
372
- // Standard CORS headers for cross-origin compatibility
373
331
  res.setHeader('Access-Control-Allow-Origin', '*');
374
- res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');
332
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
375
333
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
376
- // Handle preflight requests
377
334
  if (req.method === 'OPTIONS') {
378
335
  res.writeHead(204);
379
336
  res.end();
380
337
  return;
381
338
  }
382
339
  const url = new URL(req.url || '', `http://${req.headers.host}`);
383
- // Serve Web GUI (GraphiQL) - ONLY if explicitly enabled
384
340
  if (req.method === 'GET' && (url.pathname === '/' || url.pathname === '/graphiql')) {
385
- // Check our explicit flag
386
- if (process.env.ENABLE_HTTP === 'true') {
387
- res.writeHead(200, { 'Content-Type': 'text/html' });
388
- return res.end((0, graphiql_js_1.renderGraphiQL)(env.ENDPOINT, env.HEADERS));
389
- }
390
- else {
391
- // Forbidden if not explicitly enabled
392
- res.writeHead(403, { 'Content-Type': 'text/plain' });
393
- return res.end("Forbidden: GraphiQL UI is disabled. Start with ENABLE_HTTP=true to use it.");
394
- }
341
+ res.writeHead(200, { 'Content-Type': 'text/html' });
342
+ return res.end((0, graphiql_js_1.renderGraphiQL)(`http://localhost:${env.MCP_PORT}/mcp`, env.HEADERS));
395
343
  }
396
- // Process MCP JSON-RPC Endpoint
397
344
  if (url.pathname === '/mcp' && req.method === 'POST') {
398
345
  let body = '';
399
346
  req.on('data', chunk => { body += chunk; });
400
347
  req.on('end', async () => {
401
- let requestId = null; // Defined early to be accessible in catch block
402
348
  try {
403
- const request = JSON.parse(body);
404
- const { method, id, params } = request;
405
- requestId = id;
406
- console.error(`[HTTP-RPC] Method: ${method} | ID: ${id}`);
407
- // --- DYNAMIC DISCOVERY ---
408
- if (method === "list-tools" || method === "tools/list") {
409
- res.writeHead(200, { 'Content-Type': 'application/json' });
410
- return res.end(JSON.stringify({
411
- jsonrpc: '2.0',
412
- id: requestId,
413
- result: { tools: registeredToolsMetadata }
414
- }));
415
- }
416
- // --- TOOL EXECUTION (CALL) ---
417
- let targetMethod = method;
418
- let toolArgs = params;
419
- // Support both direct calls and standard MCP "call-tool" structure
420
- if (method === "call-tool" || method === "tools/call") {
421
- targetMethod = params.name;
422
- toolArgs = params.arguments;
423
- }
424
- const handler = toolHandlers.get(targetMethod);
349
+ const payload = JSON.parse(body);
350
+ const { method, id, params } = payload;
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);
425
354
  if (!handler) {
426
355
  res.writeHead(404);
427
- return res.end(JSON.stringify({
428
- jsonrpc: '2.0',
429
- id: requestId,
430
- error: { code: -32601, message: `Tool ${targetMethod} not found` }
431
- }));
356
+ return res.end(JSON.stringify({ error: { code: -32601, message: "Method not found" } }));
432
357
  }
433
- // Execute the actual business logic for the tool
434
- const result = await handler(toolArgs);
358
+ const result = await handler(args);
435
359
  res.writeHead(200, { 'Content-Type': 'application/json' });
436
- res.end(JSON.stringify({
437
- jsonrpc: '2.0',
438
- id: requestId,
439
- result
440
- }));
360
+ res.end(JSON.stringify({ jsonrpc: '2.0', id, result }));
441
361
  }
442
362
  catch (e) {
443
- console.error(`[HTTP-ERROR] ${e.message}`);
444
363
  res.writeHead(500);
445
- res.end(JSON.stringify({
446
- jsonrpc: '2.0',
447
- id: requestId,
448
- error: { code: -32603, message: e.message || "Internal Server Error" }
449
- }));
364
+ res.end(JSON.stringify({ error: { message: e.message } }));
450
365
  }
451
366
  });
452
367
  return;
453
368
  }
454
- // Health check endpoint for monitoring
455
- if (url.pathname === '/health') {
456
- res.writeHead(200, { 'Content-Type': 'application/json' });
457
- return res.end(JSON.stringify({ status: "ok", server: env.NAME }));
458
- }
459
- // Default 404 for unknown paths
460
369
  res.writeHead(404);
461
370
  res.end("Not Found");
462
371
  }
463
- // --- STARTUP ---
372
+ // --- SERVER LIFECYCLE ---
464
373
  async function main() {
465
374
  const isInspector = !!(process.env.MCP_INSPECTOR || process.env.INSPECTOR_PORT);
466
- const isHttpExplicitlyEnabled = process.env.ENABLE_HTTP === "true";
467
- // Open HTTP port by default for MCP SSE, unless explicitly disabled
468
375
  if (process.env.ENABLE_HTTP !== "false" && !isInspector) {
469
- const serverHttp = node_http_1.default.createServer(handleHttpRequest);
470
- serverHttp.on('error', (e) => {
471
- if (e.code === 'EADDRINUSE') {
472
- console.error(`[HTTP-ERROR] Port ${env.MCP_PORT} is busy.`);
473
- }
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.`);
474
380
  });
475
- serverHttp.listen(env.MCP_PORT, () => {
476
- // All-in-one status report
477
- console.error(`[SYSTEM] Server "${env.NAME}" v${getVersion()} active`);
478
- console.error(`🤖 MCP SSE: http://localhost:${env.MCP_PORT}/mcp`);
479
- // Show UI only if explicitly requested
480
- if (isHttpExplicitlyEnabled) {
481
- 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`);
482
386
  }
483
387
  });
484
388
  }
485
389
  const transport = new stdio_js_1.StdioServerTransport();
486
390
  await server.connect(transport);
487
- getSchema().catch(() => { });
391
+ // Initial background sync
392
+ getSchema(true).catch(err => console.error(`[BOOT-WARN] Initial sync failed: ${err.message}`));
488
393
  }
489
- process.on('SIGINT', () => process.exit(0));
490
- process.on('SIGTERM', () => process.exit(0));
491
- main().catch(error => {
492
- 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}`);
493
399
  process.exit(1);
494
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.8.1"
53
+ "version": "3.9.1"
54
54
  }