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