@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.
- package/README.md +5 -6
- package/dist/index.js +180 -258
- 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-
|
|
27
|
-
|
|
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
|
-
* **
|
|
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.
|
|
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: "
|
|
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
|
-
// ---
|
|
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
|
-
*
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
|
|
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
|
|
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;
|
|
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
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
//
|
|
176
|
-
|
|
177
|
-
|
|
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
|
|
176
|
+
return !typeName.startsWith('__') &&
|
|
181
177
|
!['Query', 'Mutation', 'Subscription'].includes(typeName) &&
|
|
182
|
-
|
|
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
|
|
183
|
+
const sourceInfo = env.SCHEMA ? 'SDL File' : `${tempSchemas.length} Active Nodes`;
|
|
193
184
|
return [
|
|
194
185
|
`✨ SCHEMA EVOLVED (${duration}s)`,
|
|
195
|
-
`📊 Source: ${
|
|
196
|
-
`🧬
|
|
186
|
+
`📊 Source: ${sourceInfo}`,
|
|
187
|
+
`🧬 Types: ${businessTypes.length}`,
|
|
197
188
|
`---`,
|
|
198
|
-
`The bridge has updated the graph model
|
|
189
|
+
`The bridge has updated the graph model.`
|
|
199
190
|
].join('\n');
|
|
200
191
|
}
|
|
201
|
-
|
|
202
|
-
return `✅ Status: Schema stable (${lastKnownTypeCount} labels).`;
|
|
203
|
-
}
|
|
192
|
+
return `✅ Status: Schema stable (${businessTypes.length} types).`;
|
|
204
193
|
}
|
|
205
194
|
catch (error) {
|
|
206
|
-
|
|
207
|
-
|
|
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
|
-
// ---
|
|
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
|
-
*
|
|
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
|
|
232
|
-
if (
|
|
233
|
-
throw new Error("
|
|
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
|
|
236
|
-
const
|
|
237
|
-
|
|
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
|
-
|
|
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:
|
|
248
|
-
body: JSON.stringify({ query, variables:
|
|
249
|
-
signal: AbortSignal.timeout(
|
|
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(`
|
|
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
|
|
231
|
+
const successes = executeResults
|
|
260
232
|
.filter((r) => r.status === 'fulfilled')
|
|
261
233
|
.map(r => r.value);
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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(
|
|
277
|
-
if (Array.isArray(
|
|
278
|
-
|
|
279
|
-
|
|
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
|
|
282
|
-
|
|
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
|
-
|
|
287
|
-
aggregatedResult[key] = data[key];
|
|
254
|
+
mergedData[key] = nodeData[key];
|
|
288
255
|
}
|
|
289
256
|
});
|
|
290
257
|
});
|
|
291
|
-
//
|
|
292
|
-
|
|
293
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
|
284
|
+
* Provides a global view of all nodes and resolves type conflicts.
|
|
331
285
|
*/
|
|
332
286
|
const introspectHandler = async ({ typeNames }) => {
|
|
333
|
-
|
|
334
|
-
if (
|
|
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
|
-
|
|
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
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
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: [{
|
|
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", "
|
|
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
|
-
// ---
|
|
408
|
-
(0, prompt_registry_js_1.registerPrompt)(server, "health
|
|
409
|
-
|
|
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
|
|
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
|
|
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
|
|
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({
|
|
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.
|
|
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
|
-
|
|
454
|
-
|
|
455
|
-
|
|
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: `
|
|
390
|
+
error: { code: -32601, message: `Method ${target} not found` }
|
|
466
391
|
}));
|
|
467
392
|
}
|
|
468
|
-
const result = await handler(
|
|
393
|
+
const result = await handler(args);
|
|
469
394
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
470
|
-
res.end(JSON.stringify({ jsonrpc: '2.0', id
|
|
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: {
|
|
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
|
-
// ---
|
|
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
|
|
494
|
-
|
|
495
|
-
if (e.code === 'EADDRINUSE')
|
|
496
|
-
console.error(`[
|
|
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
|
-
|
|
500
|
-
console.error(`[SYSTEM]
|
|
501
|
-
console.error(
|
|
502
|
-
if (
|
|
503
|
-
console.error(`🎨 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
|
-
//
|
|
510
|
-
getSchema().catch(
|
|
430
|
+
// Initial background sync
|
|
431
|
+
getSchema(true).catch(err => console.error(`[BOOT-WARN] Initial sync failed: ${err.message}`));
|
|
511
432
|
}
|
|
512
|
-
|
|
513
|
-
process.on('
|
|
514
|
-
|
|
515
|
-
|
|
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