@letoribo/mcp-graphql-enhanced 3.0.1 → 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,CACvC,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,mBAuBhC;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,mBA6EpB"}
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,24 +1,18 @@
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
- headers: {
19
- "Content-Type": "application/json",
20
- ...headers,
21
- },
15
+ headers: { "Content-Type": "application/json", ...headers },
22
16
  body: JSON.stringify({
23
17
  query: (0, graphql_1.getIntrospectionQuery)(),
24
18
  }),
@@ -27,69 +21,39 @@ async function introspectEndpoint(endpoint, headers) {
27
21
  throw new Error(`GraphQL request failed: ${response.statusText}`);
28
22
  }
29
23
  const responseJson = await response.json();
30
- // Transform to a schema object
31
24
  const schema = (0, graphql_1.buildClientSchema)(responseJson.data);
32
- // Print the schema SDL
33
25
  return (0, graphql_1.printSchema)(schema);
34
26
  }
35
27
  /**
36
- * Introspect a GraphQL schema file hosted at a URL and return the schema as the GraphQL SDL
37
- * @param url - The URL to the schema file
38
- * @returns The schema
28
+ * Introspect a local GraphQL schema file
39
29
  */
40
- async function introspectSchemaFromUrl(url) {
41
- const response = await fetch(url);
42
- if (!response.ok) {
43
- throw new Error(`Failed to fetch schema from URL: ${response.statusText}`);
44
- }
45
- const schema = await response.text();
46
- return schema;
30
+ async function introspectLocalSchema(path) {
31
+ return await (0, promises_1.readFile)(path, "utf8");
47
32
  }
48
33
  /**
49
- * Introspect a local GraphQL schema file and return the schema as the GraphQL SDL
50
- * @param path - The path to the local schema file
51
- * @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.
52
36
  */
53
- async function introspectLocalSchema(path) {
54
- const schema = await (0, promises_1.readFile)(path, "utf8");
55
- return schema;
56
- }
57
- function isObjectLikeType(type) {
58
- return 'getFields' in type;
59
- }
60
- function isUnionType(type) {
61
- return 'getTypes' in type;
62
- }
63
- function isEnumType(type) {
64
- return 'getValues' in type;
65
- }
66
- function isInputObjectType(type) {
67
- return 'getFields' in type;
68
- }
69
- async function introspectTypes(endpoint, headers = {}, typeNames) {
70
- const response = await fetch(endpoint, {
71
- method: "POST",
72
- headers: { "Content-Type": "application/json", ...headers },
73
- body: JSON.stringify({ query: (0, graphql_1.getIntrospectionQuery)() }),
74
- });
75
- const data = await response.json();
76
- const schema = (0, graphql_1.buildClientSchema)(data.data);
37
+ function introspectSpecificTypes(schema, typeNames) {
77
38
  const result = {};
78
39
  for (const name of typeNames) {
79
40
  const type = schema.getType(name);
80
41
  if (!type)
81
42
  continue;
82
- // Handle object/interface types
83
- if (isObjectLikeType(type)) {
43
+ if ((0, graphql_1.isObjectType)(type) || (0, graphql_1.isInterfaceType)(type)) {
84
44
  result[name] = {
85
- kind: type instanceof graphql_1.GraphQLObjectType ? "OBJECT" : "INTERFACE",
45
+ kind: (0, graphql_1.isInterfaceType)(type) ? "INTERFACE" : "OBJECT",
86
46
  description: type.description,
87
- fields: Object.fromEntries(Object.entries(type.getFields()).map(([fieldName, field]) => [
47
+ fields: Object.fromEntries(Object.entries(type.getFields())
48
+ .filter(([_, field]) => !field.deprecationReason)
49
+ .map(([fieldName, field]) => [
88
50
  fieldName,
89
51
  {
90
52
  type: field.type.toString(),
91
53
  description: field.description,
92
- args: field.args.map(arg => ({
54
+ args: field.args
55
+ .filter(arg => !arg.deprecationReason)
56
+ .map(arg => ({
93
57
  name: arg.name,
94
58
  type: arg.type.toString(),
95
59
  description: arg.description,
@@ -98,16 +62,19 @@ async function introspectTypes(endpoint, headers = {}, typeNames) {
98
62
  ]))
99
63
  };
100
64
  }
101
- // Handle union types
102
- else if (isUnionType(type)) {
65
+ else if ((0, graphql_1.isInputObjectType)(type)) {
103
66
  result[name] = {
104
- kind: "UNION",
67
+ kind: "INPUT_OBJECT",
105
68
  description: type.description,
106
- 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
+ ]))
107
75
  };
108
76
  }
109
- // Handle enums
110
- else if (isEnumType(type)) {
77
+ else if ((0, graphql_1.isEnumType)(type)) {
111
78
  result[name] = {
112
79
  kind: "ENUM",
113
80
  description: type.description,
@@ -117,23 +84,33 @@ async function introspectTypes(endpoint, headers = {}, typeNames) {
117
84
  }))
118
85
  };
119
86
  }
120
- // Handle scalars and input objects
121
- else if (isInputObjectType(type)) {
87
+ else if ((0, graphql_1.isUnionType)(type)) {
122
88
  result[name] = {
123
- kind: "INPUT_OBJECT",
89
+ kind: "UNION",
124
90
  description: type.description,
125
- fields: Object.fromEntries(Object.entries(type.getFields()).map(([fieldName, field]) => [
126
- fieldName,
127
- { type: field.type.toString(), description: field.description }
128
- ]))
91
+ possibleTypes: type.getTypes().map(t => t.name)
129
92
  };
130
93
  }
131
- else if (type instanceof graphql_1.GraphQLScalarType) {
94
+ else if ((0, graphql_1.isScalarType)(type)) {
132
95
  result[name] = {
133
96
  kind: "SCALAR",
134
97
  description: type.description
135
98
  };
136
99
  }
137
100
  }
138
- 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);
139
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,203 +52,178 @@ 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
  }
123
- }
124
- catch (error) {
129
+ // Use the new filtering logic from helpers/introspection.js
130
+ const filteredResult = introspectSpecificTypes(cachedSchemaObject, typeNames);
125
131
  return {
126
- isError: true,
127
- content: [{ type: "text", text: `Introspection failed: ${error}` }],
132
+ content: [{
133
+ type: "text",
134
+ text: JSON.stringify(filteredResult, null, 2)
135
+ }]
128
136
  };
129
137
  }
138
+ catch (error) {
139
+ throw new Error(`Introspection failed: ${error.message}`);
140
+ }
130
141
  };
131
142
  toolHandlers.set("introspect-schema", introspectSchemaHandler);
132
143
  server.tool("introspect-schema", "Introspect the GraphQL schema. Optionally filter to specific types.", {
133
- typeNames: z.array(z.string()).optional().describe("A list of specific type names to filter the introspection."),
134
- descriptions: z.boolean().optional().default(true),
135
- directives: z.boolean().optional().default(true),
136
- }, introspectSchemaHandler);
137
- const queryGraphqlHandler = async ({ query, variables, headers }) => {
144
+ typeNames: z.array(z.string()).optional().describe("A list of specific type names to filter (e.g. ['User', 'Post'])."),
145
+ }, async ({ typeNames }) => {
138
146
  try {
139
- const parsedQuery = parse(query);
140
- const isMutation = parsedQuery.definitions.some((def) => def.kind === "OperationDefinition" && def.operation === "mutation");
141
- if (isMutation && !env.ALLOW_MUTATIONS) {
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.`);
142
155
  return {
143
- isError: true,
144
- content: [
145
- {
156
+ content: [{
146
157
  type: "text",
147
- text: "Mutations are not allowed unless you enable them in the configuration. Please use a query operation instead.",
148
- },
149
- ],
158
+ text: `Schema is large. Please request specific types for details.\n\nAvailable types: ${allTypeNames.join(", ")}`
159
+ }]
150
160
  };
151
161
  }
152
- }
153
- catch (error) {
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);
154
165
  return {
155
- isError: true,
156
- content: [
157
- {
166
+ content: [{
158
167
  type: "text",
159
- text: `Invalid GraphQL query: ${error}`,
160
- },
161
- ],
168
+ text: JSON.stringify(filteredResult, null, 2)
169
+ }]
162
170
  };
163
171
  }
172
+ catch (error) {
173
+ console.error(`[TOOL ERROR] ${error.message}`);
174
+ throw new Error(`Introspection failed: ${error.message}`);
175
+ }
176
+ });
177
+ const queryGraphqlHandler = async ({ query, variables, headers }) => {
164
178
  try {
165
- const toolHeaders = headers
166
- ? JSON.parse(headers)
167
- : {};
168
- const allHeaders = {
169
- "Content-Type": "application/json",
170
- ...env.HEADERS,
171
- ...toolHeaders,
172
- };
173
- let parsedVariables = null;
174
- if (variables) {
175
- if (typeof variables === 'string') {
176
- parsedVariables = JSON.parse(variables);
177
- }
178
- else {
179
- parsedVariables = variables;
180
- }
179
+ const parsedQuery = parse(query);
180
+ 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.");
181
183
  }
184
+ }
185
+ catch (error) {
186
+ throw new Error(`Invalid GraphQL query: ${error}`);
187
+ }
188
+ try {
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);
182
194
  const response = await fetch(env.ENDPOINT, {
183
195
  method: "POST",
184
196
  headers: allHeaders,
185
- body: JSON.stringify({
186
- query,
187
- variables: parsedVariables,
188
- }),
197
+ body: JSON.stringify({ query, variables: parsedVariables }),
189
198
  });
190
199
  if (!response.ok) {
191
200
  const responseText = await response.text();
192
- return {
193
- isError: true,
194
- content: [
195
- {
196
- type: "text",
197
- text: `GraphQL request failed: ${response.statusText}\n${responseText}`,
198
- },
199
- ],
200
- };
201
+ throw new Error(`GraphQL request failed: ${response.statusText}\n${responseText}`);
201
202
  }
202
- const rawData = await response.json();
203
- const data = rawData;
203
+ const data = await response.json();
204
204
  if (data.errors && data.errors.length > 0) {
205
- return {
206
- isError: true,
207
- content: [
208
- {
209
- type: "text",
210
- text: `GraphQL errors: ${JSON.stringify(data.errors, null, 2)}`,
211
- },
212
- ],
213
- };
205
+ throw new Error(`GraphQL errors: ${JSON.stringify(data.errors, null, 2)}`);
214
206
  }
215
207
  return {
216
- content: [
217
- {
218
- type: "text",
219
- text: JSON.stringify(data, null, 2),
220
- },
221
- ],
208
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
222
209
  };
223
210
  }
224
211
  catch (error) {
225
- return {
226
- isError: true,
227
- content: [
228
- {
229
- type: "text",
230
- text: `Failed to execute GraphQL query: ${error}`,
231
- },
232
- ],
233
- };
212
+ throw new Error(`Failed to execute GraphQL query: ${error}`);
234
213
  }
235
214
  };
236
215
  toolHandlers.set("query-graphql", queryGraphqlHandler);
237
- 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.", {
238
217
  query: z.string(),
239
218
  variables: z.string().optional(),
240
- headers: z
241
- .string()
242
- .optional()
243
- .describe("Optional JSON string of headers to include, e.g., {\"Authorization\": \"Bearer ...\"}"),
219
+ headers: z.string().optional().describe("Optional JSON string of headers"),
244
220
  }, queryGraphqlHandler);
221
+ // --- HTTP SERVER LOGIC ---
245
222
  function readBody(req) {
246
223
  return new Promise((resolve, reject) => {
247
224
  let body = '';
248
- req.on('data', (chunk) => {
249
- body += chunk.toString();
250
- });
251
- req.on('end', () => {
252
- resolve(body);
253
- });
225
+ req.on('data', chunk => body += chunk.toString());
226
+ req.on('end', () => resolve(body));
254
227
  req.on('error', reject);
255
228
  });
256
229
  }
@@ -270,156 +243,77 @@ async function handleHttpRequest(req, res) {
270
243
  return;
271
244
  }
272
245
  if (url.pathname === '/mcp' && req.method === 'POST') {
273
- let rawBody;
274
- let request;
275
246
  try {
276
- rawBody = await readBody(req);
277
- request = JSON.parse(rawBody);
278
- }
279
- catch (e) {
280
- console.error("HTTP MCP Parse Error:", e);
281
- res.writeHead(400, { 'Content-Type': 'application/json' });
282
- res.end(JSON.stringify({
283
- jsonrpc: '2.0',
284
- id: null,
285
- error: { code: -32700, message: 'Parse Error: Invalid JSON received in request body.' }
286
- }));
287
- return;
288
- }
289
- try {
290
- const { method, params, id } = request;
291
- if (!method || typeof id === 'undefined') {
292
- res.writeHead(400, { 'Content-Type': 'application/json' });
293
- res.end(JSON.stringify({
294
- jsonrpc: '2.0',
295
- id: id || null,
296
- error: { code: -32600, message: 'Invalid JSON-RPC Request structure (missing method or id).' }
297
- }));
298
- return;
299
- }
247
+ const rawBody = await readBody(req);
248
+ const { method, params, id } = JSON.parse(rawBody);
300
249
  const handler = toolHandlers.get(method);
301
250
  if (!handler) {
302
- res.writeHead(404, { 'Content-Type': 'application/json' });
303
- res.end(JSON.stringify({
304
- jsonrpc: '2.0',
305
- id: id,
306
- error: { code: -32601, message: `Method not found: ${method}` }
307
- }));
251
+ res.writeHead(404);
252
+ res.end(JSON.stringify({ jsonrpc: '2.0', id, error: { code: -32601, message: 'Method not found' } }));
308
253
  return;
309
254
  }
310
255
  const result = await handler(params);
311
256
  res.writeHead(200, { 'Content-Type': 'application/json' });
312
- res.end(JSON.stringify({
313
- jsonrpc: '2.0',
314
- id: id,
315
- result: result
316
- }));
257
+ res.end(JSON.stringify({ jsonrpc: '2.0', id, result }));
317
258
  }
318
259
  catch (error) {
319
- console.error("HTTP MCP Execution Error:", error);
320
- res.writeHead(500, { 'Content-Type': 'application/json' });
321
- res.end(JSON.stringify({
322
- jsonrpc: '2.0',
323
- id: request?.id || null,
324
- error: { code: -32603, message: 'Internal server error during tool execution.' }
325
- }));
260
+ res.writeHead(500);
261
+ res.end(JSON.stringify({ jsonrpc: '2.0', error: { code: -32603, message: 'Internal error' } }));
326
262
  }
327
263
  return;
328
264
  }
329
- res.writeHead(404, { 'Content-Type': 'text/plain' });
330
- res.end('Not Found. Use POST /mcp for JSON-RPC or GET /health.');
265
+ res.writeHead(404);
266
+ res.end('Not Found');
331
267
  }
332
- // Single HTTP server instance
333
268
  let httpServer = null;
334
- /**
335
- * Tries to listen on a given port with a single retry attempt.
336
- * Returns the port it successfully bound to.
337
- */
338
269
  async function startHttpServer(initialPort) {
339
270
  return new Promise((resolve, reject) => {
340
- let currentPort = initialPort;
341
- const maxAttempts = 10;
342
- let attempts = 0;
343
- function tryPort(port) {
344
- if (attempts >= maxAttempts) {
345
- reject(new Error(`Failed to bind HTTP server after ${maxAttempts} attempts`));
346
- return;
347
- }
348
- if (port > 65535) {
349
- reject(new Error(`Exceeded maximum port number (65535)`));
350
- 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));
351
277
  }
352
- attempts++;
353
- const server = node_http_1.default.createServer(handleHttpRequest);
354
- server.once('error', (err) => {
355
- if (err.code === 'EADDRINUSE') {
356
- console.error(`[HTTP] Port ${port} in use, trying ${port + 1}...`);
357
- server.close();
358
- tryPort(port + 1);
359
- }
360
- else {
361
- reject(err);
362
- }
363
- });
364
- server.listen(port, () => {
365
- httpServer = server;
366
- console.error(`[HTTP] Server started on http://localhost:${port}`);
367
- resolve(port);
368
- });
369
- }
370
- tryPort(currentPort);
278
+ else
279
+ reject(err);
280
+ });
281
+ server.listen(port, () => {
282
+ httpServer = server;
283
+ resolve(port);
284
+ });
371
285
  });
372
286
  }
287
+ // --- STARTUP ---
373
288
  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
+ });
374
293
  const stdioTransport = new StdioServerTransport();
375
294
  await server.connect(stdioTransport);
376
- // Only start HTTP server if explicitly enabled
295
+ console.error(`[STDIO] Started GraphQL MCP server ${env.NAME}`);
377
296
  if (env.ENABLE_HTTP) {
378
297
  try {
379
298
  const port = await startHttpServer(env.MCP_PORT);
380
- console.error(`[HTTP] Listening on port ${port} for POST /mcp requests`);
299
+ console.error(`[HTTP] Server started on http://localhost:${port}`);
381
300
  }
382
301
  catch (error) {
383
- console.error(`[HTTP] Failed to start HTTP server: ${error}`);
384
- // Don't exit - STDIO transport is more important
302
+ console.error(`[HTTP] Failed to start: ${error}`);
385
303
  }
386
304
  }
387
- else {
388
- console.error(`[HTTP] HTTP transport disabled (ENABLE_HTTP=auto|true to enable)`);
389
- }
390
- try {
391
- await getSchema();
392
- }
393
- catch (error) {
394
- console.error(`[SCHEMA] Warning: Failed to pre-load schema: ${error}`);
395
- }
305
+ await schemaPromise;
396
306
  }
397
- // Graceful shutdown
398
- process.on('SIGINT', () => {
399
- console.error('\n[SHUTDOWN] Received SIGINT, closing server...');
400
- if (httpServer) {
401
- httpServer.close(() => {
402
- console.error('[SHUTDOWN] HTTP server closed');
403
- process.exit(0);
404
- });
405
- }
406
- else {
307
+ // Graceful exit
308
+ const shutdown = () => {
309
+ if (httpServer)
310
+ httpServer.close(() => process.exit(0));
311
+ else
407
312
  process.exit(0);
408
- }
409
- });
410
- process.on('SIGTERM', () => {
411
- console.error('\n[SHUTDOWN] Received SIGTERM, closing server...');
412
- if (httpServer) {
413
- httpServer.close(() => {
414
- console.error('[SHUTDOWN] HTTP server closed');
415
- process.exit(0);
416
- });
417
- }
418
- else {
419
- process.exit(0);
420
- }
421
- });
422
- main().catch((error) => {
423
- 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}`);
424
318
  process.exit(1);
425
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.0.1"
52
+ "version": "3.2.0"
53
53
  }