@letoribo/mcp-graphql-enhanced 3.1.0 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +143 -207
- package/package.json +1 -1
|
@@ -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
|
@@ -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,
|
|
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"),
|
|
47
|
+
.default("auto"),
|
|
50
48
|
});
|
|
51
49
|
const env = EnvSchema.parse(process.env);
|
|
52
50
|
const server = new McpServer({
|
|
@@ -54,136 +52,160 @@ const server = new McpServer({
|
|
|
54
52
|
version: getVersion(),
|
|
55
53
|
description: `GraphQL MCP server for ${env.ENDPOINT}`,
|
|
56
54
|
});
|
|
57
|
-
//
|
|
58
|
-
let
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
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
|
|
74
|
-
schema = await introspectSchemaFromUrl(env.SCHEMA);
|
|
72
|
+
if (env.SCHEMA.startsWith("http")) {
|
|
73
|
+
sdl = await introspectSchemaFromUrl(env.SCHEMA);
|
|
75
74
|
}
|
|
76
75
|
else {
|
|
77
|
-
|
|
76
|
+
sdl = await introspectLocalSchema(env.SCHEMA);
|
|
78
77
|
}
|
|
78
|
+
cachedSchemaObject = buildASTSchema(gqlParse(sdl));
|
|
79
|
+
cachedSDL = sdl;
|
|
79
80
|
}
|
|
80
81
|
else {
|
|
81
|
-
|
|
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
|
|
95
|
+
return cachedSDL;
|
|
87
96
|
}
|
|
88
97
|
catch (error) {
|
|
89
|
-
schemaLoadError = error;
|
|
90
|
-
throw
|
|
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
|
|
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
|
|
111
|
-
if (typeNames === null) {
|
|
112
|
-
typeNames = undefined;
|
|
113
|
-
}
|
|
114
|
+
const introspectSchemaHandler = async ({ typeNames }) => {
|
|
114
115
|
try {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
return {
|
|
116
|
+
// Ensure cache is populated
|
|
117
|
+
await getSchema();
|
|
118
|
+
// Safety: If no specific types requested, return the 'Map' of types to prevent bridge crash
|
|
119
|
+
if (!typeNames || typeNames.length === 0) {
|
|
120
|
+
const allTypeNames = Object.keys(cachedSchemaObject.getTypeMap())
|
|
121
|
+
.filter(t => !t.startsWith('__'));
|
|
122
|
+
return {
|
|
123
|
+
content: [{
|
|
124
|
+
type: "text",
|
|
125
|
+
text: `Schema is large. Please request specific types for full details.\n\nAvailable types: ${allTypeNames.join(", ")}`
|
|
126
|
+
}]
|
|
127
|
+
};
|
|
122
128
|
}
|
|
129
|
+
// Use the new filtering logic from helpers/introspection.js
|
|
130
|
+
const filteredResult = introspectSpecificTypes(cachedSchemaObject, typeNames);
|
|
131
|
+
return {
|
|
132
|
+
content: [{
|
|
133
|
+
type: "text",
|
|
134
|
+
text: JSON.stringify(filteredResult, null, 2)
|
|
135
|
+
}]
|
|
136
|
+
};
|
|
123
137
|
}
|
|
124
138
|
catch (error) {
|
|
125
|
-
throw new Error(`Introspection failed: ${error}`);
|
|
139
|
+
throw new Error(`Introspection failed: ${error.message}`);
|
|
126
140
|
}
|
|
127
141
|
};
|
|
128
142
|
toolHandlers.set("introspect-schema", introspectSchemaHandler);
|
|
129
143
|
server.tool("introspect-schema", "Introspect the GraphQL schema. Optionally filter to specific types.", {
|
|
130
|
-
typeNames: z.array(z.string()).optional().describe("A list of specific type names to filter
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
144
|
+
typeNames: z.array(z.string()).optional().describe("A list of specific type names to filter (e.g. ['User', 'Post'])."),
|
|
145
|
+
}, async ({ typeNames }) => {
|
|
146
|
+
try {
|
|
147
|
+
console.error(`[TOOL] Introspect called with types: ${JSON.stringify(typeNames || "NONE")}`);
|
|
148
|
+
// 1. Ensure the schema is loaded into the cache
|
|
149
|
+
await getSchema();
|
|
150
|
+
// 2. THE GATEKEEPER: If no types requested, send ONLY names.
|
|
151
|
+
if (!typeNames || typeNames.length === 0) {
|
|
152
|
+
const allTypeNames = Object.keys(cachedSchemaObject.getTypeMap())
|
|
153
|
+
.filter(t => !t.startsWith('__'));
|
|
154
|
+
console.error(`[TOOL] Sending summary list of ${allTypeNames.length} types.`);
|
|
155
|
+
return {
|
|
156
|
+
content: [{
|
|
157
|
+
type: "text",
|
|
158
|
+
text: `Schema is large. Please request specific types for details.\n\nAvailable types: ${allTypeNames.join(", ")}`
|
|
159
|
+
}]
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
// 3. DRILL DOWN: If Claude asks for specific types, use the helper.
|
|
163
|
+
console.error(`[TOOL] Filtering for: ${typeNames.join(", ")}`);
|
|
164
|
+
const filteredResult = introspectSpecificTypes(cachedSchemaObject, typeNames);
|
|
165
|
+
return {
|
|
166
|
+
content: [{
|
|
167
|
+
type: "text",
|
|
168
|
+
text: JSON.stringify(filteredResult, null, 2)
|
|
169
|
+
}]
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
console.error(`[TOOL ERROR] ${error.message}`);
|
|
174
|
+
throw new Error(`Introspection failed: ${error.message}`);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
134
177
|
const queryGraphqlHandler = async ({ query, variables, headers }) => {
|
|
135
178
|
try {
|
|
136
179
|
const parsedQuery = parse(query);
|
|
137
180
|
const isMutation = parsedQuery.definitions.some((def) => def.kind === "OperationDefinition" && def.operation === "mutation");
|
|
138
181
|
if (isMutation && !env.ALLOW_MUTATIONS) {
|
|
139
|
-
throw new Error("Mutations are not allowed
|
|
182
|
+
throw new Error("Mutations are not allowed. Enable ALLOW_MUTATIONS in config.");
|
|
140
183
|
}
|
|
141
184
|
}
|
|
142
185
|
catch (error) {
|
|
143
186
|
throw new Error(`Invalid GraphQL query: ${error}`);
|
|
144
187
|
}
|
|
145
188
|
try {
|
|
146
|
-
const toolHeaders = headers
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
...env.HEADERS,
|
|
152
|
-
...toolHeaders,
|
|
153
|
-
};
|
|
154
|
-
let parsedVariables = null;
|
|
155
|
-
if (variables) {
|
|
156
|
-
if (typeof variables === 'string') {
|
|
157
|
-
parsedVariables = JSON.parse(variables);
|
|
158
|
-
}
|
|
159
|
-
else {
|
|
160
|
-
parsedVariables = variables;
|
|
161
|
-
}
|
|
162
|
-
}
|
|
189
|
+
const toolHeaders = headers ? JSON.parse(headers) : {};
|
|
190
|
+
const allHeaders = { "Content-Type": "application/json", ...env.HEADERS, ...toolHeaders };
|
|
191
|
+
let parsedVariables = variables;
|
|
192
|
+
if (typeof variables === 'string')
|
|
193
|
+
parsedVariables = JSON.parse(variables);
|
|
163
194
|
const response = await fetch(env.ENDPOINT, {
|
|
164
195
|
method: "POST",
|
|
165
196
|
headers: allHeaders,
|
|
166
|
-
body: JSON.stringify({
|
|
167
|
-
query,
|
|
168
|
-
variables: parsedVariables,
|
|
169
|
-
}),
|
|
197
|
+
body: JSON.stringify({ query, variables: parsedVariables }),
|
|
170
198
|
});
|
|
171
199
|
if (!response.ok) {
|
|
172
200
|
const responseText = await response.text();
|
|
173
201
|
throw new Error(`GraphQL request failed: ${response.statusText}\n${responseText}`);
|
|
174
202
|
}
|
|
175
|
-
const
|
|
176
|
-
const data = rawData;
|
|
203
|
+
const data = await response.json();
|
|
177
204
|
if (data.errors && data.errors.length > 0) {
|
|
178
205
|
throw new Error(`GraphQL errors: ${JSON.stringify(data.errors, null, 2)}`);
|
|
179
206
|
}
|
|
180
207
|
return {
|
|
181
|
-
content: [
|
|
182
|
-
{
|
|
183
|
-
type: "text",
|
|
184
|
-
text: JSON.stringify(data, null, 2),
|
|
185
|
-
},
|
|
186
|
-
],
|
|
208
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
187
209
|
};
|
|
188
210
|
}
|
|
189
211
|
catch (error) {
|
|
@@ -191,23 +213,17 @@ const queryGraphqlHandler = async ({ query, variables, headers }) => {
|
|
|
191
213
|
}
|
|
192
214
|
};
|
|
193
215
|
toolHandlers.set("query-graphql", queryGraphqlHandler);
|
|
194
|
-
server.tool("query-graphql", "Query a GraphQL endpoint with the given query and variables.
|
|
216
|
+
server.tool("query-graphql", "Query a GraphQL endpoint with the given query and variables.", {
|
|
195
217
|
query: z.string(),
|
|
196
218
|
variables: z.string().optional(),
|
|
197
|
-
headers: z
|
|
198
|
-
.string()
|
|
199
|
-
.optional()
|
|
200
|
-
.describe("Optional JSON string of headers to include, e.g., {\"Authorization\": \"Bearer ...\"}"),
|
|
219
|
+
headers: z.string().optional().describe("Optional JSON string of headers"),
|
|
201
220
|
}, queryGraphqlHandler);
|
|
221
|
+
// --- HTTP SERVER LOGIC ---
|
|
202
222
|
function readBody(req) {
|
|
203
223
|
return new Promise((resolve, reject) => {
|
|
204
224
|
let body = '';
|
|
205
|
-
req.on('data',
|
|
206
|
-
|
|
207
|
-
});
|
|
208
|
-
req.on('end', () => {
|
|
209
|
-
resolve(body);
|
|
210
|
-
});
|
|
225
|
+
req.on('data', chunk => body += chunk.toString());
|
|
226
|
+
req.on('end', () => resolve(body));
|
|
211
227
|
req.on('error', reject);
|
|
212
228
|
});
|
|
213
229
|
}
|
|
@@ -227,157 +243,77 @@ async function handleHttpRequest(req, res) {
|
|
|
227
243
|
return;
|
|
228
244
|
}
|
|
229
245
|
if (url.pathname === '/mcp' && req.method === 'POST') {
|
|
230
|
-
let rawBody;
|
|
231
|
-
let request;
|
|
232
|
-
try {
|
|
233
|
-
rawBody = await readBody(req);
|
|
234
|
-
request = JSON.parse(rawBody);
|
|
235
|
-
}
|
|
236
|
-
catch (e) {
|
|
237
|
-
console.error("HTTP MCP Parse Error:", e);
|
|
238
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
239
|
-
res.end(JSON.stringify({
|
|
240
|
-
jsonrpc: '2.0',
|
|
241
|
-
id: null,
|
|
242
|
-
error: { code: -32700, message: 'Parse Error: Invalid JSON received in request body.' }
|
|
243
|
-
}));
|
|
244
|
-
return;
|
|
245
|
-
}
|
|
246
246
|
try {
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
250
|
-
res.end(JSON.stringify({
|
|
251
|
-
jsonrpc: '2.0',
|
|
252
|
-
id: id || null,
|
|
253
|
-
error: { code: -32600, message: 'Invalid JSON-RPC Request structure (missing method or id).' }
|
|
254
|
-
}));
|
|
255
|
-
return;
|
|
256
|
-
}
|
|
247
|
+
const rawBody = await readBody(req);
|
|
248
|
+
const { method, params, id } = JSON.parse(rawBody);
|
|
257
249
|
const handler = toolHandlers.get(method);
|
|
258
250
|
if (!handler) {
|
|
259
|
-
res.writeHead(404
|
|
260
|
-
res.end(JSON.stringify({
|
|
261
|
-
jsonrpc: '2.0',
|
|
262
|
-
id: id,
|
|
263
|
-
error: { code: -32601, message: `Method not found: ${method}` }
|
|
264
|
-
}));
|
|
251
|
+
res.writeHead(404);
|
|
252
|
+
res.end(JSON.stringify({ jsonrpc: '2.0', id, error: { code: -32601, message: 'Method not found' } }));
|
|
265
253
|
return;
|
|
266
254
|
}
|
|
267
255
|
const result = await handler(params);
|
|
268
256
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
269
|
-
res.end(JSON.stringify({
|
|
270
|
-
jsonrpc: '2.0',
|
|
271
|
-
id: id,
|
|
272
|
-
result: result
|
|
273
|
-
}));
|
|
257
|
+
res.end(JSON.stringify({ jsonrpc: '2.0', id, result }));
|
|
274
258
|
}
|
|
275
259
|
catch (error) {
|
|
276
|
-
|
|
277
|
-
res.
|
|
278
|
-
res.end(JSON.stringify({
|
|
279
|
-
jsonrpc: '2.0',
|
|
280
|
-
id: request?.id || null,
|
|
281
|
-
error: { code: -32603, message: 'Internal server error during tool execution.' }
|
|
282
|
-
}));
|
|
260
|
+
res.writeHead(500);
|
|
261
|
+
res.end(JSON.stringify({ jsonrpc: '2.0', error: { code: -32603, message: 'Internal error' } }));
|
|
283
262
|
}
|
|
284
263
|
return;
|
|
285
264
|
}
|
|
286
|
-
res.writeHead(404
|
|
287
|
-
res.end('Not Found
|
|
265
|
+
res.writeHead(404);
|
|
266
|
+
res.end('Not Found');
|
|
288
267
|
}
|
|
289
|
-
// Single HTTP server instance
|
|
290
268
|
let httpServer = null;
|
|
291
|
-
/**
|
|
292
|
-
* Tries to listen on a given port with a single retry attempt.
|
|
293
|
-
* Returns the port it successfully bound to.
|
|
294
|
-
*/
|
|
295
269
|
async function startHttpServer(initialPort) {
|
|
296
270
|
return new Promise((resolve, reject) => {
|
|
297
|
-
let
|
|
298
|
-
const
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
return;
|
|
271
|
+
let port = initialPort;
|
|
272
|
+
const server = node_http_1.default.createServer(handleHttpRequest);
|
|
273
|
+
server.once('error', (err) => {
|
|
274
|
+
if (err.code === 'EADDRINUSE') {
|
|
275
|
+
server.close();
|
|
276
|
+
resolve(startHttpServer(port + 1));
|
|
304
277
|
}
|
|
305
|
-
|
|
306
|
-
reject(
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
if (err.code === 'EADDRINUSE') {
|
|
313
|
-
console.error(`[HTTP] Port ${port} in use, trying ${port + 1}...`);
|
|
314
|
-
server.close();
|
|
315
|
-
tryPort(port + 1);
|
|
316
|
-
}
|
|
317
|
-
else {
|
|
318
|
-
reject(err);
|
|
319
|
-
}
|
|
320
|
-
});
|
|
321
|
-
server.listen(port, () => {
|
|
322
|
-
httpServer = server;
|
|
323
|
-
console.error(`[HTTP] Server started on http://localhost:${port}`);
|
|
324
|
-
resolve(port);
|
|
325
|
-
});
|
|
326
|
-
}
|
|
327
|
-
tryPort(currentPort);
|
|
278
|
+
else
|
|
279
|
+
reject(err);
|
|
280
|
+
});
|
|
281
|
+
server.listen(port, () => {
|
|
282
|
+
httpServer = server;
|
|
283
|
+
resolve(port);
|
|
284
|
+
});
|
|
328
285
|
});
|
|
329
286
|
}
|
|
287
|
+
// --- STARTUP ---
|
|
330
288
|
async function main() {
|
|
331
|
-
// Pre-load schema FIRST (parallel with server setup)
|
|
332
289
|
console.error(`[SCHEMA] Pre-loading GraphQL schema...`);
|
|
333
|
-
const schemaPromise = getSchema().catch(
|
|
290
|
+
const schemaPromise = getSchema().catch(error => {
|
|
334
291
|
console.error(`[SCHEMA] Warning: Failed to pre-load schema: ${error}`);
|
|
335
292
|
});
|
|
336
293
|
const stdioTransport = new StdioServerTransport();
|
|
337
294
|
await server.connect(stdioTransport);
|
|
338
|
-
console.error(`[STDIO] Started GraphQL MCP server ${env.NAME}
|
|
339
|
-
// Only start HTTP if needed
|
|
295
|
+
console.error(`[STDIO] Started GraphQL MCP server ${env.NAME}`);
|
|
340
296
|
if (env.ENABLE_HTTP) {
|
|
341
297
|
try {
|
|
342
298
|
const port = await startHttpServer(env.MCP_PORT);
|
|
343
|
-
console.error(`[HTTP]
|
|
299
|
+
console.error(`[HTTP] Server started on http://localhost:${port}`);
|
|
344
300
|
}
|
|
345
301
|
catch (error) {
|
|
346
|
-
console.error(`[HTTP] Failed to start
|
|
302
|
+
console.error(`[HTTP] Failed to start: ${error}`);
|
|
347
303
|
}
|
|
348
304
|
}
|
|
349
|
-
else {
|
|
350
|
-
console.error(`[HTTP] HTTP transport disabled (ENABLE_HTTP=auto|true to enable)`);
|
|
351
|
-
}
|
|
352
|
-
// Wait for schema to finish loading
|
|
353
305
|
await schemaPromise;
|
|
354
306
|
}
|
|
355
|
-
// Graceful
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
console.error('[SHUTDOWN] HTTP server closed');
|
|
361
|
-
process.exit(0);
|
|
362
|
-
});
|
|
363
|
-
}
|
|
364
|
-
else {
|
|
365
|
-
process.exit(0);
|
|
366
|
-
}
|
|
367
|
-
});
|
|
368
|
-
process.on('SIGTERM', () => {
|
|
369
|
-
console.error('\n[SHUTDOWN] Received SIGTERM, closing server...');
|
|
370
|
-
if (httpServer) {
|
|
371
|
-
httpServer.close(() => {
|
|
372
|
-
console.error('[SHUTDOWN] HTTP server closed');
|
|
373
|
-
process.exit(0);
|
|
374
|
-
});
|
|
375
|
-
}
|
|
376
|
-
else {
|
|
307
|
+
// Graceful exit
|
|
308
|
+
const shutdown = () => {
|
|
309
|
+
if (httpServer)
|
|
310
|
+
httpServer.close(() => process.exit(0));
|
|
311
|
+
else
|
|
377
312
|
process.exit(0);
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
313
|
+
};
|
|
314
|
+
process.on('SIGINT', shutdown);
|
|
315
|
+
process.on('SIGTERM', shutdown);
|
|
316
|
+
main().catch(error => {
|
|
317
|
+
console.error(`Fatal error: ${error}`);
|
|
382
318
|
process.exit(1);
|
|
383
319
|
});
|
package/package.json
CHANGED