@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 +3 -18
- 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 +65 -39
- package/package.json +1 -1
package/LICENSE
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
MIT License
|
|
2
2
|
|
|
3
|
-
Copyright (c) 2024 Boris Besemer
|
|
4
|
-
Copyright (c) 2025-2026
|
|
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
|
|
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
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
-
//
|
|
280
|
-
const
|
|
281
|
-
const
|
|
282
|
-
|
|
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 =>
|
|
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
|
|
291
|
-
`REASON:
|
|
292
|
-
`ACTION:
|
|
293
|
-
`
|
|
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
|
-
|
|
303
|
-
|
|
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
|
|
311
|
-
const
|
|
312
|
-
|
|
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:
|
|
320
|
-
`
|
|
321
|
-
`
|
|
322
|
-
`
|
|
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
|
-
//
|
|
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