@letoribo/mcp-graphql-enhanced 3.3.0 β†’ 3.5.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.
Files changed (3) hide show
  1. package/README.md +22 -0
  2. package/dist/index.js +191 -20
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -3,6 +3,15 @@
3
3
  An **enhanced MCP (Model Context Protocol) server for GraphQL** that fixes real-world interoperability issues between LLMs and GraphQL APIs.
4
4
  > Drop-in replacement for `mcp-graphql` β€” with dynamic headers, robust variables parsing, and zero breaking changes.
5
5
 
6
+ ## πŸ’¬ Community & Support
7
+
8
+ Join the conversation! If you have questions about using this bridge with Neo4j, Discord data graphs, or GraphQL in general, come hang out with us:
9
+
10
+ * **Discord Channel:** [#mcp-graphql-enhanced](https://discord.com/channels/625400653321076807/1480510460339159184)
11
+ * **Server:** The official **GraphQL Discord**
12
+
13
+ This is the best place to share your feedback, report issues, or suggest new "enhanced" features for the bridge.
14
+
6
15
  ## ✨ Key Enhancements
7
16
  * βœ… **Built-in GraphiQL IDE** β€” Visual playground at http://localhost:MCP_PORT/ (or /graphiql) with pre-configured headers.
8
17
  * βœ… **Dual Transport** β€” Supports both **STDIO** (for local CLI/client tools) and **HTTP/JSON-RPC** (for external/browser clients).
@@ -11,6 +20,19 @@ An **enhanced MCP (Model Context Protocol) server for GraphQL** that fixes real-
11
20
  * βœ… **Filtered introspection** β€” request only specific types (e.g., `typeNames: ["Query", "User"]`) to reduce LLM context noise
12
21
  * βœ… **Full MCP compatibility** β€” works with **Claude Desktop**, **Cursor**, **Glama**
13
22
  * βœ… **Secure by default** β€” mutations disabled unless explicitly enabled
23
+ * βœ… **Dynamic Schema Evolution** β€” Smart diagnostics and gap analysis for servers that regenerate GraphQL types on-the-fly (like Neo4j).
24
+ * βœ… **Deep Observability** β€” Automatic Cypher extraction and cleaning from GraphQL extensions.
25
+
26
+ ## πŸ” Advanced Observability & Cypher
27
+ The bridge provides deep insights into how the LLM interacts with your graph database.
28
+
29
+ ### πŸ•ΈοΈ Automated Cypher Extraction
30
+ For GraphQL server implementations that return query execution plans (like `@neo4j/graphql`), the bridge automatically:
31
+ 1. **Detects** `extensions.cypher` in the response.
32
+ 2. **Sanitizes** the output by stripping internal headers (like `CYPHER 5` or empty `PARAMS`).
33
+ 3. **Injects** a clean Cypher block directly into the tool's output for the AI to analyze.
34
+
35
+ > **Note:** This feature requires your GraphQL server to be configured to include debug information in the response extensions.
14
36
 
15
37
  ---
16
38
  ## 🎨 Visual Command Center (GraphiQL)
package/dist/index.js CHANGED
@@ -10,6 +10,7 @@ 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");
13
14
  // Helper imports
14
15
  const deprecation_js_1 = require("./helpers/deprecation.js");
15
16
  const introspection_js_1 = require("./helpers/introspection.js");
@@ -57,53 +58,153 @@ const env = EnvSchema.parse(process.env);
57
58
  const server = new mcp_js_1.McpServer({
58
59
  name: env.NAME,
59
60
  version: getVersion(),
61
+ description: "Start of the #mcp-graphql-enhanced channel on GraphQL server. Join here: https://discord.com/channels/622115132221685760/1348633379555184640"
60
62
  });
61
63
  // --- CACHE STATE ---
62
64
  let cachedSDL = null;
63
65
  let cachedSchemaObject = null;
64
66
  let schemaLoadError = null;
65
- async function getSchema() {
66
- if (cachedSDL)
67
+ let isUpdating = false;
68
+ let updatePromise = null;
69
+ let lastKnownTypeCount = 0;
70
+ let expectEmptySchema = false; // Intent flag for intentional purges
71
+ /**
72
+ * Smart Hybrid Schema Fetcher (Zero-Error Version)
73
+ * @param force If true, blocks and waits for the new schema evolution.
74
+ * If false, returns cache immediately and updates in background.
75
+ */
76
+ async function getSchema(force = false, requestedTypes) {
77
+ // 1. Hook into existing update if in progress
78
+ if (isUpdating && updatePromise) {
79
+ if (force || !cachedSDL)
80
+ return await updatePromise;
67
81
  return cachedSDL;
82
+ }
83
+ // 2. Return cache if valid and not forcing
84
+ if (cachedSDL && !force) {
85
+ // Validation check: If user wants specific types but they aren't in the cache
86
+ if (requestedTypes && cachedSchemaObject) {
87
+ const typeMap = cachedSchemaObject.getTypeMap();
88
+ const missing = requestedTypes.filter(t => !typeMap[t]);
89
+ if (missing.length > 0) {
90
+ // Force a refresh if requested types are missing from current cache
91
+ return await (updatePromise = performUpdate(true));
92
+ }
93
+ }
94
+ return cachedSDL;
95
+ }
96
+ if (force)
97
+ schemaLoadError = null;
68
98
  if (schemaLoadError)
69
99
  throw schemaLoadError;
100
+ // 3. Trigger update
101
+ updatePromise = performUpdate(force);
70
102
  try {
71
- const { buildClientSchema, getIntrospectionQuery, printSchema, buildASTSchema, parse: gqlParse } = require("graphql");
72
- let sdl;
103
+ if (force || !cachedSDL) {
104
+ return await updatePromise;
105
+ }
106
+ else {
107
+ updatePromise.catch(err => console.error("[SCHEMA] Background update failed:", err));
108
+ return cachedSDL;
109
+ }
110
+ }
111
+ finally {
112
+ // Promise reference is cleared inside performUpdate's 'finally' block
113
+ }
114
+ }
115
+ /**
116
+ * Internal logic for schema introspection and building.
117
+ * This version uses universal business-type tracking and provides
118
+ * detailed diagnostic reports instead of generic error messages.
119
+ */
120
+ async function performUpdate(force) {
121
+ isUpdating = true;
122
+ const startTime = Date.now();
123
+ try {
124
+ const { buildClientSchema, getIntrospectionQuery, printSchema, buildASTSchema, parse: gqlParse, isObjectType } = require("graphql");
125
+ let tempSchema;
126
+ // --- FETCHING LOGIC (Unified Source) ---
73
127
  if (env.SCHEMA) {
74
- // Check if it's a URL or local path
128
+ let sdl;
75
129
  if (env.SCHEMA.startsWith("http")) {
130
+ // Remote SDL File: Fetch via HTTP
76
131
  const response = await fetch(env.SCHEMA);
132
+ if (!response.ok)
133
+ throw new Error(`Remote_SDL_Fetch_Failed: ${response.statusText}`);
77
134
  sdl = await response.text();
78
135
  }
79
136
  else {
137
+ // Local SDL File: Use your custom helper (readFile inside)
80
138
  sdl = await (0, introspection_js_1.introspectLocalSchema)(env.SCHEMA);
81
139
  }
82
- cachedSchemaObject = buildASTSchema(gqlParse(sdl));
83
- cachedSDL = sdl;
140
+ // Direct path: Convert raw SDL string to GraphQLSchema object
141
+ tempSchema = buildASTSchema(gqlParse(sdl));
84
142
  }
85
143
  else {
144
+ // Standard Path: Execute Introspection Query against live ENDPOINT
86
145
  const response = await fetch(env.ENDPOINT, {
87
146
  method: "POST",
88
147
  headers: { "Content-Type": "application/json", ...env.HEADERS },
89
148
  body: JSON.stringify({ query: getIntrospectionQuery() }),
90
149
  });
91
150
  if (!response.ok)
92
- throw new Error(`Fetch failed: ${response.statusText}`);
151
+ throw new Error(`HTTP_${response.status}: ${response.statusText}`);
93
152
  const result = await response.json();
94
- cachedSchemaObject = buildClientSchema(result.data);
95
- cachedSDL = printSchema(cachedSchemaObject);
153
+ if (!result.data)
154
+ throw new Error("Invalid GraphQL response: Missing 'data' field.");
155
+ // Build Schema object from introspection JSON
156
+ tempSchema = buildClientSchema(result.data);
157
+ }
158
+ // --- UNIFIED STRUCTURAL ANALYSIS (For AI Report) ---
159
+ const typeMap = tempSchema.getTypeMap();
160
+ // Filter "Business Labels" (Nodes) while ignoring internal scalars/system types
161
+ const businessTypes = Object.keys(typeMap).filter(typeName => {
162
+ const type = typeMap[typeName];
163
+ return (!typeName.startsWith('__') &&
164
+ !['Query', 'Mutation', 'Subscription'].includes(typeName) &&
165
+ !['String', 'Int', 'Float', 'Boolean', 'ID', 'BigInt', 'DateTime'].includes(typeName) &&
166
+ isObjectType(type));
167
+ });
168
+ // Maintain state for "Gap Analysis"
169
+ lastKnownTypeCount = businessTypes.length;
170
+ const currentSDL = printSchema(tempSchema);
171
+ const duration = ((Date.now() - startTime) / 1000).toFixed(2);
172
+ // --- CACHE & NOTIFICATION ---
173
+ if (currentSDL !== cachedSDL) {
174
+ cachedSDL = currentSDL;
175
+ cachedSchemaObject = tempSchema; // Store the live Schema object for tool execution
176
+ return [
177
+ `✨ SCHEMA EVOLVED (${duration}s)`,
178
+ `πŸ“Š Source: ${env.SCHEMA ? 'Local/Remote SDL' : 'Live Endpoint'}`,
179
+ `🧬 Labels: ${businessTypes.join(', ') || 'None'}`,
180
+ `---`,
181
+ `The bridge has updated the graph model. New types are now queryable.`
182
+ ].join('\n');
183
+ }
184
+ else {
185
+ return `βœ… Status: Schema stable (${lastKnownTypeCount} labels).`;
96
186
  }
97
- console.error(`[SCHEMA] Successfully loaded and cached GraphQL schema`);
98
- return cachedSDL;
99
187
  }
100
188
  catch (error) {
101
- schemaLoadError = error instanceof Error ? error : new Error(String(error));
102
- throw schemaLoadError;
189
+ // Informative error report to prevent "AI confusion"
190
+ return [
191
+ `❌ SCHEMA SYNC FAILED`,
192
+ `πŸ” Reason: ${error.message}`,
193
+ `πŸ› οΈ Action: Verify your ${env.SCHEMA ? 'file path' : 'endpoint connection'} and retry.`
194
+ ].join('\n');
195
+ }
196
+ finally {
197
+ isUpdating = false;
198
+ updatePromise = null;
103
199
  }
104
200
  }
105
201
  // --- TOOL HANDLERS ---
106
202
  const toolHandlers = new Map();
203
+ /** * executionLogs stores the last 5 GraphQL operations.
204
+ * This allows the AI to "inspect" its own generated queries and the raw data
205
+ * for debugging or bridging to 3D visualization tools.
206
+ */
207
+ let executionLogs = [];
107
208
  // Tool: query-graphql
108
209
  const queryGraphqlHandler = async ({ query, variables, headers }) => {
109
210
  try {
@@ -121,9 +222,32 @@ const queryGraphqlHandler = async ({ query, variables, headers }) => {
121
222
  headers: allHeaders,
122
223
  body: JSON.stringify({ query, variables: parsedVariables }),
123
224
  });
124
- const data = await response.json();
225
+ const data = (await response.json());
226
+ // 1. Extract and sanitize Cypher if present in extensions
227
+ const rawCypher = data.extensions?.cypher || [];
228
+ const cleanCypher = rawCypher.map((c) => c.replace(/^CYPHER: /, '')
229
+ .replace(/^CYPHER 5\n/, '')
230
+ .replace(/\nPARAMS: \{\}$/, ''));
231
+ // 2. Update execution history for internal server state
232
+ executionLogs.push({
233
+ query,
234
+ variables: parsedVariables,
235
+ response: data,
236
+ timestamp: new Date().toISOString()
237
+ });
238
+ if (executionLogs.length > 5)
239
+ executionLogs.shift();
240
+ // 3. Prepare optimized response for Claude
241
+ const responseForClaude = {
242
+ result: data.data,
243
+ // Only add the cypher field if there's actual data to show
244
+ ...(cleanCypher.length > 0 ? { cypher: cleanCypher } : {})
245
+ };
125
246
  return {
126
- content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
247
+ content: [{
248
+ type: "text",
249
+ text: JSON.stringify(responseForClaude, null, 2)
250
+ }]
127
251
  };
128
252
  }
129
253
  catch (error) {
@@ -138,14 +262,61 @@ server.tool("query-graphql", "Execute a GraphQL query against the endpoint", {
138
262
  }, queryGraphqlHandler);
139
263
  // Tool: introspect-schema
140
264
  const introspectHandler = async ({ typeNames }) => {
141
- await getSchema();
265
+ // 1. Always pull the latest schema state
266
+ // The report from performUpdate is captured to show evolution details
267
+ const evolutionSummary = await getSchema(true);
268
+ const schema = cachedSchemaObject;
269
+ const typeMap = schema.getTypeMap();
270
+ // 2. Generate a structural fingerprint
271
+ const typeKeys = Object.keys(typeMap).filter(t => !t.startsWith('__'));
272
+ const schemaVersion = `v${typeKeys.length}.${Math.floor(Date.now() / 10000) % 1000}`;
273
+ // --- GAP ANALYSIS: Check if requested types are actually in the map ---
274
+ if (typeNames && typeNames.length > 0) {
275
+ const missing = typeNames.filter(name => !typeMap[name]);
276
+ if (missing.length > 0) {
277
+ return {
278
+ content: [{
279
+ type: "text",
280
+ text: `❌ PARTIAL RESULTS [ID: ${schemaVersion}]\n\n` +
281
+ `MISSING TYPES: ${missing.join(", ")}\n` +
282
+ `REASON: The database has been updated, but the GraphQL Schema is still regenerating these specific types.\n` +
283
+ `ACTION: Please wait 3 seconds and retry 'introspect-schema' to see the full graph.\n\n` +
284
+ `CURRENTLY AVAILABLE: ${typeKeys.filter(t => !['Query', 'Mutation', 'Report'].includes(t)).join(", ")}`
285
+ }]
286
+ };
287
+ }
288
+ }
142
289
  if (!typeNames || typeNames.length === 0) {
143
- const allTypeNames = Object.keys(cachedSchemaObject.getTypeMap()).filter(t => !t.startsWith('__'));
290
+ const queryType = schema.getQueryType();
291
+ const discoveredEntities = new Set();
292
+ if (queryType) {
293
+ const queryFields = queryType.getFields();
294
+ Object.values(queryFields).forEach((field) => {
295
+ const namedType = (0, graphql_1.getNamedType)(field.type);
296
+ if ((0, graphql_1.isObjectType)(namedType) && !namedType.name.startsWith('__')) {
297
+ discoveredEntities.add(namedType.name);
298
+ }
299
+ });
300
+ }
301
+ const coreEntities = Array.from(discoveredEntities).sort();
302
+ const communityHeader = [
303
+ `🌐 Community Channel: #mcp-graphql-enhanced`,
304
+ `πŸ”— Join the discussion: https://discord.com/channels/625400653321076807/1480510460339159184`,
305
+ `---`
306
+ ].join('\n');
144
307
  return {
145
- content: [{ type: "text", text: `Schema is large. Available types: ${allTypeNames.join(", ")}` }]
308
+ content: [{
309
+ type: "text",
310
+ text: `${communityHeader}\n${evolutionSummary}\n\n` + // Include the report from performUpdate
311
+ `GraphQL Schema Manifest [ID: ${schemaVersion}]\n\n` +
312
+ `ENTRY_POINT_ENTITIES: ${coreEntities.join(", ") || "None"}\n` +
313
+ `TOTAL_SCHEMA_TYPES: ${typeKeys.length}\n\n` +
314
+ `ALL_AVAILABLE_TYPES: ${typeKeys.join(", ")}`
315
+ }]
146
316
  };
147
317
  }
148
- const filtered = (0, introspection_js_1.introspectSpecificTypes)(cachedSchemaObject, typeNames);
318
+ // 3. Detailed introspection for specific types
319
+ const filtered = (0, introspection_js_1.introspectSpecificTypes)(schema, typeNames);
149
320
  return {
150
321
  content: [{ type: "text", text: JSON.stringify(filtered, null, 2) }]
151
322
  };
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.3.0"
53
+ "version": "3.5.0"
54
54
  }