@letoribo/mcp-graphql-enhanced 3.7.0 → 3.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE CHANGED
@@ -1,7 +1,7 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2024 Boris Besemer (Original Work)
4
- Copyright (c) 2025-2026 [letoribo] (Enhanced Architecture, NPM Refactoring & Discord Graph Infrastructure)
3
+ Copyright (c) 2024 Boris Besemer
4
+ Copyright (c) 2025-2026 letoribo
5
5
 
6
6
  Permission is hereby granted, free of charge, to any person obtaining a copy
7
7
  of this software and associated documentation files (the "Software"), to deal
@@ -19,19 +19,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
19
  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
20
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
21
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
- SOFTWARE.
23
-
24
- ---
25
- PROJECT EVOLUTION & SOVEREIGNTY NOTE:
26
- As of mid-2025, this project has undergone a fundamental architectural shift.
27
- The legacy Bun-based implementation has been decommissioned and replaced with
28
- a surgical, production-ready NPM/Node.js core for maximum stability.
29
-
30
- This "Enhanced" version was battle-tested as the architectural engine for the
31
- "Neo4j Discord-as-a-Database" project (mcp-neo4j-discord.vercel.app), handling
32
- large-scale graph datasets.
33
-
34
- While active real-time data ingestion is currently throttled due to persistent
35
- infrastructure challenges and blackouts in Odesa, the codebase remains the
36
- sovereign standard for high-performance GraphRAG, 3D visualization interface,
37
- and resilient AI intent parsing. Developed under pressure in Odesa, Ukraine.
22
+ SOFTWARE.
@@ -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 object.
12
- * Prevents "No result received" errors by only sending requested parts of the graph.
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;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,EAAE,uBAmEjF;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"}
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 object.
35
- * Prevents "No result received" errors by only sending requested parts of the graph.
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
@@ -74,7 +74,6 @@ let schemaLoadError = null;
74
74
  let isUpdating = false;
75
75
  let updatePromise = null;
76
76
  let lastKnownTypeCount = 0;
77
- let expectEmptySchema = false; // Intent flag for intentional purges
78
77
  /**
79
78
  * Smart Hybrid Schema Fetcher (Zero-Error Version)
80
79
  * @param force If true, blocks and waits for the new schema evolution.
@@ -108,15 +107,13 @@ async function getSchema(force = false, requestedTypes) {
108
107
  updatePromise = performUpdate(force);
109
108
  try {
110
109
  if (force || !cachedSDL) {
111
- return await updatePromise;
112
- }
113
- else {
114
- updatePromise.catch(err => console.error("[SCHEMA] Background update failed:", err));
115
- return cachedSDL;
110
+ await updatePromise; // Wait for update to complete
111
+ return cachedSchemaObject;
116
112
  }
113
+ return cachedSchemaObject;
117
114
  }
118
115
  finally {
119
- // Promise reference is cleared inside performUpdate's 'finally' block
116
+ updatePromise = null;
120
117
  }
121
118
  }
122
119
  /**
@@ -175,11 +172,12 @@ async function performUpdate(force) {
175
172
  // Maintain state for "Gap Analysis"
176
173
  lastKnownTypeCount = businessTypes.length;
177
174
  const currentSDL = printSchema(tempSchema);
178
- const duration = ((Date.now() - startTime) / 1000).toFixed(2);
179
175
  // --- CACHE & NOTIFICATION ---
176
+ // Always update the live object if we successfully built it
177
+ cachedSchemaObject = tempSchema;
180
178
  if (currentSDL !== cachedSDL) {
181
179
  cachedSDL = currentSDL;
182
- cachedSchemaObject = tempSchema; // Store the live Schema object for tool execution
180
+ const duration = ((Date.now() - startTime) / 1000).toFixed(2);
183
181
  return [
184
182
  `✨ SCHEMA EVOLVED (${duration}s)`,
185
183
  `📊 Source: ${env.SCHEMA ? 'Local/Remote SDL' : 'Live Endpoint'}`,
@@ -189,6 +187,7 @@ async function performUpdate(force) {
189
187
  ].join('\n');
190
188
  }
191
189
  else {
190
+ // Even if SDL string is the same, we've ensured cachedSchemaObject is set above
192
191
  return `✅ Status: Schema stable (${lastKnownTypeCount} labels).`;
193
192
  }
194
193
  }
@@ -269,62 +268,89 @@ toolHandlers.set("query-graphql", queryGraphqlHandler);
269
268
  variables: zod_1.default.string().optional(),
270
269
  headers: zod_1.default.string().optional(),
271
270
  }, queryGraphqlHandler);
272
- // Tool: introspect-schema
271
+ /**
272
+ * Tool: introspect-schema
273
+ * Main handler for the introspection tool.
274
+ * Implements "Agent Recovery" logic to guide the LLM when entities are missing.
275
+ */
273
276
  const introspectHandler = async ({ typeNames }) => {
274
- // 1. Always pull the latest schema state
275
- // The report from performUpdate is captured to show evolution details
276
- const evolutionSummary = await getSchema(true);
277
- const schema = cachedSchemaObject;
277
+ // 1. Fetch the schema directly from the source
278
+ const result = await getSchema(true);
279
+ // Explicitly check if the result is a valid GraphQLSchema object
280
+ if (!result || typeof result === 'string') {
281
+ return {
282
+ content: [{
283
+ type: "text",
284
+ text: `❌ SCHEMA_ERROR: ${typeof result === 'string' ? result : 'GraphQL schema is not initialized yet.'}\n` +
285
+ `ACTION: Please wait 5-10 seconds for the backend endpoint to respond.`
286
+ }]
287
+ };
288
+ }
289
+ // --- 1. INITIALIZE MAPPINGS ---
290
+ const schema = result;
278
291
  const typeMap = schema.getTypeMap();
279
- // 2. Generate a structural fingerprint
280
- const typeKeys = Object.keys(typeMap).filter(t => !t.startsWith('__'));
281
- const schemaVersion = `v${typeKeys.length}.${Math.floor(Date.now() / 10000) % 1000}`;
282
- // --- GAP ANALYSIS: Check if requested types are actually in the map ---
292
+ // Cache Root Type fields for rapid gap analysis
293
+ const queryType = schema.getQueryType();
294
+ const queryFields = queryType ? queryType.getFields() : {};
295
+ const mutationType = schema.getMutationType();
296
+ const mutationFields = mutationType ? mutationType.getFields() : {};
297
+ // --- 2. GAP ANALYSIS & SELF-HEALING LOOP ---
298
+ // If specific types were requested, verify their existence in the current schema
283
299
  if (typeNames && typeNames.length > 0) {
284
- const missing = typeNames.filter(name => !typeMap[name]);
300
+ const missing = typeNames.filter(name => {
301
+ const existsAsType = !!typeMap[name];
302
+ const existsAsMutation = !!mutationFields[name];
303
+ const existsAsQueryField = !!queryFields[name];
304
+ return !existsAsType && !existsAsMutation && !existsAsQueryField;
305
+ });
306
+ // If some requested entities are missing, provide the agent with a recovery map
285
307
  if (missing.length > 0) {
308
+ // Filter out internal GraphQL types to reduce noise for the agent
309
+ const internalTypes = ['Query', 'Mutation', 'Subscription'];
310
+ const availableEntities = Object.keys(typeMap).filter(t => !t.startsWith('__') && !internalTypes.includes(t));
311
+ // Generate a pseudo-version ID based on schema state and time
312
+ const schemaVersion = `v${availableEntities.length}.${Math.floor(Date.now() / 10000) % 1000}`;
286
313
  return {
287
314
  content: [{
288
315
  type: "text",
289
- text: `❌ PARTIAL RESULTS [ID: ${schemaVersion}]\n\n` +
290
- `MISSING TYPES: ${missing.join(", ")}\n` +
291
- `REASON: The database has been updated, but the GraphQL Schema is still regenerating these specific types.\n` +
292
- `ACTION: Please wait 3 seconds and retry 'introspect-schema' to see the full graph.\n\n` +
293
- `CURRENTLY AVAILABLE: ${typeKeys.filter(t => !['Query', 'Mutation', 'Report'].includes(t)).join(", ")}`
316
+ text: `❌ PARTIAL RESULTS [Schema ID: ${schemaVersion}]\n\n` +
317
+ `MISSING ENTITIES: ${missing.join(", ")}\n` +
318
+ `REASON: These specific types or fields were not found in the current schema.\n` +
319
+ `ACTION: Re-examine the available entities below and correct your query intent.\n\n` +
320
+ `AVAILABLE_ENTITIES: ${availableEntities.join(", ")}`
294
321
  }]
295
322
  };
296
323
  }
297
324
  }
325
+ // --- 3. GENERAL MANIFEST GENERATION ---
326
+ // If no typeNames provided, return a high-level overview of the entry points
298
327
  if (!typeNames || typeNames.length === 0) {
299
- const queryType = schema.getQueryType();
300
328
  const discoveredEntities = new Set();
301
329
  if (queryType) {
302
- const queryFields = queryType.getFields();
303
- Object.values(queryFields).forEach((field) => {
330
+ // Map Query fields to their underlying Object Types
331
+ const fields = queryType.getFields();
332
+ Object.values(fields).forEach((field) => {
304
333
  const namedType = (0, graphql_1.getNamedType)(field.type);
305
334
  if ((0, graphql_1.isObjectType)(namedType) && !namedType.name.startsWith('__')) {
306
335
  discoveredEntities.add(namedType.name);
307
336
  }
308
337
  });
309
338
  }
310
- const coreEntities = Array.from(discoveredEntities).sort();
311
- const communityHeader = [
312
- `🌐 Community Channel: #mcp-graphql-enhanced`,
313
- `🔗 Join the discussion: https://discord.com/channels/625400653321076807/1480510460339159184`,
314
- `---`
315
- ].join('\n');
339
+ const entryPoints = Array.from(discoveredEntities).sort();
340
+ const allTypes = Object.keys(typeMap).filter(t => !t.startsWith('__'));
341
+ const schemaVersion = `v${allTypes.length}.${Math.floor(Date.now() / 10000) % 1000}`;
316
342
  return {
317
343
  content: [{
318
344
  type: "text",
319
- text: `${communityHeader}\n${evolutionSummary}\n\n` + // Include the report from performUpdate
320
- `GraphQL Schema Manifest [ID: ${schemaVersion}]\n\n` +
321
- `ENTRY_POINT_ENTITIES: ${coreEntities.join(", ") || "None"}\n` +
322
- `TOTAL_SCHEMA_TYPES: ${typeKeys.length}\n\n` +
323
- `ALL_AVAILABLE_TYPES: ${typeKeys.join(", ")}`
345
+ text: `GraphQL Schema Manifest [ID: ${schemaVersion}]\n\n` +
346
+ `ENTRY_POINT_ENTITIES: ${entryPoints.join(", ") || "None"}\n` +
347
+ `TOTAL_SCHEMA_TYPES: ${allTypes.length}\n\n` +
348
+ `ALL_AVAILABLE_TYPES: ${allTypes.join(", ")}`
324
349
  }]
325
350
  };
326
351
  }
327
- // 3. Detailed introspection for specific types
352
+ // --- 4. DETAILED INTROSPECTION ---
353
+ // Return filtered schema metadata for the requested types or root fields
328
354
  const filtered = (0, introspection_js_1.introspectSpecificTypes)(schema, typeNames);
329
355
  return {
330
356
  content: [{ type: "text", text: JSON.stringify(filtered, null, 2) }]
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.7.0"
53
+ "version": "3.8.1"
54
54
  }