@graphql-hive/plugin-mcp 0.0.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/index.cjs +311 -0
- package/dist/index.d.cts +180 -0
- package/dist/index.d.ts +180 -0
- package/dist/index.js +309 -0
- package/package.json +56 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var graphql = require('graphql');
|
|
4
|
+
|
|
5
|
+
const SCALAR_MAP = {
|
|
6
|
+
String: "string",
|
|
7
|
+
Int: "integer",
|
|
8
|
+
Float: "number",
|
|
9
|
+
Boolean: "boolean",
|
|
10
|
+
ID: "string"
|
|
11
|
+
};
|
|
12
|
+
function graphqlTypeToJsonSchema(type, schema) {
|
|
13
|
+
if (graphql.isNonNullType(type)) {
|
|
14
|
+
return graphqlTypeToJsonSchema(type.ofType);
|
|
15
|
+
}
|
|
16
|
+
if (graphql.isListType(type)) {
|
|
17
|
+
return {
|
|
18
|
+
type: "array",
|
|
19
|
+
items: graphqlTypeToJsonSchema(type.ofType)
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
if (graphql.isEnumType(type)) {
|
|
23
|
+
return {
|
|
24
|
+
enum: type.getValues().map((v) => v.name)
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
if (graphql.isInputObjectType(type)) {
|
|
28
|
+
const fields = type.getFields();
|
|
29
|
+
const properties = {};
|
|
30
|
+
const required = [];
|
|
31
|
+
for (const [fieldName, field] of Object.entries(fields)) {
|
|
32
|
+
if (graphql.isNonNullType(field.type)) {
|
|
33
|
+
required.push(fieldName);
|
|
34
|
+
}
|
|
35
|
+
properties[fieldName] = graphqlTypeToJsonSchema(field.type);
|
|
36
|
+
if (field.description) {
|
|
37
|
+
properties[fieldName].description = field.description;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const result = {
|
|
41
|
+
type: "object",
|
|
42
|
+
properties
|
|
43
|
+
};
|
|
44
|
+
if (required.length > 0) {
|
|
45
|
+
result.required = required;
|
|
46
|
+
}
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
if (graphql.isScalarType(type)) {
|
|
50
|
+
const jsonType = SCALAR_MAP[type.name];
|
|
51
|
+
if (jsonType) {
|
|
52
|
+
return { type: jsonType };
|
|
53
|
+
}
|
|
54
|
+
return { type: "string" };
|
|
55
|
+
}
|
|
56
|
+
return { type: "object" };
|
|
57
|
+
}
|
|
58
|
+
function operationToInputSchema(operationSource, schema) {
|
|
59
|
+
const document = graphql.parse(operationSource);
|
|
60
|
+
const operationDef = document.definitions.find(
|
|
61
|
+
(def) => def.kind === graphql.Kind.OPERATION_DEFINITION
|
|
62
|
+
);
|
|
63
|
+
if (!operationDef || operationDef.kind !== graphql.Kind.OPERATION_DEFINITION) {
|
|
64
|
+
throw new Error("No operation definition found in document");
|
|
65
|
+
}
|
|
66
|
+
const variables = operationDef.variableDefinitions || [];
|
|
67
|
+
const properties = {};
|
|
68
|
+
const required = [];
|
|
69
|
+
for (const variable of variables) {
|
|
70
|
+
const varName = variable.variable.name.value;
|
|
71
|
+
const varType = graphql.typeFromAST(schema, variable.type);
|
|
72
|
+
if (!varType) {
|
|
73
|
+
throw new Error(`Unknown type for variable $${varName}`);
|
|
74
|
+
}
|
|
75
|
+
if (variable.type.kind === graphql.Kind.NON_NULL_TYPE) {
|
|
76
|
+
required.push(varName);
|
|
77
|
+
}
|
|
78
|
+
properties[varName] = graphqlTypeToJsonSchema(varType);
|
|
79
|
+
}
|
|
80
|
+
const result = {
|
|
81
|
+
type: "object",
|
|
82
|
+
properties
|
|
83
|
+
};
|
|
84
|
+
if (required.length > 0) {
|
|
85
|
+
result.required = required;
|
|
86
|
+
}
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
class ToolRegistry {
|
|
91
|
+
tools = /* @__PURE__ */ new Map();
|
|
92
|
+
constructor(configs, schema) {
|
|
93
|
+
for (const config of configs) {
|
|
94
|
+
const inputSchema = operationToInputSchema(config.query, schema);
|
|
95
|
+
const description = config.description || `Execute ${config.name}`;
|
|
96
|
+
this.tools.set(config.name, {
|
|
97
|
+
name: config.name,
|
|
98
|
+
description,
|
|
99
|
+
query: config.query,
|
|
100
|
+
inputSchema
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
getTool(name) {
|
|
105
|
+
return this.tools.get(name);
|
|
106
|
+
}
|
|
107
|
+
getToolNames() {
|
|
108
|
+
return Array.from(this.tools.keys());
|
|
109
|
+
}
|
|
110
|
+
getMCPTools() {
|
|
111
|
+
return Array.from(this.tools.values()).map((tool) => ({
|
|
112
|
+
name: tool.name,
|
|
113
|
+
description: tool.description,
|
|
114
|
+
inputSchema: tool.inputSchema
|
|
115
|
+
}));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function createMCPHandler(options) {
|
|
120
|
+
const { serverName, serverVersion, registry } = options;
|
|
121
|
+
return async function handleMCPRequest(request) {
|
|
122
|
+
const body = await request.json();
|
|
123
|
+
const { id, method, params } = body;
|
|
124
|
+
let response;
|
|
125
|
+
switch (method) {
|
|
126
|
+
case "initialize":
|
|
127
|
+
response = {
|
|
128
|
+
jsonrpc: "2.0",
|
|
129
|
+
id,
|
|
130
|
+
result: {
|
|
131
|
+
protocolVersion: "2025-11-25",
|
|
132
|
+
serverInfo: {
|
|
133
|
+
name: serverName,
|
|
134
|
+
version: serverVersion
|
|
135
|
+
},
|
|
136
|
+
capabilities: {
|
|
137
|
+
tools: {}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
break;
|
|
142
|
+
case "tools/list":
|
|
143
|
+
response = {
|
|
144
|
+
jsonrpc: "2.0",
|
|
145
|
+
id,
|
|
146
|
+
result: {
|
|
147
|
+
tools: registry.getMCPTools()
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
break;
|
|
151
|
+
case "notifications/initialized":
|
|
152
|
+
return new Response(null, { status: 204 });
|
|
153
|
+
case "tools/call": {
|
|
154
|
+
const callParams = params;
|
|
155
|
+
const tool = registry.getTool(callParams.name);
|
|
156
|
+
if (!tool) {
|
|
157
|
+
response = {
|
|
158
|
+
jsonrpc: "2.0",
|
|
159
|
+
id,
|
|
160
|
+
result: {
|
|
161
|
+
content: [{
|
|
162
|
+
type: "text",
|
|
163
|
+
text: JSON.stringify({ error: `Unknown tool: ${callParams.name}` })
|
|
164
|
+
}],
|
|
165
|
+
isError: true
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
try {
|
|
171
|
+
const result = await options.execute(callParams.name, callParams.arguments || {});
|
|
172
|
+
response = {
|
|
173
|
+
jsonrpc: "2.0",
|
|
174
|
+
id,
|
|
175
|
+
result: {
|
|
176
|
+
content: [{
|
|
177
|
+
type: "text",
|
|
178
|
+
text: JSON.stringify(result, null, 2)
|
|
179
|
+
}]
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
} catch (error) {
|
|
183
|
+
response = {
|
|
184
|
+
jsonrpc: "2.0",
|
|
185
|
+
id,
|
|
186
|
+
result: {
|
|
187
|
+
content: [{
|
|
188
|
+
type: "text",
|
|
189
|
+
text: JSON.stringify({
|
|
190
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
191
|
+
})
|
|
192
|
+
}],
|
|
193
|
+
isError: true
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
default:
|
|
200
|
+
response = {
|
|
201
|
+
jsonrpc: "2.0",
|
|
202
|
+
id,
|
|
203
|
+
error: {
|
|
204
|
+
code: -32601,
|
|
205
|
+
message: `Method not found: ${method}`
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
return new Response(JSON.stringify(response), {
|
|
210
|
+
headers: { "Content-Type": "application/json" }
|
|
211
|
+
});
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function createGraphQLExecutor(registry, graphqlEndpoint) {
|
|
216
|
+
return async function executeToolCall(toolName, args, context) {
|
|
217
|
+
const tool = registry.getTool(toolName);
|
|
218
|
+
if (!tool) {
|
|
219
|
+
throw new Error(`Unknown tool: ${toolName}`);
|
|
220
|
+
}
|
|
221
|
+
const response = await fetch(graphqlEndpoint, {
|
|
222
|
+
method: "POST",
|
|
223
|
+
headers: {
|
|
224
|
+
"Content-Type": "application/json",
|
|
225
|
+
...context?.headers
|
|
226
|
+
},
|
|
227
|
+
body: JSON.stringify({
|
|
228
|
+
query: tool.query,
|
|
229
|
+
variables: args
|
|
230
|
+
})
|
|
231
|
+
});
|
|
232
|
+
const result = await response.json();
|
|
233
|
+
return result;
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function useMCP(config) {
|
|
238
|
+
const mcpPath = config.path || "/mcp";
|
|
239
|
+
const graphqlPath = config.graphqlPath || "/graphql";
|
|
240
|
+
let registry = null;
|
|
241
|
+
let schema = null;
|
|
242
|
+
let schemaLoadingPromise = null;
|
|
243
|
+
return {
|
|
244
|
+
onSchemaChange({ schema: newSchema }) {
|
|
245
|
+
schema = newSchema;
|
|
246
|
+
registry = new ToolRegistry(config.tools, newSchema);
|
|
247
|
+
},
|
|
248
|
+
onRequest({ request, url, endResponse }) {
|
|
249
|
+
if (url.pathname !== mcpPath) {
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
const graphqlEndpoint = `${url.protocol}//${url.host}${graphqlPath}`;
|
|
253
|
+
const ensureSchema = async () => {
|
|
254
|
+
if (registry && schema) {
|
|
255
|
+
return true;
|
|
256
|
+
}
|
|
257
|
+
if (!schemaLoadingPromise) {
|
|
258
|
+
schemaLoadingPromise = (async () => {
|
|
259
|
+
try {
|
|
260
|
+
await fetch(graphqlEndpoint, {
|
|
261
|
+
method: "POST",
|
|
262
|
+
headers: { "Content-Type": "application/json" },
|
|
263
|
+
body: JSON.stringify({ query: "{ __typename }" })
|
|
264
|
+
});
|
|
265
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
266
|
+
} finally {
|
|
267
|
+
schemaLoadingPromise = null;
|
|
268
|
+
}
|
|
269
|
+
})();
|
|
270
|
+
}
|
|
271
|
+
await schemaLoadingPromise;
|
|
272
|
+
return !!(registry && schema);
|
|
273
|
+
};
|
|
274
|
+
return ensureSchema().then((ready) => {
|
|
275
|
+
if (!ready || !registry || !schema) {
|
|
276
|
+
endResponse(new Response(JSON.stringify({
|
|
277
|
+
jsonrpc: "2.0",
|
|
278
|
+
id: null,
|
|
279
|
+
error: {
|
|
280
|
+
code: -32e3,
|
|
281
|
+
message: "MCP server not ready. Schema introspection failed."
|
|
282
|
+
}
|
|
283
|
+
}), {
|
|
284
|
+
status: 503,
|
|
285
|
+
headers: { "Content-Type": "application/json" }
|
|
286
|
+
}));
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
const execute = createGraphQLExecutor(registry, graphqlEndpoint);
|
|
290
|
+
const handler = createMCPHandler({
|
|
291
|
+
serverName: config.name,
|
|
292
|
+
serverVersion: config.version || "1.0.0",
|
|
293
|
+
registry,
|
|
294
|
+
execute: async (toolName, args) => {
|
|
295
|
+
const headers = {};
|
|
296
|
+
const auth = request.headers.get("authorization");
|
|
297
|
+
if (auth) {
|
|
298
|
+
headers["authorization"] = auth;
|
|
299
|
+
}
|
|
300
|
+
return execute(toolName, args, { headers });
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
return handler(request).then((response) => {
|
|
304
|
+
endResponse(response);
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
exports.useMCP = useMCP;
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { Logger } from '@graphql-hive/logger';
|
|
2
|
+
import { DisposableSymbols } from '@whatwg-node/disposablestack';
|
|
3
|
+
import { MaybePromise } from '@whatwg-node/promise-helpers';
|
|
4
|
+
import { UnifiedGraphPlugin, Instrumentation as Instrumentation$2 } from '@graphql-mesh/fusion-runtime';
|
|
5
|
+
import { Plugin, YogaInitialContext, Instrumentation as Instrumentation$1 } from 'graphql-yoga';
|
|
6
|
+
import { MeshFetch, KeyValueCache, MeshFetchRequestInit, Logger as Logger$1 } from '@graphql-mesh/types';
|
|
7
|
+
import { FetchInstrumentation } from '@graphql-mesh/utils';
|
|
8
|
+
import { ExecutionRequest, MaybePromise as MaybePromise$1 } from '@graphql-tools/utils';
|
|
9
|
+
import { GraphQLResolveInfo } from 'graphql/type';
|
|
10
|
+
|
|
11
|
+
type TopicDataMap = {
|
|
12
|
+
[topic: string]: any;
|
|
13
|
+
};
|
|
14
|
+
type PubSubListener<Data extends TopicDataMap, Topic extends keyof Data> = (data: Data[Topic]) => void;
|
|
15
|
+
interface PubSub<M extends TopicDataMap = TopicDataMap> {
|
|
16
|
+
/**
|
|
17
|
+
* Publish {@link data} for a {@link topic}.
|
|
18
|
+
* @returns `void` or a `Promise` that resolves when the data has been successfully published
|
|
19
|
+
*/
|
|
20
|
+
publish<Topic extends keyof M>(topic: Topic, data: M[Topic]): MaybePromise<void>;
|
|
21
|
+
/**
|
|
22
|
+
* A distinct list of all topics that are currently subscribed to.
|
|
23
|
+
* Can be a promise to accomodate distributed systems where subscribers exist on other
|
|
24
|
+
* locations and we need to know about all of them.
|
|
25
|
+
*/
|
|
26
|
+
subscribedTopics(): MaybePromise<Iterable<keyof M>>;
|
|
27
|
+
/**
|
|
28
|
+
* Subscribe and listen to a {@link topic} receiving its data.
|
|
29
|
+
*
|
|
30
|
+
* If the {@link listener} is provided, it will be called whenever data is emitted for the {@link topic},
|
|
31
|
+
*
|
|
32
|
+
* @returns an unsubscribe function or a `Promise<unsubscribe function>` that resolves when the subscription is successfully established. the unsubscribe function returns `void` or a `Promise` that resolves on successful unsubscribe and subscription cleanup
|
|
33
|
+
*
|
|
34
|
+
* If the {@link listener} is not provided,
|
|
35
|
+
*
|
|
36
|
+
* @returns an `AsyncIterable` that yields data for the given {@link topic}
|
|
37
|
+
*/
|
|
38
|
+
subscribe<Topic extends keyof M>(topic: Topic): AsyncIterable<M[Topic]>;
|
|
39
|
+
subscribe<Topic extends keyof M>(topic: Topic, listener: PubSubListener<M, Topic>): MaybePromise<() => MaybePromise<void>>;
|
|
40
|
+
/**
|
|
41
|
+
* Closes active subscriptions and disposes of all resources. Publishing and subscribing after disposal
|
|
42
|
+
* is not possible and will throw an error if attempted.
|
|
43
|
+
*/
|
|
44
|
+
dispose(): MaybePromise<void>;
|
|
45
|
+
/** @see {@link dispose} */
|
|
46
|
+
[DisposableSymbols.asyncDispose](): Promise<void>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface GatewayConfigContext {
|
|
50
|
+
/**
|
|
51
|
+
* WHATWG compatible Fetch implementation.
|
|
52
|
+
*/
|
|
53
|
+
fetch: MeshFetch;
|
|
54
|
+
/**
|
|
55
|
+
* The logger to use throught Hive and its plugins.
|
|
56
|
+
*/
|
|
57
|
+
log: Logger;
|
|
58
|
+
/**
|
|
59
|
+
* Current working directory.
|
|
60
|
+
* Note that working directory does not exist in serverless environments and will therefore be empty.
|
|
61
|
+
*/
|
|
62
|
+
cwd: string;
|
|
63
|
+
/**
|
|
64
|
+
* Event bus for pub/sub.
|
|
65
|
+
*/
|
|
66
|
+
pubsub?: PubSub;
|
|
67
|
+
/**
|
|
68
|
+
* Cache Storage
|
|
69
|
+
*/
|
|
70
|
+
cache?: KeyValueCache;
|
|
71
|
+
}
|
|
72
|
+
interface GatewayContext extends GatewayConfigContext, YogaInitialContext {
|
|
73
|
+
/**
|
|
74
|
+
* Environment agnostic HTTP headers provided with the request.
|
|
75
|
+
*/
|
|
76
|
+
headers: Record<string, string>;
|
|
77
|
+
/**
|
|
78
|
+
* Runtime context available within WebSocket connections.
|
|
79
|
+
*/
|
|
80
|
+
connectionParams?: Record<string, string>;
|
|
81
|
+
}
|
|
82
|
+
type GatewayPlugin<TPluginContext extends Record<string, any> = Record<string, any>, TContext extends Record<string, any> = Record<string, any>> = Plugin<Partial<TPluginContext> & GatewayContext & TContext, GatewayConfigContext> & UnifiedGraphPlugin<Partial<TPluginContext> & GatewayContext & TContext> & {
|
|
83
|
+
onFetch?: OnFetchHook<Partial<TPluginContext> & TContext>;
|
|
84
|
+
onCacheGet?: OnCacheGetHook;
|
|
85
|
+
onCacheSet?: OnCacheSetHook;
|
|
86
|
+
onCacheDelete?: OnCacheDeleteHook;
|
|
87
|
+
/**
|
|
88
|
+
* An Instrumentation instance that will wrap each phases of the request pipeline.
|
|
89
|
+
* This should be used primarily as an observability tool (for monitoring, tracing, etc...).
|
|
90
|
+
*
|
|
91
|
+
* Note: The wrapped functions in instrumentation should always be called. Use hooks to
|
|
92
|
+
* conditionally skip a phase.
|
|
93
|
+
*/
|
|
94
|
+
instrumentation?: Instrumentation<TPluginContext & TContext & GatewayContext>;
|
|
95
|
+
};
|
|
96
|
+
interface OnFetchHookPayload<TContext> {
|
|
97
|
+
url: string;
|
|
98
|
+
setURL(url: URL | string): void;
|
|
99
|
+
options: MeshFetchRequestInit;
|
|
100
|
+
setOptions(options: MeshFetchRequestInit): void;
|
|
101
|
+
/**
|
|
102
|
+
* The context is not available in cases where "fetch" is done in
|
|
103
|
+
* order to pull a supergraph or do some internal work.
|
|
104
|
+
*
|
|
105
|
+
* The logger will be available in all cases.
|
|
106
|
+
*/
|
|
107
|
+
context: (GatewayContext & TContext) | {
|
|
108
|
+
log: Logger;
|
|
109
|
+
};
|
|
110
|
+
/** @deprecated Please use `log` from the {@link context} instead. */
|
|
111
|
+
logger: Logger$1;
|
|
112
|
+
info: GraphQLResolveInfo;
|
|
113
|
+
fetchFn: MeshFetch;
|
|
114
|
+
setFetchFn: (fetchFn: MeshFetch) => void;
|
|
115
|
+
executionRequest?: ExecutionRequest;
|
|
116
|
+
endResponse: (response$: MaybePromise$1<Response>) => void;
|
|
117
|
+
}
|
|
118
|
+
interface OnFetchHookDonePayload {
|
|
119
|
+
response: Response;
|
|
120
|
+
setResponse: (response: Response) => void;
|
|
121
|
+
}
|
|
122
|
+
type OnFetchHookDone = (payload: OnFetchHookDonePayload) => MaybePromise$1<void>;
|
|
123
|
+
type OnFetchHook<TContext> = (payload: OnFetchHookPayload<TContext>) => MaybePromise$1<void | OnFetchHookDone>;
|
|
124
|
+
type OnCacheGetHook = (payload: OnCacheGetHookEventPayload) => MaybePromise$1<OnCacheGetHookResult | void>;
|
|
125
|
+
interface OnCacheGetHookEventPayload {
|
|
126
|
+
cache: KeyValueCache;
|
|
127
|
+
key: string;
|
|
128
|
+
ttl?: number;
|
|
129
|
+
}
|
|
130
|
+
interface OnCacheGetHookResult {
|
|
131
|
+
onCacheHit?: OnCacheHitHook;
|
|
132
|
+
onCacheMiss?: OnCacheMissHook;
|
|
133
|
+
onCacheGetError?: OnCacheErrorHook;
|
|
134
|
+
}
|
|
135
|
+
type OnCacheErrorHook = (payload: OnCacheErrorHookPayload) => void;
|
|
136
|
+
interface OnCacheErrorHookPayload {
|
|
137
|
+
error: Error;
|
|
138
|
+
}
|
|
139
|
+
type OnCacheHitHook = (payload: OnCacheHitHookEventPayload) => void;
|
|
140
|
+
interface OnCacheHitHookEventPayload {
|
|
141
|
+
value: any;
|
|
142
|
+
}
|
|
143
|
+
type OnCacheMissHook = () => void;
|
|
144
|
+
type OnCacheSetHook = (payload: OnCacheSetHookEventPayload) => MaybePromise$1<OnCacheSetHookResult | void>;
|
|
145
|
+
interface OnCacheSetHookResult {
|
|
146
|
+
onCacheSetDone?: () => void;
|
|
147
|
+
onCacheSetError?: OnCacheErrorHook;
|
|
148
|
+
}
|
|
149
|
+
interface OnCacheSetHookEventPayload {
|
|
150
|
+
cache: KeyValueCache;
|
|
151
|
+
key: string;
|
|
152
|
+
value: any;
|
|
153
|
+
ttl?: number;
|
|
154
|
+
}
|
|
155
|
+
type OnCacheDeleteHook = (payload: OnCacheDeleteHookEventPayload) => MaybePromise$1<OnCacheDeleteHookResult | void>;
|
|
156
|
+
interface OnCacheDeleteHookResult {
|
|
157
|
+
onCacheDeleteDone?: () => void;
|
|
158
|
+
onCacheDeleteError?: OnCacheErrorHook;
|
|
159
|
+
}
|
|
160
|
+
interface OnCacheDeleteHookEventPayload {
|
|
161
|
+
cache: KeyValueCache;
|
|
162
|
+
key: string;
|
|
163
|
+
}
|
|
164
|
+
type Instrumentation<TContext extends Record<string, any>> = Instrumentation$1<TContext> & Instrumentation$2 & FetchInstrumentation;
|
|
165
|
+
|
|
166
|
+
interface MCPToolConfig {
|
|
167
|
+
name: string;
|
|
168
|
+
description?: string;
|
|
169
|
+
query: string;
|
|
170
|
+
}
|
|
171
|
+
interface MCPConfig {
|
|
172
|
+
name: string;
|
|
173
|
+
version?: string;
|
|
174
|
+
path?: string;
|
|
175
|
+
graphqlPath?: string;
|
|
176
|
+
tools: MCPToolConfig[];
|
|
177
|
+
}
|
|
178
|
+
declare function useMCP(config: MCPConfig): GatewayPlugin;
|
|
179
|
+
|
|
180
|
+
export { type MCPConfig, type MCPToolConfig, useMCP };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { Logger } from '@graphql-hive/logger';
|
|
2
|
+
import { DisposableSymbols } from '@whatwg-node/disposablestack';
|
|
3
|
+
import { MaybePromise } from '@whatwg-node/promise-helpers';
|
|
4
|
+
import { UnifiedGraphPlugin, Instrumentation as Instrumentation$2 } from '@graphql-mesh/fusion-runtime';
|
|
5
|
+
import { Plugin, YogaInitialContext, Instrumentation as Instrumentation$1 } from 'graphql-yoga';
|
|
6
|
+
import { MeshFetch, KeyValueCache, MeshFetchRequestInit, Logger as Logger$1 } from '@graphql-mesh/types';
|
|
7
|
+
import { FetchInstrumentation } from '@graphql-mesh/utils';
|
|
8
|
+
import { ExecutionRequest, MaybePromise as MaybePromise$1 } from '@graphql-tools/utils';
|
|
9
|
+
import { GraphQLResolveInfo } from 'graphql/type';
|
|
10
|
+
|
|
11
|
+
type TopicDataMap = {
|
|
12
|
+
[topic: string]: any;
|
|
13
|
+
};
|
|
14
|
+
type PubSubListener<Data extends TopicDataMap, Topic extends keyof Data> = (data: Data[Topic]) => void;
|
|
15
|
+
interface PubSub<M extends TopicDataMap = TopicDataMap> {
|
|
16
|
+
/**
|
|
17
|
+
* Publish {@link data} for a {@link topic}.
|
|
18
|
+
* @returns `void` or a `Promise` that resolves when the data has been successfully published
|
|
19
|
+
*/
|
|
20
|
+
publish<Topic extends keyof M>(topic: Topic, data: M[Topic]): MaybePromise<void>;
|
|
21
|
+
/**
|
|
22
|
+
* A distinct list of all topics that are currently subscribed to.
|
|
23
|
+
* Can be a promise to accomodate distributed systems where subscribers exist on other
|
|
24
|
+
* locations and we need to know about all of them.
|
|
25
|
+
*/
|
|
26
|
+
subscribedTopics(): MaybePromise<Iterable<keyof M>>;
|
|
27
|
+
/**
|
|
28
|
+
* Subscribe and listen to a {@link topic} receiving its data.
|
|
29
|
+
*
|
|
30
|
+
* If the {@link listener} is provided, it will be called whenever data is emitted for the {@link topic},
|
|
31
|
+
*
|
|
32
|
+
* @returns an unsubscribe function or a `Promise<unsubscribe function>` that resolves when the subscription is successfully established. the unsubscribe function returns `void` or a `Promise` that resolves on successful unsubscribe and subscription cleanup
|
|
33
|
+
*
|
|
34
|
+
* If the {@link listener} is not provided,
|
|
35
|
+
*
|
|
36
|
+
* @returns an `AsyncIterable` that yields data for the given {@link topic}
|
|
37
|
+
*/
|
|
38
|
+
subscribe<Topic extends keyof M>(topic: Topic): AsyncIterable<M[Topic]>;
|
|
39
|
+
subscribe<Topic extends keyof M>(topic: Topic, listener: PubSubListener<M, Topic>): MaybePromise<() => MaybePromise<void>>;
|
|
40
|
+
/**
|
|
41
|
+
* Closes active subscriptions and disposes of all resources. Publishing and subscribing after disposal
|
|
42
|
+
* is not possible and will throw an error if attempted.
|
|
43
|
+
*/
|
|
44
|
+
dispose(): MaybePromise<void>;
|
|
45
|
+
/** @see {@link dispose} */
|
|
46
|
+
[DisposableSymbols.asyncDispose](): Promise<void>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface GatewayConfigContext {
|
|
50
|
+
/**
|
|
51
|
+
* WHATWG compatible Fetch implementation.
|
|
52
|
+
*/
|
|
53
|
+
fetch: MeshFetch;
|
|
54
|
+
/**
|
|
55
|
+
* The logger to use throught Hive and its plugins.
|
|
56
|
+
*/
|
|
57
|
+
log: Logger;
|
|
58
|
+
/**
|
|
59
|
+
* Current working directory.
|
|
60
|
+
* Note that working directory does not exist in serverless environments and will therefore be empty.
|
|
61
|
+
*/
|
|
62
|
+
cwd: string;
|
|
63
|
+
/**
|
|
64
|
+
* Event bus for pub/sub.
|
|
65
|
+
*/
|
|
66
|
+
pubsub?: PubSub;
|
|
67
|
+
/**
|
|
68
|
+
* Cache Storage
|
|
69
|
+
*/
|
|
70
|
+
cache?: KeyValueCache;
|
|
71
|
+
}
|
|
72
|
+
interface GatewayContext extends GatewayConfigContext, YogaInitialContext {
|
|
73
|
+
/**
|
|
74
|
+
* Environment agnostic HTTP headers provided with the request.
|
|
75
|
+
*/
|
|
76
|
+
headers: Record<string, string>;
|
|
77
|
+
/**
|
|
78
|
+
* Runtime context available within WebSocket connections.
|
|
79
|
+
*/
|
|
80
|
+
connectionParams?: Record<string, string>;
|
|
81
|
+
}
|
|
82
|
+
type GatewayPlugin<TPluginContext extends Record<string, any> = Record<string, any>, TContext extends Record<string, any> = Record<string, any>> = Plugin<Partial<TPluginContext> & GatewayContext & TContext, GatewayConfigContext> & UnifiedGraphPlugin<Partial<TPluginContext> & GatewayContext & TContext> & {
|
|
83
|
+
onFetch?: OnFetchHook<Partial<TPluginContext> & TContext>;
|
|
84
|
+
onCacheGet?: OnCacheGetHook;
|
|
85
|
+
onCacheSet?: OnCacheSetHook;
|
|
86
|
+
onCacheDelete?: OnCacheDeleteHook;
|
|
87
|
+
/**
|
|
88
|
+
* An Instrumentation instance that will wrap each phases of the request pipeline.
|
|
89
|
+
* This should be used primarily as an observability tool (for monitoring, tracing, etc...).
|
|
90
|
+
*
|
|
91
|
+
* Note: The wrapped functions in instrumentation should always be called. Use hooks to
|
|
92
|
+
* conditionally skip a phase.
|
|
93
|
+
*/
|
|
94
|
+
instrumentation?: Instrumentation<TPluginContext & TContext & GatewayContext>;
|
|
95
|
+
};
|
|
96
|
+
interface OnFetchHookPayload<TContext> {
|
|
97
|
+
url: string;
|
|
98
|
+
setURL(url: URL | string): void;
|
|
99
|
+
options: MeshFetchRequestInit;
|
|
100
|
+
setOptions(options: MeshFetchRequestInit): void;
|
|
101
|
+
/**
|
|
102
|
+
* The context is not available in cases where "fetch" is done in
|
|
103
|
+
* order to pull a supergraph or do some internal work.
|
|
104
|
+
*
|
|
105
|
+
* The logger will be available in all cases.
|
|
106
|
+
*/
|
|
107
|
+
context: (GatewayContext & TContext) | {
|
|
108
|
+
log: Logger;
|
|
109
|
+
};
|
|
110
|
+
/** @deprecated Please use `log` from the {@link context} instead. */
|
|
111
|
+
logger: Logger$1;
|
|
112
|
+
info: GraphQLResolveInfo;
|
|
113
|
+
fetchFn: MeshFetch;
|
|
114
|
+
setFetchFn: (fetchFn: MeshFetch) => void;
|
|
115
|
+
executionRequest?: ExecutionRequest;
|
|
116
|
+
endResponse: (response$: MaybePromise$1<Response>) => void;
|
|
117
|
+
}
|
|
118
|
+
interface OnFetchHookDonePayload {
|
|
119
|
+
response: Response;
|
|
120
|
+
setResponse: (response: Response) => void;
|
|
121
|
+
}
|
|
122
|
+
type OnFetchHookDone = (payload: OnFetchHookDonePayload) => MaybePromise$1<void>;
|
|
123
|
+
type OnFetchHook<TContext> = (payload: OnFetchHookPayload<TContext>) => MaybePromise$1<void | OnFetchHookDone>;
|
|
124
|
+
type OnCacheGetHook = (payload: OnCacheGetHookEventPayload) => MaybePromise$1<OnCacheGetHookResult | void>;
|
|
125
|
+
interface OnCacheGetHookEventPayload {
|
|
126
|
+
cache: KeyValueCache;
|
|
127
|
+
key: string;
|
|
128
|
+
ttl?: number;
|
|
129
|
+
}
|
|
130
|
+
interface OnCacheGetHookResult {
|
|
131
|
+
onCacheHit?: OnCacheHitHook;
|
|
132
|
+
onCacheMiss?: OnCacheMissHook;
|
|
133
|
+
onCacheGetError?: OnCacheErrorHook;
|
|
134
|
+
}
|
|
135
|
+
type OnCacheErrorHook = (payload: OnCacheErrorHookPayload) => void;
|
|
136
|
+
interface OnCacheErrorHookPayload {
|
|
137
|
+
error: Error;
|
|
138
|
+
}
|
|
139
|
+
type OnCacheHitHook = (payload: OnCacheHitHookEventPayload) => void;
|
|
140
|
+
interface OnCacheHitHookEventPayload {
|
|
141
|
+
value: any;
|
|
142
|
+
}
|
|
143
|
+
type OnCacheMissHook = () => void;
|
|
144
|
+
type OnCacheSetHook = (payload: OnCacheSetHookEventPayload) => MaybePromise$1<OnCacheSetHookResult | void>;
|
|
145
|
+
interface OnCacheSetHookResult {
|
|
146
|
+
onCacheSetDone?: () => void;
|
|
147
|
+
onCacheSetError?: OnCacheErrorHook;
|
|
148
|
+
}
|
|
149
|
+
interface OnCacheSetHookEventPayload {
|
|
150
|
+
cache: KeyValueCache;
|
|
151
|
+
key: string;
|
|
152
|
+
value: any;
|
|
153
|
+
ttl?: number;
|
|
154
|
+
}
|
|
155
|
+
type OnCacheDeleteHook = (payload: OnCacheDeleteHookEventPayload) => MaybePromise$1<OnCacheDeleteHookResult | void>;
|
|
156
|
+
interface OnCacheDeleteHookResult {
|
|
157
|
+
onCacheDeleteDone?: () => void;
|
|
158
|
+
onCacheDeleteError?: OnCacheErrorHook;
|
|
159
|
+
}
|
|
160
|
+
interface OnCacheDeleteHookEventPayload {
|
|
161
|
+
cache: KeyValueCache;
|
|
162
|
+
key: string;
|
|
163
|
+
}
|
|
164
|
+
type Instrumentation<TContext extends Record<string, any>> = Instrumentation$1<TContext> & Instrumentation$2 & FetchInstrumentation;
|
|
165
|
+
|
|
166
|
+
interface MCPToolConfig {
|
|
167
|
+
name: string;
|
|
168
|
+
description?: string;
|
|
169
|
+
query: string;
|
|
170
|
+
}
|
|
171
|
+
interface MCPConfig {
|
|
172
|
+
name: string;
|
|
173
|
+
version?: string;
|
|
174
|
+
path?: string;
|
|
175
|
+
graphqlPath?: string;
|
|
176
|
+
tools: MCPToolConfig[];
|
|
177
|
+
}
|
|
178
|
+
declare function useMCP(config: MCPConfig): GatewayPlugin;
|
|
179
|
+
|
|
180
|
+
export { type MCPConfig, type MCPToolConfig, useMCP };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import { parse, Kind, typeFromAST, isNonNullType, isListType, isEnumType, isInputObjectType, isScalarType } from 'graphql';
|
|
2
|
+
|
|
3
|
+
const SCALAR_MAP = {
|
|
4
|
+
String: "string",
|
|
5
|
+
Int: "integer",
|
|
6
|
+
Float: "number",
|
|
7
|
+
Boolean: "boolean",
|
|
8
|
+
ID: "string"
|
|
9
|
+
};
|
|
10
|
+
function graphqlTypeToJsonSchema(type, schema) {
|
|
11
|
+
if (isNonNullType(type)) {
|
|
12
|
+
return graphqlTypeToJsonSchema(type.ofType);
|
|
13
|
+
}
|
|
14
|
+
if (isListType(type)) {
|
|
15
|
+
return {
|
|
16
|
+
type: "array",
|
|
17
|
+
items: graphqlTypeToJsonSchema(type.ofType)
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
if (isEnumType(type)) {
|
|
21
|
+
return {
|
|
22
|
+
enum: type.getValues().map((v) => v.name)
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
if (isInputObjectType(type)) {
|
|
26
|
+
const fields = type.getFields();
|
|
27
|
+
const properties = {};
|
|
28
|
+
const required = [];
|
|
29
|
+
for (const [fieldName, field] of Object.entries(fields)) {
|
|
30
|
+
if (isNonNullType(field.type)) {
|
|
31
|
+
required.push(fieldName);
|
|
32
|
+
}
|
|
33
|
+
properties[fieldName] = graphqlTypeToJsonSchema(field.type);
|
|
34
|
+
if (field.description) {
|
|
35
|
+
properties[fieldName].description = field.description;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const result = {
|
|
39
|
+
type: "object",
|
|
40
|
+
properties
|
|
41
|
+
};
|
|
42
|
+
if (required.length > 0) {
|
|
43
|
+
result.required = required;
|
|
44
|
+
}
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
if (isScalarType(type)) {
|
|
48
|
+
const jsonType = SCALAR_MAP[type.name];
|
|
49
|
+
if (jsonType) {
|
|
50
|
+
return { type: jsonType };
|
|
51
|
+
}
|
|
52
|
+
return { type: "string" };
|
|
53
|
+
}
|
|
54
|
+
return { type: "object" };
|
|
55
|
+
}
|
|
56
|
+
function operationToInputSchema(operationSource, schema) {
|
|
57
|
+
const document = parse(operationSource);
|
|
58
|
+
const operationDef = document.definitions.find(
|
|
59
|
+
(def) => def.kind === Kind.OPERATION_DEFINITION
|
|
60
|
+
);
|
|
61
|
+
if (!operationDef || operationDef.kind !== Kind.OPERATION_DEFINITION) {
|
|
62
|
+
throw new Error("No operation definition found in document");
|
|
63
|
+
}
|
|
64
|
+
const variables = operationDef.variableDefinitions || [];
|
|
65
|
+
const properties = {};
|
|
66
|
+
const required = [];
|
|
67
|
+
for (const variable of variables) {
|
|
68
|
+
const varName = variable.variable.name.value;
|
|
69
|
+
const varType = typeFromAST(schema, variable.type);
|
|
70
|
+
if (!varType) {
|
|
71
|
+
throw new Error(`Unknown type for variable $${varName}`);
|
|
72
|
+
}
|
|
73
|
+
if (variable.type.kind === Kind.NON_NULL_TYPE) {
|
|
74
|
+
required.push(varName);
|
|
75
|
+
}
|
|
76
|
+
properties[varName] = graphqlTypeToJsonSchema(varType);
|
|
77
|
+
}
|
|
78
|
+
const result = {
|
|
79
|
+
type: "object",
|
|
80
|
+
properties
|
|
81
|
+
};
|
|
82
|
+
if (required.length > 0) {
|
|
83
|
+
result.required = required;
|
|
84
|
+
}
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
class ToolRegistry {
|
|
89
|
+
tools = /* @__PURE__ */ new Map();
|
|
90
|
+
constructor(configs, schema) {
|
|
91
|
+
for (const config of configs) {
|
|
92
|
+
const inputSchema = operationToInputSchema(config.query, schema);
|
|
93
|
+
const description = config.description || `Execute ${config.name}`;
|
|
94
|
+
this.tools.set(config.name, {
|
|
95
|
+
name: config.name,
|
|
96
|
+
description,
|
|
97
|
+
query: config.query,
|
|
98
|
+
inputSchema
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
getTool(name) {
|
|
103
|
+
return this.tools.get(name);
|
|
104
|
+
}
|
|
105
|
+
getToolNames() {
|
|
106
|
+
return Array.from(this.tools.keys());
|
|
107
|
+
}
|
|
108
|
+
getMCPTools() {
|
|
109
|
+
return Array.from(this.tools.values()).map((tool) => ({
|
|
110
|
+
name: tool.name,
|
|
111
|
+
description: tool.description,
|
|
112
|
+
inputSchema: tool.inputSchema
|
|
113
|
+
}));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function createMCPHandler(options) {
|
|
118
|
+
const { serverName, serverVersion, registry } = options;
|
|
119
|
+
return async function handleMCPRequest(request) {
|
|
120
|
+
const body = await request.json();
|
|
121
|
+
const { id, method, params } = body;
|
|
122
|
+
let response;
|
|
123
|
+
switch (method) {
|
|
124
|
+
case "initialize":
|
|
125
|
+
response = {
|
|
126
|
+
jsonrpc: "2.0",
|
|
127
|
+
id,
|
|
128
|
+
result: {
|
|
129
|
+
protocolVersion: "2025-11-25",
|
|
130
|
+
serverInfo: {
|
|
131
|
+
name: serverName,
|
|
132
|
+
version: serverVersion
|
|
133
|
+
},
|
|
134
|
+
capabilities: {
|
|
135
|
+
tools: {}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
break;
|
|
140
|
+
case "tools/list":
|
|
141
|
+
response = {
|
|
142
|
+
jsonrpc: "2.0",
|
|
143
|
+
id,
|
|
144
|
+
result: {
|
|
145
|
+
tools: registry.getMCPTools()
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
break;
|
|
149
|
+
case "notifications/initialized":
|
|
150
|
+
return new Response(null, { status: 204 });
|
|
151
|
+
case "tools/call": {
|
|
152
|
+
const callParams = params;
|
|
153
|
+
const tool = registry.getTool(callParams.name);
|
|
154
|
+
if (!tool) {
|
|
155
|
+
response = {
|
|
156
|
+
jsonrpc: "2.0",
|
|
157
|
+
id,
|
|
158
|
+
result: {
|
|
159
|
+
content: [{
|
|
160
|
+
type: "text",
|
|
161
|
+
text: JSON.stringify({ error: `Unknown tool: ${callParams.name}` })
|
|
162
|
+
}],
|
|
163
|
+
isError: true
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
try {
|
|
169
|
+
const result = await options.execute(callParams.name, callParams.arguments || {});
|
|
170
|
+
response = {
|
|
171
|
+
jsonrpc: "2.0",
|
|
172
|
+
id,
|
|
173
|
+
result: {
|
|
174
|
+
content: [{
|
|
175
|
+
type: "text",
|
|
176
|
+
text: JSON.stringify(result, null, 2)
|
|
177
|
+
}]
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
} catch (error) {
|
|
181
|
+
response = {
|
|
182
|
+
jsonrpc: "2.0",
|
|
183
|
+
id,
|
|
184
|
+
result: {
|
|
185
|
+
content: [{
|
|
186
|
+
type: "text",
|
|
187
|
+
text: JSON.stringify({
|
|
188
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
189
|
+
})
|
|
190
|
+
}],
|
|
191
|
+
isError: true
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
default:
|
|
198
|
+
response = {
|
|
199
|
+
jsonrpc: "2.0",
|
|
200
|
+
id,
|
|
201
|
+
error: {
|
|
202
|
+
code: -32601,
|
|
203
|
+
message: `Method not found: ${method}`
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
return new Response(JSON.stringify(response), {
|
|
208
|
+
headers: { "Content-Type": "application/json" }
|
|
209
|
+
});
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function createGraphQLExecutor(registry, graphqlEndpoint) {
|
|
214
|
+
return async function executeToolCall(toolName, args, context) {
|
|
215
|
+
const tool = registry.getTool(toolName);
|
|
216
|
+
if (!tool) {
|
|
217
|
+
throw new Error(`Unknown tool: ${toolName}`);
|
|
218
|
+
}
|
|
219
|
+
const response = await fetch(graphqlEndpoint, {
|
|
220
|
+
method: "POST",
|
|
221
|
+
headers: {
|
|
222
|
+
"Content-Type": "application/json",
|
|
223
|
+
...context?.headers
|
|
224
|
+
},
|
|
225
|
+
body: JSON.stringify({
|
|
226
|
+
query: tool.query,
|
|
227
|
+
variables: args
|
|
228
|
+
})
|
|
229
|
+
});
|
|
230
|
+
const result = await response.json();
|
|
231
|
+
return result;
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function useMCP(config) {
|
|
236
|
+
const mcpPath = config.path || "/mcp";
|
|
237
|
+
const graphqlPath = config.graphqlPath || "/graphql";
|
|
238
|
+
let registry = null;
|
|
239
|
+
let schema = null;
|
|
240
|
+
let schemaLoadingPromise = null;
|
|
241
|
+
return {
|
|
242
|
+
onSchemaChange({ schema: newSchema }) {
|
|
243
|
+
schema = newSchema;
|
|
244
|
+
registry = new ToolRegistry(config.tools, newSchema);
|
|
245
|
+
},
|
|
246
|
+
onRequest({ request, url, endResponse }) {
|
|
247
|
+
if (url.pathname !== mcpPath) {
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
const graphqlEndpoint = `${url.protocol}//${url.host}${graphqlPath}`;
|
|
251
|
+
const ensureSchema = async () => {
|
|
252
|
+
if (registry && schema) {
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
if (!schemaLoadingPromise) {
|
|
256
|
+
schemaLoadingPromise = (async () => {
|
|
257
|
+
try {
|
|
258
|
+
await fetch(graphqlEndpoint, {
|
|
259
|
+
method: "POST",
|
|
260
|
+
headers: { "Content-Type": "application/json" },
|
|
261
|
+
body: JSON.stringify({ query: "{ __typename }" })
|
|
262
|
+
});
|
|
263
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
264
|
+
} finally {
|
|
265
|
+
schemaLoadingPromise = null;
|
|
266
|
+
}
|
|
267
|
+
})();
|
|
268
|
+
}
|
|
269
|
+
await schemaLoadingPromise;
|
|
270
|
+
return !!(registry && schema);
|
|
271
|
+
};
|
|
272
|
+
return ensureSchema().then((ready) => {
|
|
273
|
+
if (!ready || !registry || !schema) {
|
|
274
|
+
endResponse(new Response(JSON.stringify({
|
|
275
|
+
jsonrpc: "2.0",
|
|
276
|
+
id: null,
|
|
277
|
+
error: {
|
|
278
|
+
code: -32e3,
|
|
279
|
+
message: "MCP server not ready. Schema introspection failed."
|
|
280
|
+
}
|
|
281
|
+
}), {
|
|
282
|
+
status: 503,
|
|
283
|
+
headers: { "Content-Type": "application/json" }
|
|
284
|
+
}));
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
const execute = createGraphQLExecutor(registry, graphqlEndpoint);
|
|
288
|
+
const handler = createMCPHandler({
|
|
289
|
+
serverName: config.name,
|
|
290
|
+
serverVersion: config.version || "1.0.0",
|
|
291
|
+
registry,
|
|
292
|
+
execute: async (toolName, args) => {
|
|
293
|
+
const headers = {};
|
|
294
|
+
const auth = request.headers.get("authorization");
|
|
295
|
+
if (auth) {
|
|
296
|
+
headers["authorization"] = auth;
|
|
297
|
+
}
|
|
298
|
+
return execute(toolName, args, { headers });
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
return handler(request).then((response) => {
|
|
302
|
+
endResponse(response);
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export { useMCP };
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@graphql-hive/plugin-mcp",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/graphql-hive/gateway.git",
|
|
8
|
+
"directory": "packages/plugins/mcp"
|
|
9
|
+
},
|
|
10
|
+
"author": {
|
|
11
|
+
"email": "contact@the-guild.dev",
|
|
12
|
+
"name": "The Guild",
|
|
13
|
+
"url": "https://the-guild.dev"
|
|
14
|
+
},
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=20.0.0"
|
|
18
|
+
},
|
|
19
|
+
"main": "./dist/index.js",
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"require": {
|
|
24
|
+
"types": "./dist/index.d.cts",
|
|
25
|
+
"default": "./dist/index.cjs"
|
|
26
|
+
},
|
|
27
|
+
"import": {
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"default": "./dist/index.js"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"./package.json": "./package.json"
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"dist"
|
|
36
|
+
],
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "pkgroll --clean-dist",
|
|
39
|
+
"prepack": "yarn build"
|
|
40
|
+
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"graphql": "^15.9.0 || ^16.9.0"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@graphql-hive/logger": "^1.0.10",
|
|
46
|
+
"@graphql-mesh/fusion-runtime": "^1.6.7",
|
|
47
|
+
"tslib": "^2.8.1"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@graphql-hive/gateway-runtime": "2.5.5",
|
|
51
|
+
"graphql": "^16.12.0",
|
|
52
|
+
"graphql-yoga": "^5.16.2",
|
|
53
|
+
"pkgroll": "2.23.0"
|
|
54
|
+
},
|
|
55
|
+
"sideEffects": false
|
|
56
|
+
}
|