@letoribo/mcp-graphql-enhanced 3.8.0 → 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/helpers/introspection.d.ts +3 -2
- package/dist/helpers/introspection.d.ts.map +1 -1
- package/dist/helpers/introspection.js +29 -2
- package/dist/index.js +167 -131
- 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
|
|
|
@@ -8,8 +8,9 @@ export declare function introspectEndpoint(endpoint: string, headers?: Record<st
|
|
|
8
8
|
*/
|
|
9
9
|
export declare function introspectLocalSchema(path: string): Promise<string>;
|
|
10
10
|
/**
|
|
11
|
-
* Extract and filter specific types from a schema
|
|
12
|
-
*
|
|
11
|
+
* Extract and filter specific types or root fields from a GraphQL schema.
|
|
12
|
+
* This prevents "No result received" errors by only sending the requested
|
|
13
|
+
* parts of the graph to the agent, maintaining a stable context window.
|
|
13
14
|
*/
|
|
14
15
|
export declare function introspectSpecificTypes(schema: GraphQLSchema, typeNames: string[]): Record<string, any>;
|
|
15
16
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"introspection.d.ts","sourceRoot":"","sources":["../../src/helpers/introspection.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,aAAa,EAQd,MAAM,SAAS,CAAC;AAGjB;;GAEG;AACH,wBAAsB,kBAAkB,CACtC,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,mBAiBjC;AAED;;GAEG;AACH,wBAAsB,qBAAqB,CAAC,IAAI,EAAE,MAAM,mBAEvD;AAED
|
|
1
|
+
{"version":3,"file":"introspection.d.ts","sourceRoot":"","sources":["../../src/helpers/introspection.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,aAAa,EAQd,MAAM,SAAS,CAAC;AAGjB;;GAEG;AACH,wBAAsB,kBAAkB,CACtC,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,mBAiBjC;AAED;;GAEG;AACH,wBAAsB,qBAAqB,CAAC,IAAI,EAAE,MAAM,mBAEvD;AAED;;;;GAIG;AAEH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,EAAE,uBAgGjF;AAED;;GAEG;AACH,wBAAsB,eAAe,CACnC,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,YAAK,EACpC,SAAS,EAAE,MAAM,EAAE,mBAYpB"}
|
|
@@ -31,12 +31,39 @@ async function introspectLocalSchema(path) {
|
|
|
31
31
|
return await (0, promises_1.readFile)(path, "utf8");
|
|
32
32
|
}
|
|
33
33
|
/**
|
|
34
|
-
* Extract and filter specific types from a schema
|
|
35
|
-
*
|
|
34
|
+
* Extract and filter specific types or root fields from a GraphQL schema.
|
|
35
|
+
* This prevents "No result received" errors by only sending the requested
|
|
36
|
+
* parts of the graph to the agent, maintaining a stable context window.
|
|
36
37
|
*/
|
|
37
38
|
function introspectSpecificTypes(schema, typeNames) {
|
|
38
39
|
const result = {};
|
|
40
|
+
// Cache root field maps to avoid repeated lookups during the loop
|
|
41
|
+
const queryType = schema.getQueryType();
|
|
42
|
+
const queryFields = queryType ? queryType.getFields() : {};
|
|
43
|
+
const mutationType = schema.getMutationType();
|
|
44
|
+
const mutationFields = mutationType ? mutationType.getFields() : {};
|
|
39
45
|
for (const name of typeNames) {
|
|
46
|
+
// --- ROOT FIELD RESOLUTION ---
|
|
47
|
+
// Check if the name refers to a root field (Query or Mutation)
|
|
48
|
+
// rather than a named Type.
|
|
49
|
+
const rootField = queryFields[name] || mutationFields[name];
|
|
50
|
+
if (rootField) {
|
|
51
|
+
result[name] = {
|
|
52
|
+
kind: queryFields[name] ? "QUERY_FIELD" : "MUTATION_FIELD",
|
|
53
|
+
description: rootField.description,
|
|
54
|
+
type: rootField.type.toString(),
|
|
55
|
+
args: rootField.args
|
|
56
|
+
.filter(arg => !arg.deprecationReason)
|
|
57
|
+
.map(arg => ({
|
|
58
|
+
name: arg.name,
|
|
59
|
+
type: arg.type.toString(),
|
|
60
|
+
description: arg.description,
|
|
61
|
+
}))
|
|
62
|
+
};
|
|
63
|
+
continue; // Field found, move to next requested name
|
|
64
|
+
}
|
|
65
|
+
// --- NAMED TYPE RESOLUTION ---
|
|
66
|
+
// Fallback to standard type introspection if no root field matches
|
|
40
67
|
const type = schema.getType(name);
|
|
41
68
|
if (!type)
|
|
42
69
|
continue;
|
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: [{
|
|
@@ -268,76 +325,76 @@ toolHandlers.set("query-graphql", queryGraphqlHandler);
|
|
|
268
325
|
variables: zod_1.default.string().optional(),
|
|
269
326
|
headers: zod_1.default.string().optional(),
|
|
270
327
|
}, queryGraphqlHandler);
|
|
271
|
-
|
|
272
|
-
|
|
328
|
+
/**
|
|
329
|
+
* Tool: introspect-schema
|
|
330
|
+
* Provides schema exploration with built-in agent recovery logic.
|
|
331
|
+
*/
|
|
273
332
|
const introspectHandler = async ({ typeNames }) => {
|
|
274
|
-
// 1. Fetch the schema directly
|
|
275
333
|
const result = await getSchema(true);
|
|
276
|
-
// Explicitly check if the result is an error string or null
|
|
277
334
|
if (!result || typeof result === 'string') {
|
|
278
335
|
return {
|
|
279
336
|
content: [{
|
|
280
337
|
type: "text",
|
|
281
338
|
text: `❌ SCHEMA_ERROR: ${typeof result === 'string' ? result : 'GraphQL schema is not initialized yet.'}\n` +
|
|
282
|
-
`ACTION: Please wait 5-10 seconds for the
|
|
339
|
+
`ACTION: Please wait 5-10 seconds for the backend endpoint to respond.`
|
|
283
340
|
}]
|
|
284
341
|
};
|
|
285
342
|
}
|
|
286
|
-
// TYPE FIX: Cast to GraphQLSchema so TS knows methods like getTypeMap exist
|
|
287
343
|
const schema = result;
|
|
288
344
|
const typeMap = schema.getTypeMap();
|
|
345
|
+
const queryType = schema.getQueryType();
|
|
346
|
+
const queryFields = queryType ? queryType.getFields() : {};
|
|
289
347
|
const mutationType = schema.getMutationType();
|
|
290
348
|
const mutationFields = mutationType ? mutationType.getFields() : {};
|
|
291
|
-
//
|
|
349
|
+
// Gap analysis for requested types
|
|
292
350
|
if (typeNames && typeNames.length > 0) {
|
|
293
351
|
const missing = typeNames.filter(name => {
|
|
294
352
|
const existsAsType = !!typeMap[name];
|
|
295
353
|
const existsAsMutation = !!mutationFields[name];
|
|
296
|
-
|
|
354
|
+
const existsAsQueryField = !!queryFields[name];
|
|
355
|
+
return !existsAsType && !existsAsMutation && !existsAsQueryField;
|
|
297
356
|
});
|
|
298
357
|
if (missing.length > 0) {
|
|
299
|
-
const
|
|
300
|
-
const
|
|
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}`;
|
|
301
361
|
return {
|
|
302
362
|
content: [{
|
|
303
363
|
type: "text",
|
|
304
|
-
text: `❌ PARTIAL RESULTS [ID: ${schemaVersion}]\n\n` +
|
|
364
|
+
text: `❌ PARTIAL RESULTS [Schema ID: ${schemaVersion}]\n\n` +
|
|
305
365
|
`MISSING ENTITIES: ${missing.join(", ")}\n` +
|
|
306
|
-
`REASON:
|
|
307
|
-
`ACTION:
|
|
308
|
-
`AVAILABLE_ENTITIES: ${
|
|
366
|
+
`REASON: Requested entities not found.\n` +
|
|
367
|
+
`ACTION: Re-examine available types below.\n\n` +
|
|
368
|
+
`AVAILABLE_ENTITIES: ${availableEntities.join(", ")}`
|
|
309
369
|
}]
|
|
310
370
|
};
|
|
311
371
|
}
|
|
312
372
|
}
|
|
313
|
-
//
|
|
373
|
+
// Return general manifest if no specific types requested
|
|
314
374
|
if (!typeNames || typeNames.length === 0) {
|
|
315
|
-
const queryType = schema.getQueryType();
|
|
316
375
|
const discoveredEntities = new Set();
|
|
317
376
|
if (queryType) {
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
Object.values(queryFields).forEach((field) => {
|
|
377
|
+
const fields = queryType.getFields();
|
|
378
|
+
Object.values(fields).forEach((field) => {
|
|
321
379
|
const namedType = (0, graphql_1.getNamedType)(field.type);
|
|
322
380
|
if ((0, graphql_1.isObjectType)(namedType) && !namedType.name.startsWith('__')) {
|
|
323
381
|
discoveredEntities.add(namedType.name);
|
|
324
382
|
}
|
|
325
383
|
});
|
|
326
384
|
}
|
|
327
|
-
const
|
|
328
|
-
const
|
|
329
|
-
const schemaVersion = `v${
|
|
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}`;
|
|
330
388
|
return {
|
|
331
389
|
content: [{
|
|
332
390
|
type: "text",
|
|
333
391
|
text: `GraphQL Schema Manifest [ID: ${schemaVersion}]\n\n` +
|
|
334
|
-
`ENTRY_POINT_ENTITIES: ${
|
|
335
|
-
`TOTAL_SCHEMA_TYPES: ${
|
|
336
|
-
`ALL_AVAILABLE_TYPES: ${
|
|
392
|
+
`ENTRY_POINT_ENTITIES: ${entryPoints.join(", ") || "None"}\n` +
|
|
393
|
+
`TOTAL_SCHEMA_TYPES: ${allTypes.length}\n\n` +
|
|
394
|
+
`ALL_AVAILABLE_TYPES: ${allTypes.join(", ")}`
|
|
337
395
|
}]
|
|
338
396
|
};
|
|
339
397
|
}
|
|
340
|
-
// 3. Detailed introspection
|
|
341
398
|
const filtered = (0, introspection_js_1.introspectSpecificTypes)(schema, typeNames);
|
|
342
399
|
return {
|
|
343
400
|
content: [{ type: "text", text: JSON.stringify(filtered, null, 2) }]
|
|
@@ -347,63 +404,54 @@ toolHandlers.set("introspect-schema", introspectHandler);
|
|
|
347
404
|
(0, tool_registry_js_1.registerTool)(server, toolHandlers, registeredToolsMetadata, "introspect-schema", "Introspect the GraphQL schema with optional type filtering", {
|
|
348
405
|
typeNames: zod_1.default.array(zod_1.default.string()).optional(),
|
|
349
406
|
}, introspectHandler);
|
|
350
|
-
// --- PROMPTS
|
|
351
|
-
// 1. Connection check
|
|
407
|
+
// --- PROMPTS ---
|
|
352
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.");
|
|
353
|
-
// 2. High-level overview
|
|
354
409
|
(0, prompt_registry_js_1.registerPrompt)(server, "schema-overview", "List all available types", "Run 'introspect-schema' to see all types and entry points.");
|
|
355
|
-
// 3. Data types analysis
|
|
356
410
|
(0, prompt_registry_js_1.registerPrompt)(server, "list-scalars", "List all scalar types", "Run 'introspect-schema' and identify all scalars in the schema.");
|
|
357
411
|
// --- HTTP SERVER LOGIC ---
|
|
412
|
+
/**
|
|
413
|
+
* Local HTTP server to support GraphiQL UI and SSE transport
|
|
414
|
+
*/
|
|
358
415
|
async function handleHttpRequest(req, res) {
|
|
359
|
-
// Standard CORS headers
|
|
416
|
+
// Standard CORS headers
|
|
360
417
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
361
|
-
res.setHeader('Access-Control-Allow-Methods', '
|
|
362
|
-
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
363
|
-
// 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');
|
|
364
420
|
if (req.method === 'OPTIONS') {
|
|
365
421
|
res.writeHead(204);
|
|
366
422
|
res.end();
|
|
367
423
|
return;
|
|
368
424
|
}
|
|
369
425
|
const url = new URL(req.url || '', `http://${req.headers.host}`);
|
|
370
|
-
//
|
|
426
|
+
// Render GraphiQL UI
|
|
371
427
|
if (req.method === 'GET' && (url.pathname === '/' || url.pathname === '/graphiql')) {
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
375
|
-
return res.end((0, graphiql_js_1.renderGraphiQL)(env.ENDPOINT, env.HEADERS));
|
|
376
|
-
}
|
|
377
|
-
else {
|
|
378
|
-
// Forbidden if not explicitly enabled
|
|
379
|
-
res.writeHead(403, { 'Content-Type': 'text/plain' });
|
|
380
|
-
return res.end("Forbidden: GraphiQL UI is disabled. Start with ENABLE_HTTP=true to use it.");
|
|
381
|
-
}
|
|
428
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
429
|
+
return res.end((0, graphiql_js_1.renderGraphiQL)(`http://localhost:6274/mcp`, env.HEADERS));
|
|
382
430
|
}
|
|
383
|
-
// Process MCP
|
|
431
|
+
// Process MCP/GraphQL requests
|
|
384
432
|
if (url.pathname === '/mcp' && req.method === 'POST') {
|
|
385
433
|
let body = '';
|
|
386
434
|
req.on('data', chunk => { body += chunk; });
|
|
387
435
|
req.on('end', async () => {
|
|
388
|
-
let requestId = null;
|
|
436
|
+
let requestId = null;
|
|
389
437
|
try {
|
|
390
|
-
const
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
}));
|
|
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
|
+
}
|
|
402
449
|
}
|
|
403
|
-
//
|
|
450
|
+
// Standard MCP JSON-RPC handling
|
|
451
|
+
const { method, id, params } = payload;
|
|
452
|
+
requestId = id;
|
|
404
453
|
let targetMethod = method;
|
|
405
454
|
let toolArgs = params;
|
|
406
|
-
// Support both direct calls and standard MCP "call-tool" structure
|
|
407
455
|
if (method === "call-tool" || method === "tools/call") {
|
|
408
456
|
targetMethod = params.name;
|
|
409
457
|
toolArgs = params.arguments;
|
|
@@ -417,14 +465,9 @@ async function handleHttpRequest(req, res) {
|
|
|
417
465
|
error: { code: -32601, message: `Tool ${targetMethod} not found` }
|
|
418
466
|
}));
|
|
419
467
|
}
|
|
420
|
-
// Execute the actual business logic for the tool
|
|
421
468
|
const result = await handler(toolArgs);
|
|
422
469
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
423
|
-
res.end(JSON.stringify({
|
|
424
|
-
jsonrpc: '2.0',
|
|
425
|
-
id: requestId,
|
|
426
|
-
result
|
|
427
|
-
}));
|
|
470
|
+
res.end(JSON.stringify({ jsonrpc: '2.0', id: requestId, result }));
|
|
428
471
|
}
|
|
429
472
|
catch (e) {
|
|
430
473
|
console.error(`[HTTP-ERROR] ${e.message}`);
|
|
@@ -432,18 +475,12 @@ async function handleHttpRequest(req, res) {
|
|
|
432
475
|
res.end(JSON.stringify({
|
|
433
476
|
jsonrpc: '2.0',
|
|
434
477
|
id: requestId,
|
|
435
|
-
error: { code: -32603, message: e.message
|
|
478
|
+
error: { code: -32603, message: e.message }
|
|
436
479
|
}));
|
|
437
480
|
}
|
|
438
481
|
});
|
|
439
482
|
return;
|
|
440
483
|
}
|
|
441
|
-
// Health check endpoint for monitoring
|
|
442
|
-
if (url.pathname === '/health') {
|
|
443
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
444
|
-
return res.end(JSON.stringify({ status: "ok", server: env.NAME }));
|
|
445
|
-
}
|
|
446
|
-
// Default 404 for unknown paths
|
|
447
484
|
res.writeHead(404);
|
|
448
485
|
res.end("Not Found");
|
|
449
486
|
}
|
|
@@ -451,7 +488,7 @@ async function handleHttpRequest(req, res) {
|
|
|
451
488
|
async function main() {
|
|
452
489
|
const isInspector = !!(process.env.MCP_INSPECTOR || process.env.INSPECTOR_PORT);
|
|
453
490
|
const isHttpExplicitlyEnabled = process.env.ENABLE_HTTP === "true";
|
|
454
|
-
//
|
|
491
|
+
// Enable HTTP port by default unless explicitly disabled or in inspector mode
|
|
455
492
|
if (process.env.ENABLE_HTTP !== "false" && !isInspector) {
|
|
456
493
|
const serverHttp = node_http_1.default.createServer(handleHttpRequest);
|
|
457
494
|
serverHttp.on('error', (e) => {
|
|
@@ -460,10 +497,8 @@ async function main() {
|
|
|
460
497
|
}
|
|
461
498
|
});
|
|
462
499
|
serverHttp.listen(env.MCP_PORT, () => {
|
|
463
|
-
// All-in-one status report
|
|
464
500
|
console.error(`[SYSTEM] Server "${env.NAME}" v${getVersion()} active`);
|
|
465
501
|
console.error(`🤖 MCP SSE: http://localhost:${env.MCP_PORT}/mcp`);
|
|
466
|
-
// Show UI only if explicitly requested
|
|
467
502
|
if (isHttpExplicitlyEnabled) {
|
|
468
503
|
console.error(`🎨 GraphiQL UI: http://localhost:${env.MCP_PORT}/graphiql`);
|
|
469
504
|
}
|
|
@@ -471,6 +506,7 @@ async function main() {
|
|
|
471
506
|
}
|
|
472
507
|
const transport = new stdio_js_1.StdioServerTransport();
|
|
473
508
|
await server.connect(transport);
|
|
509
|
+
// Background schema initialization
|
|
474
510
|
getSchema().catch(() => { });
|
|
475
511
|
}
|
|
476
512
|
process.on('SIGINT', () => process.exit(0));
|
package/package.json
CHANGED