@letoribo/mcp-graphql-enhanced 3.2.0 → 3.2.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.
Files changed (3) hide show
  1. package/README.md +7 -3
  2. package/dist/index.js +95 -198
  3. package/package.json +3 -2
package/README.md CHANGED
@@ -17,13 +17,16 @@ An **enhanced MCP (Model Context Protocol) server for GraphQL** that fixes real-
17
17
 
18
18
  This server now runs in **dual transport mode**, supporting both the standard **STDIO** communication (used by most MCP clients) and a new **HTTP JSON-RPC** endpoint on port `6274`.
19
19
 
20
- This allows external systems, web applications, and direct `curl` commands to access the server's tools.
20
+ This allows external systems, web applications, and direct `curl` commands to access the server's tools with **live request logging** in your terminal (`[HTTP-RPC]` logs).
21
21
 
22
22
  | **Endpoint** | **Method** | **Description** |
23
23
  | :--- | :--- | :--- |
24
- | `/mcp` | `POST` | The main JSON-RPC endpoint for tool execution. |
24
+ | `/mcp` | `POST` | The main JSON-RPC 2.0 endpoint for tool execution. |
25
25
  | `/health` | `GET` | Simple health check, returns `{ status: 'ok' }`. |
26
26
 
27
+ ### Automatic Port Selection
28
+ The server defaults to port `6274`. If you encounter an `EADDRINUSE` error, the server will automatically find the next available port. **Check the server logs for the final bound port** (e.g., `[HTTP] Started server on http://localhost:6275`).
29
+
27
30
  ### Resolving Port Conflicts (EADDRINUSE) and Automatic Port Selection
28
31
 
29
32
  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 find the next available port (up to 10 attempts, not spawning multiple servers).
@@ -62,9 +65,10 @@ npx @modelcontextprotocol/inspector \
62
65
  | `HEADERS` | JSON string containing headers for requests | `{}` |
63
66
  | `ALLOW_MUTATIONS` | Enable mutation operations (disabled by default) | `false` |
64
67
  | `NAME` | Name of the MCP server | `mcp-graphql-enhanced` |
65
- | `SCHEMA` | Path to a local GraphQL schema file or URL (optional) | - |
68
+ | `SCHEMA` | Path to a local GraphQL schema file or URL | - |
66
69
  | `MCP_PORT` | Port for the HTTP/JSON-RPC server. | `6274` |
67
70
  | `ENABLE_HTTP` | Enable HTTP transport: `auto` (default), `true`, or `false` | `auto` |
71
+ | `DEBUG` | Set to `mcp:*` for detailed SDK logs | - |
68
72
  **Note on `ENABLE_HTTP`:**
69
73
  - `auto` (default): Automatically enables HTTP only when running in MCP Inspector...
70
74
  - `true`: Always enable HTTP server
package/dist/index.js CHANGED
@@ -5,25 +5,31 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  };
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
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, introspectSpecificTypes, } = require("./helpers/introspection.js");
8
+ const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
9
+ const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
10
+ const language_1 = require("graphql/language");
11
+ const zod_1 = __importDefault(require("zod"));
12
+ // Helper imports
13
+ const deprecation_js_1 = require("./helpers/deprecation.js");
14
+ const introspection_js_1 = require("./helpers/introspection.js");
14
15
  const getVersion = () => {
15
- const pkg = require("../package.json");
16
- return pkg.version;
16
+ try {
17
+ const pkg = require("../package.json");
18
+ return pkg.version;
19
+ }
20
+ catch {
21
+ return "3.2.1";
22
+ }
17
23
  };
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
24
+ (0, deprecation_js_1.checkDeprecatedArguments)();
25
+ const EnvSchema = zod_1.default.object({
26
+ NAME: zod_1.default.string().default("mcp-graphql-enhanced"),
27
+ ENDPOINT: zod_1.default.preprocess((val) => (typeof val === 'string' ? val.trim() : val), zod_1.default.string().url()).default("https://mcp-neo4j-discord.vercel.app/api/graphiql"),
28
+ ALLOW_MUTATIONS: zod_1.default
23
29
  .enum(["true", "false"])
24
30
  .transform((value) => value === "true")
25
31
  .default("false"),
26
- HEADERS: z
32
+ HEADERS: zod_1.default
27
33
  .string()
28
34
  .default("{}")
29
35
  .transform((val) => {
@@ -34,32 +40,27 @@ const EnvSchema = z.object({
34
40
  throw new Error("HEADERS must be a valid JSON string");
35
41
  }
36
42
  }),
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
- ENABLE_HTTP: z
43
+ SCHEMA: zod_1.default.string().optional(),
44
+ MCP_PORT: zod_1.default.preprocess((val) => (val ? parseInt(val) : 6274), zod_1.default.number().int().min(1024).max(65535)).default(6274),
45
+ ENABLE_HTTP: zod_1.default
40
46
  .enum(["true", "false", "auto"])
41
47
  .transform((value) => {
42
48
  if (value === "auto") {
43
- return !!(process.env.MCP_INSPECTOR || process.env.INSPECTOR_PORT);
49
+ return !!(process.env.MCP_INSPECTOR || process.env.INSPECTOR_PORT || process.env.MCP_PORT);
44
50
  }
45
51
  return value === "true";
46
52
  })
47
53
  .default("auto"),
48
54
  });
49
55
  const env = EnvSchema.parse(process.env);
50
- const server = new McpServer({
56
+ const server = new mcp_js_1.McpServer({
51
57
  name: env.NAME,
52
58
  version: getVersion(),
53
- description: `GraphQL MCP server for ${env.ENDPOINT}`,
54
59
  });
55
60
  // --- CACHE STATE ---
56
61
  let cachedSDL = null;
57
62
  let cachedSchemaObject = null;
58
63
  let schemaLoadError = null;
59
- /**
60
- * Loads the schema into memory.
61
- * Populates both cachedSDL (string) and cachedSchemaObject (GraphQLSchema object).
62
- */
63
64
  async function getSchema() {
64
65
  if (cachedSDL)
65
66
  return cachedSDL;
@@ -69,17 +70,18 @@ async function getSchema() {
69
70
  const { buildClientSchema, getIntrospectionQuery, printSchema, buildASTSchema, parse: gqlParse } = require("graphql");
70
71
  let sdl;
71
72
  if (env.SCHEMA) {
73
+ // Check if it's a URL or local path
72
74
  if (env.SCHEMA.startsWith("http")) {
73
- sdl = await introspectSchemaFromUrl(env.SCHEMA);
75
+ const response = await fetch(env.SCHEMA);
76
+ sdl = await response.text();
74
77
  }
75
78
  else {
76
- sdl = await introspectLocalSchema(env.SCHEMA);
79
+ sdl = await (0, introspection_js_1.introspectLocalSchema)(env.SCHEMA);
77
80
  }
78
81
  cachedSchemaObject = buildASTSchema(gqlParse(sdl));
79
82
  cachedSDL = sdl;
80
83
  }
81
84
  else {
82
- // Live Introspection
83
85
  const response = await fetch(env.ENDPOINT, {
84
86
  method: "POST",
85
87
  headers: { "Content-Type": "application/json", ...env.HEADERS },
@@ -99,93 +101,15 @@ async function getSchema() {
99
101
  throw schemaLoadError;
100
102
  }
101
103
  }
102
- // --- RESOURCES ---
103
- server.resource("graphql-schema", new URL(env.ENDPOINT).href, async (uri) => {
104
- try {
105
- const sdl = await getSchema();
106
- return { contents: [{ uri: uri.href, text: sdl }] };
107
- }
108
- catch (error) {
109
- throw error;
110
- }
111
- });
112
104
  // --- TOOL HANDLERS ---
113
105
  const toolHandlers = new Map();
114
- const introspectSchemaHandler = async ({ typeNames }) => {
115
- try {
116
- // Ensure cache is populated
117
- await getSchema();
118
- // Safety: If no specific types requested, return the 'Map' of types to prevent bridge crash
119
- if (!typeNames || typeNames.length === 0) {
120
- const allTypeNames = Object.keys(cachedSchemaObject.getTypeMap())
121
- .filter(t => !t.startsWith('__'));
122
- return {
123
- content: [{
124
- type: "text",
125
- text: `Schema is large. Please request specific types for full details.\n\nAvailable types: ${allTypeNames.join(", ")}`
126
- }]
127
- };
128
- }
129
- // Use the new filtering logic from helpers/introspection.js
130
- const filteredResult = introspectSpecificTypes(cachedSchemaObject, typeNames);
131
- return {
132
- content: [{
133
- type: "text",
134
- text: JSON.stringify(filteredResult, null, 2)
135
- }]
136
- };
137
- }
138
- catch (error) {
139
- throw new Error(`Introspection failed: ${error.message}`);
140
- }
141
- };
142
- toolHandlers.set("introspect-schema", introspectSchemaHandler);
143
- server.tool("introspect-schema", "Introspect the GraphQL schema. Optionally filter to specific types.", {
144
- typeNames: z.array(z.string()).optional().describe("A list of specific type names to filter (e.g. ['User', 'Post'])."),
145
- }, async ({ typeNames }) => {
146
- try {
147
- console.error(`[TOOL] Introspect called with types: ${JSON.stringify(typeNames || "NONE")}`);
148
- // 1. Ensure the schema is loaded into the cache
149
- await getSchema();
150
- // 2. THE GATEKEEPER: If no types requested, send ONLY names.
151
- if (!typeNames || typeNames.length === 0) {
152
- const allTypeNames = Object.keys(cachedSchemaObject.getTypeMap())
153
- .filter(t => !t.startsWith('__'));
154
- console.error(`[TOOL] Sending summary list of ${allTypeNames.length} types.`);
155
- return {
156
- content: [{
157
- type: "text",
158
- text: `Schema is large. Please request specific types for details.\n\nAvailable types: ${allTypeNames.join(", ")}`
159
- }]
160
- };
161
- }
162
- // 3. DRILL DOWN: If Claude asks for specific types, use the helper.
163
- console.error(`[TOOL] Filtering for: ${typeNames.join(", ")}`);
164
- const filteredResult = introspectSpecificTypes(cachedSchemaObject, typeNames);
165
- return {
166
- content: [{
167
- type: "text",
168
- text: JSON.stringify(filteredResult, null, 2)
169
- }]
170
- };
171
- }
172
- catch (error) {
173
- console.error(`[TOOL ERROR] ${error.message}`);
174
- throw new Error(`Introspection failed: ${error.message}`);
175
- }
176
- });
106
+ // Tool: query-graphql
177
107
  const queryGraphqlHandler = async ({ query, variables, headers }) => {
178
108
  try {
179
- const parsedQuery = parse(query);
109
+ const parsedQuery = (0, language_1.parse)(query);
180
110
  const isMutation = parsedQuery.definitions.some((def) => def.kind === "OperationDefinition" && def.operation === "mutation");
181
- if (isMutation && !env.ALLOW_MUTATIONS) {
182
- throw new Error("Mutations are not allowed. Enable ALLOW_MUTATIONS in config.");
183
- }
184
- }
185
- catch (error) {
186
- throw new Error(`Invalid GraphQL query: ${error}`);
187
- }
188
- try {
111
+ if (isMutation && !env.ALLOW_MUTATIONS)
112
+ throw new Error("Mutations are not allowed.");
189
113
  const toolHeaders = headers ? JSON.parse(headers) : {};
190
114
  const allHeaders = { "Content-Type": "application/json", ...env.HEADERS, ...toolHeaders };
191
115
  let parsedVariables = variables;
@@ -196,124 +120,97 @@ const queryGraphqlHandler = async ({ query, variables, headers }) => {
196
120
  headers: allHeaders,
197
121
  body: JSON.stringify({ query, variables: parsedVariables }),
198
122
  });
199
- if (!response.ok) {
200
- const responseText = await response.text();
201
- throw new Error(`GraphQL request failed: ${response.statusText}\n${responseText}`);
202
- }
203
123
  const data = await response.json();
204
- if (data.errors && data.errors.length > 0) {
205
- throw new Error(`GraphQL errors: ${JSON.stringify(data.errors, null, 2)}`);
206
- }
207
124
  return {
208
- content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
125
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
209
126
  };
210
127
  }
211
128
  catch (error) {
212
- throw new Error(`Failed to execute GraphQL query: ${error}`);
129
+ throw new Error(`Execution failed: ${error.message}`);
213
130
  }
214
131
  };
215
132
  toolHandlers.set("query-graphql", queryGraphqlHandler);
216
- server.tool("query-graphql", "Query a GraphQL endpoint with the given query and variables.", {
217
- query: z.string(),
218
- variables: z.string().optional(),
219
- headers: z.string().optional().describe("Optional JSON string of headers"),
133
+ server.tool("query-graphql", "Execute a GraphQL query against the endpoint", {
134
+ query: zod_1.default.string(),
135
+ variables: zod_1.default.string().optional(),
136
+ headers: zod_1.default.string().optional(),
220
137
  }, queryGraphqlHandler);
138
+ // Tool: introspect-schema
139
+ const introspectHandler = async ({ typeNames }) => {
140
+ await getSchema();
141
+ if (!typeNames || typeNames.length === 0) {
142
+ const allTypeNames = Object.keys(cachedSchemaObject.getTypeMap()).filter(t => !t.startsWith('__'));
143
+ return {
144
+ content: [{ type: "text", text: `Schema is large. Available types: ${allTypeNames.join(", ")}` }]
145
+ };
146
+ }
147
+ const filtered = (0, introspection_js_1.introspectSpecificTypes)(cachedSchemaObject, typeNames);
148
+ return {
149
+ content: [{ type: "text", text: JSON.stringify(filtered, null, 2) }]
150
+ };
151
+ };
152
+ toolHandlers.set("introspect-schema", introspectHandler);
153
+ server.tool("introspect-schema", "Introspect the GraphQL schema with optional type filtering", {
154
+ typeNames: zod_1.default.array(zod_1.default.string()).optional(),
155
+ }, introspectHandler);
221
156
  // --- HTTP SERVER LOGIC ---
222
- function readBody(req) {
223
- return new Promise((resolve, reject) => {
224
- let body = '';
225
- req.on('data', chunk => body += chunk.toString());
226
- req.on('end', () => resolve(body));
227
- req.on('error', reject);
228
- });
229
- }
230
157
  async function handleHttpRequest(req, res) {
231
158
  res.setHeader('Access-Control-Allow-Origin', '*');
232
- res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
159
+ res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');
233
160
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
234
161
  if (req.method === 'OPTIONS') {
235
162
  res.writeHead(204);
236
163
  res.end();
237
164
  return;
238
165
  }
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
- }
166
+ const url = new URL(req.url || '', `http://${req.headers.host}`);
245
167
  if (url.pathname === '/mcp' && req.method === 'POST') {
246
- try {
247
- const rawBody = await readBody(req);
248
- const { method, params, id } = JSON.parse(rawBody);
249
- const handler = toolHandlers.get(method);
250
- if (!handler) {
251
- res.writeHead(404);
252
- res.end(JSON.stringify({ jsonrpc: '2.0', id, error: { code: -32601, message: 'Method not found' } }));
253
- return;
168
+ let body = '';
169
+ req.on('data', chunk => { body += chunk; });
170
+ req.on('end', async () => {
171
+ try {
172
+ const { method, params, id } = JSON.parse(body);
173
+ console.error(`[HTTP-RPC] Method: ${method} | ID: ${id}`);
174
+ const handler = toolHandlers.get(method);
175
+ if (!handler) {
176
+ res.writeHead(404);
177
+ return res.end(JSON.stringify({ jsonrpc: '2.0', id, error: { code: -32601, message: "Method not found" } }));
178
+ }
179
+ const result = await handler(params);
180
+ res.writeHead(200, { 'Content-Type': 'application/json' });
181
+ res.end(JSON.stringify({ jsonrpc: '2.0', id, result }));
254
182
  }
255
- const result = await handler(params);
256
- res.writeHead(200, { 'Content-Type': 'application/json' });
257
- res.end(JSON.stringify({ jsonrpc: '2.0', id, result }));
258
- }
259
- catch (error) {
260
- res.writeHead(500);
261
- res.end(JSON.stringify({ jsonrpc: '2.0', error: { code: -32603, message: 'Internal error' } }));
262
- }
183
+ catch (e) {
184
+ console.error(`[HTTP-ERROR] ${e.message}`);
185
+ res.writeHead(500);
186
+ res.end(JSON.stringify({ jsonrpc: '2.0', error: { code: -32603, message: "Internal Error" } }));
187
+ }
188
+ });
263
189
  return;
264
190
  }
191
+ if (url.pathname === '/health') {
192
+ res.writeHead(200, { 'Content-Type': 'application/json' });
193
+ return res.end(JSON.stringify({ status: "ok", server: env.NAME }));
194
+ }
265
195
  res.writeHead(404);
266
- res.end('Not Found');
267
- }
268
- let httpServer = null;
269
- async function startHttpServer(initialPort) {
270
- return new Promise((resolve, reject) => {
271
- let port = initialPort;
272
- const server = node_http_1.default.createServer(handleHttpRequest);
273
- server.once('error', (err) => {
274
- if (err.code === 'EADDRINUSE') {
275
- server.close();
276
- resolve(startHttpServer(port + 1));
277
- }
278
- else
279
- reject(err);
280
- });
281
- server.listen(port, () => {
282
- httpServer = server;
283
- resolve(port);
284
- });
285
- });
196
+ res.end("Not Found");
286
197
  }
287
198
  // --- STARTUP ---
288
199
  async function main() {
289
- console.error(`[SCHEMA] Pre-loading GraphQL schema...`);
290
- const schemaPromise = getSchema().catch(error => {
291
- console.error(`[SCHEMA] Warning: Failed to pre-load schema: ${error}`);
292
- });
293
- const stdioTransport = new StdioServerTransport();
294
- await server.connect(stdioTransport);
295
- console.error(`[STDIO] Started GraphQL MCP server ${env.NAME}`);
296
200
  if (env.ENABLE_HTTP) {
297
- try {
298
- const port = await startHttpServer(env.MCP_PORT);
299
- console.error(`[HTTP] Server started on http://localhost:${port}`);
300
- }
301
- catch (error) {
302
- console.error(`[HTTP] Failed to start: ${error}`);
303
- }
201
+ const serverHttp = node_http_1.default.createServer(handleHttpRequest);
202
+ serverHttp.listen(env.MCP_PORT, () => {
203
+ console.error(`[HTTP] Server started on http://localhost:${env.MCP_PORT}`);
204
+ });
304
205
  }
305
- await schemaPromise;
206
+ const transport = new stdio_js_1.StdioServerTransport();
207
+ await server.connect(transport);
208
+ console.error(`[STDIO] MCP Server "${env.NAME}" v${getVersion()} started`);
209
+ getSchema().catch(e => console.error(`[SCHEMA] Warning: Preload failed: ${e.message}`));
306
210
  }
307
- // Graceful exit
308
- const shutdown = () => {
309
- if (httpServer)
310
- httpServer.close(() => process.exit(0));
311
- else
312
- process.exit(0);
313
- };
314
- process.on('SIGINT', shutdown);
315
- process.on('SIGTERM', shutdown);
211
+ process.on('SIGINT', () => process.exit(0));
212
+ process.on('SIGTERM', () => process.exit(0));
316
213
  main().catch(error => {
317
- console.error(`Fatal error: ${error}`);
214
+ console.error(`[FATAL] ${error}`);
318
215
  process.exit(1);
319
216
  });
package/package.json CHANGED
@@ -29,7 +29,7 @@
29
29
  "node": ">=18"
30
30
  },
31
31
  "scripts": {
32
- "dev": "ts-node src/index.ts",
32
+ "dev": "tsx --watch src/index.ts",
33
33
  "build": "tsc && chmod +x dist/index.js",
34
34
  "start": "node dist/index.js",
35
35
  "format": "prettier --write ."
@@ -47,7 +47,8 @@
47
47
  "@types/yargs": "17.0.33",
48
48
  "prettier": "^3.6.2",
49
49
  "ts-node": "^10.9.2",
50
+ "tsx": "^4.21.0",
50
51
  "typescript": "5.8.3"
51
52
  },
52
- "version": "3.2.0"
53
+ "version": "3.2.1"
53
54
  }