@letoribo/mcp-graphql-enhanced 3.1.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.
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
@@ -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
@@ -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, introspectTypes, } = 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,350 +40,177 @@ 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
- // Auto-detect: enable HTTP if running in MCP Inspector
44
- // Inspector sets specific environment variables
45
- return !!(process.env.MCP_INSPECTOR || process.env.INSPECTOR_PORT);
49
+ return !!(process.env.MCP_INSPECTOR || process.env.INSPECTOR_PORT || process.env.MCP_PORT);
46
50
  }
47
51
  return value === "true";
48
52
  })
49
- .default("auto"), // Auto-detect by default
53
+ .default("auto"),
50
54
  });
51
55
  const env = EnvSchema.parse(process.env);
52
- const server = new McpServer({
56
+ const server = new mcp_js_1.McpServer({
53
57
  name: env.NAME,
54
58
  version: getVersion(),
55
- description: `GraphQL MCP server for ${env.ENDPOINT}`,
56
59
  });
57
- // Cache schema to avoid repeated introspection
58
- let cachedSchema = null;
60
+ // --- CACHE STATE ---
61
+ let cachedSDL = null;
62
+ let cachedSchemaObject = null;
59
63
  let schemaLoadError = null;
60
64
  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) {
65
+ if (cachedSDL)
66
+ return cachedSDL;
67
+ if (schemaLoadError)
67
68
  throw schemaLoadError;
68
- }
69
69
  try {
70
- let schema;
70
+ const { buildClientSchema, getIntrospectionQuery, printSchema, buildASTSchema, parse: gqlParse } = require("graphql");
71
+ let sdl;
71
72
  if (env.SCHEMA) {
72
- if (env.SCHEMA.startsWith("http://") ||
73
- env.SCHEMA.startsWith("https://")) {
74
- schema = await introspectSchemaFromUrl(env.SCHEMA);
73
+ // Check if it's a URL or local path
74
+ if (env.SCHEMA.startsWith("http")) {
75
+ const response = await fetch(env.SCHEMA);
76
+ sdl = await response.text();
75
77
  }
76
78
  else {
77
- schema = await introspectLocalSchema(env.SCHEMA);
79
+ sdl = await (0, introspection_js_1.introspectLocalSchema)(env.SCHEMA);
78
80
  }
81
+ cachedSchemaObject = buildASTSchema(gqlParse(sdl));
82
+ cachedSDL = sdl;
79
83
  }
80
84
  else {
81
- schema = await introspectEndpoint(env.ENDPOINT, env.HEADERS);
85
+ const response = await fetch(env.ENDPOINT, {
86
+ method: "POST",
87
+ headers: { "Content-Type": "application/json", ...env.HEADERS },
88
+ body: JSON.stringify({ query: getIntrospectionQuery() }),
89
+ });
90
+ if (!response.ok)
91
+ throw new Error(`Fetch failed: ${response.statusText}`);
92
+ const result = await response.json();
93
+ cachedSchemaObject = buildClientSchema(result.data);
94
+ cachedSDL = printSchema(cachedSchemaObject);
82
95
  }
83
- // Cache the schema
84
- cachedSchema = schema;
85
96
  console.error(`[SCHEMA] Successfully loaded and cached GraphQL schema`);
86
- return schema;
97
+ return cachedSDL;
87
98
  }
88
99
  catch (error) {
89
- schemaLoadError = error;
90
- throw new Error(`Failed to get GraphQL schema: ${error}`);
100
+ schemaLoadError = error instanceof Error ? error : new Error(String(error));
101
+ throw schemaLoadError;
91
102
  }
92
103
  }
93
- server.resource("graphql-schema", new URL(env.ENDPOINT).href, async (uri) => {
94
- try {
95
- const schema = await getSchema();
96
- return {
97
- contents: [
98
- {
99
- uri: uri.href,
100
- text: schema,
101
- },
102
- ],
103
- };
104
- }
105
- catch (error) {
106
- throw error;
107
- }
108
- });
104
+ // --- TOOL HANDLERS ---
109
105
  const toolHandlers = new Map();
110
- const introspectSchemaHandler = async ({ typeNames, descriptions = true, directives = true }) => {
111
- if (typeNames === null) {
112
- typeNames = undefined;
113
- }
114
- 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 }] };
122
- }
123
- }
124
- catch (error) {
125
- throw new Error(`Introspection failed: ${error}`); // ✅ Throw instead
126
- }
127
- };
128
- toolHandlers.set("introspect-schema", introspectSchemaHandler);
129
- 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);
106
+ // Tool: query-graphql
134
107
  const queryGraphqlHandler = async ({ query, variables, headers }) => {
135
108
  try {
136
- const parsedQuery = parse(query);
109
+ const parsedQuery = (0, language_1.parse)(query);
137
110
  const isMutation = parsedQuery.definitions.some((def) => def.kind === "OperationDefinition" && def.operation === "mutation");
138
- 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.");
140
- }
141
- }
142
- catch (error) {
143
- throw new Error(`Invalid GraphQL query: ${error}`);
144
- }
145
- 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
- }
111
+ if (isMutation && !env.ALLOW_MUTATIONS)
112
+ throw new Error("Mutations are not allowed.");
113
+ const toolHeaders = headers ? JSON.parse(headers) : {};
114
+ const allHeaders = { "Content-Type": "application/json", ...env.HEADERS, ...toolHeaders };
115
+ let parsedVariables = variables;
116
+ if (typeof variables === 'string')
117
+ parsedVariables = JSON.parse(variables);
163
118
  const response = await fetch(env.ENDPOINT, {
164
119
  method: "POST",
165
120
  headers: allHeaders,
166
- body: JSON.stringify({
167
- query,
168
- variables: parsedVariables,
169
- }),
121
+ body: JSON.stringify({ query, variables: parsedVariables }),
170
122
  });
171
- if (!response.ok) {
172
- const responseText = await response.text();
173
- throw new Error(`GraphQL request failed: ${response.statusText}\n${responseText}`);
174
- }
175
- const rawData = await response.json();
176
- const data = rawData;
177
- if (data.errors && data.errors.length > 0) {
178
- throw new Error(`GraphQL errors: ${JSON.stringify(data.errors, null, 2)}`);
179
- }
123
+ const data = await response.json();
180
124
  return {
181
- content: [
182
- {
183
- type: "text",
184
- text: JSON.stringify(data, null, 2),
185
- },
186
- ],
125
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
187
126
  };
188
127
  }
189
128
  catch (error) {
190
- throw new Error(`Failed to execute GraphQL query: ${error}`);
129
+ throw new Error(`Execution failed: ${error.message}`);
191
130
  }
192
131
  };
193
132
  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).", {
195
- query: z.string(),
196
- variables: z.string().optional(),
197
- headers: z
198
- .string()
199
- .optional()
200
- .describe("Optional JSON string of headers to include, e.g., {\"Authorization\": \"Bearer ...\"}"),
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(),
201
137
  }, queryGraphqlHandler);
202
- function readBody(req) {
203
- return new Promise((resolve, reject) => {
204
- let body = '';
205
- req.on('data', (chunk) => {
206
- body += chunk.toString();
207
- });
208
- req.on('end', () => {
209
- resolve(body);
210
- });
211
- req.on('error', reject);
212
- });
213
- }
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);
156
+ // --- HTTP SERVER LOGIC ---
214
157
  async function handleHttpRequest(req, res) {
215
158
  res.setHeader('Access-Control-Allow-Origin', '*');
216
- res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
159
+ res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');
217
160
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
218
161
  if (req.method === 'OPTIONS') {
219
162
  res.writeHead(204);
220
163
  res.end();
221
164
  return;
222
165
  }
223
- const url = new URL(req.url, `http://${req.headers.host}`);
224
- if (url.pathname === '/health' && req.method === 'GET') {
225
- res.writeHead(200, { 'Content-Type': 'application/json' });
226
- res.end(JSON.stringify({ status: 'ok', server: env.NAME }));
227
- return;
228
- }
166
+ const url = new URL(req.url || '', `http://${req.headers.host}`);
229
167
  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
- 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;
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 }));
256
182
  }
257
- const handler = toolHandlers.get(method);
258
- 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
- }));
265
- return;
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" } }));
266
187
  }
267
- const result = await handler(params);
268
- res.writeHead(200, { 'Content-Type': 'application/json' });
269
- res.end(JSON.stringify({
270
- jsonrpc: '2.0',
271
- id: id,
272
- result: result
273
- }));
274
- }
275
- 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
- }));
283
- }
188
+ });
284
189
  return;
285
190
  }
286
- res.writeHead(404, { 'Content-Type': 'text/plain' });
287
- res.end('Not Found. Use POST /mcp for JSON-RPC or GET /health.');
288
- }
289
- // Single HTTP server instance
290
- 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
- async function startHttpServer(initialPort) {
296
- 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;
304
- }
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);
328
- });
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
+ }
195
+ res.writeHead(404);
196
+ res.end("Not Found");
329
197
  }
198
+ // --- STARTUP ---
330
199
  async function main() {
331
- // Pre-load schema FIRST (parallel with server setup)
332
- console.error(`[SCHEMA] Pre-loading GraphQL schema...`);
333
- const schemaPromise = getSchema().catch((error) => {
334
- console.error(`[SCHEMA] Warning: Failed to pre-load schema: ${error}`);
335
- });
336
- const stdioTransport = new StdioServerTransport();
337
- await server.connect(stdioTransport);
338
- console.error(`[STDIO] Started GraphQL MCP server ${env.NAME} for endpoint: ${env.ENDPOINT}`);
339
- // Only start HTTP if needed
340
200
  if (env.ENABLE_HTTP) {
341
- try {
342
- const port = await startHttpServer(env.MCP_PORT);
343
- console.error(`[HTTP] Listening on port ${port} for POST /mcp requests`);
344
- }
345
- catch (error) {
346
- console.error(`[HTTP] Failed to start HTTP server: ${error}`);
347
- }
348
- }
349
- else {
350
- console.error(`[HTTP] HTTP transport disabled (ENABLE_HTTP=auto|true to enable)`);
351
- }
352
- // Wait for schema to finish loading
353
- await schemaPromise;
354
- }
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);
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}`);
374
204
  });
375
205
  }
376
- else {
377
- process.exit(0);
378
- }
379
- });
380
- main().catch((error) => {
381
- console.error(`Fatal error in main(): ${error}`);
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}`));
210
+ }
211
+ process.on('SIGINT', () => process.exit(0));
212
+ process.on('SIGTERM', () => process.exit(0));
213
+ main().catch(error => {
214
+ console.error(`[FATAL] ${error}`);
382
215
  process.exit(1);
383
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.1.0"
53
+ "version": "3.2.1"
53
54
  }