@letoribo/mcp-graphql-enhanced 3.8.1 → 3.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -0
- package/dist/index.js +146 -123
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -23,6 +23,31 @@ This is the best place to share your feedback, report issues, or suggest new "en
|
|
|
23
23
|
* ✅ **Dynamic Schema Evolution** — Smart diagnostics and gap analysis for servers that regenerate GraphQL types on-the-fly (like Neo4j).
|
|
24
24
|
* ✅ **Deep Observability** — Automatic Cypher extraction and cleaning from GraphQL extensions.
|
|
25
25
|
|
|
26
|
+
## 🚀 Multi-Endpoint Broadcast (Experimental in v3.9.0+)
|
|
27
|
+
Starting from **v3.9.0**, the server supports querying multiple GraphQL endpoints simultaneously. This was originally designed to synchronize mutations across different environments (e.g., Node.js and Python backends), but it opens up powerful possibilities for data aggregation.
|
|
28
|
+
|
|
29
|
+
* **Zero Breaking Changes**: If you provide a single URL in `ENDPOINT`, the server behaves exactly as before.
|
|
30
|
+
* **Smart Aggregation**: When multiple comma-separated URLs are provided, the server broadcasts the query to all of them and **merges the resulting arrays**.
|
|
31
|
+
* **Bypass Free Tier Limits**: Perfect for users of "Free Tier" cloud databases (like Neo4j Aura). You can split your data across multiple free instances and use this bridge to query them as a **single unified graph**, effectively bypassing entity count limitations.
|
|
32
|
+
* **Deduplication**: The bridge automatically removes duplicate objects based on their unique fields to keep the AI's context window clean.
|
|
33
|
+
|
|
34
|
+
> **⚠️ Use at your own risk:** This feature assumes all endpoints share the same (or very similar) GraphQL schema. The introspection is performed against the **first** endpoint in the list.
|
|
35
|
+
|
|
36
|
+
#### 💡 Use Case: Bridging WSL and Windows (PowerShell)
|
|
37
|
+
A common challenge for Windows developers is the network isolation between the Windows Subsystem for Linux (WSL) and the host OS. This feature allows you to bridge these two worlds into a "Unified Nervous System".
|
|
38
|
+
|
|
39
|
+
Example configuration for Claude Desktop:
|
|
40
|
+
```bash
|
|
41
|
+
{
|
|
42
|
+
"ENDPOINT": "http://DESKTOP-NAME.local:2311/graphql,http://127.0.0.1:4000/graphql"
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
* **Hybrid Ecosystem**: Seamlessly query and aggregate data across Windows-native processes (PowerShell) and Linux-based environments (WSL).
|
|
46
|
+
|
|
47
|
+
* **mDNS Support**: By using .local addresses, the bridge automatically resolves the host machine's IP from within the WSL environment.
|
|
48
|
+
|
|
49
|
+
* **Transparent Aggregation**: The AI assistant interacts with a single unified schema, unaware that the data is being fetched from different operating systems simultaneously.
|
|
50
|
+
|
|
26
51
|
## 🔍 Advanced Observability & Cypher
|
|
27
52
|
The bridge provides deep insights into how the LLM interacts with your graph database.
|
|
28
53
|
|
package/dist/index.js
CHANGED
|
@@ -16,19 +16,31 @@ const deprecation_js_1 = require("./helpers/deprecation.js");
|
|
|
16
16
|
const introspection_js_1 = require("./helpers/introspection.js");
|
|
17
17
|
const tool_registry_js_1 = require("./helpers/tool-registry.js");
|
|
18
18
|
const prompt_registry_js_1 = require("./helpers/prompt-registry.js");
|
|
19
|
+
/**
|
|
20
|
+
* Retrieves the current version from package.json
|
|
21
|
+
*/
|
|
19
22
|
const getVersion = () => {
|
|
20
23
|
try {
|
|
21
24
|
const pkg = require("../package.json");
|
|
22
25
|
return pkg.version;
|
|
23
26
|
}
|
|
24
27
|
catch {
|
|
25
|
-
return "3.
|
|
28
|
+
return "3.9.0";
|
|
26
29
|
}
|
|
27
30
|
};
|
|
28
31
|
(0, deprecation_js_1.checkDeprecatedArguments)();
|
|
32
|
+
/**
|
|
33
|
+
* Environment configuration schema
|
|
34
|
+
*/
|
|
29
35
|
const EnvSchema = zod_1.default.object({
|
|
30
36
|
NAME: zod_1.default.string().default("mcp-graphql-enhanced"),
|
|
31
|
-
ENDPOINT: zod_1.default.preprocess((val) =>
|
|
37
|
+
ENDPOINT: zod_1.default.preprocess((val) => {
|
|
38
|
+
if (typeof val === 'string') {
|
|
39
|
+
// Support for multiple endpoints via comma-separated string
|
|
40
|
+
return val.trim();
|
|
41
|
+
}
|
|
42
|
+
return val;
|
|
43
|
+
}, zod_1.default.string().min(1)).default("https://mcp-neo4j-discord.vercel.app/api/graphiql"),
|
|
32
44
|
ALLOW_MUTATIONS: zod_1.default
|
|
33
45
|
.enum(["true", "false"])
|
|
34
46
|
.transform((value) => value === "true")
|
|
@@ -60,7 +72,7 @@ const env = EnvSchema.parse(process.env);
|
|
|
60
72
|
const server = new mcp_js_1.McpServer({
|
|
61
73
|
name: env.NAME,
|
|
62
74
|
version: getVersion(),
|
|
63
|
-
description: "
|
|
75
|
+
description: "Unified GraphQL-to-MCP bridge with dynamic schema support."
|
|
64
76
|
}, {
|
|
65
77
|
capabilities: {
|
|
66
78
|
prompts: {},
|
|
@@ -75,7 +87,7 @@ let isUpdating = false;
|
|
|
75
87
|
let updatePromise = null;
|
|
76
88
|
let lastKnownTypeCount = 0;
|
|
77
89
|
/**
|
|
78
|
-
* Smart Hybrid Schema Fetcher
|
|
90
|
+
* Smart Hybrid Schema Fetcher
|
|
79
91
|
* @param force If true, blocks and waits for the new schema evolution.
|
|
80
92
|
* If false, returns cache immediately and updates in background.
|
|
81
93
|
*/
|
|
@@ -88,12 +100,12 @@ async function getSchema(force = false, requestedTypes) {
|
|
|
88
100
|
}
|
|
89
101
|
// 2. Return cache if valid and not forcing
|
|
90
102
|
if (cachedSDL && !force) {
|
|
91
|
-
// Validation check:
|
|
103
|
+
// Validation check: ensure requested types exist in current cache
|
|
92
104
|
if (requestedTypes && cachedSchemaObject) {
|
|
93
105
|
const typeMap = cachedSchemaObject.getTypeMap();
|
|
94
106
|
const missing = requestedTypes.filter(t => !typeMap[t]);
|
|
95
107
|
if (missing.length > 0) {
|
|
96
|
-
// Force a refresh if requested types are missing
|
|
108
|
+
// Force a refresh if requested types are missing
|
|
97
109
|
return await (updatePromise = performUpdate(true));
|
|
98
110
|
}
|
|
99
111
|
}
|
|
@@ -118,8 +130,7 @@ async function getSchema(force = false, requestedTypes) {
|
|
|
118
130
|
}
|
|
119
131
|
/**
|
|
120
132
|
* Internal logic for schema introspection and building.
|
|
121
|
-
*
|
|
122
|
-
* detailed diagnostic reports instead of generic error messages.
|
|
133
|
+
* Optimized for Multi-Endpoint Broadcast: Uses the first available URL for discovery.
|
|
123
134
|
*/
|
|
124
135
|
async function performUpdate(force) {
|
|
125
136
|
isUpdating = true;
|
|
@@ -127,41 +138,43 @@ async function performUpdate(force) {
|
|
|
127
138
|
try {
|
|
128
139
|
const { buildClientSchema, getIntrospectionQuery, printSchema, buildASTSchema, parse: gqlParse, isObjectType } = require("graphql");
|
|
129
140
|
let tempSchema;
|
|
130
|
-
// --- FETCHING LOGIC
|
|
141
|
+
// --- FETCHING LOGIC ---
|
|
131
142
|
if (env.SCHEMA) {
|
|
132
143
|
let sdl;
|
|
133
144
|
if (env.SCHEMA.startsWith("http")) {
|
|
134
|
-
// Remote SDL File
|
|
145
|
+
// Remote SDL File
|
|
135
146
|
const response = await fetch(env.SCHEMA);
|
|
136
147
|
if (!response.ok)
|
|
137
148
|
throw new Error(`Remote_SDL_Fetch_Failed: ${response.statusText}`);
|
|
138
149
|
sdl = await response.text();
|
|
139
150
|
}
|
|
140
151
|
else {
|
|
141
|
-
// Local SDL File
|
|
152
|
+
// Local SDL File via helper
|
|
142
153
|
sdl = await (0, introspection_js_1.introspectLocalSchema)(env.SCHEMA);
|
|
143
154
|
}
|
|
144
|
-
// Direct path: Convert raw SDL string to GraphQLSchema object
|
|
145
155
|
tempSchema = buildASTSchema(gqlParse(sdl));
|
|
146
156
|
}
|
|
147
157
|
else {
|
|
148
|
-
//
|
|
149
|
-
|
|
158
|
+
// --- BROADCAST ADAPTATION ---
|
|
159
|
+
// Extract the primary endpoint for introspection
|
|
160
|
+
const endpoints = env.ENDPOINT.split(',').map(url => url.trim());
|
|
161
|
+
const primaryEndpoint = endpoints[0];
|
|
162
|
+
// Execute Introspection against the primary target
|
|
163
|
+
const response = await fetch(primaryEndpoint, {
|
|
150
164
|
method: "POST",
|
|
151
165
|
headers: { "Content-Type": "application/json", ...env.HEADERS },
|
|
152
166
|
body: JSON.stringify({ query: getIntrospectionQuery() }),
|
|
153
167
|
});
|
|
154
168
|
if (!response.ok)
|
|
155
|
-
throw new Error(`HTTP_${response.status}: ${response.statusText}`);
|
|
169
|
+
throw new Error(`HTTP_${response.status} at ${primaryEndpoint}: ${response.statusText}`);
|
|
156
170
|
const result = await response.json();
|
|
157
171
|
if (!result.data)
|
|
158
172
|
throw new Error("Invalid GraphQL response: Missing 'data' field.");
|
|
159
|
-
// Build Schema object from introspection JSON
|
|
160
173
|
tempSchema = buildClientSchema(result.data);
|
|
161
174
|
}
|
|
162
|
-
// --- UNIFIED STRUCTURAL ANALYSIS
|
|
175
|
+
// --- UNIFIED STRUCTURAL ANALYSIS ---
|
|
163
176
|
const typeMap = tempSchema.getTypeMap();
|
|
164
|
-
// Filter
|
|
177
|
+
// Filter domain types while ignoring internal system scalars and types
|
|
165
178
|
const businessTypes = Object.keys(typeMap).filter(typeName => {
|
|
166
179
|
const type = typeMap[typeName];
|
|
167
180
|
return (!typeName.startsWith('__') &&
|
|
@@ -169,34 +182,31 @@ async function performUpdate(force) {
|
|
|
169
182
|
!['String', 'Int', 'Float', 'Boolean', 'ID', 'BigInt', 'DateTime'].includes(typeName) &&
|
|
170
183
|
isObjectType(type));
|
|
171
184
|
});
|
|
172
|
-
// Maintain state for "Gap Analysis"
|
|
173
185
|
lastKnownTypeCount = businessTypes.length;
|
|
174
186
|
const currentSDL = printSchema(tempSchema);
|
|
175
|
-
//
|
|
176
|
-
// Always update the live object if we successfully built it
|
|
187
|
+
// Always update the live object upon successful build
|
|
177
188
|
cachedSchemaObject = tempSchema;
|
|
178
189
|
if (currentSDL !== cachedSDL) {
|
|
179
190
|
cachedSDL = currentSDL;
|
|
180
191
|
const duration = ((Date.now() - startTime) / 1000).toFixed(2);
|
|
192
|
+
const endpointLabel = env.SCHEMA ? 'Local/Remote SDL' : `Live Broadcast Node (${env.ENDPOINT.split(',').length} targets)`;
|
|
181
193
|
return [
|
|
182
194
|
`✨ SCHEMA EVOLVED (${duration}s)`,
|
|
183
|
-
`📊 Source: ${
|
|
195
|
+
`📊 Source: ${endpointLabel}`,
|
|
184
196
|
`🧬 Labels: ${businessTypes.join(', ') || 'None'}`,
|
|
185
197
|
`---`,
|
|
186
198
|
`The bridge has updated the graph model. New types are now queryable.`
|
|
187
199
|
].join('\n');
|
|
188
200
|
}
|
|
189
201
|
else {
|
|
190
|
-
// Even if SDL string is the same, we've ensured cachedSchemaObject is set above
|
|
191
202
|
return `✅ Status: Schema stable (${lastKnownTypeCount} labels).`;
|
|
192
203
|
}
|
|
193
204
|
}
|
|
194
205
|
catch (error) {
|
|
195
|
-
// Informative error report to prevent "AI confusion"
|
|
196
206
|
return [
|
|
197
207
|
`❌ SCHEMA SYNC FAILED`,
|
|
198
208
|
`🔍 Reason: ${error.message}`,
|
|
199
|
-
`🛠️ Action: Verify your
|
|
209
|
+
`🛠️ Action: Verify your connection and retry.`
|
|
200
210
|
].join('\n');
|
|
201
211
|
}
|
|
202
212
|
finally {
|
|
@@ -206,50 +216,97 @@ async function performUpdate(force) {
|
|
|
206
216
|
}
|
|
207
217
|
// --- TOOL REGISTRY ---
|
|
208
218
|
const toolHandlers = new Map();
|
|
209
|
-
// This will store schemas for our dynamic HTTP 'list-tools' response
|
|
210
219
|
const registeredToolsMetadata = [];
|
|
211
|
-
/**
|
|
212
|
-
*
|
|
213
|
-
* for debugging or bridging to 3D visualization tools.
|
|
220
|
+
/**
|
|
221
|
+
* History buffer for the last 5 operations to support debugging and visualization.
|
|
214
222
|
*/
|
|
215
223
|
let executionLogs = [];
|
|
216
|
-
|
|
224
|
+
/**
|
|
225
|
+
* Tool: query-graphql
|
|
226
|
+
* Handles query broadcast and execution across multiple endpoints.
|
|
227
|
+
*/
|
|
217
228
|
const queryGraphqlHandler = async ({ query, variables, headers }) => {
|
|
218
229
|
try {
|
|
219
230
|
const parsedQuery = (0, language_1.parse)(query);
|
|
220
231
|
const isMutation = parsedQuery.definitions.some((def) => def.kind === "OperationDefinition" && def.operation === "mutation");
|
|
221
|
-
if (isMutation && !env.ALLOW_MUTATIONS)
|
|
232
|
+
if (isMutation && !env.ALLOW_MUTATIONS) {
|
|
222
233
|
throw new Error("Mutations are not allowed.");
|
|
234
|
+
}
|
|
223
235
|
const toolHeaders = headers ? JSON.parse(headers) : {};
|
|
224
236
|
const allHeaders = { "Content-Type": "application/json", ...env.HEADERS, ...toolHeaders };
|
|
225
237
|
let parsedVariables = variables;
|
|
226
238
|
if (typeof variables === 'string')
|
|
227
239
|
parsedVariables = JSON.parse(variables);
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
240
|
+
// Split multiple endpoints for broadcast
|
|
241
|
+
const endpoints = env.ENDPOINT.split(',').map(url => url.trim());
|
|
242
|
+
// Execute parallel requests to all targets
|
|
243
|
+
const settleResults = await Promise.allSettled(endpoints.map(async (url) => {
|
|
244
|
+
console.error(`[QUERY] Sending to ${url}`);
|
|
245
|
+
const response = await fetch(url, {
|
|
246
|
+
method: "POST",
|
|
247
|
+
headers: allHeaders,
|
|
248
|
+
body: JSON.stringify({ query, variables: parsedVariables }),
|
|
249
|
+
signal: AbortSignal.timeout(5000) // 5s timeout protection
|
|
250
|
+
});
|
|
251
|
+
if (!response.ok) {
|
|
252
|
+
throw new Error(`HTTP ${response.status} from ${url}`);
|
|
253
|
+
}
|
|
254
|
+
return {
|
|
255
|
+
url,
|
|
256
|
+
data: await response.json()
|
|
257
|
+
};
|
|
258
|
+
}));
|
|
259
|
+
const successfulResponses = settleResults
|
|
260
|
+
.filter((r) => r.status === 'fulfilled')
|
|
261
|
+
.map(r => r.value);
|
|
262
|
+
const failedResponses = settleResults
|
|
263
|
+
.filter((r) => r.status === 'rejected')
|
|
264
|
+
.map(r => r.reason.message || r.reason);
|
|
265
|
+
if (successfulResponses.length === 0) {
|
|
266
|
+
throw new Error(`All endpoints failed: ${failedResponses.join('; ')}`);
|
|
267
|
+
}
|
|
268
|
+
const aggregatedResult = {};
|
|
269
|
+
successfulResponses.forEach((response) => {
|
|
270
|
+
const data = response.data.data;
|
|
271
|
+
if (data && data.patterns) {
|
|
272
|
+
console.error(`[DEBUG] Received ${data.patterns.length} patterns from ${response.url}`);
|
|
273
|
+
}
|
|
274
|
+
if (!data)
|
|
275
|
+
return;
|
|
276
|
+
Object.keys(data).forEach(key => {
|
|
277
|
+
if (Array.isArray(data[key])) {
|
|
278
|
+
// Merge arrays from different sources
|
|
279
|
+
aggregatedResult[key] = [...(aggregatedResult[key] || []), ...data[key]];
|
|
280
|
+
}
|
|
281
|
+
else if (typeof data[key] === 'object' && data[key] !== null) {
|
|
282
|
+
// Merge objects (e.g. metadata)
|
|
283
|
+
aggregatedResult[key] = { ...(aggregatedResult[key] || {}), ...data[key] };
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
// Use last value for primitives
|
|
287
|
+
aggregatedResult[key] = data[key];
|
|
288
|
+
}
|
|
289
|
+
});
|
|
245
290
|
});
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
291
|
+
// 2. Deduplication (Removes duplicates by 'sticking' field)
|
|
292
|
+
if (aggregatedResult.patterns) {
|
|
293
|
+
const unique = new Map();
|
|
294
|
+
aggregatedResult.patterns.forEach((p) => unique.set(p.sticking, p));
|
|
295
|
+
aggregatedResult.patterns = Array.from(unique.values());
|
|
296
|
+
}
|
|
297
|
+
// 3. Extract Cypher queries from extensions
|
|
298
|
+
const allCypher = successfulResponses.flatMap(r => r.data.extensions?.cypher || []);
|
|
299
|
+
const cleanCypher = allCypher.map((c) => c.replace(/^CYPHER: /, '').replace(/^CYPHER 5\n/, '').replace(/\nPARAMS: \{\}$/, ''));
|
|
300
|
+
// 4. Final response for the AI agent
|
|
249
301
|
const responseForClaude = {
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
302
|
+
status: {
|
|
303
|
+
total: endpoints.length,
|
|
304
|
+
success: successfulResponses.length,
|
|
305
|
+
failed: failedResponses.length
|
|
306
|
+
},
|
|
307
|
+
result: aggregatedResult,
|
|
308
|
+
...(cleanCypher.length > 0 ? { cypher: Array.from(new Set(cleanCypher)) } : {}),
|
|
309
|
+
...(failedResponses.length > 0 ? { warnings: failedResponses } : {})
|
|
253
310
|
};
|
|
254
311
|
return {
|
|
255
312
|
content: [{
|
|
@@ -270,13 +327,10 @@ toolHandlers.set("query-graphql", queryGraphqlHandler);
|
|
|
270
327
|
}, queryGraphqlHandler);
|
|
271
328
|
/**
|
|
272
329
|
* Tool: introspect-schema
|
|
273
|
-
*
|
|
274
|
-
* Implements "Agent Recovery" logic to guide the LLM when entities are missing.
|
|
330
|
+
* Provides schema exploration with built-in agent recovery logic.
|
|
275
331
|
*/
|
|
276
332
|
const introspectHandler = async ({ typeNames }) => {
|
|
277
|
-
// 1. Fetch the schema directly from the source
|
|
278
333
|
const result = await getSchema(true);
|
|
279
|
-
// Explicitly check if the result is a valid GraphQLSchema object
|
|
280
334
|
if (!result || typeof result === 'string') {
|
|
281
335
|
return {
|
|
282
336
|
content: [{
|
|
@@ -286,16 +340,13 @@ const introspectHandler = async ({ typeNames }) => {
|
|
|
286
340
|
}]
|
|
287
341
|
};
|
|
288
342
|
}
|
|
289
|
-
// --- 1. INITIALIZE MAPPINGS ---
|
|
290
343
|
const schema = result;
|
|
291
344
|
const typeMap = schema.getTypeMap();
|
|
292
|
-
// Cache Root Type fields for rapid gap analysis
|
|
293
345
|
const queryType = schema.getQueryType();
|
|
294
346
|
const queryFields = queryType ? queryType.getFields() : {};
|
|
295
347
|
const mutationType = schema.getMutationType();
|
|
296
348
|
const mutationFields = mutationType ? mutationType.getFields() : {};
|
|
297
|
-
//
|
|
298
|
-
// If specific types were requested, verify their existence in the current schema
|
|
349
|
+
// Gap analysis for requested types
|
|
299
350
|
if (typeNames && typeNames.length > 0) {
|
|
300
351
|
const missing = typeNames.filter(name => {
|
|
301
352
|
const existsAsType = !!typeMap[name];
|
|
@@ -303,31 +354,26 @@ const introspectHandler = async ({ typeNames }) => {
|
|
|
303
354
|
const existsAsQueryField = !!queryFields[name];
|
|
304
355
|
return !existsAsType && !existsAsMutation && !existsAsQueryField;
|
|
305
356
|
});
|
|
306
|
-
// If some requested entities are missing, provide the agent with a recovery map
|
|
307
357
|
if (missing.length > 0) {
|
|
308
|
-
// Filter out internal GraphQL types to reduce noise for the agent
|
|
309
358
|
const internalTypes = ['Query', 'Mutation', 'Subscription'];
|
|
310
359
|
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
360
|
const schemaVersion = `v${availableEntities.length}.${Math.floor(Date.now() / 10000) % 1000}`;
|
|
313
361
|
return {
|
|
314
362
|
content: [{
|
|
315
363
|
type: "text",
|
|
316
364
|
text: `❌ PARTIAL RESULTS [Schema ID: ${schemaVersion}]\n\n` +
|
|
317
365
|
`MISSING ENTITIES: ${missing.join(", ")}\n` +
|
|
318
|
-
`REASON:
|
|
319
|
-
`ACTION: Re-examine
|
|
366
|
+
`REASON: Requested entities not found.\n` +
|
|
367
|
+
`ACTION: Re-examine available types below.\n\n` +
|
|
320
368
|
`AVAILABLE_ENTITIES: ${availableEntities.join(", ")}`
|
|
321
369
|
}]
|
|
322
370
|
};
|
|
323
371
|
}
|
|
324
372
|
}
|
|
325
|
-
//
|
|
326
|
-
// If no typeNames provided, return a high-level overview of the entry points
|
|
373
|
+
// Return general manifest if no specific types requested
|
|
327
374
|
if (!typeNames || typeNames.length === 0) {
|
|
328
375
|
const discoveredEntities = new Set();
|
|
329
376
|
if (queryType) {
|
|
330
|
-
// Map Query fields to their underlying Object Types
|
|
331
377
|
const fields = queryType.getFields();
|
|
332
378
|
Object.values(fields).forEach((field) => {
|
|
333
379
|
const namedType = (0, graphql_1.getNamedType)(field.type);
|
|
@@ -349,8 +395,6 @@ const introspectHandler = async ({ typeNames }) => {
|
|
|
349
395
|
}]
|
|
350
396
|
};
|
|
351
397
|
}
|
|
352
|
-
// --- 4. DETAILED INTROSPECTION ---
|
|
353
|
-
// Return filtered schema metadata for the requested types or root fields
|
|
354
398
|
const filtered = (0, introspection_js_1.introspectSpecificTypes)(schema, typeNames);
|
|
355
399
|
return {
|
|
356
400
|
content: [{ type: "text", text: JSON.stringify(filtered, null, 2) }]
|
|
@@ -360,63 +404,54 @@ toolHandlers.set("introspect-schema", introspectHandler);
|
|
|
360
404
|
(0, tool_registry_js_1.registerTool)(server, toolHandlers, registeredToolsMetadata, "introspect-schema", "Introspect the GraphQL schema with optional type filtering", {
|
|
361
405
|
typeNames: zod_1.default.array(zod_1.default.string()).optional(),
|
|
362
406
|
}, introspectHandler);
|
|
363
|
-
// --- PROMPTS
|
|
364
|
-
// 1. Connection check
|
|
407
|
+
// --- PROMPTS ---
|
|
365
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.");
|
|
366
|
-
// 2. High-level overview
|
|
367
409
|
(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
410
|
(0, prompt_registry_js_1.registerPrompt)(server, "list-scalars", "List all scalar types", "Run 'introspect-schema' and identify all scalars in the schema.");
|
|
370
411
|
// --- HTTP SERVER LOGIC ---
|
|
412
|
+
/**
|
|
413
|
+
* Local HTTP server to support GraphiQL UI and SSE transport
|
|
414
|
+
*/
|
|
371
415
|
async function handleHttpRequest(req, res) {
|
|
372
|
-
// Standard CORS headers
|
|
416
|
+
// Standard CORS headers
|
|
373
417
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
374
|
-
res.setHeader('Access-Control-Allow-Methods', '
|
|
375
|
-
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
376
|
-
// Handle preflight requests
|
|
418
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
419
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, x-mcp-protocol-version, x-mcp-sdk-version');
|
|
377
420
|
if (req.method === 'OPTIONS') {
|
|
378
421
|
res.writeHead(204);
|
|
379
422
|
res.end();
|
|
380
423
|
return;
|
|
381
424
|
}
|
|
382
425
|
const url = new URL(req.url || '', `http://${req.headers.host}`);
|
|
383
|
-
//
|
|
426
|
+
// Render GraphiQL UI
|
|
384
427
|
if (req.method === 'GET' && (url.pathname === '/' || url.pathname === '/graphiql')) {
|
|
385
|
-
|
|
386
|
-
|
|
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
|
-
}
|
|
428
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
429
|
+
return res.end((0, graphiql_js_1.renderGraphiQL)(`http://localhost:6274/mcp`, env.HEADERS));
|
|
395
430
|
}
|
|
396
|
-
// Process MCP
|
|
431
|
+
// Process MCP/GraphQL requests
|
|
397
432
|
if (url.pathname === '/mcp' && req.method === 'POST') {
|
|
398
433
|
let body = '';
|
|
399
434
|
req.on('data', chunk => { body += chunk; });
|
|
400
435
|
req.on('end', async () => {
|
|
401
|
-
let requestId = null;
|
|
436
|
+
let requestId = null;
|
|
402
437
|
try {
|
|
403
|
-
const
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
}));
|
|
438
|
+
const payload = JSON.parse(body);
|
|
439
|
+
// Handle raw GraphQL queries (e.g., from Docs or Playground)
|
|
440
|
+
if (!payload.method && payload.query) {
|
|
441
|
+
const handler = toolHandlers.get("query-graphql");
|
|
442
|
+
if (handler) {
|
|
443
|
+
const mcpResult = await handler({ query: payload.query, variables: payload.variables });
|
|
444
|
+
const parsed = JSON.parse(mcpResult.content[0].text);
|
|
445
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
446
|
+
const graphQLResponse = parsed.result.data ? parsed.result : { data: parsed.result };
|
|
447
|
+
return res.end(JSON.stringify(graphQLResponse));
|
|
448
|
+
}
|
|
415
449
|
}
|
|
416
|
-
//
|
|
450
|
+
// Standard MCP JSON-RPC handling
|
|
451
|
+
const { method, id, params } = payload;
|
|
452
|
+
requestId = id;
|
|
417
453
|
let targetMethod = method;
|
|
418
454
|
let toolArgs = params;
|
|
419
|
-
// Support both direct calls and standard MCP "call-tool" structure
|
|
420
455
|
if (method === "call-tool" || method === "tools/call") {
|
|
421
456
|
targetMethod = params.name;
|
|
422
457
|
toolArgs = params.arguments;
|
|
@@ -430,14 +465,9 @@ async function handleHttpRequest(req, res) {
|
|
|
430
465
|
error: { code: -32601, message: `Tool ${targetMethod} not found` }
|
|
431
466
|
}));
|
|
432
467
|
}
|
|
433
|
-
// Execute the actual business logic for the tool
|
|
434
468
|
const result = await handler(toolArgs);
|
|
435
469
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
436
|
-
res.end(JSON.stringify({
|
|
437
|
-
jsonrpc: '2.0',
|
|
438
|
-
id: requestId,
|
|
439
|
-
result
|
|
440
|
-
}));
|
|
470
|
+
res.end(JSON.stringify({ jsonrpc: '2.0', id: requestId, result }));
|
|
441
471
|
}
|
|
442
472
|
catch (e) {
|
|
443
473
|
console.error(`[HTTP-ERROR] ${e.message}`);
|
|
@@ -445,18 +475,12 @@ async function handleHttpRequest(req, res) {
|
|
|
445
475
|
res.end(JSON.stringify({
|
|
446
476
|
jsonrpc: '2.0',
|
|
447
477
|
id: requestId,
|
|
448
|
-
error: { code: -32603, message: e.message
|
|
478
|
+
error: { code: -32603, message: e.message }
|
|
449
479
|
}));
|
|
450
480
|
}
|
|
451
481
|
});
|
|
452
482
|
return;
|
|
453
483
|
}
|
|
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
484
|
res.writeHead(404);
|
|
461
485
|
res.end("Not Found");
|
|
462
486
|
}
|
|
@@ -464,7 +488,7 @@ async function handleHttpRequest(req, res) {
|
|
|
464
488
|
async function main() {
|
|
465
489
|
const isInspector = !!(process.env.MCP_INSPECTOR || process.env.INSPECTOR_PORT);
|
|
466
490
|
const isHttpExplicitlyEnabled = process.env.ENABLE_HTTP === "true";
|
|
467
|
-
//
|
|
491
|
+
// Enable HTTP port by default unless explicitly disabled or in inspector mode
|
|
468
492
|
if (process.env.ENABLE_HTTP !== "false" && !isInspector) {
|
|
469
493
|
const serverHttp = node_http_1.default.createServer(handleHttpRequest);
|
|
470
494
|
serverHttp.on('error', (e) => {
|
|
@@ -473,10 +497,8 @@ async function main() {
|
|
|
473
497
|
}
|
|
474
498
|
});
|
|
475
499
|
serverHttp.listen(env.MCP_PORT, () => {
|
|
476
|
-
// All-in-one status report
|
|
477
500
|
console.error(`[SYSTEM] Server "${env.NAME}" v${getVersion()} active`);
|
|
478
501
|
console.error(`🤖 MCP SSE: http://localhost:${env.MCP_PORT}/mcp`);
|
|
479
|
-
// Show UI only if explicitly requested
|
|
480
502
|
if (isHttpExplicitlyEnabled) {
|
|
481
503
|
console.error(`🎨 GraphiQL UI: http://localhost:${env.MCP_PORT}/graphiql`);
|
|
482
504
|
}
|
|
@@ -484,6 +506,7 @@ async function main() {
|
|
|
484
506
|
}
|
|
485
507
|
const transport = new stdio_js_1.StdioServerTransport();
|
|
486
508
|
await server.connect(transport);
|
|
509
|
+
// Background schema initialization
|
|
487
510
|
getSchema().catch(() => { });
|
|
488
511
|
}
|
|
489
512
|
process.on('SIGINT', () => process.exit(0));
|
package/package.json
CHANGED