@letoribo/mcp-graphql-enhanced 3.1.0 → 3.2.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.
@@ -1,21 +1,19 @@
1
+ import { GraphQLSchema } from "graphql";
1
2
  /**
2
3
  * Introspect a GraphQL endpoint and return the schema as the GraphQL SDL
3
- * @param endpoint - The endpoint to introspect
4
- * @param headers - Optional headers to include in the request
5
- * @returns The schema
6
4
  */
7
5
  export declare function introspectEndpoint(endpoint: string, headers?: Record<string, string>): Promise<string>;
8
6
  /**
9
- * Introspect a GraphQL schema file hosted at a URL and return the schema as the GraphQL SDL
10
- * @param url - The URL to the schema file
11
- * @returns The schema
7
+ * Introspect a local GraphQL schema file
12
8
  */
13
- export declare function introspectSchemaFromUrl(url: string): Promise<string>;
9
+ export declare function introspectLocalSchema(path: string): Promise<string>;
14
10
  /**
15
- * Introspect a local GraphQL schema file and return the schema as the GraphQL SDL
16
- * @param path - The path to the local schema file
17
- * @returns The schema
11
+ * Extract and filter specific types from a schema object.
12
+ * Prevents "No result received" errors by only sending requested parts of the graph.
13
+ */
14
+ export declare function introspectSpecificTypes(schema: GraphQLSchema, typeNames: string[]): Record<string, any>;
15
+ /**
16
+ * Backwards compatibility helper for direct endpoint introspection
18
17
  */
19
- export declare function introspectLocalSchema(path: string): Promise<string>;
20
18
  export declare function introspectTypes(endpoint: string, headers: Record<string, string> | undefined, typeNames: string[]): Promise<string>;
21
19
  //# sourceMappingURL=introspection.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"introspection.d.ts","sourceRoot":"","sources":["../../src/helpers/introspection.ts"],"names":[],"mappings":"AAcA;;;;;GAKG;AACH,wBAAsB,kBAAkB,CACtC,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,mBAiBjC;AAED;;;;GAIG;AACH,wBAAsB,uBAAuB,CAAC,GAAG,EAAE,MAAM,mBASxD;AAED;;;;GAIG;AACH,wBAAsB,qBAAqB,CAAC,IAAI,EAAE,MAAM,mBAGvD;AAkBD,wBAAsB,eAAe,CACnC,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,YAAK,EACpC,SAAS,EAAE,MAAM,EAAE,mBAsFpB"}
1
+ {"version":3,"file":"introspection.d.ts","sourceRoot":"","sources":["../../src/helpers/introspection.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,aAAa,EAQd,MAAM,SAAS,CAAC;AAGjB;;GAEG;AACH,wBAAsB,kBAAkB,CACtC,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,mBAiBjC;AAED;;GAEG;AACH,wBAAsB,qBAAqB,CAAC,IAAI,EAAE,MAAM,mBAEvD;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,EAAE,uBAmEjF;AAED;;GAEG;AACH,wBAAsB,eAAe,CACnC,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,YAAK,EACpC,SAAS,EAAE,MAAM,EAAE,mBAYpB"}
@@ -1,23 +1,20 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.introspectEndpoint = introspectEndpoint;
4
- exports.introspectSchemaFromUrl = introspectSchemaFromUrl;
5
4
  exports.introspectLocalSchema = introspectLocalSchema;
5
+ exports.introspectSpecificTypes = introspectSpecificTypes;
6
6
  exports.introspectTypes = introspectTypes;
7
7
  const graphql_1 = require("graphql");
8
8
  const promises_1 = require("node:fs/promises");
9
9
  /**
10
10
  * Introspect a GraphQL endpoint and return the schema as the GraphQL SDL
11
- * @param endpoint - The endpoint to introspect
12
- * @param headers - Optional headers to include in the request
13
- * @returns The schema
14
11
  */
15
12
  async function introspectEndpoint(endpoint, headers) {
16
13
  const response = await fetch(endpoint, {
17
14
  method: "POST",
18
15
  headers: { "Content-Type": "application/json", ...headers },
19
16
  body: JSON.stringify({
20
- query: (0, graphql_1.getIntrospectionQuery)(), // Removed invalid options
17
+ query: (0, graphql_1.getIntrospectionQuery)(),
21
18
  }),
22
19
  });
23
20
  if (!response.ok) {
@@ -28,59 +25,26 @@ async function introspectEndpoint(endpoint, headers) {
28
25
  return (0, graphql_1.printSchema)(schema);
29
26
  }
30
27
  /**
31
- * Introspect a GraphQL schema file hosted at a URL and return the schema as the GraphQL SDL
32
- * @param url - The URL to the schema file
33
- * @returns The schema
28
+ * Introspect a local GraphQL schema file
34
29
  */
35
- async function introspectSchemaFromUrl(url) {
36
- const response = await fetch(url);
37
- if (!response.ok) {
38
- throw new Error(`Failed to fetch schema from URL: ${response.statusText}`);
39
- }
40
- const schema = await response.text();
41
- return schema;
30
+ async function introspectLocalSchema(path) {
31
+ return await (0, promises_1.readFile)(path, "utf8");
42
32
  }
43
33
  /**
44
- * Introspect a local GraphQL schema file and return the schema as the GraphQL SDL
45
- * @param path - The path to the local schema file
46
- * @returns The schema
34
+ * Extract and filter specific types from a schema object.
35
+ * Prevents "No result received" errors by only sending requested parts of the graph.
47
36
  */
48
- async function introspectLocalSchema(path) {
49
- const schema = await (0, promises_1.readFile)(path, "utf8");
50
- return schema;
51
- }
52
- function isObjectLikeType(type) {
53
- return 'getFields' in type;
54
- }
55
- function isUnionType(type) {
56
- return 'getTypes' in type;
57
- }
58
- function isEnumType(type) {
59
- return 'getValues' in type;
60
- }
61
- function isInputObjectType(type) {
62
- return 'getFields' in type;
63
- }
64
- async function introspectTypes(endpoint, headers = {}, typeNames) {
65
- const response = await fetch(endpoint, {
66
- method: "POST",
67
- headers: { "Content-Type": "application/json", ...headers },
68
- body: JSON.stringify({ query: (0, graphql_1.getIntrospectionQuery)() }),
69
- });
70
- const data = await response.json();
71
- const schema = (0, graphql_1.buildClientSchema)(data.data);
37
+ function introspectSpecificTypes(schema, typeNames) {
72
38
  const result = {};
73
39
  for (const name of typeNames) {
74
40
  const type = schema.getType(name);
75
41
  if (!type)
76
42
  continue;
77
- // Handle object/interface types
78
- if (isObjectLikeType(type)) {
43
+ if ((0, graphql_1.isObjectType)(type) || (0, graphql_1.isInterfaceType)(type)) {
79
44
  result[name] = {
80
- kind: type instanceof graphql_1.GraphQLObjectType ? "OBJECT" : "INTERFACE",
45
+ kind: (0, graphql_1.isInterfaceType)(type) ? "INTERFACE" : "OBJECT",
81
46
  description: type.description,
82
47
  fields: Object.fromEntries(Object.entries(type.getFields())
83
- // Filter out deprecated fields
84
48
  .filter(([_, field]) => !field.deprecationReason)
85
49
  .map(([fieldName, field]) => [
86
50
  fieldName,
@@ -88,7 +52,6 @@ async function introspectTypes(endpoint, headers = {}, typeNames) {
88
52
  type: field.type.toString(),
89
53
  description: field.description,
90
54
  args: field.args
91
- // Filter out deprecated arguments
92
55
  .filter(arg => !arg.deprecationReason)
93
56
  .map(arg => ({
94
57
  name: arg.name,
@@ -99,16 +62,19 @@ async function introspectTypes(endpoint, headers = {}, typeNames) {
99
62
  ]))
100
63
  };
101
64
  }
102
- // Handle union types
103
- else if (isUnionType(type)) {
65
+ else if ((0, graphql_1.isInputObjectType)(type)) {
104
66
  result[name] = {
105
- kind: "UNION",
67
+ kind: "INPUT_OBJECT",
106
68
  description: type.description,
107
- possibleTypes: type.getTypes().map(t => t.name)
69
+ fields: Object.fromEntries(Object.entries(type.getFields())
70
+ .filter(([_, field]) => !field.deprecationReason)
71
+ .map(([fieldName, field]) => [
72
+ fieldName,
73
+ { type: field.type.toString(), description: field.description }
74
+ ]))
108
75
  };
109
76
  }
110
- // Handle enums
111
- else if (isEnumType(type)) {
77
+ else if ((0, graphql_1.isEnumType)(type)) {
112
78
  result[name] = {
113
79
  kind: "ENUM",
114
80
  description: type.description,
@@ -118,26 +84,33 @@ async function introspectTypes(endpoint, headers = {}, typeNames) {
118
84
  }))
119
85
  };
120
86
  }
121
- // Handle scalars and input objects
122
- else if (isInputObjectType(type)) {
87
+ else if ((0, graphql_1.isUnionType)(type)) {
123
88
  result[name] = {
124
- kind: "INPUT_OBJECT",
89
+ kind: "UNION",
125
90
  description: type.description,
126
- fields: Object.fromEntries(Object.entries(type.getFields())
127
- // FILTER: Skip deprecated input fields
128
- .filter(([_, field]) => !field.deprecationReason)
129
- .map(([fieldName, field]) => [
130
- fieldName,
131
- { type: field.type.toString(), description: field.description }
132
- ]))
91
+ possibleTypes: type.getTypes().map(t => t.name)
133
92
  };
134
93
  }
135
- else if (type instanceof graphql_1.GraphQLScalarType) {
94
+ else if ((0, graphql_1.isScalarType)(type)) {
136
95
  result[name] = {
137
96
  kind: "SCALAR",
138
97
  description: type.description
139
98
  };
140
99
  }
141
100
  }
142
- return JSON.stringify(result, null, 2);
101
+ return result;
102
+ }
103
+ /**
104
+ * Backwards compatibility helper for direct endpoint introspection
105
+ */
106
+ async function introspectTypes(endpoint, headers = {}, typeNames) {
107
+ const response = await fetch(endpoint, {
108
+ method: "POST",
109
+ headers: { "Content-Type": "application/json", ...headers },
110
+ body: JSON.stringify({ query: (0, graphql_1.getIntrospectionQuery)() }),
111
+ });
112
+ const data = await response.json();
113
+ const schema = (0, graphql_1.buildClientSchema)(data.data);
114
+ const result = introspectSpecificTypes(schema, typeNames);
115
+ return JSON.stringify(result);
143
116
  }
package/dist/index.js CHANGED
@@ -10,7 +10,7 @@ const { StdioServerTransport } = require("@modelcontextprotocol/sdk/server/stdio
10
10
  const { parse } = require("graphql/language");
11
11
  const z = require("zod").default;
12
12
  const { checkDeprecatedArguments } = require("./helpers/deprecation.js");
13
- const { introspectEndpoint, introspectLocalSchema, introspectSchemaFromUrl, introspectTypes, } = require("./helpers/introspection.js");
13
+ const { introspectEndpoint, introspectLocalSchema, introspectSchemaFromUrl, introspectSpecificTypes, } = require("./helpers/introspection.js");
14
14
  const getVersion = () => {
15
15
  const pkg = require("../package.json");
16
16
  return pkg.version;
@@ -40,13 +40,11 @@ const EnvSchema = z.object({
40
40
  .enum(["true", "false", "auto"])
41
41
  .transform((value) => {
42
42
  if (value === "auto") {
43
- // Auto-detect: enable HTTP if running in MCP Inspector
44
- // Inspector sets specific environment variables
45
43
  return !!(process.env.MCP_INSPECTOR || process.env.INSPECTOR_PORT);
46
44
  }
47
45
  return value === "true";
48
46
  })
49
- .default("auto"), // Auto-detect by default
47
+ .default("auto"),
50
48
  });
51
49
  const env = EnvSchema.parse(process.env);
52
50
  const server = new McpServer({
@@ -54,136 +52,160 @@ const server = new McpServer({
54
52
  version: getVersion(),
55
53
  description: `GraphQL MCP server for ${env.ENDPOINT}`,
56
54
  });
57
- // Cache schema to avoid repeated introspection
58
- let cachedSchema = null;
55
+ // --- CACHE STATE ---
56
+ let cachedSDL = null;
57
+ let cachedSchemaObject = null;
59
58
  let schemaLoadError = null;
59
+ /**
60
+ * Loads the schema into memory.
61
+ * Populates both cachedSDL (string) and cachedSchemaObject (GraphQLSchema object).
62
+ */
60
63
  async function getSchema() {
61
- // Return cached schema if available
62
- if (cachedSchema) {
63
- return cachedSchema;
64
- }
65
- // Return cached error if schema failed to load
66
- if (schemaLoadError) {
64
+ if (cachedSDL)
65
+ return cachedSDL;
66
+ if (schemaLoadError)
67
67
  throw schemaLoadError;
68
- }
69
68
  try {
70
- let schema;
69
+ const { buildClientSchema, getIntrospectionQuery, printSchema, buildASTSchema, parse: gqlParse } = require("graphql");
70
+ let sdl;
71
71
  if (env.SCHEMA) {
72
- if (env.SCHEMA.startsWith("http://") ||
73
- env.SCHEMA.startsWith("https://")) {
74
- schema = await introspectSchemaFromUrl(env.SCHEMA);
72
+ if (env.SCHEMA.startsWith("http")) {
73
+ sdl = await introspectSchemaFromUrl(env.SCHEMA);
75
74
  }
76
75
  else {
77
- schema = await introspectLocalSchema(env.SCHEMA);
76
+ sdl = await introspectLocalSchema(env.SCHEMA);
78
77
  }
78
+ cachedSchemaObject = buildASTSchema(gqlParse(sdl));
79
+ cachedSDL = sdl;
79
80
  }
80
81
  else {
81
- schema = await introspectEndpoint(env.ENDPOINT, env.HEADERS);
82
+ // Live Introspection
83
+ const response = await fetch(env.ENDPOINT, {
84
+ method: "POST",
85
+ headers: { "Content-Type": "application/json", ...env.HEADERS },
86
+ body: JSON.stringify({ query: getIntrospectionQuery() }),
87
+ });
88
+ if (!response.ok)
89
+ throw new Error(`Fetch failed: ${response.statusText}`);
90
+ const result = await response.json();
91
+ cachedSchemaObject = buildClientSchema(result.data);
92
+ cachedSDL = printSchema(cachedSchemaObject);
82
93
  }
83
- // Cache the schema
84
- cachedSchema = schema;
85
94
  console.error(`[SCHEMA] Successfully loaded and cached GraphQL schema`);
86
- return schema;
95
+ return cachedSDL;
87
96
  }
88
97
  catch (error) {
89
- schemaLoadError = error;
90
- throw new Error(`Failed to get GraphQL schema: ${error}`);
98
+ schemaLoadError = error instanceof Error ? error : new Error(String(error));
99
+ throw schemaLoadError;
91
100
  }
92
101
  }
102
+ // --- RESOURCES ---
93
103
  server.resource("graphql-schema", new URL(env.ENDPOINT).href, async (uri) => {
94
104
  try {
95
- const schema = await getSchema();
96
- return {
97
- contents: [
98
- {
99
- uri: uri.href,
100
- text: schema,
101
- },
102
- ],
103
- };
105
+ const sdl = await getSchema();
106
+ return { contents: [{ uri: uri.href, text: sdl }] };
104
107
  }
105
108
  catch (error) {
106
109
  throw error;
107
110
  }
108
111
  });
112
+ // --- TOOL HANDLERS ---
109
113
  const toolHandlers = new Map();
110
- const introspectSchemaHandler = async ({ typeNames, descriptions = true, directives = true }) => {
111
- if (typeNames === null) {
112
- typeNames = undefined;
113
- }
114
+ const introspectSchemaHandler = async ({ typeNames }) => {
114
115
  try {
115
- if (typeNames && typeNames.length > 0) {
116
- const filtered = await introspectTypes(env.ENDPOINT, env.HEADERS, typeNames);
117
- return { content: [{ type: "text", text: filtered }] };
118
- }
119
- else {
120
- const schema = await getSchema();
121
- return { content: [{ type: "text", text: schema }] };
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
+ };
122
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
+ };
123
137
  }
124
138
  catch (error) {
125
- throw new Error(`Introspection failed: ${error}`); // ✅ Throw instead
139
+ throw new Error(`Introspection failed: ${error.message}`);
126
140
  }
127
141
  };
128
142
  toolHandlers.set("introspect-schema", introspectSchemaHandler);
129
143
  server.tool("introspect-schema", "Introspect the GraphQL schema. Optionally filter to specific types.", {
130
- typeNames: z.array(z.string()).optional().describe("A list of specific type names to filter the introspection."),
131
- descriptions: z.boolean().optional().default(true),
132
- directives: z.boolean().optional().default(true),
133
- }, introspectSchemaHandler);
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
+ });
134
177
  const queryGraphqlHandler = async ({ query, variables, headers }) => {
135
178
  try {
136
179
  const parsedQuery = parse(query);
137
180
  const isMutation = parsedQuery.definitions.some((def) => def.kind === "OperationDefinition" && def.operation === "mutation");
138
181
  if (isMutation && !env.ALLOW_MUTATIONS) {
139
- throw new Error("Mutations are not allowed unless you enable them in the configuration. Please use a query operation instead.");
182
+ throw new Error("Mutations are not allowed. Enable ALLOW_MUTATIONS in config.");
140
183
  }
141
184
  }
142
185
  catch (error) {
143
186
  throw new Error(`Invalid GraphQL query: ${error}`);
144
187
  }
145
188
  try {
146
- const toolHeaders = headers
147
- ? JSON.parse(headers)
148
- : {};
149
- const allHeaders = {
150
- "Content-Type": "application/json",
151
- ...env.HEADERS,
152
- ...toolHeaders,
153
- };
154
- let parsedVariables = null;
155
- if (variables) {
156
- if (typeof variables === 'string') {
157
- parsedVariables = JSON.parse(variables);
158
- }
159
- else {
160
- parsedVariables = variables;
161
- }
162
- }
189
+ const toolHeaders = headers ? JSON.parse(headers) : {};
190
+ const allHeaders = { "Content-Type": "application/json", ...env.HEADERS, ...toolHeaders };
191
+ let parsedVariables = variables;
192
+ if (typeof variables === 'string')
193
+ parsedVariables = JSON.parse(variables);
163
194
  const response = await fetch(env.ENDPOINT, {
164
195
  method: "POST",
165
196
  headers: allHeaders,
166
- body: JSON.stringify({
167
- query,
168
- variables: parsedVariables,
169
- }),
197
+ body: JSON.stringify({ query, variables: parsedVariables }),
170
198
  });
171
199
  if (!response.ok) {
172
200
  const responseText = await response.text();
173
201
  throw new Error(`GraphQL request failed: ${response.statusText}\n${responseText}`);
174
202
  }
175
- const rawData = await response.json();
176
- const data = rawData;
203
+ const data = await response.json();
177
204
  if (data.errors && data.errors.length > 0) {
178
205
  throw new Error(`GraphQL errors: ${JSON.stringify(data.errors, null, 2)}`);
179
206
  }
180
207
  return {
181
- content: [
182
- {
183
- type: "text",
184
- text: JSON.stringify(data, null, 2),
185
- },
186
- ],
208
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
187
209
  };
188
210
  }
189
211
  catch (error) {
@@ -191,23 +213,17 @@ const queryGraphqlHandler = async ({ query, variables, headers }) => {
191
213
  }
192
214
  };
193
215
  toolHandlers.set("query-graphql", queryGraphqlHandler);
194
- server.tool("query-graphql", "Query a GraphQL endpoint with the given query and variables. Optionally pass headers (e.g., for Authorization).", {
216
+ server.tool("query-graphql", "Query a GraphQL endpoint with the given query and variables.", {
195
217
  query: z.string(),
196
218
  variables: z.string().optional(),
197
- headers: z
198
- .string()
199
- .optional()
200
- .describe("Optional JSON string of headers to include, e.g., {\"Authorization\": \"Bearer ...\"}"),
219
+ headers: z.string().optional().describe("Optional JSON string of headers"),
201
220
  }, queryGraphqlHandler);
221
+ // --- HTTP SERVER LOGIC ---
202
222
  function readBody(req) {
203
223
  return new Promise((resolve, reject) => {
204
224
  let body = '';
205
- req.on('data', (chunk) => {
206
- body += chunk.toString();
207
- });
208
- req.on('end', () => {
209
- resolve(body);
210
- });
225
+ req.on('data', chunk => body += chunk.toString());
226
+ req.on('end', () => resolve(body));
211
227
  req.on('error', reject);
212
228
  });
213
229
  }
@@ -227,157 +243,77 @@ async function handleHttpRequest(req, res) {
227
243
  return;
228
244
  }
229
245
  if (url.pathname === '/mcp' && req.method === 'POST') {
230
- let rawBody;
231
- let request;
232
- try {
233
- rawBody = await readBody(req);
234
- request = JSON.parse(rawBody);
235
- }
236
- catch (e) {
237
- console.error("HTTP MCP Parse Error:", e);
238
- res.writeHead(400, { 'Content-Type': 'application/json' });
239
- res.end(JSON.stringify({
240
- jsonrpc: '2.0',
241
- id: null,
242
- error: { code: -32700, message: 'Parse Error: Invalid JSON received in request body.' }
243
- }));
244
- return;
245
- }
246
246
  try {
247
- const { method, params, id } = request;
248
- if (!method || typeof id === 'undefined') {
249
- res.writeHead(400, { 'Content-Type': 'application/json' });
250
- res.end(JSON.stringify({
251
- jsonrpc: '2.0',
252
- id: id || null,
253
- error: { code: -32600, message: 'Invalid JSON-RPC Request structure (missing method or id).' }
254
- }));
255
- return;
256
- }
247
+ const rawBody = await readBody(req);
248
+ const { method, params, id } = JSON.parse(rawBody);
257
249
  const handler = toolHandlers.get(method);
258
250
  if (!handler) {
259
- res.writeHead(404, { 'Content-Type': 'application/json' });
260
- res.end(JSON.stringify({
261
- jsonrpc: '2.0',
262
- id: id,
263
- error: { code: -32601, message: `Method not found: ${method}` }
264
- }));
251
+ res.writeHead(404);
252
+ res.end(JSON.stringify({ jsonrpc: '2.0', id, error: { code: -32601, message: 'Method not found' } }));
265
253
  return;
266
254
  }
267
255
  const result = await handler(params);
268
256
  res.writeHead(200, { 'Content-Type': 'application/json' });
269
- res.end(JSON.stringify({
270
- jsonrpc: '2.0',
271
- id: id,
272
- result: result
273
- }));
257
+ res.end(JSON.stringify({ jsonrpc: '2.0', id, result }));
274
258
  }
275
259
  catch (error) {
276
- console.error("HTTP MCP Execution Error:", error);
277
- res.writeHead(500, { 'Content-Type': 'application/json' });
278
- res.end(JSON.stringify({
279
- jsonrpc: '2.0',
280
- id: request?.id || null,
281
- error: { code: -32603, message: 'Internal server error during tool execution.' }
282
- }));
260
+ res.writeHead(500);
261
+ res.end(JSON.stringify({ jsonrpc: '2.0', error: { code: -32603, message: 'Internal error' } }));
283
262
  }
284
263
  return;
285
264
  }
286
- res.writeHead(404, { 'Content-Type': 'text/plain' });
287
- res.end('Not Found. Use POST /mcp for JSON-RPC or GET /health.');
265
+ res.writeHead(404);
266
+ res.end('Not Found');
288
267
  }
289
- // Single HTTP server instance
290
268
  let httpServer = null;
291
- /**
292
- * Tries to listen on a given port with a single retry attempt.
293
- * Returns the port it successfully bound to.
294
- */
295
269
  async function startHttpServer(initialPort) {
296
270
  return new Promise((resolve, reject) => {
297
- let currentPort = initialPort;
298
- const maxAttempts = 10;
299
- let attempts = 0;
300
- function tryPort(port) {
301
- if (attempts >= maxAttempts) {
302
- reject(new Error(`Failed to bind HTTP server after ${maxAttempts} attempts`));
303
- return;
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));
304
277
  }
305
- if (port > 65535) {
306
- reject(new Error(`Exceeded maximum port number (65535)`));
307
- return;
308
- }
309
- attempts++;
310
- const server = node_http_1.default.createServer(handleHttpRequest);
311
- server.once('error', (err) => {
312
- if (err.code === 'EADDRINUSE') {
313
- console.error(`[HTTP] Port ${port} in use, trying ${port + 1}...`);
314
- server.close();
315
- tryPort(port + 1);
316
- }
317
- else {
318
- reject(err);
319
- }
320
- });
321
- server.listen(port, () => {
322
- httpServer = server;
323
- console.error(`[HTTP] Server started on http://localhost:${port}`);
324
- resolve(port);
325
- });
326
- }
327
- tryPort(currentPort);
278
+ else
279
+ reject(err);
280
+ });
281
+ server.listen(port, () => {
282
+ httpServer = server;
283
+ resolve(port);
284
+ });
328
285
  });
329
286
  }
287
+ // --- STARTUP ---
330
288
  async function main() {
331
- // Pre-load schema FIRST (parallel with server setup)
332
289
  console.error(`[SCHEMA] Pre-loading GraphQL schema...`);
333
- const schemaPromise = getSchema().catch((error) => {
290
+ const schemaPromise = getSchema().catch(error => {
334
291
  console.error(`[SCHEMA] Warning: Failed to pre-load schema: ${error}`);
335
292
  });
336
293
  const stdioTransport = new StdioServerTransport();
337
294
  await server.connect(stdioTransport);
338
- console.error(`[STDIO] Started GraphQL MCP server ${env.NAME} for endpoint: ${env.ENDPOINT}`);
339
- // Only start HTTP if needed
295
+ console.error(`[STDIO] Started GraphQL MCP server ${env.NAME}`);
340
296
  if (env.ENABLE_HTTP) {
341
297
  try {
342
298
  const port = await startHttpServer(env.MCP_PORT);
343
- console.error(`[HTTP] Listening on port ${port} for POST /mcp requests`);
299
+ console.error(`[HTTP] Server started on http://localhost:${port}`);
344
300
  }
345
301
  catch (error) {
346
- console.error(`[HTTP] Failed to start HTTP server: ${error}`);
302
+ console.error(`[HTTP] Failed to start: ${error}`);
347
303
  }
348
304
  }
349
- else {
350
- console.error(`[HTTP] HTTP transport disabled (ENABLE_HTTP=auto|true to enable)`);
351
- }
352
- // Wait for schema to finish loading
353
305
  await schemaPromise;
354
306
  }
355
- // Graceful shutdown
356
- process.on('SIGINT', () => {
357
- console.error('\n[SHUTDOWN] Received SIGINT, closing server...');
358
- if (httpServer) {
359
- httpServer.close(() => {
360
- console.error('[SHUTDOWN] HTTP server closed');
361
- process.exit(0);
362
- });
363
- }
364
- else {
365
- process.exit(0);
366
- }
367
- });
368
- process.on('SIGTERM', () => {
369
- console.error('\n[SHUTDOWN] Received SIGTERM, closing server...');
370
- if (httpServer) {
371
- httpServer.close(() => {
372
- console.error('[SHUTDOWN] HTTP server closed');
373
- process.exit(0);
374
- });
375
- }
376
- else {
307
+ // Graceful exit
308
+ const shutdown = () => {
309
+ if (httpServer)
310
+ httpServer.close(() => process.exit(0));
311
+ else
377
312
  process.exit(0);
378
- }
379
- });
380
- main().catch((error) => {
381
- console.error(`Fatal error in main(): ${error}`);
313
+ };
314
+ process.on('SIGINT', shutdown);
315
+ process.on('SIGTERM', shutdown);
316
+ main().catch(error => {
317
+ console.error(`Fatal error: ${error}`);
382
318
  process.exit(1);
383
319
  });
package/package.json CHANGED
@@ -49,5 +49,5 @@
49
49
  "ts-node": "^10.9.2",
50
50
  "typescript": "5.8.3"
51
51
  },
52
- "version": "3.1.0"
52
+ "version": "3.2.0"
53
53
  }