@letoribo/mcp-graphql-enhanced 3.2.1 → 3.4.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 +31 -4
- package/dist/helpers/graphiql.d.ts +7 -0
- package/dist/helpers/graphiql.d.ts.map +1 -0
- package/dist/helpers/graphiql.js +76 -0
- package/dist/index.js +194 -20
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,14 +4,33 @@ An **enhanced MCP (Model Context Protocol) server for GraphQL** that fixes real-
|
|
|
4
4
|
> Drop-in replacement for `mcp-graphql` — with dynamic headers, robust variables parsing, and zero breaking changes.
|
|
5
5
|
|
|
6
6
|
## ✨ Key Enhancements
|
|
7
|
+
* ✅ **Built-in GraphiQL IDE** — Visual playground at http://localhost:MCP_PORT/ (or /graphiql) with pre-configured headers.
|
|
7
8
|
* ✅ **Dual Transport** — Supports both **STDIO** (for local CLI/client tools) and **HTTP/JSON-RPC** (for external/browser clients).
|
|
8
9
|
* ✅ **Dynamic headers** — pass `Authorization`, `X-API-Key`, etc., via tool arguments (no config restarts)
|
|
9
10
|
* ✅ **Robust variables parsing** — fixes `“Query variables must be a null or an object”` error
|
|
10
11
|
* ✅ **Filtered introspection** — request only specific types (e.g., `typeNames: ["Query", "User"]`) to reduce LLM context noise
|
|
11
12
|
* ✅ **Full MCP compatibility** — works with **Claude Desktop**, **Cursor**, **Glama**
|
|
12
13
|
* ✅ **Secure by default** — mutations disabled unless explicitly enabled
|
|
14
|
+
* ✅ **Dynamic Schema Evolution** — Smart diagnostics and gap analysis for servers that regenerate GraphQL types on-the-fly (like Neo4j).
|
|
15
|
+
* ✅ **Deep Observability** — Automatic Cypher extraction and cleaning from GraphQL extensions.
|
|
16
|
+
|
|
17
|
+
## 🔍 Advanced Observability & Cypher
|
|
18
|
+
The bridge provides deep insights into how the LLM interacts with your graph database.
|
|
19
|
+
|
|
20
|
+
### 🕸️ Automated Cypher Extraction
|
|
21
|
+
For GraphQL server implementations that return query execution plans (like `@neo4j/graphql`), the bridge automatically:
|
|
22
|
+
1. **Detects** `extensions.cypher` in the response.
|
|
23
|
+
2. **Sanitizes** the output by stripping internal headers (like `CYPHER 5` or empty `PARAMS`).
|
|
24
|
+
3. **Injects** a clean Cypher block directly into the tool's output for the AI to analyze.
|
|
25
|
+
|
|
26
|
+
> **Note:** This feature requires your GraphQL server to be configured to include debug information in the response extensions.
|
|
13
27
|
|
|
14
28
|
---
|
|
29
|
+
## 🎨 Visual Command Center (GraphiQL)
|
|
30
|
+
Unlike standard MCP servers, this one provides a visual interface for humans. When running with `ENABLE_HTTP=true`, you can open a full-featured **GraphiQL IDE** in your browser.
|
|
31
|
+
|
|
32
|
+
* **Endpoint:** `http://localhost:6274/` (or `/graphiql`)
|
|
33
|
+
* **Header Sync:** Any headers set in your environment (like GitHub tokens) are automatically injected into the GraphiQL "Headers" tab for immediate testing.
|
|
15
34
|
|
|
16
35
|
## 💻 HTTP / Dual Transport
|
|
17
36
|
|
|
@@ -21,6 +40,7 @@ This allows external systems, web applications, and direct `curl` commands to ac
|
|
|
21
40
|
|
|
22
41
|
| **Endpoint** | **Method** | **Description** |
|
|
23
42
|
| :--- | :--- | :--- |
|
|
43
|
+
| `/graphiql` | `GET` | Human Interface: The visual GraphQL IDE. |
|
|
24
44
|
| `/mcp` | `POST` | The main JSON-RPC 2.0 endpoint for tool execution. |
|
|
25
45
|
| `/health` | `GET` | Simple health check, returns `{ status: 'ok' }`. |
|
|
26
46
|
|
|
@@ -46,21 +66,21 @@ curl http://localhost:6274/health
|
|
|
46
66
|
# Example: Test the query tool via JSON-RPC (using port 6275 if 6274 was busy)
|
|
47
67
|
curl -X POST http://localhost:6275/mcp -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","method":"query-graphql","params":{"query":"query { __typename }"},"id":1}'
|
|
48
68
|
|
|
49
|
-
## 🔍 Filtered Introspection
|
|
69
|
+
## 🔍 Filtered Introspection
|
|
50
70
|
Avoid 50k-line schema dumps. Ask for only what you need:
|
|
51
|
-
|
|
71
|
+
`@introspect-schema typeNames ["Query", "User"]`
|
|
52
72
|
## 🔍 Debug & Inspect
|
|
53
73
|
Use the official MCP Inspector to test your server live:
|
|
54
74
|
```bash
|
|
55
75
|
npx @modelcontextprotocol/inspector \
|
|
56
76
|
-e ENDPOINT=https://api.example.com/graphql \
|
|
57
|
-
npx @letoribo/mcp-graphql-enhanced
|
|
77
|
+
npx @letoribo/mcp-graphql-enhanced
|
|
58
78
|
```
|
|
59
79
|
### Environment Variables (Breaking change in 1.0.0)
|
|
60
80
|
> **Note:** As of version 1.0.0, command line arguments have been replaced with environment variables.
|
|
61
81
|
|
|
62
82
|
| Environment Variable | Description | Default |
|
|
63
|
-
|
|
83
|
+
| :--- | :--- | :--- |
|
|
64
84
|
| `ENDPOINT` | GraphQL endpoint URL | `https://mcp-neo4j-discord.vercel.app/api/graphiql` |
|
|
65
85
|
| `HEADERS` | JSON string containing headers for requests | `{}` |
|
|
66
86
|
| `ALLOW_MUTATIONS` | Enable mutation operations (disabled by default) | `false` |
|
|
@@ -93,6 +113,13 @@ npx @letoribo/mcp-graphql-enhanced
|
|
|
93
113
|
MCP_PORT=8080 npx @letoribo/mcp-graphql-enhanced
|
|
94
114
|
# Disable HTTP transport (fastest, recommended for Claude Desktop)
|
|
95
115
|
ENABLE_HTTP=false npx @letoribo/mcp-graphql-enhanced
|
|
116
|
+
# Test the surgical precision and the IDE immediately:
|
|
117
|
+
ENDPOINT=https://api.github.com/graphql \
|
|
118
|
+
HEADERS='{"Authorization":"Bearer YOUR_GITHUB_TOKEN"}' \
|
|
119
|
+
ENABLE_HTTP=true \
|
|
120
|
+
npx @letoribo/mcp-graphql-enhanced
|
|
121
|
+
|
|
122
|
+
# Then visit http://localhost:6274/graphiql
|
|
96
123
|
```
|
|
97
124
|
### 🖥️ Claude Desktop Configuration Examples
|
|
98
125
|
You can connect Claude Desktop to your GraphQL API using either the npx package (recommended for simplicity) or the Docker image (ideal for reproducibility and isolation).
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Renders the GraphiQL IDE template with visible headers
|
|
3
|
+
* @param endpoint The GraphQL API endpoint to connect to
|
|
4
|
+
* @param headers Optional default headers to inject into the editor
|
|
5
|
+
*/
|
|
6
|
+
export declare function renderGraphiQL(endpoint: string, headers?: object): string;
|
|
7
|
+
//# sourceMappingURL=graphiql.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"graphiql.d.ts","sourceRoot":"","sources":["../../src/helpers/graphiql.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,GAAE,MAAW,GAAG,MAAM,CAoE7E"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.renderGraphiQL = renderGraphiQL;
|
|
4
|
+
/**
|
|
5
|
+
* Renders the GraphiQL IDE template with visible headers
|
|
6
|
+
* @param endpoint The GraphQL API endpoint to connect to
|
|
7
|
+
* @param headers Optional default headers to inject into the editor
|
|
8
|
+
*/
|
|
9
|
+
function renderGraphiQL(endpoint, headers = {}) {
|
|
10
|
+
// Stringify headers for the injection into the script
|
|
11
|
+
const stringifiedHeaders = JSON.stringify(headers, null, 2);
|
|
12
|
+
return `
|
|
13
|
+
<!DOCTYPE html>
|
|
14
|
+
<html lang="en">
|
|
15
|
+
<head>
|
|
16
|
+
<meta charset="utf-8" />
|
|
17
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
18
|
+
<title>GraphiQL Explorer | Surgical MCP</title>
|
|
19
|
+
<link href="https://unpkg.com/graphiql@3.8.2/graphiql.min.css" rel="stylesheet" />
|
|
20
|
+
<style>
|
|
21
|
+
body {
|
|
22
|
+
margin: 0;
|
|
23
|
+
height: 100vh;
|
|
24
|
+
width: 100vw;
|
|
25
|
+
overflow: hidden;
|
|
26
|
+
background: #0b1016;
|
|
27
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
28
|
+
}
|
|
29
|
+
#graphiql { height: 100vh; }
|
|
30
|
+
.loading-screen {
|
|
31
|
+
color: white;
|
|
32
|
+
display: flex;
|
|
33
|
+
justify-content: center;
|
|
34
|
+
align-items: center;
|
|
35
|
+
height: 100%;
|
|
36
|
+
font-size: 1.2rem;
|
|
37
|
+
}
|
|
38
|
+
</style>
|
|
39
|
+
</head>
|
|
40
|
+
<body>
|
|
41
|
+
<div id="graphiql">
|
|
42
|
+
<div class="loading-screen">Initializing Surgical GraphiQL...</div>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<script src="https://unpkg.com/react@18.2.0/umd/react.production.min.js" crossorigin referrerpolicy="no-referrer"></script>
|
|
46
|
+
<script src="https://unpkg.com/react-dom@18.2.0/umd/react-dom.production.min.js" crossorigin referrerpolicy="no-referrer"></script>
|
|
47
|
+
<script src="https://unpkg.com/graphiql@3.8.2/graphiql.min.js" crossorigin referrerpolicy="no-referrer"></script>
|
|
48
|
+
|
|
49
|
+
<script>
|
|
50
|
+
window.addEventListener('load', function() {
|
|
51
|
+
if (typeof GraphiQL !== 'undefined') {
|
|
52
|
+
const fetcher = GraphiQL.createFetcher({
|
|
53
|
+
url: '${endpoint}'
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const root = ReactDOM.createRoot(document.getElementById('graphiql'));
|
|
57
|
+
root.render(
|
|
58
|
+
React.createElement(GraphiQL, {
|
|
59
|
+
fetcher: fetcher,
|
|
60
|
+
headerEditorEnabled: true,
|
|
61
|
+
shouldPersistHeaders: true,
|
|
62
|
+
// This makes the headers visible in the UI tab
|
|
63
|
+
defaultHeaders: \`${stringifiedHeaders}\`,
|
|
64
|
+
theme: 'dark',
|
|
65
|
+
defaultEditorToolsVisibility: true
|
|
66
|
+
})
|
|
67
|
+
);
|
|
68
|
+
} else {
|
|
69
|
+
document.getElementById('graphiql').innerHTML =
|
|
70
|
+
'<div class="loading-screen" style="color: #ff4d4d">Error: GraphiQL SDK failed to load.</div>';
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
</script>
|
|
74
|
+
</body>
|
|
75
|
+
</html>`;
|
|
76
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,8 @@ const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
|
9
9
|
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
|
+
const graphiql_js_1 = require("./helpers/graphiql.js");
|
|
13
|
+
const graphql_1 = require("graphql");
|
|
12
14
|
// Helper imports
|
|
13
15
|
const deprecation_js_1 = require("./helpers/deprecation.js");
|
|
14
16
|
const introspection_js_1 = require("./helpers/introspection.js");
|
|
@@ -61,48 +63,147 @@ const server = new mcp_js_1.McpServer({
|
|
|
61
63
|
let cachedSDL = null;
|
|
62
64
|
let cachedSchemaObject = null;
|
|
63
65
|
let schemaLoadError = null;
|
|
64
|
-
|
|
65
|
-
|
|
66
|
+
let isUpdating = false;
|
|
67
|
+
let updatePromise = null;
|
|
68
|
+
let lastKnownTypeCount = 0;
|
|
69
|
+
let expectEmptySchema = false; // Intent flag for intentional purges
|
|
70
|
+
/**
|
|
71
|
+
* Smart Hybrid Schema Fetcher (Zero-Error Version)
|
|
72
|
+
* @param force If true, blocks and waits for the new schema evolution.
|
|
73
|
+
* If false, returns cache immediately and updates in background.
|
|
74
|
+
*/
|
|
75
|
+
async function getSchema(force = false, requestedTypes) {
|
|
76
|
+
// 1. Hook into existing update if in progress
|
|
77
|
+
if (isUpdating && updatePromise) {
|
|
78
|
+
if (force || !cachedSDL)
|
|
79
|
+
return await updatePromise;
|
|
66
80
|
return cachedSDL;
|
|
81
|
+
}
|
|
82
|
+
// 2. Return cache if valid and not forcing
|
|
83
|
+
if (cachedSDL && !force) {
|
|
84
|
+
// Validation check: If user wants specific types but they aren't in the cache
|
|
85
|
+
if (requestedTypes && cachedSchemaObject) {
|
|
86
|
+
const typeMap = cachedSchemaObject.getTypeMap();
|
|
87
|
+
const missing = requestedTypes.filter(t => !typeMap[t]);
|
|
88
|
+
if (missing.length > 0) {
|
|
89
|
+
// Force a refresh if requested types are missing from current cache
|
|
90
|
+
return await (updatePromise = performUpdate(true));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return cachedSDL;
|
|
94
|
+
}
|
|
95
|
+
if (force)
|
|
96
|
+
schemaLoadError = null;
|
|
67
97
|
if (schemaLoadError)
|
|
68
98
|
throw schemaLoadError;
|
|
99
|
+
// 3. Trigger update
|
|
100
|
+
updatePromise = performUpdate(force);
|
|
101
|
+
try {
|
|
102
|
+
if (force || !cachedSDL) {
|
|
103
|
+
return await updatePromise;
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
updatePromise.catch(err => console.error("[SCHEMA] Background update failed:", err));
|
|
107
|
+
return cachedSDL;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
// Promise reference is cleared inside performUpdate's 'finally' block
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Internal logic for schema introspection and building.
|
|
116
|
+
* This version uses universal business-type tracking and provides
|
|
117
|
+
* detailed diagnostic reports instead of generic error messages.
|
|
118
|
+
*/
|
|
119
|
+
async function performUpdate(force) {
|
|
120
|
+
isUpdating = true;
|
|
121
|
+
const startTime = Date.now();
|
|
69
122
|
try {
|
|
70
|
-
const { buildClientSchema, getIntrospectionQuery, printSchema, buildASTSchema, parse: gqlParse } = require("graphql");
|
|
71
|
-
let
|
|
123
|
+
const { buildClientSchema, getIntrospectionQuery, printSchema, buildASTSchema, parse: gqlParse, isObjectType } = require("graphql");
|
|
124
|
+
let tempSchema;
|
|
125
|
+
// --- FETCHING LOGIC (Unified Source) ---
|
|
72
126
|
if (env.SCHEMA) {
|
|
73
|
-
|
|
127
|
+
let sdl;
|
|
74
128
|
if (env.SCHEMA.startsWith("http")) {
|
|
129
|
+
// Remote SDL File: Fetch via HTTP
|
|
75
130
|
const response = await fetch(env.SCHEMA);
|
|
131
|
+
if (!response.ok)
|
|
132
|
+
throw new Error(`Remote_SDL_Fetch_Failed: ${response.statusText}`);
|
|
76
133
|
sdl = await response.text();
|
|
77
134
|
}
|
|
78
135
|
else {
|
|
136
|
+
// Local SDL File: Use your custom helper (readFile inside)
|
|
79
137
|
sdl = await (0, introspection_js_1.introspectLocalSchema)(env.SCHEMA);
|
|
80
138
|
}
|
|
81
|
-
|
|
82
|
-
|
|
139
|
+
// Direct path: Convert raw SDL string to GraphQLSchema object
|
|
140
|
+
tempSchema = buildASTSchema(gqlParse(sdl));
|
|
83
141
|
}
|
|
84
142
|
else {
|
|
143
|
+
// Standard Path: Execute Introspection Query against live ENDPOINT
|
|
85
144
|
const response = await fetch(env.ENDPOINT, {
|
|
86
145
|
method: "POST",
|
|
87
146
|
headers: { "Content-Type": "application/json", ...env.HEADERS },
|
|
88
147
|
body: JSON.stringify({ query: getIntrospectionQuery() }),
|
|
89
148
|
});
|
|
90
149
|
if (!response.ok)
|
|
91
|
-
throw new Error(`
|
|
150
|
+
throw new Error(`HTTP_${response.status}: ${response.statusText}`);
|
|
92
151
|
const result = await response.json();
|
|
93
|
-
|
|
94
|
-
|
|
152
|
+
if (!result.data)
|
|
153
|
+
throw new Error("Invalid GraphQL response: Missing 'data' field.");
|
|
154
|
+
// Build Schema object from introspection JSON
|
|
155
|
+
tempSchema = buildClientSchema(result.data);
|
|
156
|
+
}
|
|
157
|
+
// --- UNIFIED STRUCTURAL ANALYSIS (For AI Report) ---
|
|
158
|
+
const typeMap = tempSchema.getTypeMap();
|
|
159
|
+
// Filter "Business Labels" (Nodes) while ignoring internal scalars/system types
|
|
160
|
+
const businessTypes = Object.keys(typeMap).filter(typeName => {
|
|
161
|
+
const type = typeMap[typeName];
|
|
162
|
+
return (!typeName.startsWith('__') &&
|
|
163
|
+
!['Query', 'Mutation', 'Subscription'].includes(typeName) &&
|
|
164
|
+
!['String', 'Int', 'Float', 'Boolean', 'ID', 'BigInt', 'DateTime'].includes(typeName) &&
|
|
165
|
+
isObjectType(type));
|
|
166
|
+
});
|
|
167
|
+
// Maintain state for "Gap Analysis"
|
|
168
|
+
lastKnownTypeCount = businessTypes.length;
|
|
169
|
+
const currentSDL = printSchema(tempSchema);
|
|
170
|
+
const duration = ((Date.now() - startTime) / 1000).toFixed(2);
|
|
171
|
+
// --- CACHE & NOTIFICATION ---
|
|
172
|
+
if (currentSDL !== cachedSDL) {
|
|
173
|
+
cachedSDL = currentSDL;
|
|
174
|
+
cachedSchemaObject = tempSchema; // Store the live Schema object for tool execution
|
|
175
|
+
return [
|
|
176
|
+
`✨ SCHEMA EVOLVED (${duration}s)`,
|
|
177
|
+
`📊 Source: ${env.SCHEMA ? 'Local/Remote SDL' : 'Live Endpoint'}`,
|
|
178
|
+
`🧬 Labels: ${businessTypes.join(', ') || 'None'}`,
|
|
179
|
+
`---`,
|
|
180
|
+
`The bridge has updated the graph model. New types are now queryable.`
|
|
181
|
+
].join('\n');
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
return `✅ Status: Schema stable (${lastKnownTypeCount} labels).`;
|
|
95
185
|
}
|
|
96
|
-
console.error(`[SCHEMA] Successfully loaded and cached GraphQL schema`);
|
|
97
|
-
return cachedSDL;
|
|
98
186
|
}
|
|
99
187
|
catch (error) {
|
|
100
|
-
|
|
101
|
-
|
|
188
|
+
// Informative error report to prevent "AI confusion"
|
|
189
|
+
return [
|
|
190
|
+
`❌ SCHEMA SYNC FAILED`,
|
|
191
|
+
`🔍 Reason: ${error.message}`,
|
|
192
|
+
`🛠️ Action: Verify your ${env.SCHEMA ? 'file path' : 'endpoint connection'} and retry.`
|
|
193
|
+
].join('\n');
|
|
194
|
+
}
|
|
195
|
+
finally {
|
|
196
|
+
isUpdating = false;
|
|
197
|
+
updatePromise = null;
|
|
102
198
|
}
|
|
103
199
|
}
|
|
104
200
|
// --- TOOL HANDLERS ---
|
|
105
201
|
const toolHandlers = new Map();
|
|
202
|
+
/** * executionLogs stores the last 5 GraphQL operations.
|
|
203
|
+
* This allows the AI to "inspect" its own generated queries and the raw data
|
|
204
|
+
* for debugging or bridging to 3D visualization tools.
|
|
205
|
+
*/
|
|
206
|
+
let executionLogs = [];
|
|
106
207
|
// Tool: query-graphql
|
|
107
208
|
const queryGraphqlHandler = async ({ query, variables, headers }) => {
|
|
108
209
|
try {
|
|
@@ -120,9 +221,32 @@ const queryGraphqlHandler = async ({ query, variables, headers }) => {
|
|
|
120
221
|
headers: allHeaders,
|
|
121
222
|
body: JSON.stringify({ query, variables: parsedVariables }),
|
|
122
223
|
});
|
|
123
|
-
const data = await response.json();
|
|
224
|
+
const data = (await response.json());
|
|
225
|
+
// 1. Extract and sanitize Cypher if present in extensions
|
|
226
|
+
const rawCypher = data.extensions?.cypher || [];
|
|
227
|
+
const cleanCypher = rawCypher.map((c) => c.replace(/^CYPHER: /, '')
|
|
228
|
+
.replace(/^CYPHER 5\n/, '')
|
|
229
|
+
.replace(/\nPARAMS: \{\}$/, ''));
|
|
230
|
+
// 2. Update execution history for internal server state
|
|
231
|
+
executionLogs.push({
|
|
232
|
+
query,
|
|
233
|
+
variables: parsedVariables,
|
|
234
|
+
response: data,
|
|
235
|
+
timestamp: new Date().toISOString()
|
|
236
|
+
});
|
|
237
|
+
if (executionLogs.length > 5)
|
|
238
|
+
executionLogs.shift();
|
|
239
|
+
// 3. Prepare optimized response for Claude
|
|
240
|
+
const responseForClaude = {
|
|
241
|
+
result: data.data,
|
|
242
|
+
// Only add the cypher field if there's actual data to show
|
|
243
|
+
...(cleanCypher.length > 0 ? { cypher: cleanCypher } : {})
|
|
244
|
+
};
|
|
124
245
|
return {
|
|
125
|
-
content: [{
|
|
246
|
+
content: [{
|
|
247
|
+
type: "text",
|
|
248
|
+
text: JSON.stringify(responseForClaude, null, 2)
|
|
249
|
+
}]
|
|
126
250
|
};
|
|
127
251
|
}
|
|
128
252
|
catch (error) {
|
|
@@ -137,14 +261,56 @@ server.tool("query-graphql", "Execute a GraphQL query against the endpoint", {
|
|
|
137
261
|
}, queryGraphqlHandler);
|
|
138
262
|
// Tool: introspect-schema
|
|
139
263
|
const introspectHandler = async ({ typeNames }) => {
|
|
140
|
-
|
|
264
|
+
// 1. Always pull the latest schema state
|
|
265
|
+
// The report from performUpdate is captured to show evolution details
|
|
266
|
+
const evolutionSummary = await getSchema(true);
|
|
267
|
+
const schema = cachedSchemaObject;
|
|
268
|
+
const typeMap = schema.getTypeMap();
|
|
269
|
+
// 2. Generate a structural fingerprint
|
|
270
|
+
const typeKeys = Object.keys(typeMap).filter(t => !t.startsWith('__'));
|
|
271
|
+
const schemaVersion = `v${typeKeys.length}.${Math.floor(Date.now() / 10000) % 1000}`;
|
|
272
|
+
// --- GAP ANALYSIS: Check if requested types are actually in the map ---
|
|
273
|
+
if (typeNames && typeNames.length > 0) {
|
|
274
|
+
const missing = typeNames.filter(name => !typeMap[name]);
|
|
275
|
+
if (missing.length > 0) {
|
|
276
|
+
return {
|
|
277
|
+
content: [{
|
|
278
|
+
type: "text",
|
|
279
|
+
text: `❌ PARTIAL RESULTS [ID: ${schemaVersion}]\n\n` +
|
|
280
|
+
`MISSING TYPES: ${missing.join(", ")}\n` +
|
|
281
|
+
`REASON: The database has been updated, but the GraphQL Schema is still regenerating these specific types.\n` +
|
|
282
|
+
`ACTION: Please wait 3 seconds and retry 'introspect-schema' to see the full graph.\n\n` +
|
|
283
|
+
`CURRENTLY AVAILABLE: ${typeKeys.filter(t => !['Query', 'Mutation', 'Report'].includes(t)).join(", ")}`
|
|
284
|
+
}]
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
}
|
|
141
288
|
if (!typeNames || typeNames.length === 0) {
|
|
142
|
-
const
|
|
289
|
+
const queryType = schema.getQueryType();
|
|
290
|
+
const discoveredEntities = new Set();
|
|
291
|
+
if (queryType) {
|
|
292
|
+
const queryFields = queryType.getFields();
|
|
293
|
+
Object.values(queryFields).forEach((field) => {
|
|
294
|
+
const namedType = (0, graphql_1.getNamedType)(field.type);
|
|
295
|
+
if ((0, graphql_1.isObjectType)(namedType) && !namedType.name.startsWith('__')) {
|
|
296
|
+
discoveredEntities.add(namedType.name);
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
const coreEntities = Array.from(discoveredEntities).sort();
|
|
143
301
|
return {
|
|
144
|
-
content: [{
|
|
302
|
+
content: [{
|
|
303
|
+
type: "text",
|
|
304
|
+
text: `${evolutionSummary}\n\n` + // Include the report from performUpdate
|
|
305
|
+
`GraphQL Schema Manifest [ID: ${schemaVersion}]\n\n` +
|
|
306
|
+
`ENTRY_POINT_ENTITIES: ${coreEntities.join(", ") || "None"}\n` +
|
|
307
|
+
`TOTAL_SCHEMA_TYPES: ${typeKeys.length}\n\n` +
|
|
308
|
+
`ALL_AVAILABLE_TYPES: ${typeKeys.join(", ")}`
|
|
309
|
+
}]
|
|
145
310
|
};
|
|
146
311
|
}
|
|
147
|
-
|
|
312
|
+
// 3. Detailed introspection for specific types
|
|
313
|
+
const filtered = (0, introspection_js_1.introspectSpecificTypes)(schema, typeNames);
|
|
148
314
|
return {
|
|
149
315
|
content: [{ type: "text", text: JSON.stringify(filtered, null, 2) }]
|
|
150
316
|
};
|
|
@@ -164,6 +330,12 @@ async function handleHttpRequest(req, res) {
|
|
|
164
330
|
return;
|
|
165
331
|
}
|
|
166
332
|
const url = new URL(req.url || '', `http://${req.headers.host}`);
|
|
333
|
+
// NEW FEATURE: Web GUI for Humans
|
|
334
|
+
if (req.method === 'GET' && (url.pathname === '/' || url.pathname === '/graphiql')) {
|
|
335
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
336
|
+
// Pass env.HEADERS to the explorer
|
|
337
|
+
return res.end((0, graphiql_js_1.renderGraphiQL)(env.ENDPOINT, env.HEADERS));
|
|
338
|
+
}
|
|
167
339
|
if (url.pathname === '/mcp' && req.method === 'POST') {
|
|
168
340
|
let body = '';
|
|
169
341
|
req.on('data', chunk => { body += chunk; });
|
|
@@ -201,6 +373,8 @@ async function main() {
|
|
|
201
373
|
const serverHttp = node_http_1.default.createServer(handleHttpRequest);
|
|
202
374
|
serverHttp.listen(env.MCP_PORT, () => {
|
|
203
375
|
console.error(`[HTTP] Server started on http://localhost:${env.MCP_PORT}`);
|
|
376
|
+
console.error(`🎨 GraphiQL IDE: http://localhost:${env.MCP_PORT}/graphiql`);
|
|
377
|
+
console.error(`🤖 MCP Endpoint: http://localhost:${env.MCP_PORT}/mcp\n`);
|
|
204
378
|
});
|
|
205
379
|
}
|
|
206
380
|
const transport = new stdio_js_1.StdioServerTransport();
|
package/package.json
CHANGED