@letoribo/mcp-graphql-enhanced 2.2.3 → 2.3.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/README.md +12 -4
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +312 -324
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -24,17 +24,25 @@ This allows external systems, web applications, and direct `curl` commands to ac
|
|
|
24
24
|
| `/mcp` | `POST` | The main JSON-RPC endpoint for tool execution. |
|
|
25
25
|
| `/health` | `GET` | Simple health check, returns `{ status: 'ok' }`. |
|
|
26
26
|
|
|
27
|
+
### Resolving Port Conflicts (EADDRINUSE) and Automatic Port Selection
|
|
28
|
+
|
|
29
|
+
The server defaults to port `6274`. If you encounter an `EADDRINUSE: address already in use :::6274` error (common in local development due to stale processes), the server will automatically **increment the port and retry** (e.g., bind to `6275`, then `6276`, etc., up to 5 times).
|
|
30
|
+
|
|
31
|
+
This ensures the server starts successfully even when the default is blocked. **Always check the server logs for the final bound port** (e.g., `[HTTP] Started server on http://localhost:6275`) if your `curl` or client tool fails on the default `6274`.
|
|
32
|
+
|
|
33
|
+
To **force a specific port** (e.g., for guaranteed external firewall settings), you can still explicitly set the `MCP_PORT` environment variable:
|
|
34
|
+
|
|
27
35
|
### Testing the HTTP Endpoint
|
|
28
36
|
|
|
29
37
|
You can test the endpoint using `curl` as long as the server is running (e.g., via `npm run dev`):
|
|
30
38
|
|
|
31
39
|
```bash
|
|
32
|
-
# Test the health check
|
|
40
|
+
# Test the health check (assuming the server bound to the default or found the next available port)
|
|
33
41
|
curl http://localhost:6274/health
|
|
34
42
|
|
|
35
|
-
# Test the query tool via JSON-RPC
|
|
36
|
-
curl -X POST http://localhost:
|
|
37
|
-
|
|
43
|
+
# Example: Test the query tool via JSON-RPC (using port 6275 if 6274 was busy)
|
|
44
|
+
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}'
|
|
45
|
+
```
|
|
38
46
|
## 🔍 Filtered Introspection (New!)
|
|
39
47
|
Avoid 50k-line schema dumps. Ask for only what you need:
|
|
40
48
|
```@introspect-schema typeNames ["Query", "User"]```
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
export
|
|
1
|
+
declare const app: import("express-serve-static-core").Express;
|
|
2
|
+
export default app;
|
|
3
3
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AA2UA,QAAA,MAAM,GAAG,6CAAY,CAAC;AA0CtB,eAAe,GAAG,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,361 +1,349 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
3
35
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
37
|
};
|
|
6
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
.string()
|
|
28
|
-
.default("{}")
|
|
29
|
-
.transform((val) => {
|
|
30
|
-
try {
|
|
31
|
-
return JSON.parse(val);
|
|
32
|
-
}
|
|
33
|
-
catch (e) {
|
|
34
|
-
throw new Error("HEADERS must be a valid JSON string");
|
|
35
|
-
}
|
|
36
|
-
}),
|
|
37
|
-
SCHEMA: z.string().optional(),
|
|
38
|
-
MCP_PORT: z.preprocess((val) => (val ? parseInt(val) : 6274), z.number().int().min(1024).max(65535)).default(6274),
|
|
39
|
-
});
|
|
40
|
-
const env = EnvSchema.parse(process.env);
|
|
41
|
-
const server = new McpServer({
|
|
42
|
-
name: env.NAME,
|
|
43
|
-
version: getVersion(),
|
|
44
|
-
description: `GraphQL MCP server for ${env.ENDPOINT}`,
|
|
45
|
-
});
|
|
46
|
-
server.resource("graphql-schema", new URL(env.ENDPOINT).href, async (uri) => {
|
|
47
|
-
try {
|
|
48
|
-
let schema;
|
|
49
|
-
if (env.SCHEMA) {
|
|
50
|
-
if (env.SCHEMA.startsWith("http://") ||
|
|
51
|
-
env.SCHEMA.startsWith("https://")) {
|
|
52
|
-
schema = await introspectSchemaFromUrl(env.SCHEMA);
|
|
53
|
-
}
|
|
54
|
-
else {
|
|
55
|
-
schema = await introspectLocalSchema(env.SCHEMA);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
else {
|
|
59
|
-
schema = await introspectEndpoint(env.ENDPOINT, env.HEADERS);
|
|
60
|
-
}
|
|
61
|
-
return {
|
|
62
|
-
contents: [
|
|
63
|
-
{
|
|
64
|
-
uri: uri.href,
|
|
65
|
-
text: schema,
|
|
66
|
-
},
|
|
67
|
-
],
|
|
68
|
-
};
|
|
39
|
+
const express_1 = __importDefault(require("express"));
|
|
40
|
+
// CRITICAL FIX: Explicitly import the CJS file path and use HttpLink for Vercel/NodeNext environments.
|
|
41
|
+
// NOTE: We import it as 'any' to temporarily bypass the missing type declaration warning
|
|
42
|
+
// for '@apollo/client/core/core.cjs', but you must install the @types/express package.
|
|
43
|
+
const apolloModule = __importStar(require("@apollo/client/core/core.cjs"));
|
|
44
|
+
const graphql_1 = require("graphql");
|
|
45
|
+
// -----------------------------------------------------------------------------------
|
|
46
|
+
// 1. GraphQL Endpoint Configuration
|
|
47
|
+
const MCP_GRAPHQL_ENDPOINT = process.env.ENDPOINT ||
|
|
48
|
+
process.env.MCP_GRAPHQL_ENDPOINT ||
|
|
49
|
+
'https://countries.trevorblades.com/';
|
|
50
|
+
// Initialize the client immediately
|
|
51
|
+
let client;
|
|
52
|
+
let coreExports; // Variable to hold the correctly resolved Apollo components
|
|
53
|
+
try {
|
|
54
|
+
// Dynamically determine the correct namespace for Apollo components.
|
|
55
|
+
// This is necessary because CJS module formats often wrap exports differently.
|
|
56
|
+
if (apolloModule.HttpLink) {
|
|
57
|
+
coreExports = apolloModule;
|
|
58
|
+
console.log('[Setup] Apollo exports resolved at the root level.');
|
|
69
59
|
}
|
|
70
|
-
|
|
71
|
-
|
|
60
|
+
else if (apolloModule.default && apolloModule.default.HttpLink) {
|
|
61
|
+
coreExports = apolloModule.default;
|
|
62
|
+
console.log('[Setup] Apollo exports resolved nested under .default.');
|
|
72
63
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
64
|
+
else {
|
|
65
|
+
throw new Error('Could not resolve Apollo Client components (HttpLink, ApolloClient) from imported module.');
|
|
66
|
+
}
|
|
67
|
+
// Now use coreExports for all constructors and functions
|
|
68
|
+
const httpLink = new coreExports.HttpLink({
|
|
69
|
+
uri: MCP_GRAPHQL_ENDPOINT,
|
|
70
|
+
// Pass the global fetch function
|
|
71
|
+
fetch: fetch,
|
|
72
|
+
});
|
|
73
|
+
client = new coreExports.ApolloClient({
|
|
74
|
+
link: httpLink,
|
|
75
|
+
cache: new coreExports.InMemoryCache(),
|
|
76
|
+
});
|
|
77
|
+
console.log('[Setup] Apollo Client Initialized.');
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
console.error('[Setup] CRITICAL RUNTIME ERROR: Failed to initialize Apollo Client:', error instanceof Error ? error.message : String(error));
|
|
81
|
+
}
|
|
82
|
+
// --- GraphQL Introspection Helpers (Using global fetch) ---
|
|
83
|
+
/**
|
|
84
|
+
* Executes a GraphQL introspection query against a remote endpoint.
|
|
85
|
+
*/
|
|
86
|
+
const executeIntrospection = async (endpoint, headers, query) => {
|
|
87
|
+
let response;
|
|
76
88
|
try {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
else {
|
|
88
|
-
schema = await introspectLocalSchema(env.SCHEMA);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
else {
|
|
92
|
-
schema = await introspectEndpoint(env.ENDPOINT, env.HEADERS);
|
|
93
|
-
}
|
|
94
|
-
return { content: [{ type: "text", text: schema }] };
|
|
95
|
-
}
|
|
89
|
+
// Use the global fetch function
|
|
90
|
+
response = await fetch(endpoint, {
|
|
91
|
+
method: "POST",
|
|
92
|
+
headers: {
|
|
93
|
+
"Content-Type": "application/json",
|
|
94
|
+
...headers,
|
|
95
|
+
},
|
|
96
|
+
body: JSON.stringify({ query }),
|
|
97
|
+
});
|
|
96
98
|
}
|
|
97
|
-
catch (
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
99
|
+
catch (networkError) {
|
|
100
|
+
console.error('NETWORK ERROR during introspection:', networkError);
|
|
101
|
+
throw new Error(`Failed to connect to GraphQL endpoint at ${endpoint}. Network error: ${networkError instanceof Error ? networkError.message : String(networkError)}`);
|
|
102
|
+
}
|
|
103
|
+
if (!response.ok) {
|
|
104
|
+
const responseText = await response.text();
|
|
105
|
+
throw new Error(`Introspection query failed with status ${response.status} (${response.statusText}). Response: ${responseText.substring(0, 200)}...`);
|
|
106
|
+
}
|
|
107
|
+
const json = await response.json();
|
|
108
|
+
if (json.errors) {
|
|
109
|
+
throw new Error(`GraphQL errors during introspection: ${JSON.stringify(json.errors)}`);
|
|
102
110
|
}
|
|
111
|
+
return json.data;
|
|
103
112
|
};
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
113
|
+
/**
|
|
114
|
+
* Introspects the endpoint and returns the full schema string (SDL).
|
|
115
|
+
*/
|
|
116
|
+
const introspectEndpoint = async (endpoint, headers) => {
|
|
117
|
+
const data = await executeIntrospection(endpoint, headers, (0, graphql_1.getIntrospectionQuery)());
|
|
118
|
+
const schema = (0, graphql_1.buildClientSchema)(data);
|
|
119
|
+
return (0, graphql_1.printSchema)(schema);
|
|
120
|
+
};
|
|
121
|
+
/**
|
|
122
|
+
* Introspects the endpoint and returns an annotated schema, optionally filtering root fields.
|
|
123
|
+
*/
|
|
124
|
+
const introspectTypes = async (endpoint, headers, typeNames) => {
|
|
125
|
+
const data = await executeIntrospection(endpoint, headers, (0, graphql_1.getIntrospectionQuery)());
|
|
126
|
+
const schema = (0, graphql_1.buildClientSchema)(data);
|
|
127
|
+
// Fallback to full schema if no typeNames are provided or if filtering is not feasible
|
|
128
|
+
if (!typeNames || typeNames.length === 0) {
|
|
129
|
+
return `--- Full Schema Introspection ---\nEndpoint: ${endpoint}\n\n${(0, graphql_1.printSchema)(schema)}`;
|
|
130
|
+
}
|
|
131
|
+
const requestedTypeSet = new Set(typeNames.map(name => name.trim()));
|
|
132
|
+
let filteredSchema = `--- Annotated Schema Introspection ---\nEndpoint: ${endpoint}\nRequested Types: ${typeNames.join(', ')}\n\n`;
|
|
133
|
+
// 1. Prune the Query type fields to only include those that resolve to requested types
|
|
134
|
+
const queryType = schema.getQueryType();
|
|
135
|
+
if (queryType) {
|
|
136
|
+
const fields = queryType.getFields();
|
|
137
|
+
const keptQueryFields = [];
|
|
138
|
+
// Flag: If the user requested the "Query" type itself, include all root fields for utility.
|
|
139
|
+
const includeAllQueryFields = requestedTypeSet.has(queryType.name);
|
|
140
|
+
Object.keys(fields).forEach(fieldName => {
|
|
141
|
+
const field = fields[fieldName];
|
|
142
|
+
const returnType = (0, graphql_1.getNamedType)(field.type);
|
|
143
|
+
// Keep the field if:
|
|
144
|
+
// 1. Its return type is one of the requested types, OR
|
|
145
|
+
// 2. The user explicitly requested the "Query" type itself.
|
|
146
|
+
if (includeAllQueryFields || requestedTypeSet.has(returnType.name)) {
|
|
147
|
+
// Build a representation of the field including arguments and full type string
|
|
148
|
+
const args = field.args.map(arg => `${arg.name}: ${arg.type.toString()}`).join(', ');
|
|
149
|
+
keptQueryFields.push(` ${fieldName}${args ? `(${args})` : ''}: ${field.type.toString()}`);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
// CRITICAL FIX: Ensure the Annotated Query block is output if fields were kept.
|
|
153
|
+
if (keptQueryFields.length > 0) {
|
|
154
|
+
filteredSchema += `type Query { \n${keptQueryFields.join('\n')}\n\n`;
|
|
124
155
|
}
|
|
125
156
|
}
|
|
126
|
-
|
|
157
|
+
// 2. Add the definition of every requested type (and their dependencies for robustness)
|
|
158
|
+
filteredSchema += `--- Full Type Definitions (Including Dependencies) ---\n\n`;
|
|
159
|
+
filteredSchema += (0, graphql_1.printSchema)(schema);
|
|
160
|
+
return filteredSchema;
|
|
161
|
+
};
|
|
162
|
+
/**
|
|
163
|
+
* Fetches a schema file from a remote URL.
|
|
164
|
+
*/
|
|
165
|
+
const introspectSchemaFromUrl = async (schemaUrl) => {
|
|
166
|
+
// Use the global fetch function
|
|
167
|
+
const response = await fetch(schemaUrl);
|
|
168
|
+
if (!response.ok) {
|
|
169
|
+
throw new Error(`Failed to fetch schema from ${schemaUrl}: ${response.statusText}`);
|
|
170
|
+
}
|
|
171
|
+
return response.text();
|
|
172
|
+
};
|
|
173
|
+
/**
|
|
174
|
+
* Handles local file loading (not supported on Vercel).
|
|
175
|
+
*/
|
|
176
|
+
const introspectLocalSchema = async (filePath) => {
|
|
177
|
+
throw new Error("Local schema file loading is not supported in the Vercel environment.");
|
|
178
|
+
};
|
|
179
|
+
// --- JSON-RPC Handler ---
|
|
180
|
+
/**
|
|
181
|
+
* Handles incoming JSON-RPC 2.0 requests.
|
|
182
|
+
*/
|
|
183
|
+
async function handleJsonRpc(reqBody) {
|
|
184
|
+
const { method, params, id } = reqBody;
|
|
185
|
+
// Guard clause to ensure Apollo Client was initialized successfully
|
|
186
|
+
if (!client) {
|
|
187
|
+
console.error('Apollo Client is unavailable.');
|
|
127
188
|
return {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
],
|
|
189
|
+
jsonrpc: '2.0',
|
|
190
|
+
id: id || null,
|
|
191
|
+
error: {
|
|
192
|
+
code: -32000, // Server error
|
|
193
|
+
message: 'Server failed to initialize Apollo Client.',
|
|
194
|
+
},
|
|
135
195
|
};
|
|
136
196
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
197
|
+
// --- 1. Query GraphQL ---
|
|
198
|
+
if (method === 'query-graphql' && params && params.query) {
|
|
199
|
+
try {
|
|
200
|
+
const { query } = params;
|
|
201
|
+
// Execute the GraphQL query against the configured endpoint
|
|
202
|
+
const result = await client.query({
|
|
203
|
+
// Note: Using 'gql' via coreExports
|
|
204
|
+
query: coreExports.gql(query),
|
|
205
|
+
fetchPolicy: 'no-cache', // Ensure fresh data on every call
|
|
206
|
+
});
|
|
207
|
+
// CRITICAL FIX:
|
|
208
|
+
// A successful JSON-RPC response must contain the actual data under the 'result' key.
|
|
209
|
+
// The actual GraphQL data is located in the Apollo result's 'data' property.
|
|
210
|
+
if (result.data) {
|
|
211
|
+
return {
|
|
212
|
+
jsonrpc: '2.0',
|
|
213
|
+
id: id,
|
|
214
|
+
// Map the raw GraphQL data to the JSON-RPC 'result' field
|
|
215
|
+
result: result.data,
|
|
216
|
+
};
|
|
150
217
|
}
|
|
151
|
-
|
|
152
|
-
|
|
218
|
+
// Handle GraphQL errors returned in the result object (e.g., validation/execution errors)
|
|
219
|
+
if (result.errors) {
|
|
220
|
+
console.error('GraphQL Execution Errors Found:', result.errors);
|
|
221
|
+
// Return a JSON-RPC Error object if GraphQL execution failed
|
|
222
|
+
return {
|
|
223
|
+
jsonrpc: "2.0",
|
|
224
|
+
error: {
|
|
225
|
+
code: -32000, // Standard JSON-RPC error code for Server Error
|
|
226
|
+
message: "GraphQL Execution Error",
|
|
227
|
+
data: result.errors
|
|
228
|
+
},
|
|
229
|
+
id: id
|
|
230
|
+
};
|
|
153
231
|
}
|
|
154
|
-
|
|
155
|
-
const response = await fetch(env.ENDPOINT, {
|
|
156
|
-
method: "POST",
|
|
157
|
-
headers: allHeaders,
|
|
158
|
-
body: JSON.stringify({
|
|
159
|
-
query,
|
|
160
|
-
variables: parsedVariables,
|
|
161
|
-
}),
|
|
162
|
-
});
|
|
163
|
-
if (!response.ok) {
|
|
164
|
-
const responseText = await response.text();
|
|
232
|
+
// Fallback for unexpected empty result
|
|
165
233
|
return {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
],
|
|
234
|
+
jsonrpc: '2.0',
|
|
235
|
+
id: id,
|
|
236
|
+
error: {
|
|
237
|
+
code: -32603, // Internal error
|
|
238
|
+
message: 'Error executing GraphQL query: Response was empty.',
|
|
239
|
+
},
|
|
173
240
|
};
|
|
174
241
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
if (data.errors && data.errors.length > 0) {
|
|
242
|
+
catch (error) {
|
|
243
|
+
console.error('GraphQL Execution Error (Catch Block):', error);
|
|
178
244
|
return {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
245
|
+
jsonrpc: '2.0',
|
|
246
|
+
id: id,
|
|
247
|
+
error: {
|
|
248
|
+
code: -32603, // Internal error
|
|
249
|
+
message: 'Error executing GraphQL query',
|
|
250
|
+
data: error.message,
|
|
251
|
+
},
|
|
186
252
|
};
|
|
187
253
|
}
|
|
188
|
-
return {
|
|
189
|
-
content: [
|
|
190
|
-
{
|
|
191
|
-
type: "text",
|
|
192
|
-
text: JSON.stringify(data, null, 2),
|
|
193
|
-
},
|
|
194
|
-
],
|
|
195
|
-
};
|
|
196
254
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
isError: true,
|
|
200
|
-
content: [
|
|
201
|
-
{
|
|
202
|
-
type: "text",
|
|
203
|
-
text: `Failed to execute GraphQL query: ${error}`,
|
|
204
|
-
},
|
|
205
|
-
],
|
|
206
|
-
};
|
|
207
|
-
}
|
|
208
|
-
};
|
|
209
|
-
toolHandlers.set("query-graphql", queryGraphqlHandler);
|
|
210
|
-
server.tool("query-graphql", "Query a GraphQL endpoint with the given query and variables. Optionally pass headers (e.g., for Authorization).", {
|
|
211
|
-
query: z.string(),
|
|
212
|
-
variables: z.string().optional(),
|
|
213
|
-
headers: z
|
|
214
|
-
.string()
|
|
215
|
-
.optional()
|
|
216
|
-
.describe("Optional JSON string of headers to include, e.g., {\"Authorization\": \"Bearer ...\"}"),
|
|
217
|
-
}, queryGraphqlHandler);
|
|
218
|
-
function readBody(req) {
|
|
219
|
-
return new Promise((resolve, reject) => {
|
|
220
|
-
let body = '';
|
|
221
|
-
req.on('data', (chunk) => {
|
|
222
|
-
body += chunk.toString();
|
|
223
|
-
});
|
|
224
|
-
req.on('end', () => {
|
|
225
|
-
resolve(body);
|
|
226
|
-
});
|
|
227
|
-
req.on('error', reject);
|
|
228
|
-
});
|
|
229
|
-
}
|
|
230
|
-
async function handleHttpRequest(req, res) {
|
|
231
|
-
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
232
|
-
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
233
|
-
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
234
|
-
if (req.method === 'OPTIONS') {
|
|
235
|
-
res.writeHead(204);
|
|
236
|
-
res.end();
|
|
237
|
-
return;
|
|
238
|
-
}
|
|
239
|
-
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
240
|
-
if (url.pathname === '/health' && req.method === 'GET') {
|
|
241
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
242
|
-
res.end(JSON.stringify({ status: 'ok', server: env.NAME }));
|
|
243
|
-
return;
|
|
244
|
-
}
|
|
245
|
-
if (url.pathname === '/mcp' && req.method === 'POST') {
|
|
246
|
-
let rawBody;
|
|
247
|
-
let request;
|
|
248
|
-
try {
|
|
249
|
-
rawBody = await readBody(req);
|
|
250
|
-
request = JSON.parse(rawBody);
|
|
251
|
-
}
|
|
252
|
-
catch (e) {
|
|
253
|
-
console.error("HTTP MCP Parse Error:", e);
|
|
254
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
255
|
-
res.end(JSON.stringify({
|
|
256
|
-
jsonrpc: '2.0',
|
|
257
|
-
id: null,
|
|
258
|
-
error: { code: -32700, message: 'Parse Error: Invalid JSON received in request body.' }
|
|
259
|
-
}));
|
|
260
|
-
return;
|
|
261
|
-
}
|
|
255
|
+
// --- 2. Introspect Schema ---
|
|
256
|
+
if (method === 'introspect-schema') {
|
|
262
257
|
try {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
}));
|
|
271
|
-
return;
|
|
258
|
+
// Default to using the main endpoint
|
|
259
|
+
let schemaSource = MCP_GRAPHQL_ENDPOINT;
|
|
260
|
+
let resultText;
|
|
261
|
+
const headers = {};
|
|
262
|
+
// Check if a remote schema URL is provided
|
|
263
|
+
if (params?.schemaUrl) {
|
|
264
|
+
resultText = await introspectSchemaFromUrl(params.schemaUrl);
|
|
272
265
|
}
|
|
273
|
-
|
|
274
|
-
if (
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
}));
|
|
281
|
-
return;
|
|
266
|
+
// Check if typeNames are provided for filtered introspection
|
|
267
|
+
else if (params?.typeNames) {
|
|
268
|
+
resultText = await introspectTypes(schemaSource, headers, params.typeNames);
|
|
269
|
+
}
|
|
270
|
+
// Default: Full introspection
|
|
271
|
+
else {
|
|
272
|
+
resultText = await introspectEndpoint(schemaSource, headers);
|
|
282
273
|
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
274
|
+
// Introspection methods return the schema as a string, which is correct
|
|
275
|
+
// for the 'text' type output used here.
|
|
276
|
+
return {
|
|
286
277
|
jsonrpc: '2.0',
|
|
287
278
|
id: id,
|
|
288
|
-
result:
|
|
289
|
-
|
|
279
|
+
result: {
|
|
280
|
+
content: [
|
|
281
|
+
{
|
|
282
|
+
type: 'text',
|
|
283
|
+
text: resultText,
|
|
284
|
+
},
|
|
285
|
+
],
|
|
286
|
+
},
|
|
287
|
+
};
|
|
290
288
|
}
|
|
291
289
|
catch (error) {
|
|
292
|
-
console.error(
|
|
293
|
-
|
|
294
|
-
res.end(JSON.stringify({
|
|
290
|
+
console.error('Introspection Error:', error);
|
|
291
|
+
return {
|
|
295
292
|
jsonrpc: '2.0',
|
|
296
|
-
id:
|
|
297
|
-
error: {
|
|
298
|
-
|
|
293
|
+
id: id,
|
|
294
|
+
error: {
|
|
295
|
+
code: -32603, // Internal error
|
|
296
|
+
message: 'Error during schema introspection',
|
|
297
|
+
data: error.message,
|
|
298
|
+
},
|
|
299
|
+
};
|
|
299
300
|
}
|
|
300
|
-
return;
|
|
301
301
|
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
* @returns Resolves with the bound server instance.
|
|
312
|
-
*/
|
|
313
|
-
function tryListen(server, port, maxRetries = 5, attempt = 0) {
|
|
314
|
-
return new Promise((resolve, reject) => {
|
|
315
|
-
if (attempt >= maxRetries) {
|
|
316
|
-
reject(new Error(`Failed to bind HTTP server after ${maxRetries} attempts, starting from port ${env.MCP_PORT}.`));
|
|
317
|
-
return;
|
|
318
|
-
}
|
|
319
|
-
if (port > 65535) {
|
|
320
|
-
reject(new Error(`Exceeded maximum port number (65535) during retry.`));
|
|
321
|
-
return;
|
|
322
|
-
}
|
|
323
|
-
const errorHandler = (err) => {
|
|
324
|
-
server.removeListener('error', errorHandler); // Remove listener to prevent memory leak
|
|
325
|
-
if (err.code === 'EADDRINUSE') {
|
|
326
|
-
const nextPort = port + 1;
|
|
327
|
-
// Use console.error so it appears in the Inspector log
|
|
328
|
-
console.error(`[HTTP] Port ${port} is in use (EADDRINUSE). Retrying on ${nextPort}...`);
|
|
329
|
-
server.close(() => {
|
|
330
|
-
// Recursively call tryListen with the next port
|
|
331
|
-
resolve(tryListen(server, nextPort, maxRetries, attempt + 1));
|
|
332
|
-
});
|
|
333
|
-
}
|
|
334
|
-
else {
|
|
335
|
-
reject(err);
|
|
336
|
-
}
|
|
337
|
-
};
|
|
338
|
-
server.on('error', errorHandler);
|
|
339
|
-
server.listen(port, () => {
|
|
340
|
-
server.removeListener('error', errorHandler); // success, remove the error listener
|
|
341
|
-
console.error(`[HTTP] Started server on http://localhost:${port}. Listening for POST /mcp requests.`);
|
|
342
|
-
resolve(server);
|
|
343
|
-
});
|
|
344
|
-
});
|
|
345
|
-
}
|
|
346
|
-
function startHttpTransport() {
|
|
347
|
-
const serverInstance = node_http_1.default.createServer(handleHttpRequest);
|
|
348
|
-
tryListen(serverInstance, env.MCP_PORT).catch((error) => {
|
|
349
|
-
console.error(`[HTTP] Failed to start HTTP transport: ${error.message}`);
|
|
350
|
-
});
|
|
302
|
+
// --- 3. Handle unsupported method ---
|
|
303
|
+
return {
|
|
304
|
+
jsonrpc: '2.0',
|
|
305
|
+
id: id || null,
|
|
306
|
+
error: {
|
|
307
|
+
code: -32601, // Method not found
|
|
308
|
+
message: `Method not found: ${method}`,
|
|
309
|
+
},
|
|
310
|
+
};
|
|
351
311
|
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
312
|
+
// --- Express App Setup ---
|
|
313
|
+
const app = (0, express_1.default)();
|
|
314
|
+
// Use express.json()
|
|
315
|
+
app.use(express_1.default.json());
|
|
316
|
+
// **CRITICAL FIX FOR LOCAL DEVELOPMENT (vercel dev):**
|
|
317
|
+
// When running locally, the full path /mcp is passed to Express.
|
|
318
|
+
app.post('/mcp', async (req, res) => {
|
|
319
|
+
const response = await handleJsonRpc(req.body);
|
|
320
|
+
res.json(response);
|
|
321
|
+
});
|
|
322
|
+
// **CRITICAL FIX FOR VERCEL PRODUCTION:**
|
|
323
|
+
// When deployed, Vercel strips the /mcp prefix, so the function receives the path as '/'.
|
|
324
|
+
app.post('/', async (req, res) => {
|
|
325
|
+
const response = await handleJsonRpc(req.body);
|
|
326
|
+
res.json(response);
|
|
327
|
+
});
|
|
328
|
+
// Health check endpoints for both environments
|
|
329
|
+
// FIX: Changed unused 'req' parameters to '_req' to silence the linting warning.
|
|
330
|
+
app.get('/health', (_req, res) => {
|
|
331
|
+
// Note: Checking client status (it will be an instance of ApolloClient if successful)
|
|
332
|
+
if (client && client instanceof coreExports.ApolloClient) {
|
|
333
|
+
res.status(200).send('OK');
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
res.status(500).send('Initialization Failed');
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
app.get('/mcp/health', (_req, res) => {
|
|
340
|
+
// This route is primarily for local testing convenience
|
|
341
|
+
if (client && client instanceof coreExports.ApolloClient) {
|
|
342
|
+
res.status(200).send('OK');
|
|
343
|
+
}
|
|
344
|
+
else {
|
|
345
|
+
res.status(500).send('Initialization Failed');
|
|
346
|
+
}
|
|
361
347
|
});
|
|
348
|
+
// Export the app instance for Vercel
|
|
349
|
+
exports.default = app;
|
package/package.json
CHANGED
|
@@ -43,6 +43,8 @@
|
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@graphql-tools/schema": "^10.0.23",
|
|
46
|
+
"@types/express": "^5.0.5",
|
|
47
|
+
"@types/graphql": "^14.2.3",
|
|
46
48
|
"@types/node": "^24.7.2",
|
|
47
49
|
"@types/yargs": "17.0.33",
|
|
48
50
|
"graphql-yoga": "^5.13.5",
|
|
@@ -50,5 +52,5 @@
|
|
|
50
52
|
"ts-node": "^10.9.2",
|
|
51
53
|
"typescript": "5.8.3"
|
|
52
54
|
},
|
|
53
|
-
"version": "2.
|
|
55
|
+
"version": "2.3.1"
|
|
54
56
|
}
|