@letoribo/mcp-graphql-enhanced 2.3.1 → 2.3.2

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 CHANGED
@@ -42,7 +42,7 @@ curl http://localhost:6274/health
42
42
 
43
43
  # Example: Test the query tool via JSON-RPC (using port 6275 if 6274 was busy)
44
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
- ```
45
+
46
46
  ## 🔍 Filtered Introspection (New!)
47
47
  Avoid 50k-line schema dumps. Ask for only what you need:
48
48
  ```@introspect-schema typeNames ["Query", "User"]```
package/dist/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- declare const app: import("express-serve-static-core").Express;
2
- export default app;
1
+ #!/usr/bin/env node
2
+ export {};
3
3
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AA2UA,QAAA,MAAM,GAAG,6CAAY,CAAC;AA0CtB,eAAe,GAAG,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
package/dist/index.js CHANGED
@@ -1,349 +1,361 @@
1
+ #!/usr/bin/env node
1
2
  "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
- })();
35
3
  var __importDefault = (this && this.__importDefault) || function (mod) {
36
4
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
5
  };
38
6
  Object.defineProperty(exports, "__esModule", { value: true });
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.');
59
- }
60
- else if (apolloModule.default && apolloModule.default.HttpLink) {
61
- coreExports = apolloModule.default;
62
- console.log('[Setup] Apollo exports resolved nested under .default.');
63
- }
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;
7
+ const node_http_1 = __importDefault(require("node:http"));
8
+ const { McpServer } = require("@modelcontextprotocol/sdk/server/mcp.js");
9
+ const { StdioServerTransport } = require("@modelcontextprotocol/sdk/server/stdio.js");
10
+ const { parse } = require("graphql/language");
11
+ const z = require("zod").default;
12
+ const { checkDeprecatedArguments } = require("./helpers/deprecation.js");
13
+ const { introspectEndpoint, introspectLocalSchema, introspectSchemaFromUrl, introspectTypes, } = require("./helpers/introspection.js");
14
+ const getVersion = () => {
15
+ const pkg = require("../package.json");
16
+ return pkg.version;
17
+ };
18
+ checkDeprecatedArguments();
19
+ const EnvSchema = z.object({
20
+ NAME: z.string().default("mcp-graphql-enhanced"),
21
+ ENDPOINT: z.preprocess((val) => (typeof val === 'string' ? val.trim() : val), z.string().url("ENDPOINT must be a valid URL (e.g., https://example.com/graphql)")).default("https://mcp-neo4j-discord.vercel.app/api/graphiql"),
22
+ ALLOW_MUTATIONS: z
23
+ .enum(["true", "false"])
24
+ .transform((value) => value === "true")
25
+ .default("false"),
26
+ HEADERS: z
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) => {
88
47
  try {
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
- });
98
- }
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)}...`);
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
+ };
106
69
  }
107
- const json = await response.json();
108
- if (json.errors) {
109
- throw new Error(`GraphQL errors during introspection: ${JSON.stringify(json.errors)}`);
70
+ catch (error) {
71
+ throw new Error(`Failed to get GraphQL schema: ${error}`);
110
72
  }
111
- return json.data;
112
- };
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()}`);
73
+ });
74
+ const toolHandlers = new Map();
75
+ const introspectSchemaHandler = async ({ typeNames, descriptions = true, directives = true }) => {
76
+ try {
77
+ if (typeNames && typeNames.length > 0) {
78
+ const filtered = await introspectTypes(env.ENDPOINT, env.HEADERS, typeNames);
79
+ return { content: [{ type: "text", text: filtered }] };
80
+ }
81
+ else {
82
+ let schema;
83
+ if (env.SCHEMA) {
84
+ if (env.SCHEMA.startsWith("http://") || env.SCHEMA.startsWith("https://")) {
85
+ schema = await introspectSchemaFromUrl(env.SCHEMA);
86
+ }
87
+ else {
88
+ schema = await introspectLocalSchema(env.SCHEMA);
89
+ }
150
90
  }
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`;
91
+ else {
92
+ schema = await introspectEndpoint(env.ENDPOINT, env.HEADERS);
93
+ }
94
+ return { content: [{ type: "text", text: schema }] };
155
95
  }
156
96
  }
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}`);
97
+ catch (error) {
98
+ return {
99
+ isError: true,
100
+ content: [{ type: "text", text: `Introspection failed: ${error}` }],
101
+ };
170
102
  }
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
103
  };
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.');
104
+ toolHandlers.set("introspect-schema", introspectSchemaHandler);
105
+ server.tool("introspect-schema", "Introspect the GraphQL schema. Optionally filter to specific types.", {
106
+ typeNames: z.array(z.string()).optional().describe("A list of specific type names to filter the introspection."),
107
+ descriptions: z.boolean().optional().default(true),
108
+ directives: z.boolean().optional().default(true),
109
+ }, introspectSchemaHandler);
110
+ const queryGraphqlHandler = async ({ query, variables, headers }) => {
111
+ try {
112
+ const parsedQuery = parse(query);
113
+ const isMutation = parsedQuery.definitions.some((def) => def.kind === "OperationDefinition" && def.operation === "mutation");
114
+ if (isMutation && !env.ALLOW_MUTATIONS) {
115
+ return {
116
+ isError: true,
117
+ content: [
118
+ {
119
+ type: "text",
120
+ text: "Mutations are not allowed unless you enable them in the configuration. Please use a query operation instead.",
121
+ },
122
+ ],
123
+ };
124
+ }
125
+ }
126
+ catch (error) {
188
127
  return {
189
- jsonrpc: '2.0',
190
- id: id || null,
191
- error: {
192
- code: -32000, // Server error
193
- message: 'Server failed to initialize Apollo Client.',
194
- },
128
+ isError: true,
129
+ content: [
130
+ {
131
+ type: "text",
132
+ text: `Invalid GraphQL query: ${error}`,
133
+ },
134
+ ],
195
135
  };
196
136
  }
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
- };
137
+ try {
138
+ const toolHeaders = headers
139
+ ? JSON.parse(headers)
140
+ : {};
141
+ const allHeaders = {
142
+ "Content-Type": "application/json",
143
+ ...env.HEADERS,
144
+ ...toolHeaders,
145
+ };
146
+ let parsedVariables = null;
147
+ if (variables) {
148
+ if (typeof variables === 'string') {
149
+ parsedVariables = JSON.parse(variables);
217
150
  }
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
- };
151
+ else {
152
+ parsedVariables = variables;
231
153
  }
232
- // Fallback for unexpected empty result
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();
233
165
  return {
234
- jsonrpc: '2.0',
235
- id: id,
236
- error: {
237
- code: -32603, // Internal error
238
- message: 'Error executing GraphQL query: Response was empty.',
239
- },
166
+ isError: true,
167
+ content: [
168
+ {
169
+ type: "text",
170
+ text: `GraphQL request failed: ${response.statusText}\n${responseText}`,
171
+ },
172
+ ],
240
173
  };
241
174
  }
242
- catch (error) {
243
- console.error('GraphQL Execution Error (Catch Block):', error);
175
+ const rawData = await response.json();
176
+ const data = rawData;
177
+ if (data.errors && data.errors.length > 0) {
244
178
  return {
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
- },
179
+ isError: true,
180
+ content: [
181
+ {
182
+ type: "text",
183
+ text: `GraphQL errors: ${JSON.stringify(data.errors, null, 2)}`,
184
+ },
185
+ ],
252
186
  };
253
187
  }
188
+ return {
189
+ content: [
190
+ {
191
+ type: "text",
192
+ text: JSON.stringify(data, null, 2),
193
+ },
194
+ ],
195
+ };
254
196
  }
255
- // --- 2. Introspect Schema ---
256
- if (method === 'introspect-schema') {
197
+ catch (error) {
198
+ return {
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;
257
248
  try {
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);
265
- }
266
- // Check if typeNames are provided for filtered introspection
267
- else if (params?.typeNames) {
268
- resultText = await introspectTypes(schemaSource, headers, params.typeNames);
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
+ }
262
+ try {
263
+ const { method, params, id } = request;
264
+ if (!method || typeof id === 'undefined') {
265
+ res.writeHead(400, { 'Content-Type': 'application/json' });
266
+ res.end(JSON.stringify({
267
+ jsonrpc: '2.0',
268
+ id: id || null,
269
+ error: { code: -32600, message: 'Invalid JSON-RPC Request structure (missing method or id).' }
270
+ }));
271
+ return;
269
272
  }
270
- // Default: Full introspection
271
- else {
272
- resultText = await introspectEndpoint(schemaSource, headers);
273
+ const handler = toolHandlers.get(method);
274
+ if (!handler) {
275
+ res.writeHead(404, { 'Content-Type': 'application/json' });
276
+ res.end(JSON.stringify({
277
+ jsonrpc: '2.0',
278
+ id: id,
279
+ error: { code: -32601, message: `Method not found: ${method}` }
280
+ }));
281
+ return;
273
282
  }
274
- // Introspection methods return the schema as a string, which is correct
275
- // for the 'text' type output used here.
276
- return {
283
+ const result = await handler(params);
284
+ res.writeHead(200, { 'Content-Type': 'application/json' });
285
+ res.end(JSON.stringify({
277
286
  jsonrpc: '2.0',
278
287
  id: id,
279
- result: {
280
- content: [
281
- {
282
- type: 'text',
283
- text: resultText,
284
- },
285
- ],
286
- },
287
- };
288
+ result: result
289
+ }));
288
290
  }
289
291
  catch (error) {
290
- console.error('Introspection Error:', error);
291
- return {
292
+ console.error("HTTP MCP Execution Error:", error);
293
+ res.writeHead(500, { 'Content-Type': 'application/json' });
294
+ res.end(JSON.stringify({
292
295
  jsonrpc: '2.0',
293
- id: id,
294
- error: {
295
- code: -32603, // Internal error
296
- message: 'Error during schema introspection',
297
- data: error.message,
298
- },
299
- };
296
+ id: request?.id || null,
297
+ error: { code: -32603, message: 'Internal server error during tool execution.' }
298
+ }));
300
299
  }
300
+ return;
301
301
  }
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
- };
302
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
303
+ res.end('Not Found. Use POST /mcp for JSON-RPC or GET /health.');
311
304
  }
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
- }
305
+ /**
306
+ * Tries to listen on a given port, automatically retrying on the next port if EADDRINUSE occurs.
307
+ * @param server - The HTTP server instance.
308
+ * @param port - The port to attempt binding to.
309
+ * @param maxRetries - Maximum number of retries.
310
+ * @param attempt - Current attempt number.
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
+ });
351
+ }
352
+ async function main() {
353
+ const stdioTransport = new StdioServerTransport();
354
+ await server.connect(stdioTransport);
355
+ startHttpTransport();
356
+ console.error(`[STDIO] Started graphql mcp server ${env.NAME} for endpoint: ${env.ENDPOINT}`);
357
+ }
358
+ main().catch((error) => {
359
+ console.error(`Fatal error in main(): ${error}`);
360
+ process.exit(1);
347
361
  });
348
- // Export the app instance for Vercel
349
- exports.default = app;
package/package.json CHANGED
@@ -43,14 +43,11 @@
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",
48
46
  "@types/node": "^24.7.2",
49
47
  "@types/yargs": "17.0.33",
50
- "graphql-yoga": "^5.13.5",
51
48
  "prettier": "^3.6.2",
52
49
  "ts-node": "^10.9.2",
53
50
  "typescript": "5.8.3"
54
51
  },
55
- "version": "2.3.1"
52
+ "version": "2.3.2"
56
53
  }