@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/plugin.js ADDED
@@ -0,0 +1,650 @@
1
+ import fp from 'fastify-plugin';
2
+ import mercurius from 'mercurius';
3
+ import { GraphQLError } from 'graphql';
4
+ import { createRequire } from 'node:module';
5
+ import { LayerError } from '@mcp-layer/error';
6
+ import { createCallContext, createRuntime, executeWithBreaker } from '@mcp-layer/gateway';
7
+ import { schema as buildSchema } from './schema.js';
8
+ import { validateOptions } from './config/validate.js';
9
+
10
+ const require = createRequire(import.meta.url);
11
+ const templateLib = require('uri-template');
12
+
13
+ /**
14
+ * Map JSON-RPC/MCP numeric errors to GraphQL extension codes.
15
+ * @type {Record<number, { code: string, title: string }>}
16
+ */
17
+ const MCP_ERROR_MAP = {
18
+ [-32700]: { code: 'BAD_REQUEST', title: 'Parse Error' },
19
+ [-32600]: { code: 'BAD_REQUEST', title: 'Invalid Request' },
20
+ [-32601]: { code: 'NOT_FOUND', title: 'Method Not Found' },
21
+ [-32602]: { code: 'BAD_USER_INPUT', title: 'Invalid Parameters' },
22
+ [-32603]: { code: 'INTERNAL_SERVER_ERROR', title: 'Internal Error' },
23
+ [-32000]: { code: 'INTERNAL_SERVER_ERROR', title: 'Server Error' },
24
+ [-32001]: { code: 'TIMEOUT', title: 'Request Timeout' },
25
+ [-32002]: { code: 'NOT_FOUND', title: 'Resource Not Found' }
26
+ };
27
+
28
+ /**
29
+ * Ensure an input value is a plain object.
30
+ * @param {unknown} value - Candidate value.
31
+ * @returns {Record<string, unknown>}
32
+ */
33
+ function asRecord(value) {
34
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
35
+ return /** @type {Record<string, unknown>} */ (value);
36
+ }
37
+
38
+ return {};
39
+ }
40
+
41
+ /**
42
+ * Normalize tool call payload for GraphQL response shape.
43
+ * @param {Record<string, unknown> | null | undefined} value - Tool call payload.
44
+ * @returns {{ content: unknown, isError: boolean, structuredContent?: unknown }}
45
+ */
46
+ function toolPayload(value) {
47
+ const payload = value && typeof value === 'object' ? value : {};
48
+ return {
49
+ content: Array.isArray(payload.content) ? payload.content : [],
50
+ isError: false,
51
+ structuredContent: payload.structuredContent
52
+ };
53
+ }
54
+
55
+ /**
56
+ * Normalize prompt payload for GraphQL response shape.
57
+ * @param {Record<string, unknown> | null | undefined} value - Prompt payload.
58
+ * @returns {{ messages: unknown, payload: unknown }}
59
+ */
60
+ function promptPayload(value) {
61
+ const payload = value && typeof value === 'object' ? value : {};
62
+ const messages = Array.isArray(payload.messages) ? payload.messages : [];
63
+ return {
64
+ messages,
65
+ payload
66
+ };
67
+ }
68
+
69
+ /**
70
+ * Normalize resource payload for GraphQL response shape.
71
+ * @param {Record<string, unknown> | null | undefined} value - Resource payload.
72
+ * @returns {{ contents: unknown, text?: string, mimeType?: string, payload: unknown }}
73
+ */
74
+ function resourcePayload(value) {
75
+ const payload = value && typeof value === 'object' ? value : {};
76
+ const contents = Array.isArray(payload.contents) ? payload.contents : [];
77
+ const first = contents.length > 0 ? contents[0] : null;
78
+
79
+ return {
80
+ contents,
81
+ text: typeof first?.text === 'string' ? first.text : undefined,
82
+ mimeType: typeof first?.mimeType === 'string' ? first.mimeType : undefined,
83
+ payload
84
+ };
85
+ }
86
+
87
+ /**
88
+ * Validate template parameter value sizes.
89
+ * @param {Record<string, unknown>} params - Parameter values.
90
+ * @param {number} max - Maximum allowed parameter length.
91
+ * @returns {Array<{ path: string, message: string }>}
92
+ */
93
+ function validateTemplateParams(params, max) {
94
+ const errors = [];
95
+
96
+ for (const [key, value] of Object.entries(params)) {
97
+ if (value === undefined || value === null) continue;
98
+ const text = typeof value === 'string' ? value : String(value);
99
+ if (text.length > max) {
100
+ errors.push({
101
+ path: `params.${key}`,
102
+ message: `Parameter exceeds maximum length of ${max}.`
103
+ });
104
+ }
105
+ }
106
+
107
+ return errors;
108
+ }
109
+
110
+ /**
111
+ * Expand an RFC6570 URI template with argument values.
112
+ * @param {string} template - URI template expression.
113
+ * @param {Record<string, unknown>} values - Template variables.
114
+ * @returns {string}
115
+ */
116
+ function expandTemplate(template, values) {
117
+ const parsed = templateLib.parse(template);
118
+ return parsed.expand(values);
119
+ }
120
+
121
+ /**
122
+ * Build a GraphiQL redirect path for a runtime prefix.
123
+ * @param {string} prefix - Runtime route prefix.
124
+ * @returns {string}
125
+ */
126
+ function graphiqlRedirect(prefix) {
127
+ const base = prefix === '/' ? '' : prefix.replace(/\/+$/, '');
128
+ return `${base}/graphiql`;
129
+ }
130
+
131
+ /**
132
+ * Create Mercurius context factory for a runtime session.
133
+ * @param {{ session: import('@mcp-layer/session').Session, validator: import('@mcp-layer/gateway').SchemaValidator, telemetry: ReturnType<import('@mcp-layer/gateway').createTelemetry> | null, resolve: (request: import('fastify').FastifyRequest) => Promise<{ session: import('@mcp-layer/session').Session, breaker: import('opossum') | null }>, normalize: (error: Error & { code?: string | number }, instance: string, requestId?: string) => unknown }} runtime - Runtime context.
134
+ * @param {{ validation: { maxTemplateParamLength: number }, errors: { exposeDetails: boolean } }} config - GraphQL plugin config.
135
+ * @returns {(request: import('fastify').FastifyRequest, reply: import('fastify').FastifyReply) => Promise<{ request: import('fastify').FastifyRequest, reply: import('fastify').FastifyReply, callTool: (name: string, input: Record<string, unknown>) => Promise<{ content: unknown, isError: boolean, structuredContent?: unknown }>, getPrompt: (name: string, input: Record<string, unknown>) => Promise<{ messages: unknown, payload: unknown }>, readResource: (uri: string) => Promise<{ contents: unknown, text?: string, mimeType?: string, payload: unknown }>, readTemplate: (template: string, params: Record<string, unknown>) => Promise<{ contents: unknown, text?: string, mimeType?: string, payload: unknown }> }>}
136
+ */
137
+ function createContextFactory(runtime, config) {
138
+ /**
139
+ * Create context object for a GraphQL request.
140
+ * @param {import('fastify').FastifyRequest} request - Fastify request.
141
+ * @param {import('fastify').FastifyReply} reply - Fastify reply.
142
+ * @returns {Promise<{ request: import('fastify').FastifyRequest, reply: import('fastify').FastifyReply, callTool: (name: string, input: Record<string, unknown>) => Promise<{ content: unknown, isError: boolean, structuredContent?: unknown }>, getPrompt: (name: string, input: Record<string, unknown>) => Promise<{ messages: unknown, payload: unknown }>, readResource: (uri: string) => Promise<{ contents: unknown, text?: string, mimeType?: string, payload: unknown }>, readTemplate: (template: string, params: Record<string, unknown>) => Promise<{ contents: unknown, text?: string, mimeType?: string, payload: unknown }> }>}
143
+ */
144
+ async function createContext(request, reply) {
145
+ /**
146
+ * Execute an MCP tool call.
147
+ * @param {string} name - Tool name.
148
+ * @param {Record<string, unknown>} input - Tool input.
149
+ * @returns {Promise<{ content: unknown, isError: boolean, structuredContent?: unknown }>}
150
+ */
151
+ async function callTool(name, input) {
152
+ const requestId = request.id;
153
+ const instance = request.url;
154
+ let trace;
155
+
156
+ try {
157
+ const args = asRecord(input);
158
+ const check = runtime.validator.validate('tool', name, args);
159
+ if (!check.valid) {
160
+ throw new LayerError({
161
+ name: 'graphql',
162
+ method: 'callTool',
163
+ message: 'Request payload failed schema validation.',
164
+ code: 'GRAPHQL_VALIDATION',
165
+ details: check.errors ?? [],
166
+ instance,
167
+ requestId
168
+ });
169
+ }
170
+
171
+ const resolved = await runtime.resolve(request);
172
+
173
+ trace = createCallContext({
174
+ telemetry: runtime.telemetry,
175
+ spanName: 'mcp.tools/call',
176
+ attributes: {
177
+ 'mcp.tool.name': name,
178
+ 'mcp.session.name': resolved.session.name,
179
+ 'http.request.id': requestId
180
+ },
181
+ labels: { tool: name, session: resolved.session.name },
182
+ validationLabels: { tool: name }
183
+ });
184
+
185
+ const result = await executeWithBreaker(resolved.breaker, resolved.session, 'tools/call', {
186
+ name,
187
+ arguments: args
188
+ });
189
+
190
+ if (result?.isError) {
191
+ trace.recordStatus('tool_error', 'tool_error');
192
+ throw new LayerError({
193
+ name: 'graphql',
194
+ method: 'callTool',
195
+ message: 'Tool "{tool}" reported an error.',
196
+ vars: { tool: name },
197
+ code: 'GRAPHQL_TOOL',
198
+ tool: name,
199
+ session: resolved.session.name,
200
+ result,
201
+ instance,
202
+ requestId
203
+ });
204
+ }
205
+
206
+ trace.recordSuccess();
207
+ return toolPayload(result);
208
+ } catch (error) {
209
+ const runtimeError = /** @type {Error & { code?: string | number, details?: unknown, result?: unknown, session?: unknown, tool?: unknown, package?: string, method?: string, sessionName?: string }} */ (error);
210
+ if (runtimeError.code !== 'GRAPHQL_TOOL' && runtimeError.code !== 'GRAPHQL_VALIDATION') trace?.recordError(runtimeError);
211
+
212
+ if (runtimeError.code === 'GRAPHQL_VALIDATION') {
213
+ throw new GraphQLError('Request payload failed schema validation.', {
214
+ extensions: {
215
+ code: 'BAD_USER_INPUT',
216
+ type: 'error-validation',
217
+ instance,
218
+ requestId,
219
+ errors: Array.isArray(runtimeError.details) ? runtimeError.details : []
220
+ }
221
+ });
222
+ }
223
+
224
+ if (runtimeError.code === 'GRAPHQL_TOOL') {
225
+ throw new GraphQLError(`Tool "${runtimeError.tool}" reported an error.`, {
226
+ extensions: {
227
+ code: 'TOOL_ERROR',
228
+ type: 'error-tool',
229
+ instance,
230
+ requestId,
231
+ tool: typeof runtimeError.tool === 'string' ? runtimeError.tool : 'unknown',
232
+ session: typeof runtimeError.session === 'string' ? runtimeError.session : 'unknown',
233
+ toolError: {
234
+ content: Array.isArray(runtimeError.result?.content) ? runtimeError.result.content : [],
235
+ isError: true
236
+ }
237
+ }
238
+ });
239
+ }
240
+
241
+ const normalized = runtime.normalize(runtimeError, instance, requestId);
242
+ if (normalized instanceof GraphQLError) {
243
+ const extensions = normalized.extensions ? { ...normalized.extensions } : undefined;
244
+
245
+ throw new GraphQLError(normalized.message, { extensions });
246
+ }
247
+
248
+ const isManagerIdentityError = runtimeError.name === 'LayerError'
249
+ && runtimeError.package === '@mcp-layer/manager'
250
+ && runtimeError.method === 'identity';
251
+
252
+ const isAuthRequired = runtimeError.code === 'AUTH_REQUIRED'
253
+ || (isManagerIdentityError && runtimeError.message.includes('Authorization header is required.'));
254
+ const isAuthInvalid = runtimeError.code === 'AUTH_INVALID'
255
+ || (isManagerIdentityError && runtimeError.message.includes('Authorization header must use '));
256
+
257
+ if (isAuthRequired || isAuthInvalid) {
258
+ throw new GraphQLError('Authorization failed.', {
259
+ extensions: {
260
+ code: 'UNAUTHENTICATED',
261
+ type: 'error-auth',
262
+ instance,
263
+ requestId
264
+ }
265
+ });
266
+ }
267
+
268
+ if (runtimeError.code === 'CIRCUIT_OPEN') {
269
+ throw new GraphQLError('Service temporarily unavailable.', {
270
+ extensions: {
271
+ code: 'SERVICE_UNAVAILABLE',
272
+ type: 'error-circuit-open',
273
+ instance,
274
+ requestId,
275
+ session: runtimeError.sessionName
276
+ }
277
+ });
278
+ }
279
+
280
+ const numericCode = typeof runtimeError.code === 'number' ? runtimeError.code : -32603;
281
+ const mapped = MCP_ERROR_MAP[numericCode] ?? MCP_ERROR_MAP[-32603];
282
+ const detail = config.errors.exposeDetails ? runtimeError.message : 'Upstream service error';
283
+
284
+ throw new GraphQLError(detail, {
285
+ extensions: {
286
+ code: mapped.code,
287
+ title: mapped.title,
288
+ type: 'error-runtime',
289
+ instance,
290
+ requestId,
291
+ mcpErrorCode: numericCode
292
+ }
293
+ });
294
+ } finally {
295
+ trace?.finish();
296
+ }
297
+ }
298
+
299
+ /**
300
+ * Execute an MCP prompt request.
301
+ * @param {string} name - Prompt name.
302
+ * @param {Record<string, unknown>} input - Prompt input.
303
+ * @returns {Promise<{ messages: unknown, payload: unknown }>}
304
+ */
305
+ async function getPrompt(name, input) {
306
+ const requestId = request.id;
307
+ const instance = request.url;
308
+ let trace;
309
+
310
+ try {
311
+ const args = asRecord(input);
312
+ const check = runtime.validator.validate('prompt', name, args);
313
+ if (!check.valid) {
314
+ throw new LayerError({
315
+ name: 'graphql',
316
+ method: 'getPrompt',
317
+ message: 'Request payload failed schema validation.',
318
+ code: 'GRAPHQL_VALIDATION',
319
+ details: check.errors ?? [],
320
+ instance,
321
+ requestId
322
+ });
323
+ }
324
+
325
+ const resolved = await runtime.resolve(request);
326
+
327
+ trace = createCallContext({
328
+ telemetry: runtime.telemetry,
329
+ spanName: 'mcp.prompts/get',
330
+ attributes: {
331
+ 'mcp.prompt.name': name,
332
+ 'mcp.session.name': resolved.session.name,
333
+ 'http.request.id': requestId
334
+ },
335
+ labels: { prompt: name, session: resolved.session.name },
336
+ validationLabels: { prompt: name }
337
+ });
338
+
339
+ const result = await executeWithBreaker(resolved.breaker, resolved.session, 'prompts/get', {
340
+ name,
341
+ arguments: args
342
+ });
343
+
344
+ trace.recordSuccess();
345
+ return promptPayload(result);
346
+ } catch (error) {
347
+ const runtimeError = /** @type {Error & { code?: string | number, details?: unknown, package?: string, method?: string, sessionName?: string }} */ (error);
348
+ if (runtimeError.code !== 'GRAPHQL_VALIDATION') trace?.recordError(runtimeError);
349
+
350
+ if (runtimeError.code === 'GRAPHQL_VALIDATION') {
351
+ throw new GraphQLError('Request payload failed schema validation.', {
352
+ extensions: {
353
+ code: 'BAD_USER_INPUT',
354
+ type: 'error-validation',
355
+ instance,
356
+ requestId,
357
+ errors: Array.isArray(runtimeError.details) ? runtimeError.details : []
358
+ }
359
+ });
360
+ }
361
+
362
+ const normalized = runtime.normalize(runtimeError, instance, requestId);
363
+ if (normalized instanceof GraphQLError) {
364
+ const extensions = normalized.extensions ? { ...normalized.extensions } : undefined;
365
+
366
+ throw new GraphQLError(normalized.message, { extensions });
367
+ }
368
+
369
+ const isManagerIdentityError = runtimeError.name === 'LayerError'
370
+ && runtimeError.package === '@mcp-layer/manager'
371
+ && runtimeError.method === 'identity';
372
+
373
+ const isAuthRequired = runtimeError.code === 'AUTH_REQUIRED'
374
+ || (isManagerIdentityError && runtimeError.message.includes('Authorization header is required.'));
375
+ const isAuthInvalid = runtimeError.code === 'AUTH_INVALID'
376
+ || (isManagerIdentityError && runtimeError.message.includes('Authorization header must use '));
377
+
378
+ if (isAuthRequired || isAuthInvalid) {
379
+ throw new GraphQLError('Authorization failed.', {
380
+ extensions: {
381
+ code: 'UNAUTHENTICATED',
382
+ type: 'error-auth',
383
+ instance,
384
+ requestId
385
+ }
386
+ });
387
+ }
388
+
389
+ if (runtimeError.code === 'CIRCUIT_OPEN') {
390
+ throw new GraphQLError('Service temporarily unavailable.', {
391
+ extensions: {
392
+ code: 'SERVICE_UNAVAILABLE',
393
+ type: 'error-circuit-open',
394
+ instance,
395
+ requestId,
396
+ session: runtimeError.sessionName
397
+ }
398
+ });
399
+ }
400
+
401
+ const numericCode = typeof runtimeError.code === 'number' ? runtimeError.code : -32603;
402
+ const mapped = MCP_ERROR_MAP[numericCode] ?? MCP_ERROR_MAP[-32603];
403
+ const detail = config.errors.exposeDetails ? runtimeError.message : 'Upstream service error';
404
+
405
+ throw new GraphQLError(detail, {
406
+ extensions: {
407
+ code: mapped.code,
408
+ title: mapped.title,
409
+ type: 'error-runtime',
410
+ instance,
411
+ requestId,
412
+ mcpErrorCode: numericCode
413
+ }
414
+ });
415
+ } finally {
416
+ trace?.finish();
417
+ }
418
+ }
419
+
420
+ /**
421
+ * Execute an MCP resource read.
422
+ * @param {string} uri - Resource uri.
423
+ * @returns {Promise<{ contents: unknown, text?: string, mimeType?: string, payload: unknown }>}
424
+ */
425
+ async function readResource(uri) {
426
+ const requestId = request.id;
427
+ const instance = request.url;
428
+ let trace;
429
+
430
+ try {
431
+ const resolved = await runtime.resolve(request);
432
+
433
+ trace = createCallContext({
434
+ telemetry: runtime.telemetry,
435
+ spanName: 'mcp.resources/read',
436
+ attributes: {
437
+ 'mcp.resource.uri': uri,
438
+ 'mcp.session.name': resolved.session.name,
439
+ 'http.request.id': requestId
440
+ },
441
+ labels: { resource: uri, session: resolved.session.name }
442
+ });
443
+
444
+ const result = await executeWithBreaker(resolved.breaker, resolved.session, 'resources/read', { uri });
445
+ trace.recordSuccess();
446
+ return resourcePayload(result);
447
+ } catch (error) {
448
+ const runtimeError = /** @type {Error & { code?: string | number, package?: string, method?: string, sessionName?: string }} */ (error);
449
+ trace?.recordError(runtimeError);
450
+
451
+ const normalized = runtime.normalize(runtimeError, instance, requestId);
452
+ if (normalized instanceof GraphQLError) {
453
+ const extensions = normalized.extensions ? { ...normalized.extensions } : undefined;
454
+
455
+ throw new GraphQLError(normalized.message, { extensions });
456
+ }
457
+
458
+ const isManagerIdentityError = runtimeError.name === 'LayerError'
459
+ && runtimeError.package === '@mcp-layer/manager'
460
+ && runtimeError.method === 'identity';
461
+
462
+ const isAuthRequired = runtimeError.code === 'AUTH_REQUIRED'
463
+ || (isManagerIdentityError && runtimeError.message.includes('Authorization header is required.'));
464
+ const isAuthInvalid = runtimeError.code === 'AUTH_INVALID'
465
+ || (isManagerIdentityError && runtimeError.message.includes('Authorization header must use '));
466
+
467
+ if (isAuthRequired || isAuthInvalid) {
468
+ throw new GraphQLError('Authorization failed.', {
469
+ extensions: {
470
+ code: 'UNAUTHENTICATED',
471
+ type: 'error-auth',
472
+ instance,
473
+ requestId
474
+ }
475
+ });
476
+ }
477
+
478
+ if (runtimeError.code === 'CIRCUIT_OPEN') {
479
+ throw new GraphQLError('Service temporarily unavailable.', {
480
+ extensions: {
481
+ code: 'SERVICE_UNAVAILABLE',
482
+ type: 'error-circuit-open',
483
+ instance,
484
+ requestId,
485
+ session: runtimeError.sessionName
486
+ }
487
+ });
488
+ }
489
+
490
+ const numericCode = typeof runtimeError.code === 'number' ? runtimeError.code : -32603;
491
+ const mapped = MCP_ERROR_MAP[numericCode] ?? MCP_ERROR_MAP[-32603];
492
+ const detail = config.errors.exposeDetails ? runtimeError.message : 'Upstream service error';
493
+
494
+ throw new GraphQLError(detail, {
495
+ extensions: {
496
+ code: mapped.code,
497
+ title: mapped.title,
498
+ type: 'error-runtime',
499
+ instance,
500
+ requestId,
501
+ mcpErrorCode: numericCode
502
+ }
503
+ });
504
+ } finally {
505
+ trace?.finish();
506
+ }
507
+ }
508
+
509
+ /**
510
+ * Execute an MCP template read.
511
+ * @param {string} template - URI template.
512
+ * @param {Record<string, unknown>} params - Template parameters.
513
+ * @returns {Promise<{ contents: unknown, text?: string, mimeType?: string, payload: unknown }>}
514
+ */
515
+ async function readTemplate(template, params) {
516
+ const requestId = request.id;
517
+ const instance = request.url;
518
+ const values = asRecord(params);
519
+ const errors = validateTemplateParams(values, config.validation.maxTemplateParamLength);
520
+
521
+ if (errors.length > 0) {
522
+ throw new GraphQLError('Request payload failed schema validation.', {
523
+ extensions: {
524
+ code: 'BAD_USER_INPUT',
525
+ type: 'error-validation',
526
+ instance,
527
+ requestId,
528
+ errors
529
+ }
530
+ });
531
+ }
532
+
533
+ const uri = expandTemplate(template, values);
534
+
535
+ return readResource(uri);
536
+ }
537
+
538
+ return {
539
+ request,
540
+ reply,
541
+ callTool,
542
+ getPrompt,
543
+ readResource,
544
+ readTemplate
545
+ };
546
+ }
547
+
548
+ return createContext;
549
+ }
550
+
551
+ /**
552
+ * Register GraphQL routes for a runtime context.
553
+ * @param {import('fastify').FastifyInstance} fastify - Fastify instance.
554
+ * @param {{ catalog: { items?: Array<Record<string, unknown>> }, prefix: string, session: import('@mcp-layer/session').Session, validator: import('@mcp-layer/gateway').SchemaValidator, telemetry: ReturnType<import('@mcp-layer/gateway').createTelemetry> | null, resolve: (request: import('fastify').FastifyRequest) => Promise<{ session: import('@mcp-layer/session').Session, breaker: import('opossum') | null }>, normalize: (error: Error & { code?: string | number }, instance: string, requestId?: string) => unknown }} runtime - Runtime context.
555
+ * @param {{ endpoint: string, ide: { enabled: boolean, path: string }, operations: { generated: boolean, generic: boolean }, validation: { maxTemplateParamLength: number }, errors: { exposeDetails: boolean } }} config - Plugin config.
556
+ * @returns {Promise<void>}
557
+ */
558
+ async function registerContext(fastify, runtime, config) {
559
+ const built = buildSchema(runtime.catalog, {
560
+ operations: config.operations
561
+ });
562
+
563
+ const contextFactory = createContextFactory(runtime, config);
564
+ const graphiql = config.ide.enabled;
565
+
566
+ /**
567
+ * Register scoped GraphQL endpoint.
568
+ * @param {import('fastify').FastifyInstance} scoped - Scoped instance.
569
+ * @returns {Promise<void>}
570
+ */
571
+ async function registerScoped(scoped) {
572
+ const graphiqlPath = graphiqlRedirect(runtime.prefix);
573
+
574
+ await scoped.register(mercurius, {
575
+ schema: built.schema,
576
+ path: config.endpoint,
577
+ graphiql,
578
+ context: contextFactory
579
+ });
580
+
581
+ if (config.ide.enabled && config.ide.path !== '/graphiql') {
582
+ /**
583
+ * Redirect custom IDE path to Mercurius default graphiql endpoint.
584
+ * @param {import('fastify').FastifyRequest} request - Fastify request.
585
+ * @param {import('fastify').FastifyReply} reply - Fastify reply.
586
+ * @returns {Promise<void>}
587
+ */
588
+ async function ideAlias(request, reply) {
589
+ reply.redirect(graphiqlPath);
590
+ }
591
+
592
+ scoped.get(config.ide.path, ideAlias);
593
+ }
594
+ }
595
+
596
+ await fastify.register(registerScoped, { prefix: runtime.prefix });
597
+ }
598
+
599
+ /**
600
+ * Fastify plugin that exposes MCP sessions as GraphQL endpoints.
601
+ * @param {import('fastify').FastifyInstance} fastify - Fastify instance.
602
+ * @param {Record<string, unknown>} opts - Plugin options.
603
+ * @returns {Promise<void>}
604
+ */
605
+ async function mcpGraphqlPlugin(fastify, opts) {
606
+ const config = validateOptions(opts);
607
+ const runtime = await createRuntime(
608
+ {
609
+ ...config,
610
+ telemetry: {
611
+ ...config.telemetry,
612
+ metricPrefix: 'graphql'
613
+ }
614
+ },
615
+ {
616
+ name: 'graphql',
617
+ serviceName: config.telemetry.serviceName
618
+ }
619
+ );
620
+
621
+ if (!fastify.mcpBreakers) {
622
+ fastify.decorate('mcpBreakers', runtime.breakers);
623
+ } else {
624
+ fastify.mcpBreakers = runtime.breakers;
625
+ }
626
+
627
+ for (const context of runtime.contexts) {
628
+ await registerContext(fastify, context, config);
629
+ }
630
+
631
+ /**
632
+ * Close runtime resources on plugin close.
633
+ * @returns {Promise<void>}
634
+ */
635
+ async function onClose() {
636
+ await runtime.close();
637
+ }
638
+
639
+ fastify.addHook('onClose', onClose);
640
+ }
641
+
642
+ const plugin = fp(mcpGraphqlPlugin, {
643
+ fastify: '5.x',
644
+ name: '@mcp-layer/graphql',
645
+ decorators: { fastify: [] },
646
+ dependencies: []
647
+ });
648
+
649
+ export default plugin;
650
+ export { plugin };