@letoribo/mcp-graphql-enhanced 3.2.0 → 3.3.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 CHANGED
@@ -4,6 +4,7 @@ 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
@@ -12,18 +13,27 @@ An **enhanced MCP (Model Context Protocol) server for GraphQL** that fixes real-
12
13
  * ✅ **Secure by default** — mutations disabled unless explicitly enabled
13
14
 
14
15
  ---
16
+ ## 🎨 Visual Command Center (GraphiQL)
17
+ 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.
18
+
19
+ * **Endpoint:** `http://localhost:6274/` (or `/graphiql`)
20
+ * **Header Sync:** Any headers set in your environment (like GitHub tokens) are automatically injected into the GraphiQL "Headers" tab for immediate testing.
15
21
 
16
22
  ## 💻 HTTP / Dual Transport
17
23
 
18
24
  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
25
 
20
- This allows external systems, web applications, and direct `curl` commands to access the server's tools.
26
+ 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
27
 
22
28
  | **Endpoint** | **Method** | **Description** |
23
29
  | :--- | :--- | :--- |
24
- | `/mcp` | `POST` | The main JSON-RPC endpoint for tool execution. |
30
+ | `/graphiql` | `GET` | Human Interface: The visual GraphQL IDE. |
31
+ | `/mcp` | `POST` | The main JSON-RPC 2.0 endpoint for tool execution. |
25
32
  | `/health` | `GET` | Simple health check, returns `{ status: 'ok' }`. |
26
33
 
34
+ ### Automatic Port Selection
35
+ 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`).
36
+
27
37
  ### Resolving Port Conflicts (EADDRINUSE) and Automatic Port Selection
28
38
 
29
39
  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).
@@ -43,28 +53,29 @@ curl http://localhost:6274/health
43
53
  # Example: Test the query tool via JSON-RPC (using port 6275 if 6274 was busy)
44
54
  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
55
 
46
- ## 🔍 Filtered Introspection (New!)
56
+ ## 🔍 Filtered Introspection
47
57
  Avoid 50k-line schema dumps. Ask for only what you need:
48
- ```@introspect-schema typeNames ["Query", "User"]```
58
+ `@introspect-schema typeNames ["Query", "User"]`
49
59
  ## 🔍 Debug & Inspect
50
60
  Use the official MCP Inspector to test your server live:
51
61
  ```bash
52
62
  npx @modelcontextprotocol/inspector \
53
63
  -e ENDPOINT=https://api.example.com/graphql \
54
- npx @letoribo/mcp-graphql-enhanced --debug
64
+ npx @letoribo/mcp-graphql-enhanced
55
65
  ```
56
66
  ### Environment Variables (Breaking change in 1.0.0)
57
67
  > **Note:** As of version 1.0.0, command line arguments have been replaced with environment variables.
58
68
 
59
69
  | Environment Variable | Description | Default |
60
- |----------|-------------|---------|
70
+ | :--- | :--- | :--- |
61
71
  | `ENDPOINT` | GraphQL endpoint URL | `https://mcp-neo4j-discord.vercel.app/api/graphiql` |
62
72
  | `HEADERS` | JSON string containing headers for requests | `{}` |
63
73
  | `ALLOW_MUTATIONS` | Enable mutation operations (disabled by default) | `false` |
64
74
  | `NAME` | Name of the MCP server | `mcp-graphql-enhanced` |
65
- | `SCHEMA` | Path to a local GraphQL schema file or URL (optional) | - |
75
+ | `SCHEMA` | Path to a local GraphQL schema file or URL | - |
66
76
  | `MCP_PORT` | Port for the HTTP/JSON-RPC server. | `6274` |
67
77
  | `ENABLE_HTTP` | Enable HTTP transport: `auto` (default), `true`, or `false` | `auto` |
78
+ | `DEBUG` | Set to `mcp:*` for detailed SDK logs | - |
68
79
  **Note on `ENABLE_HTTP`:**
69
80
  - `auto` (default): Automatically enables HTTP only when running in MCP Inspector...
70
81
  - `true`: Always enable HTTP server
@@ -89,6 +100,13 @@ npx @letoribo/mcp-graphql-enhanced
89
100
  MCP_PORT=8080 npx @letoribo/mcp-graphql-enhanced
90
101
  # Disable HTTP transport (fastest, recommended for Claude Desktop)
91
102
  ENABLE_HTTP=false npx @letoribo/mcp-graphql-enhanced
103
+ # Test the surgical precision and the IDE immediately:
104
+ ENDPOINT=https://api.github.com/graphql \
105
+ HEADERS='{"Authorization":"Bearer YOUR_GITHUB_TOKEN"}' \
106
+ ENABLE_HTTP=true \
107
+ npx @letoribo/mcp-graphql-enhanced
108
+
109
+ # Then visit http://localhost:6274/graphiql
92
110
  ```
93
111
  ### 🖥️ Claude Desktop Configuration Examples
94
112
  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
@@ -5,25 +5,32 @@ 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
+ const graphiql_js_1 = require("./helpers/graphiql.js");
13
+ // Helper imports
14
+ const deprecation_js_1 = require("./helpers/deprecation.js");
15
+ const introspection_js_1 = require("./helpers/introspection.js");
14
16
  const getVersion = () => {
15
- const pkg = require("../package.json");
16
- return pkg.version;
17
+ try {
18
+ const pkg = require("../package.json");
19
+ return pkg.version;
20
+ }
21
+ catch {
22
+ return "3.2.1";
23
+ }
17
24
  };
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
25
+ (0, deprecation_js_1.checkDeprecatedArguments)();
26
+ const EnvSchema = zod_1.default.object({
27
+ NAME: zod_1.default.string().default("mcp-graphql-enhanced"),
28
+ 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"),
29
+ ALLOW_MUTATIONS: zod_1.default
23
30
  .enum(["true", "false"])
24
31
  .transform((value) => value === "true")
25
32
  .default("false"),
26
- HEADERS: z
33
+ HEADERS: zod_1.default
27
34
  .string()
28
35
  .default("{}")
29
36
  .transform((val) => {
@@ -34,32 +41,27 @@ const EnvSchema = z.object({
34
41
  throw new Error("HEADERS must be a valid JSON string");
35
42
  }
36
43
  }),
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
44
+ SCHEMA: zod_1.default.string().optional(),
45
+ MCP_PORT: zod_1.default.preprocess((val) => (val ? parseInt(val) : 6274), zod_1.default.number().int().min(1024).max(65535)).default(6274),
46
+ ENABLE_HTTP: zod_1.default
40
47
  .enum(["true", "false", "auto"])
41
48
  .transform((value) => {
42
49
  if (value === "auto") {
43
- return !!(process.env.MCP_INSPECTOR || process.env.INSPECTOR_PORT);
50
+ return !!(process.env.MCP_INSPECTOR || process.env.INSPECTOR_PORT || process.env.MCP_PORT);
44
51
  }
45
52
  return value === "true";
46
53
  })
47
54
  .default("auto"),
48
55
  });
49
56
  const env = EnvSchema.parse(process.env);
50
- const server = new McpServer({
57
+ const server = new mcp_js_1.McpServer({
51
58
  name: env.NAME,
52
59
  version: getVersion(),
53
- description: `GraphQL MCP server for ${env.ENDPOINT}`,
54
60
  });
55
61
  // --- CACHE STATE ---
56
62
  let cachedSDL = null;
57
63
  let cachedSchemaObject = null;
58
64
  let schemaLoadError = null;
59
- /**
60
- * Loads the schema into memory.
61
- * Populates both cachedSDL (string) and cachedSchemaObject (GraphQLSchema object).
62
- */
63
65
  async function getSchema() {
64
66
  if (cachedSDL)
65
67
  return cachedSDL;
@@ -69,17 +71,18 @@ async function getSchema() {
69
71
  const { buildClientSchema, getIntrospectionQuery, printSchema, buildASTSchema, parse: gqlParse } = require("graphql");
70
72
  let sdl;
71
73
  if (env.SCHEMA) {
74
+ // Check if it's a URL or local path
72
75
  if (env.SCHEMA.startsWith("http")) {
73
- sdl = await introspectSchemaFromUrl(env.SCHEMA);
76
+ const response = await fetch(env.SCHEMA);
77
+ sdl = await response.text();
74
78
  }
75
79
  else {
76
- sdl = await introspectLocalSchema(env.SCHEMA);
80
+ sdl = await (0, introspection_js_1.introspectLocalSchema)(env.SCHEMA);
77
81
  }
78
82
  cachedSchemaObject = buildASTSchema(gqlParse(sdl));
79
83
  cachedSDL = sdl;
80
84
  }
81
85
  else {
82
- // Live Introspection
83
86
  const response = await fetch(env.ENDPOINT, {
84
87
  method: "POST",
85
88
  headers: { "Content-Type": "application/json", ...env.HEADERS },
@@ -99,93 +102,15 @@ async function getSchema() {
99
102
  throw schemaLoadError;
100
103
  }
101
104
  }
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
105
  // --- TOOL HANDLERS ---
113
106
  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
- });
107
+ // Tool: query-graphql
177
108
  const queryGraphqlHandler = async ({ query, variables, headers }) => {
178
109
  try {
179
- const parsedQuery = parse(query);
110
+ const parsedQuery = (0, language_1.parse)(query);
180
111
  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 {
112
+ if (isMutation && !env.ALLOW_MUTATIONS)
113
+ throw new Error("Mutations are not allowed.");
189
114
  const toolHeaders = headers ? JSON.parse(headers) : {};
190
115
  const allHeaders = { "Content-Type": "application/json", ...env.HEADERS, ...toolHeaders };
191
116
  let parsedVariables = variables;
@@ -196,124 +121,105 @@ const queryGraphqlHandler = async ({ query, variables, headers }) => {
196
121
  headers: allHeaders,
197
122
  body: JSON.stringify({ query, variables: parsedVariables }),
198
123
  });
199
- if (!response.ok) {
200
- const responseText = await response.text();
201
- throw new Error(`GraphQL request failed: ${response.statusText}\n${responseText}`);
202
- }
203
124
  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
125
  return {
208
- content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
126
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
209
127
  };
210
128
  }
211
129
  catch (error) {
212
- throw new Error(`Failed to execute GraphQL query: ${error}`);
130
+ throw new Error(`Execution failed: ${error.message}`);
213
131
  }
214
132
  };
215
133
  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"),
134
+ server.tool("query-graphql", "Execute a GraphQL query against the endpoint", {
135
+ query: zod_1.default.string(),
136
+ variables: zod_1.default.string().optional(),
137
+ headers: zod_1.default.string().optional(),
220
138
  }, queryGraphqlHandler);
139
+ // Tool: introspect-schema
140
+ const introspectHandler = async ({ typeNames }) => {
141
+ await getSchema();
142
+ if (!typeNames || typeNames.length === 0) {
143
+ const allTypeNames = Object.keys(cachedSchemaObject.getTypeMap()).filter(t => !t.startsWith('__'));
144
+ return {
145
+ content: [{ type: "text", text: `Schema is large. Available types: ${allTypeNames.join(", ")}` }]
146
+ };
147
+ }
148
+ const filtered = (0, introspection_js_1.introspectSpecificTypes)(cachedSchemaObject, typeNames);
149
+ return {
150
+ content: [{ type: "text", text: JSON.stringify(filtered, null, 2) }]
151
+ };
152
+ };
153
+ toolHandlers.set("introspect-schema", introspectHandler);
154
+ server.tool("introspect-schema", "Introspect the GraphQL schema with optional type filtering", {
155
+ typeNames: zod_1.default.array(zod_1.default.string()).optional(),
156
+ }, introspectHandler);
221
157
  // --- 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
158
  async function handleHttpRequest(req, res) {
231
159
  res.setHeader('Access-Control-Allow-Origin', '*');
232
- res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
160
+ res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');
233
161
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
234
162
  if (req.method === 'OPTIONS') {
235
163
  res.writeHead(204);
236
164
  res.end();
237
165
  return;
238
166
  }
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;
167
+ const url = new URL(req.url || '', `http://${req.headers.host}`);
168
+ // NEW FEATURE: Web GUI for Humans
169
+ if (req.method === 'GET' && (url.pathname === '/' || url.pathname === '/graphiql')) {
170
+ res.writeHead(200, { 'Content-Type': 'text/html' });
171
+ // Pass env.HEADERS to the explorer
172
+ return res.end((0, graphiql_js_1.renderGraphiQL)(env.ENDPOINT, env.HEADERS));
244
173
  }
245
174
  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;
175
+ let body = '';
176
+ req.on('data', chunk => { body += chunk; });
177
+ req.on('end', async () => {
178
+ try {
179
+ const { method, params, id } = JSON.parse(body);
180
+ console.error(`[HTTP-RPC] Method: ${method} | ID: ${id}`);
181
+ const handler = toolHandlers.get(method);
182
+ if (!handler) {
183
+ res.writeHead(404);
184
+ return res.end(JSON.stringify({ jsonrpc: '2.0', id, error: { code: -32601, message: "Method not found" } }));
185
+ }
186
+ const result = await handler(params);
187
+ res.writeHead(200, { 'Content-Type': 'application/json' });
188
+ res.end(JSON.stringify({ jsonrpc: '2.0', id, result }));
254
189
  }
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
- }
190
+ catch (e) {
191
+ console.error(`[HTTP-ERROR] ${e.message}`);
192
+ res.writeHead(500);
193
+ res.end(JSON.stringify({ jsonrpc: '2.0', error: { code: -32603, message: "Internal Error" } }));
194
+ }
195
+ });
263
196
  return;
264
197
  }
198
+ if (url.pathname === '/health') {
199
+ res.writeHead(200, { 'Content-Type': 'application/json' });
200
+ return res.end(JSON.stringify({ status: "ok", server: env.NAME }));
201
+ }
265
202
  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
- });
203
+ res.end("Not Found");
286
204
  }
287
205
  // --- STARTUP ---
288
206
  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
207
  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
- }
208
+ const serverHttp = node_http_1.default.createServer(handleHttpRequest);
209
+ serverHttp.listen(env.MCP_PORT, () => {
210
+ console.error(`[HTTP] Server started on http://localhost:${env.MCP_PORT}`);
211
+ console.error(`🎨 GraphiQL IDE: http://localhost:${env.MCP_PORT}/graphiql`);
212
+ console.error(`🤖 MCP Endpoint: http://localhost:${env.MCP_PORT}/mcp\n`);
213
+ });
304
214
  }
305
- await schemaPromise;
215
+ const transport = new stdio_js_1.StdioServerTransport();
216
+ await server.connect(transport);
217
+ console.error(`[STDIO] MCP Server "${env.NAME}" v${getVersion()} started`);
218
+ getSchema().catch(e => console.error(`[SCHEMA] Warning: Preload failed: ${e.message}`));
306
219
  }
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);
220
+ process.on('SIGINT', () => process.exit(0));
221
+ process.on('SIGTERM', () => process.exit(0));
316
222
  main().catch(error => {
317
- console.error(`Fatal error: ${error}`);
223
+ console.error(`[FATAL] ${error}`);
318
224
  process.exit(1);
319
225
  });
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.3.0"
53
54
  }