@mcp-layer/graphql 0.1.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/src/schema.js ADDED
@@ -0,0 +1,418 @@
1
+ import { makeExecutableSchema } from '@graphql-tools/schema';
2
+ import GraphQLJSON from 'graphql-type-json';
3
+ import { map } from './mapping.js';
4
+
5
+ /**
6
+ * Build GraphQL schema options.
7
+ * @param {{ operations?: { generated?: boolean, generic?: boolean } }} [options] - Schema options.
8
+ * @returns {{ operations: { generated: boolean, generic: boolean } }}
9
+ */
10
+ function normalizeOptions(options = {}) {
11
+ const operations = options?.operations && typeof options.operations === 'object'
12
+ ? options.operations
13
+ : {};
14
+
15
+ return {
16
+ operations: {
17
+ generated: operations.generated === undefined ? true : Boolean(operations.generated),
18
+ generic: operations.generic === undefined ? true : Boolean(operations.generic)
19
+ }
20
+ };
21
+ }
22
+
23
+ /**
24
+ * Build GraphQL field definitions for generated entries.
25
+ * @param {ReturnType<typeof map>} mapping - Catalog map.
26
+ * @returns {{ query: string[], mutation: string[] }}
27
+ */
28
+ function generatedFields(mapping) {
29
+ const query = [];
30
+ const mutation = [];
31
+
32
+ for (const entry of mapping.entries) {
33
+ if (entry.type === 'tool') {
34
+ mutation.push(`${entry.field}(input: JSON): ToolResult!`);
35
+ continue;
36
+ }
37
+
38
+ if (entry.type === 'prompt') {
39
+ mutation.push(`${entry.field}(input: JSON): PromptResult!`);
40
+ continue;
41
+ }
42
+
43
+ if (entry.type === 'resource') {
44
+ query.push(`${entry.field}: ResourceResult!`);
45
+ continue;
46
+ }
47
+
48
+ if (entry.type === 'resource-template') {
49
+ query.push(`${entry.field}(params: JSON): ResourceResult!`);
50
+ }
51
+ }
52
+
53
+ return { query, mutation };
54
+ }
55
+
56
+ /**
57
+ * Build GraphQL SDL for the adapter schema.
58
+ * @param {ReturnType<typeof map>} mapping - Catalog map.
59
+ * @param {{ operations: { generated: boolean, generic: boolean } }} config - Normalized config.
60
+ * @returns {string}
61
+ */
62
+ function buildTypeDefs(mapping, config) {
63
+ const query = ['catalog: Catalog!'];
64
+ const mutation = [];
65
+
66
+ if (config.operations.generic) {
67
+ query.push('readResource(uri: String!): ResourceResult!');
68
+ query.push('readTemplate(uriTemplate: String!, params: JSON): ResourceResult!');
69
+ mutation.push('callTool(name: String!, input: JSON): ToolResult!');
70
+ mutation.push('getPrompt(name: String!, input: JSON): PromptResult!');
71
+ }
72
+
73
+ if (config.operations.generated) {
74
+ const generated = generatedFields(mapping);
75
+ query.push(...generated.query);
76
+ mutation.push(...generated.mutation);
77
+ }
78
+
79
+ const lines = [
80
+ `scalar JSON`,
81
+ ``,
82
+ `type ToolResult {`,
83
+ ` content: JSON!`,
84
+ ` isError: Boolean!`,
85
+ ` structuredContent: JSON`,
86
+ `}`,
87
+ ``,
88
+ `type PromptResult {`,
89
+ ` messages: JSON!`,
90
+ ` payload: JSON!`,
91
+ `}`,
92
+ ``,
93
+ `type ResourceResult {`,
94
+ ` contents: JSON!`,
95
+ ` text: String`,
96
+ ` mimeType: String`,
97
+ ` payload: JSON!`,
98
+ `}`,
99
+ ``,
100
+ `type CatalogEntry {`,
101
+ ` type: String!`,
102
+ ` name: String!`,
103
+ ` field: String!`,
104
+ ` root: String!`,
105
+ ` description: String`,
106
+ `}`,
107
+ ``,
108
+ `type Catalog {`,
109
+ ` tools: [CatalogEntry!]!`,
110
+ ` prompts: [CatalogEntry!]!`,
111
+ ` resources: [CatalogEntry!]!`,
112
+ ` templates: [CatalogEntry!]!`,
113
+ `}`,
114
+ ``,
115
+ `type Query {`
116
+ ];
117
+
118
+ for (const field of query) {
119
+ lines.push(` ${field}`);
120
+ }
121
+ lines.push(`}`);
122
+
123
+ if (mutation.length > 0) {
124
+ lines.push(``);
125
+ lines.push(`type Mutation {`);
126
+ for (const field of mutation) {
127
+ lines.push(` ${field}`);
128
+ }
129
+ lines.push(`}`);
130
+ }
131
+
132
+ return `${lines.join('\n')}\n`;
133
+ }
134
+
135
+ /**
136
+ * Build a catalog entry view model.
137
+ * @param {{ type: string, root: 'query' | 'mutation', name: string, field: string, item: Record<string, unknown> }} source - Mapping entry.
138
+ * @returns {{ type: string, root: string, name: string, field: string, description?: string }}
139
+ */
140
+ function catalogEntry(source) {
141
+ return {
142
+ type: source.type,
143
+ root: source.root,
144
+ name: source.name,
145
+ field: source.field,
146
+ description: typeof source.item.description === 'string' ? source.item.description : undefined
147
+ };
148
+ }
149
+
150
+ /**
151
+ * Build catalog payload for GraphQL consumers.
152
+ * @param {ReturnType<typeof map>} mapping - Catalog map.
153
+ * @returns {{ tools: Array<{ type: string, root: string, name: string, field: string, description?: string }>, prompts: Array<{ type: string, root: string, name: string, field: string, description?: string }>, resources: Array<{ type: string, root: string, name: string, field: string, description?: string }>, templates: Array<{ type: string, root: string, name: string, field: string, description?: string }> }}
154
+ */
155
+ function catalogPayload(mapping) {
156
+ const tools = [];
157
+ const prompts = [];
158
+ const resources = [];
159
+ const templates = [];
160
+
161
+ for (const item of mapping.entries) {
162
+ const view = catalogEntry(item);
163
+ if (item.type === 'tool') tools.push(view);
164
+ if (item.type === 'prompt') prompts.push(view);
165
+ if (item.type === 'resource') resources.push(view);
166
+ if (item.type === 'resource-template') templates.push(view);
167
+ }
168
+
169
+ return { tools, prompts, resources, templates };
170
+ }
171
+
172
+ /**
173
+ * Create resolver for catalog query.
174
+ * @param {ReturnType<typeof map>} mapping - Catalog map.
175
+ * @returns {(root: unknown, args: unknown, context: Record<string, unknown>) => { tools: Array<{ type: string, root: string, name: string, field: string, description?: string }>, prompts: Array<{ type: string, root: string, name: string, field: string, description?: string }>, resources: Array<{ type: string, root: string, name: string, field: string, description?: string }>, templates: Array<{ type: string, root: string, name: string, field: string, description?: string }> }}
176
+ */
177
+ function createCatalogResolver(mapping) {
178
+ /**
179
+ * Resolve catalog metadata.
180
+ * @returns {{ tools: Array<{ type: string, root: string, name: string, field: string, description?: string }>, prompts: Array<{ type: string, root: string, name: string, field: string, description?: string }>, resources: Array<{ type: string, root: string, name: string, field: string, description?: string }>, templates: Array<{ type: string, root: string, name: string, field: string, description?: string }> }}
181
+ */
182
+ function resolve() {
183
+ return catalogPayload(mapping);
184
+ }
185
+
186
+ return resolve;
187
+ }
188
+
189
+ /**
190
+ * Create resolver for generic tool calls.
191
+ * @returns {(root: unknown, args: { name: string, input?: Record<string, unknown> }, context: { callTool: (name: string, input: Record<string, unknown>) => Promise<Record<string, unknown>> }) => Promise<Record<string, unknown>>}
192
+ */
193
+ function genericTool() {
194
+ /**
195
+ * Execute generic tool operation.
196
+ * @param {unknown} _root - GraphQL root.
197
+ * @param {{ name: string, input?: Record<string, unknown> }} args - Resolver args.
198
+ * @param {{ callTool: (name: string, input: Record<string, unknown>) => Promise<Record<string, unknown>> }} context - Resolver context.
199
+ * @returns {Promise<Record<string, unknown>>}
200
+ */
201
+ async function resolve(_root, args, context) {
202
+ return context.callTool(args.name, args.input ?? {});
203
+ }
204
+
205
+ return resolve;
206
+ }
207
+
208
+ /**
209
+ * Create resolver for generic prompt calls.
210
+ * @returns {(root: unknown, args: { name: string, input?: Record<string, unknown> }, context: { getPrompt: (name: string, input: Record<string, unknown>) => Promise<Record<string, unknown>> }) => Promise<Record<string, unknown>>}
211
+ */
212
+ function genericPrompt() {
213
+ /**
214
+ * Execute generic prompt operation.
215
+ * @param {unknown} _root - GraphQL root.
216
+ * @param {{ name: string, input?: Record<string, unknown> }} args - Resolver args.
217
+ * @param {{ getPrompt: (name: string, input: Record<string, unknown>) => Promise<Record<string, unknown>> }} context - Resolver context.
218
+ * @returns {Promise<Record<string, unknown>>}
219
+ */
220
+ async function resolve(_root, args, context) {
221
+ return context.getPrompt(args.name, args.input ?? {});
222
+ }
223
+
224
+ return resolve;
225
+ }
226
+
227
+ /**
228
+ * Create resolver for generic resource reads.
229
+ * @returns {(root: unknown, args: { uri: string }, context: { readResource: (uri: string) => Promise<Record<string, unknown>> }) => Promise<Record<string, unknown>>}
230
+ */
231
+ function genericResource() {
232
+ /**
233
+ * Execute generic resource read operation.
234
+ * @param {unknown} _root - GraphQL root.
235
+ * @param {{ uri: string }} args - Resolver args.
236
+ * @param {{ readResource: (uri: string) => Promise<Record<string, unknown>> }} context - Resolver context.
237
+ * @returns {Promise<Record<string, unknown>>}
238
+ */
239
+ async function resolve(_root, args, context) {
240
+ return context.readResource(args.uri);
241
+ }
242
+
243
+ return resolve;
244
+ }
245
+
246
+ /**
247
+ * Create resolver for generic template reads.
248
+ * @returns {(root: unknown, args: { uriTemplate: string, params?: Record<string, unknown> }, context: { readTemplate: (template: string, params: Record<string, unknown>) => Promise<Record<string, unknown>> }) => Promise<Record<string, unknown>>}
249
+ */
250
+ function genericTemplate() {
251
+ /**
252
+ * Execute generic template read operation.
253
+ * @param {unknown} _root - GraphQL root.
254
+ * @param {{ uriTemplate: string, params?: Record<string, unknown> }} args - Resolver args.
255
+ * @param {{ readTemplate: (template: string, params: Record<string, unknown>) => Promise<Record<string, unknown>> }} context - Resolver context.
256
+ * @returns {Promise<Record<string, unknown>>}
257
+ */
258
+ async function resolve(_root, args, context) {
259
+ return context.readTemplate(args.uriTemplate, args.params ?? {});
260
+ }
261
+
262
+ return resolve;
263
+ }
264
+
265
+ /**
266
+ * Create generated tool resolver.
267
+ * @param {string} name - Tool name.
268
+ * @returns {(root: unknown, args: { input?: Record<string, unknown> }, context: { callTool: (name: string, input: Record<string, unknown>) => Promise<Record<string, unknown>> }) => Promise<Record<string, unknown>>}
269
+ */
270
+ function generatedTool(name) {
271
+ /**
272
+ * Execute generated tool resolver.
273
+ * @param {unknown} _root - GraphQL root.
274
+ * @param {{ input?: Record<string, unknown> }} args - Resolver args.
275
+ * @param {{ callTool: (name: string, input: Record<string, unknown>) => Promise<Record<string, unknown>> }} context - Resolver context.
276
+ * @returns {Promise<Record<string, unknown>>}
277
+ */
278
+ async function resolve(_root, args, context) {
279
+ return context.callTool(name, args.input ?? {});
280
+ }
281
+
282
+ return resolve;
283
+ }
284
+
285
+ /**
286
+ * Create generated prompt resolver.
287
+ * @param {string} name - Prompt name.
288
+ * @returns {(root: unknown, args: { input?: Record<string, unknown> }, context: { getPrompt: (name: string, input: Record<string, unknown>) => Promise<Record<string, unknown>> }) => Promise<Record<string, unknown>>}
289
+ */
290
+ function generatedPrompt(name) {
291
+ /**
292
+ * Execute generated prompt resolver.
293
+ * @param {unknown} _root - GraphQL root.
294
+ * @param {{ input?: Record<string, unknown> }} args - Resolver args.
295
+ * @param {{ getPrompt: (name: string, input: Record<string, unknown>) => Promise<Record<string, unknown>> }} context - Resolver context.
296
+ * @returns {Promise<Record<string, unknown>>}
297
+ */
298
+ async function resolve(_root, args, context) {
299
+ return context.getPrompt(name, args.input ?? {});
300
+ }
301
+
302
+ return resolve;
303
+ }
304
+
305
+ /**
306
+ * Create generated resource resolver.
307
+ * @param {string} uri - Resource uri.
308
+ * @returns {(root: unknown, args: unknown, context: { readResource: (uri: string) => Promise<Record<string, unknown>> }) => Promise<Record<string, unknown>>}
309
+ */
310
+ function generatedResource(uri) {
311
+ /**
312
+ * Execute generated resource resolver.
313
+ * @param {unknown} _root - GraphQL root.
314
+ * @param {unknown} _args - Resolver args.
315
+ * @param {{ readResource: (uri: string) => Promise<Record<string, unknown>> }} context - Resolver context.
316
+ * @returns {Promise<Record<string, unknown>>}
317
+ */
318
+ async function resolve(_root, _args, context) {
319
+ return context.readResource(uri);
320
+ }
321
+
322
+ return resolve;
323
+ }
324
+
325
+ /**
326
+ * Create generated resource template resolver.
327
+ * @param {string} source - Resource template.
328
+ * @returns {(root: unknown, args: { params?: Record<string, unknown> }, context: { readTemplate: (template: string, params: Record<string, unknown>) => Promise<Record<string, unknown>> }) => Promise<Record<string, unknown>>}
329
+ */
330
+ function generatedTemplate(source) {
331
+ /**
332
+ * Execute generated template resolver.
333
+ * @param {unknown} _root - GraphQL root.
334
+ * @param {{ params?: Record<string, unknown> }} args - Resolver args.
335
+ * @param {{ readTemplate: (template: string, params: Record<string, unknown>) => Promise<Record<string, unknown>> }} context - Resolver context.
336
+ * @returns {Promise<Record<string, unknown>>}
337
+ */
338
+ async function resolve(_root, args, context) {
339
+ return context.readTemplate(source, args.params ?? {});
340
+ }
341
+
342
+ return resolve;
343
+ }
344
+
345
+ /**
346
+ * Build resolver map for the schema.
347
+ * @param {ReturnType<typeof map>} mapping - Catalog map.
348
+ * @param {{ operations: { generated: boolean, generic: boolean } }} config - Normalized config.
349
+ * @returns {Record<string, unknown>}
350
+ */
351
+ function buildResolvers(mapping, config) {
352
+ const query = { catalog: createCatalogResolver(mapping) };
353
+ const mutation = {};
354
+
355
+ if (config.operations.generic) {
356
+ query.readResource = genericResource();
357
+ query.readTemplate = genericTemplate();
358
+ mutation.callTool = genericTool();
359
+ mutation.getPrompt = genericPrompt();
360
+ }
361
+
362
+ if (config.operations.generated) {
363
+ for (const entry of mapping.entries) {
364
+ if (entry.type === 'tool') {
365
+ mutation[entry.field] = generatedTool(entry.name);
366
+ continue;
367
+ }
368
+
369
+ if (entry.type === 'prompt') {
370
+ mutation[entry.field] = generatedPrompt(entry.name);
371
+ continue;
372
+ }
373
+
374
+ if (entry.type === 'resource') {
375
+ const uri = entry.item.detail?.uri ? String(entry.item.detail.uri) : '';
376
+ query[entry.field] = generatedResource(uri);
377
+ continue;
378
+ }
379
+
380
+ if (entry.type === 'resource-template') {
381
+ const source = entry.item.detail?.uriTemplate ? String(entry.item.detail.uriTemplate) : '';
382
+ query[entry.field] = generatedTemplate(source);
383
+ }
384
+ }
385
+ }
386
+
387
+ const resolvers = {
388
+ JSON: GraphQLJSON,
389
+ Query: query
390
+ };
391
+
392
+ if (Object.keys(mutation).length > 0) {
393
+ resolvers.Mutation = mutation;
394
+ }
395
+
396
+ return resolvers;
397
+ }
398
+
399
+ /**
400
+ * Build an executable GraphQL schema from an MCP catalog.
401
+ * @param {{ items?: Array<Record<string, unknown>> }} catalog - Extracted catalog.
402
+ * @param {{ operations?: { generated?: boolean, generic?: boolean } }} [options] - Build options.
403
+ * @returns {{ schema: import('graphql').GraphQLSchema, typeDefs: string, resolvers: Record<string, unknown>, mapping: ReturnType<typeof map> }}
404
+ */
405
+ export function schema(catalog = {}, options = {}) {
406
+ const config = normalizeOptions(options);
407
+ const mapping = map(catalog);
408
+ const typeDefs = buildTypeDefs(mapping, config);
409
+ const resolvers = buildResolvers(mapping, config);
410
+ const executable = makeExecutableSchema({ typeDefs, resolvers });
411
+
412
+ return {
413
+ schema: executable,
414
+ typeDefs,
415
+ resolvers,
416
+ mapping
417
+ };
418
+ }