@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 +7 -3
- package/dist/helpers/introspection.d.ts +9 -11
- package/dist/helpers/introspection.d.ts.map +1 -1
- package/dist/helpers/introspection.js +38 -65
- package/dist/index.js +131 -298
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -17,13 +17,16 @@ An **enhanced MCP (Model Context Protocol) server for GraphQL** that fixes real-
|
|
|
17
17
|
|
|
18
18
|
This server now runs in **dual transport mode**, supporting both the standard **STDIO** communication (used by most MCP clients) and a new **HTTP JSON-RPC** endpoint on port `6274`.
|
|
19
19
|
|
|
20
|
-
This allows external systems, web applications, and direct `curl` commands to access the server's tools.
|
|
20
|
+
This allows external systems, web applications, and direct `curl` commands to access the server's tools with **live request logging** in your terminal (`[HTTP-RPC]` logs).
|
|
21
21
|
|
|
22
22
|
| **Endpoint** | **Method** | **Description** |
|
|
23
23
|
| :--- | :--- | :--- |
|
|
24
|
-
| `/mcp` | `POST` | The main JSON-RPC endpoint for tool execution. |
|
|
24
|
+
| `/mcp` | `POST` | The main JSON-RPC 2.0 endpoint for tool execution. |
|
|
25
25
|
| `/health` | `GET` | Simple health check, returns `{ status: 'ok' }`. |
|
|
26
26
|
|
|
27
|
+
### Automatic Port Selection
|
|
28
|
+
The server defaults to port `6274`. If you encounter an `EADDRINUSE` error, the server will automatically find the next available port. **Check the server logs for the final bound port** (e.g., `[HTTP] Started server on http://localhost:6275`).
|
|
29
|
+
|
|
27
30
|
### Resolving Port Conflicts (EADDRINUSE) and Automatic Port Selection
|
|
28
31
|
|
|
29
32
|
The server defaults to port `6274`. If you encounter an `EADDRINUSE: address already in use :::6274` error (common in local development due to stale processes), the server will automatically find the next available port (up to 10 attempts, not spawning multiple servers).
|
|
@@ -62,9 +65,10 @@ npx @modelcontextprotocol/inspector \
|
|
|
62
65
|
| `HEADERS` | JSON string containing headers for requests | `{}` |
|
|
63
66
|
| `ALLOW_MUTATIONS` | Enable mutation operations (disabled by default) | `false` |
|
|
64
67
|
| `NAME` | Name of the MCP server | `mcp-graphql-enhanced` |
|
|
65
|
-
| `SCHEMA` | Path to a local GraphQL schema file or URL
|
|
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
|
|
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
|
|
9
|
+
export declare function introspectLocalSchema(path: string): Promise<string>;
|
|
14
10
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
|
|
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":"
|
|
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)(),
|
|
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
|
|
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
|
|
36
|
-
|
|
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
|
-
*
|
|
45
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
78
|
-
if (isObjectLikeType(type)) {
|
|
43
|
+
if ((0, graphql_1.isObjectType)(type) || (0, graphql_1.isInterfaceType)(type)) {
|
|
79
44
|
result[name] = {
|
|
80
|
-
kind:
|
|
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
|
-
|
|
103
|
-
else if (isUnionType(type)) {
|
|
65
|
+
else if ((0, graphql_1.isInputObjectType)(type)) {
|
|
104
66
|
result[name] = {
|
|
105
|
-
kind: "
|
|
67
|
+
kind: "INPUT_OBJECT",
|
|
106
68
|
description: type.description,
|
|
107
|
-
|
|
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
|
-
|
|
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
|
-
|
|
122
|
-
else if (isInputObjectType(type)) {
|
|
87
|
+
else if ((0, graphql_1.isUnionType)(type)) {
|
|
123
88
|
result[name] = {
|
|
124
|
-
kind: "
|
|
89
|
+
kind: "UNION",
|
|
125
90
|
description: type.description,
|
|
126
|
-
|
|
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 (
|
|
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
|
|
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
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
const
|
|
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
|
-
|
|
16
|
-
|
|
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 =
|
|
20
|
-
NAME:
|
|
21
|
-
ENDPOINT:
|
|
22
|
-
ALLOW_MUTATIONS:
|
|
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:
|
|
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:
|
|
38
|
-
MCP_PORT:
|
|
39
|
-
ENABLE_HTTP:
|
|
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
|
-
|
|
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"),
|
|
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
|
-
//
|
|
58
|
-
let
|
|
60
|
+
// --- CACHE STATE ---
|
|
61
|
+
let cachedSDL = null;
|
|
62
|
+
let cachedSchemaObject = null;
|
|
59
63
|
let schemaLoadError = null;
|
|
60
64
|
async function getSchema() {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
70
|
+
const { buildClientSchema, getIntrospectionQuery, printSchema, buildASTSchema, parse: gqlParse } = require("graphql");
|
|
71
|
+
let sdl;
|
|
71
72
|
if (env.SCHEMA) {
|
|
72
|
-
if
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
97
|
+
return cachedSDL;
|
|
87
98
|
}
|
|
88
99
|
catch (error) {
|
|
89
|
-
schemaLoadError = error;
|
|
90
|
-
throw
|
|
100
|
+
schemaLoadError = error instanceof Error ? error : new Error(String(error));
|
|
101
|
+
throw schemaLoadError;
|
|
91
102
|
}
|
|
92
103
|
}
|
|
93
|
-
|
|
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
|
-
|
|
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
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
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(`
|
|
129
|
+
throw new Error(`Execution failed: ${error.message}`);
|
|
191
130
|
}
|
|
192
131
|
};
|
|
193
132
|
toolHandlers.set("query-graphql", queryGraphqlHandler);
|
|
194
|
-
server.tool("query-graphql", "
|
|
195
|
-
query:
|
|
196
|
-
variables:
|
|
197
|
-
headers:
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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', '
|
|
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
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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
|
-
|
|
258
|
-
|
|
259
|
-
res.writeHead(
|
|
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
|
-
|
|
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
|
-
|
|
287
|
-
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
|
|
342
|
-
|
|
343
|
-
console.error(`[HTTP]
|
|
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
|
-
|
|
377
|
-
|
|
378
|
-
}
|
|
379
|
-
});
|
|
380
|
-
|
|
381
|
-
|
|
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": "
|
|
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
|
|
53
|
+
"version": "3.2.1"
|
|
53
54
|
}
|