@mcp-layer/gateway 0.2.1 → 0.2.3
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/README.md +95 -14
- package/package.json +5 -5
- package/src/config/validate.js +13 -3
- package/src/runtime.js +58 -6
- package/src/validation/trust.js +5 -1
- package/src/validation/validator.js +2 -2
- package/types/config/validate.d.ts +8 -2
- package/types/runtime.d.ts +2 -2
- package/types/validation/trust.d.ts +5 -2
- package/types/validation/validator.d.ts +4 -4
package/README.md
CHANGED
|
@@ -35,7 +35,7 @@ Use this package when you are building an interface layer on top of MCP that nee
|
|
|
35
35
|
|
|
36
36
|
This example demonstrates how adapter packages bootstrap runtime state once and reuse it per request. This matters because repeated catalog extraction and ad-hoc breaker creation quickly cause drift between adapters.
|
|
37
37
|
|
|
38
|
-
Expected behavior: one runtime context is created per bootstrap session, with a stable prefix/version and reusable resolver/validator/executor primitives.
|
|
38
|
+
Expected behavior: one runtime context is created per bootstrap session or precomputed catalog, with a stable prefix/version and reusable resolver/validator/executor primitives.
|
|
39
39
|
|
|
40
40
|
```js
|
|
41
41
|
import { createRuntime, createMap } from '@mcp-layer/gateway';
|
|
@@ -81,7 +81,8 @@ Signature:
|
|
|
81
81
|
```ts
|
|
82
82
|
createRuntime(
|
|
83
83
|
options: {
|
|
84
|
-
session
|
|
84
|
+
session?: Session | Session[];
|
|
85
|
+
catalog?: { server?: { info?: Record<string, unknown> }, items?: Array<Record<string, unknown>> };
|
|
85
86
|
manager?: { get(request): Promise<Session>; close?(): Promise<void> };
|
|
86
87
|
prefix?: string | ((version, info, sessionName) => string);
|
|
87
88
|
validation?: {
|
|
@@ -117,7 +118,7 @@ createRuntime(
|
|
|
117
118
|
): Promise<{
|
|
118
119
|
config;
|
|
119
120
|
contexts: Array<{
|
|
120
|
-
session;
|
|
121
|
+
session | undefined;
|
|
121
122
|
catalog;
|
|
122
123
|
info;
|
|
123
124
|
version;
|
|
@@ -136,7 +137,9 @@ createRuntime(
|
|
|
136
137
|
|
|
137
138
|
Behavior notes:
|
|
138
139
|
|
|
139
|
-
- `manager` requires a bootstrap `session`
|
|
140
|
+
- `manager` requires either a bootstrap `session` or a precomputed `catalog`.
|
|
141
|
+
- when a bootstrap `session` exists, runtime metadata is always extracted from that live session.
|
|
142
|
+
- `catalog` bootstrap is only used when manager mode has no bootstrap session yet.
|
|
140
143
|
- manager mode does not support `session` arrays.
|
|
141
144
|
- `close()` shuts down breaker instances and calls `manager.close()` when available.
|
|
142
145
|
- validation registration is preloaded from catalog tool/prompt input schemas.
|
|
@@ -278,12 +281,12 @@ Thrown from: `validateRuntimeOptions`
|
|
|
278
281
|
This happens when `manager` is provided with `session` as an array.
|
|
279
282
|
|
|
280
283
|
Step-by-step resolution:
|
|
281
|
-
1. In manager mode, provide exactly one bootstrap session.
|
|
284
|
+
1. In manager mode, provide exactly one bootstrap session or one precomputed catalog.
|
|
282
285
|
2. If you need multiple static sessions, remove manager mode.
|
|
283
286
|
3. Register separate adapter instances for each static session surface.
|
|
284
287
|
4. Add explicit startup tests for manager mode vs multi-session mode.
|
|
285
288
|
|
|
286
|
-
This example demonstrates valid manager-mode bootstrapping. This matters because manager mode resolves sessions per request and needs one catalog source at startup. Expected behavior: runtime initializes and delegates per-request resolution through manager.
|
|
289
|
+
This example demonstrates valid manager-mode bootstrapping. This matters because manager mode resolves sessions per request and still needs one catalog source at startup. Expected behavior: runtime initializes and delegates per-request resolution through manager.
|
|
287
290
|
|
|
288
291
|
<details>
|
|
289
292
|
<summary>Fix Example: manager mode with one bootstrap session</summary>
|
|
@@ -297,20 +300,20 @@ await createRuntime({
|
|
|
297
300
|
|
|
298
301
|
</details>
|
|
299
302
|
|
|
300
|
-
<a id="error-
|
|
301
|
-
### `session is required when manager is provided (used for catalog bootstrap).`
|
|
303
|
+
<a id="error-b010d3"></a>
|
|
304
|
+
### `session or catalog is required when manager is provided (used for catalog bootstrap).`
|
|
302
305
|
|
|
303
306
|
Thrown from: `validateRuntimeOptions`
|
|
304
307
|
|
|
305
|
-
This happens when `manager` is configured without a bootstrap `session`.
|
|
308
|
+
This happens when `manager` is configured without a bootstrap `session` or a precomputed `catalog`.
|
|
306
309
|
|
|
307
310
|
Step-by-step resolution:
|
|
308
|
-
1. Provide a bootstrap session alongside manager.
|
|
309
|
-
2. Ensure bootstrap session connects before runtime creation.
|
|
310
|
-
3. Keep
|
|
311
|
+
1. Provide a bootstrap `session` or a precomputed `catalog` alongside manager.
|
|
312
|
+
2. Ensure bootstrap session connects before runtime creation, or ensure the catalog matches the manager-provided session surface.
|
|
313
|
+
3. Keep the catalog aligned with manager-provided session capabilities.
|
|
311
314
|
4. Add a startup test that asserts this requirement.
|
|
312
315
|
|
|
313
|
-
This example shows manager mode with explicit bootstrap catalog source. This matters because the runtime must
|
|
316
|
+
This example shows manager mode with explicit bootstrap catalog source. This matters because the runtime must know route and validator metadata before serving requests. Expected behavior: runtime can build validators and route metadata before request execution begins.
|
|
314
317
|
|
|
315
318
|
<details>
|
|
316
319
|
<summary>Fix Example: include bootstrap session when manager is enabled</summary>
|
|
@@ -324,6 +327,50 @@ await createRuntime({
|
|
|
324
327
|
|
|
325
328
|
</details>
|
|
326
329
|
|
|
330
|
+
<details>
|
|
331
|
+
<summary>Fix Example: include catalog when manager is enabled</summary>
|
|
332
|
+
|
|
333
|
+
```js
|
|
334
|
+
await createRuntime({
|
|
335
|
+
catalog,
|
|
336
|
+
manager
|
|
337
|
+
});
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
</details>
|
|
341
|
+
|
|
342
|
+
<a id="error-17712f"></a>
|
|
343
|
+
### `catalog must be an object.`
|
|
344
|
+
|
|
345
|
+
Thrown from: `validateRuntimeOptions`
|
|
346
|
+
|
|
347
|
+
This happens when `catalog` is provided as a primitive, array, or other non-object value.
|
|
348
|
+
|
|
349
|
+
Step-by-step resolution:
|
|
350
|
+
1. Pass the extracted or composed catalog as a plain object.
|
|
351
|
+
2. Do not pass serialized JSON strings or arrays in place of the catalog root.
|
|
352
|
+
3. Ensure the object shape includes `server` and `items` only where needed.
|
|
353
|
+
4. Add tests that reject malformed catalog bootstrap values.
|
|
354
|
+
|
|
355
|
+
This example shows valid catalog bootstrap input. This matters because manager mode can depend on catalog metadata before any request-scoped session exists. Expected behavior: runtime accepts the catalog and registers validators and route metadata from it.
|
|
356
|
+
|
|
357
|
+
<details>
|
|
358
|
+
<summary>Fix Example: pass catalog as an object</summary>
|
|
359
|
+
|
|
360
|
+
```js
|
|
361
|
+
await createRuntime({
|
|
362
|
+
catalog: {
|
|
363
|
+
server: {
|
|
364
|
+
info: { name: 'example-server', version: '1.0.0' }
|
|
365
|
+
},
|
|
366
|
+
items: []
|
|
367
|
+
},
|
|
368
|
+
manager
|
|
369
|
+
});
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
</details>
|
|
373
|
+
|
|
327
374
|
<a id="error-6e22f2"></a>
|
|
328
375
|
### `session or manager option is required.`
|
|
329
376
|
|
|
@@ -333,7 +380,7 @@ This happens when runtime options include neither `session` nor `manager`.
|
|
|
333
380
|
|
|
334
381
|
Step-by-step resolution:
|
|
335
382
|
1. Pass a connected `session` for static mode.
|
|
336
|
-
2. Or pass `manager` plus a bootstrap `session` for dynamic mode.
|
|
383
|
+
2. Or pass `manager` plus a bootstrap `session` or `catalog` for dynamic mode.
|
|
337
384
|
3. Add adapter-level assertions before calling `createRuntime`.
|
|
338
385
|
4. Add tests that verify startup failure without session sources.
|
|
339
386
|
|
|
@@ -463,6 +510,40 @@ await createRuntime({
|
|
|
463
510
|
|
|
464
511
|
</details>
|
|
465
512
|
|
|
513
|
+
<a id="error-a05713"></a>
|
|
514
|
+
### `No session available for request resolution.`
|
|
515
|
+
|
|
516
|
+
Thrown from: `resolveSession`
|
|
517
|
+
|
|
518
|
+
This happens when request-time session resolution returns `undefined` and no bootstrap session was available to fall back to.
|
|
519
|
+
|
|
520
|
+
Step-by-step resolution:
|
|
521
|
+
1. Verify manager mode always returns a `Session` from `get(request)`.
|
|
522
|
+
2. Ensure static mode passes a connected bootstrap `session`.
|
|
523
|
+
3. Confirm request routing reaches the adapter instance backed by the expected manager or session.
|
|
524
|
+
4. Add a request-path test that exercises the failing resolution path directly.
|
|
525
|
+
|
|
526
|
+
This example demonstrates a manager that always resolves a concrete session. This matters because runtime execution cannot validate or execute MCP methods without a request-scoped target session. Expected behavior: each request resolves a session and proceeds to breaker-backed execution.
|
|
527
|
+
|
|
528
|
+
<details>
|
|
529
|
+
<summary>Fix Example: always return a session from manager.get</summary>
|
|
530
|
+
|
|
531
|
+
```js
|
|
532
|
+
const manager = {
|
|
533
|
+
async get(request) {
|
|
534
|
+
const session = await lookupSession(request);
|
|
535
|
+
if (!session) throw new Error('session lookup failed');
|
|
536
|
+
return session;
|
|
537
|
+
}
|
|
538
|
+
};
|
|
539
|
+
await createRuntime({
|
|
540
|
+
catalog,
|
|
541
|
+
manager
|
|
542
|
+
});
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
</details>
|
|
546
|
+
|
|
466
547
|
## License
|
|
467
548
|
|
|
468
549
|
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mcp-layer/gateway",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "Shared runtime primitives for MCP HTTP/GraphQL adapters with validation, resilience, telemetry, and catalog mapping.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -36,13 +36,13 @@
|
|
|
36
36
|
"access": "public"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"ajv": "^8.
|
|
39
|
+
"ajv": "^8.20.0",
|
|
40
40
|
"ajv-formats": "^3.0.0",
|
|
41
41
|
"json-schema-traverse": "^1.0.0",
|
|
42
42
|
"opossum": "^8.1.0",
|
|
43
43
|
"safe-regex2": "^2.0.0",
|
|
44
44
|
"@mcp-layer/error": "0.2.1",
|
|
45
|
-
"@mcp-layer/schema": "
|
|
45
|
+
"@mcp-layer/schema": "2.0.0"
|
|
46
46
|
},
|
|
47
47
|
"peerDependencies": {
|
|
48
48
|
"@opentelemetry/api": "^1.9.0"
|
|
@@ -53,8 +53,8 @@
|
|
|
53
53
|
}
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
|
-
"fastify": "^5.
|
|
57
|
-
"@mcp-layer/attach": "1.
|
|
56
|
+
"fastify": "^5.12.6",
|
|
57
|
+
"@mcp-layer/attach": "1.2.0",
|
|
58
58
|
"@mcp-layer/manager": "0.2.1",
|
|
59
59
|
"@mcp-layer/test-server": "1.0.4"
|
|
60
60
|
},
|
package/src/config/validate.js
CHANGED
|
@@ -59,7 +59,7 @@ function trustMode(value, pack) {
|
|
|
59
59
|
* Validate base runtime options and apply defaults.
|
|
60
60
|
* @param {Record<string, unknown>} opts - User-supplied options.
|
|
61
61
|
* @param {{ name?: string, serviceName?: string }} [meta] - Validation metadata.
|
|
62
|
-
* @returns {{ session
|
|
62
|
+
* @returns {{ session?: unknown, catalog?: { server?: { info?: Record<string, unknown> }, items?: Array<Record<string, unknown>> }, manager?: { get: (request: import('fastify').FastifyRequest) => Promise<import('@mcp-layer/session').Session>, close?: () => Promise<void> }, prefix?: string | ((version: string, info: Record<string, unknown> | undefined, name: string) => string), validation: { trustSchemas: 'auto' | true | false, maxSchemaDepth: number, maxSchemaSize: number, maxPatternLength: number, maxToolNameLength: number, maxTemplateParamLength: number }, resilience: { enabled: boolean, timeout: number, errorThresholdPercentage: number, resetTimeout: number, volumeThreshold: number }, telemetry: { enabled: boolean, serviceName: string, metricPrefix: string, api?: import('@opentelemetry/api') }, errors: { exposeDetails: boolean }, normalizeError?: (error: Error & { code?: string | number }, instance: string, requestId?: string, options?: { exposeDetails?: boolean }) => unknown }}
|
|
63
63
|
*/
|
|
64
64
|
export function validateRuntimeOptions(opts, meta = {}) {
|
|
65
65
|
const pack = typeof meta.name === 'string' && meta.name.length > 0 ? meta.name : 'gateway';
|
|
@@ -69,6 +69,7 @@ export function validateRuntimeOptions(opts, meta = {}) {
|
|
|
69
69
|
const base = defaults(serviceName);
|
|
70
70
|
const input = isrecord(opts) ? opts : {};
|
|
71
71
|
const session = input.session;
|
|
72
|
+
const catalog = input.catalog;
|
|
72
73
|
const manager = input.manager;
|
|
73
74
|
|
|
74
75
|
if (!session && !manager) {
|
|
@@ -79,6 +80,14 @@ export function validateRuntimeOptions(opts, meta = {}) {
|
|
|
79
80
|
});
|
|
80
81
|
}
|
|
81
82
|
|
|
83
|
+
if (catalog !== undefined && !isrecord(catalog)) {
|
|
84
|
+
throw new LayerError({
|
|
85
|
+
name: pack,
|
|
86
|
+
method: 'validateRuntimeOptions',
|
|
87
|
+
message: 'catalog must be an object.',
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
82
91
|
if (manager !== undefined) {
|
|
83
92
|
if (!isrecord(manager) || typeof manager.get !== 'function') {
|
|
84
93
|
throw new LayerError({
|
|
@@ -87,11 +96,11 @@ export function validateRuntimeOptions(opts, meta = {}) {
|
|
|
87
96
|
message: 'manager must be an object with a get(request) function.',
|
|
88
97
|
});
|
|
89
98
|
}
|
|
90
|
-
if (!session) {
|
|
99
|
+
if (!session && !catalog) {
|
|
91
100
|
throw new LayerError({
|
|
92
101
|
name: pack,
|
|
93
102
|
method: 'validateRuntimeOptions',
|
|
94
|
-
message: 'session is required when manager is provided (used for catalog bootstrap).',
|
|
103
|
+
message: 'session or catalog is required when manager is provided (used for catalog bootstrap).',
|
|
95
104
|
});
|
|
96
105
|
}
|
|
97
106
|
if (Array.isArray(session)) {
|
|
@@ -154,6 +163,7 @@ export function validateRuntimeOptions(opts, meta = {}) {
|
|
|
154
163
|
|
|
155
164
|
return {
|
|
156
165
|
session,
|
|
166
|
+
catalog: /** @type {{ server?: { info?: Record<string, unknown> }, items?: Array<Record<string, unknown>> } | undefined} */ (catalog),
|
|
157
167
|
manager: /** @type {{ get: (request: import('fastify').FastifyRequest) => Promise<import('@mcp-layer/session').Session>, close?: () => Promise<void> } | undefined} */ (manager),
|
|
158
168
|
prefix: input.prefix,
|
|
159
169
|
validation: normalizedValidation,
|
package/src/runtime.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { extract } from '@mcp-layer/schema';
|
|
2
|
+
import { LayerError } from '@mcp-layer/error';
|
|
2
3
|
import { createValidator } from './validation/validator.js';
|
|
3
4
|
import { createCircuitBreaker, executeWithBreaker } from './resilience/breaker.js';
|
|
4
5
|
import { deriveApiVersion, resolvePrefix } from './version.js';
|
|
@@ -14,6 +15,28 @@ function list(session) {
|
|
|
14
15
|
return Array.isArray(session) ? session : [session];
|
|
15
16
|
}
|
|
16
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Resolve the logical context name used for prefix callbacks.
|
|
20
|
+
* Prefer the explicit session name, then the catalog server name, and finally a
|
|
21
|
+
* stable fallback so catalog-driven manager mode can register routes without a
|
|
22
|
+
* bootstrap session.
|
|
23
|
+
*
|
|
24
|
+
* @param {import('@mcp-layer/session').Session | undefined} session - Optional bootstrap session.
|
|
25
|
+
* @param {Record<string, unknown> | undefined} info - Catalog server metadata.
|
|
26
|
+
* @returns {string}
|
|
27
|
+
*/
|
|
28
|
+
function contextName(session, info) {
|
|
29
|
+
if (session && typeof session.name === 'string' && session.name.length > 0) {
|
|
30
|
+
return session.name;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (info && typeof info.name === 'string' && info.name.length > 0) {
|
|
34
|
+
return String(info.name);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return 'session';
|
|
38
|
+
}
|
|
39
|
+
|
|
17
40
|
/**
|
|
18
41
|
* Register tool and prompt validators from a catalog.
|
|
19
42
|
* @param {import('./validation/validator.js').SchemaValidator} validator - Schema validator.
|
|
@@ -69,7 +92,7 @@ function ensureBreaker(breakers, session, config, telemetry) {
|
|
|
69
92
|
|
|
70
93
|
/**
|
|
71
94
|
* Resolve request-scoped session and breaker.
|
|
72
|
-
* @param {import('@mcp-layer/session').Session} bootstrap -
|
|
95
|
+
* @param {import('@mcp-layer/session').Session | undefined} bootstrap - Optional bootstrap session.
|
|
73
96
|
* @param {{ manager?: { get: (request: import('fastify').FastifyRequest) => Promise<import('@mcp-layer/session').Session> }, resilience: { enabled: boolean, timeout: number, errorThresholdPercentage: number, resetTimeout: number, volumeThreshold: number } }} config - Runtime config.
|
|
74
97
|
* @param {Map<string, import('opossum')>} breakers - Breaker storage.
|
|
75
98
|
* @param {ReturnType<import('./telemetry/index.js').createTelemetry> | null} telemetry - Telemetry helper.
|
|
@@ -78,6 +101,13 @@ function ensureBreaker(breakers, session, config, telemetry) {
|
|
|
78
101
|
*/
|
|
79
102
|
async function resolveSession(bootstrap, config, breakers, telemetry, request) {
|
|
80
103
|
const target = config.manager ? await config.manager.get(request) : bootstrap;
|
|
104
|
+
if (!target) {
|
|
105
|
+
throw new LayerError({
|
|
106
|
+
name: 'gateway',
|
|
107
|
+
method: 'resolveSession',
|
|
108
|
+
message: 'No session available for request resolution.',
|
|
109
|
+
});
|
|
110
|
+
}
|
|
81
111
|
const breaker = ensureBreaker(breakers, target, config.resilience, telemetry);
|
|
82
112
|
return { session: target, breaker };
|
|
83
113
|
}
|
|
@@ -86,11 +116,13 @@ async function resolveSession(bootstrap, config, breakers, telemetry, request) {
|
|
|
86
116
|
* Create shared runtime contexts for MCP adapters.
|
|
87
117
|
* @param {Record<string, unknown>} opts - Runtime options.
|
|
88
118
|
* @param {{ name?: string, serviceName?: string }} [meta] - Runtime metadata.
|
|
89
|
-
* @returns {Promise<{ config: ReturnType<typeof validateRuntimeOptions>, contexts: Array<{ session: import('@mcp-layer/session').Session, catalog: { server?: { info?: Record<string, unknown> }, items?: Array<Record<string, unknown>> }, info: Record<string, unknown> | undefined, version: string, prefix: string, validator: import('./validation/validator.js').SchemaValidator, telemetry: ReturnType<typeof createTelemetry> | null, resolve: (request: import('fastify').FastifyRequest) => Promise<{ session: import('@mcp-layer/session').Session, breaker: import('opossum') | null }>, execute: (request: import('fastify').FastifyRequest, method: string, params: Record<string, unknown>) => Promise<Record<string, unknown>>, normalize: (error: Error & { code?: string | number }, instance: string, requestId?: string) => unknown }>, breakers: Map<string, import('opossum')>, normalize: (error: Error & { code?: string | number }, instance: string, requestId?: string) => unknown, close: () => Promise<void> }>}
|
|
119
|
+
* @returns {Promise<{ config: ReturnType<typeof validateRuntimeOptions>, contexts: Array<{ session: import('@mcp-layer/session').Session | undefined, catalog: { server?: { info?: Record<string, unknown> }, items?: Array<Record<string, unknown>> }, info: Record<string, unknown> | undefined, version: string, prefix: string, validator: import('./validation/validator.js').SchemaValidator, telemetry: ReturnType<typeof createTelemetry> | null, resolve: (request: import('fastify').FastifyRequest) => Promise<{ session: import('@mcp-layer/session').Session, breaker: import('opossum') | null }>, execute: (request: import('fastify').FastifyRequest, method: string, params: Record<string, unknown>) => Promise<Record<string, unknown>>, normalize: (error: Error & { code?: string | number }, instance: string, requestId?: string) => unknown }>, breakers: Map<string, import('opossum')>, normalize: (error: Error & { code?: string | number }, instance: string, requestId?: string) => unknown, close: () => Promise<void> }>}
|
|
90
120
|
*/
|
|
91
121
|
export async function createRuntime(opts, meta = {}) {
|
|
92
122
|
const config = validateRuntimeOptions(opts, meta);
|
|
93
|
-
const sessions = config.manager
|
|
123
|
+
const sessions = config.manager
|
|
124
|
+
? (config.session ? [config.session] : [])
|
|
125
|
+
: list(config.session);
|
|
94
126
|
const breakers = new Map();
|
|
95
127
|
const contexts = [];
|
|
96
128
|
|
|
@@ -114,11 +146,19 @@ export async function createRuntime(opts, meta = {}) {
|
|
|
114
146
|
};
|
|
115
147
|
}
|
|
116
148
|
|
|
117
|
-
|
|
118
|
-
|
|
149
|
+
/**
|
|
150
|
+
* Build and store one runtime context from a bootstrap session or a
|
|
151
|
+
* precomputed catalog. Manager mode can omit the session entirely when a
|
|
152
|
+
* catalog was provided explicitly.
|
|
153
|
+
*
|
|
154
|
+
* @param {import('@mcp-layer/session').Session | undefined} session - Optional bootstrap session.
|
|
155
|
+
* @param {{ server?: { info?: Record<string, unknown> }, items?: Array<Record<string, unknown>> }} catalog - Extracted or precomputed catalog.
|
|
156
|
+
* @returns {void}
|
|
157
|
+
*/
|
|
158
|
+
function pushContext(session, catalog) {
|
|
119
159
|
const info = catalog.server?.info;
|
|
120
160
|
const version = deriveApiVersion(info);
|
|
121
|
-
const prefix = resolvePrefix(config.prefix, version, info, session
|
|
161
|
+
const prefix = resolvePrefix(config.prefix, version, info, contextName(session, info));
|
|
122
162
|
const validator = createValidator(config.validation, session);
|
|
123
163
|
const telemetry = createTelemetry(config.telemetry);
|
|
124
164
|
|
|
@@ -170,6 +210,18 @@ export async function createRuntime(opts, meta = {}) {
|
|
|
170
210
|
});
|
|
171
211
|
}
|
|
172
212
|
|
|
213
|
+
if (config.manager && !config.session) {
|
|
214
|
+
pushContext(undefined, config.catalog);
|
|
215
|
+
} else {
|
|
216
|
+
for (const session of sessions) {
|
|
217
|
+
// A live bootstrap session is the source of truth for route metadata.
|
|
218
|
+
// Explicit catalogs are only for manager mode when no bootstrap session
|
|
219
|
+
// exists yet.
|
|
220
|
+
const catalog = await extract(session);
|
|
221
|
+
pushContext(session, catalog);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
173
225
|
/**
|
|
174
226
|
* Close runtime resources.
|
|
175
227
|
* @returns {Promise<void>}
|
package/src/validation/trust.js
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Determine whether schemas from a session should be trusted.
|
|
3
|
-
*
|
|
3
|
+
* Missing bootstrap sessions are treated conservatively because catalog-only
|
|
4
|
+
* manager mode may be registering schemas sourced from a remote runtime.
|
|
5
|
+
*
|
|
6
|
+
* @param {import('@mcp-layer/session').Session | undefined} session - Optional MCP session to evaluate.
|
|
4
7
|
* @param {'auto' | true | false} policy - Trust policy.
|
|
5
8
|
* @returns {boolean}
|
|
6
9
|
*/
|
|
7
10
|
export function shouldTrustSchemas(session, policy) {
|
|
8
11
|
if (policy === true) return true;
|
|
9
12
|
if (policy === false) return false;
|
|
13
|
+
if (!session) return false;
|
|
10
14
|
|
|
11
15
|
const type = session?.transport?.constructor?.name;
|
|
12
16
|
const remote = type === 'StreamableHTTPClientTransport';
|
|
@@ -9,7 +9,7 @@ import { checkSchemaSafety } from './safety.js';
|
|
|
9
9
|
export class SchemaValidator {
|
|
10
10
|
/**
|
|
11
11
|
* @param {{ trustSchemas: 'auto' | true | false, maxSchemaDepth: number, maxSchemaSize: number, maxPatternLength: number, maxToolNameLength: number, maxTemplateParamLength: number }} config - Validation configuration.
|
|
12
|
-
* @param {import('@mcp-layer/session').Session} session -
|
|
12
|
+
* @param {import('@mcp-layer/session').Session | undefined} session - Optional bootstrap session.
|
|
13
13
|
*/
|
|
14
14
|
constructor(config, session) {
|
|
15
15
|
this.#config = config;
|
|
@@ -139,7 +139,7 @@ export class SchemaValidator {
|
|
|
139
139
|
/**
|
|
140
140
|
* Create a schema validator instance.
|
|
141
141
|
* @param {{ trustSchemas: 'auto' | true | false, maxSchemaDepth: number, maxSchemaSize: number, maxPatternLength: number, maxToolNameLength: number, maxTemplateParamLength: number }} config - Validation configuration.
|
|
142
|
-
* @param {import('@mcp-layer/session').Session} session -
|
|
142
|
+
* @param {import('@mcp-layer/session').Session | undefined} session - Optional bootstrap session.
|
|
143
143
|
* @returns {SchemaValidator}
|
|
144
144
|
*/
|
|
145
145
|
export function createValidator(config, session) {
|
|
@@ -2,13 +2,19 @@
|
|
|
2
2
|
* Validate base runtime options and apply defaults.
|
|
3
3
|
* @param {Record<string, unknown>} opts - User-supplied options.
|
|
4
4
|
* @param {{ name?: string, serviceName?: string }} [meta] - Validation metadata.
|
|
5
|
-
* @returns {{ session
|
|
5
|
+
* @returns {{ session?: unknown, catalog?: { server?: { info?: Record<string, unknown> }, items?: Array<Record<string, unknown>> }, manager?: { get: (request: import('fastify').FastifyRequest) => Promise<import('@mcp-layer/session').Session>, close?: () => Promise<void> }, prefix?: string | ((version: string, info: Record<string, unknown> | undefined, name: string) => string), validation: { trustSchemas: 'auto' | true | false, maxSchemaDepth: number, maxSchemaSize: number, maxPatternLength: number, maxToolNameLength: number, maxTemplateParamLength: number }, resilience: { enabled: boolean, timeout: number, errorThresholdPercentage: number, resetTimeout: number, volumeThreshold: number }, telemetry: { enabled: boolean, serviceName: string, metricPrefix: string, api?: import('@opentelemetry/api') }, errors: { exposeDetails: boolean }, normalizeError?: (error: Error & { code?: string | number }, instance: string, requestId?: string, options?: { exposeDetails?: boolean }) => unknown }}
|
|
6
6
|
*/
|
|
7
7
|
export function validateRuntimeOptions(opts: Record<string, unknown>, meta?: {
|
|
8
8
|
name?: string;
|
|
9
9
|
serviceName?: string;
|
|
10
10
|
}): {
|
|
11
|
-
session
|
|
11
|
+
session?: unknown;
|
|
12
|
+
catalog?: {
|
|
13
|
+
server?: {
|
|
14
|
+
info?: Record<string, unknown>;
|
|
15
|
+
};
|
|
16
|
+
items?: Array<Record<string, unknown>>;
|
|
17
|
+
};
|
|
12
18
|
manager?: {
|
|
13
19
|
get: (request: import("fastify").FastifyRequest) => Promise<any>;
|
|
14
20
|
close?: () => Promise<void>;
|
package/types/runtime.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Create shared runtime contexts for MCP adapters.
|
|
3
3
|
* @param {Record<string, unknown>} opts - Runtime options.
|
|
4
4
|
* @param {{ name?: string, serviceName?: string }} [meta] - Runtime metadata.
|
|
5
|
-
* @returns {Promise<{ config: ReturnType<typeof validateRuntimeOptions>, contexts: Array<{ session: import('@mcp-layer/session').Session, catalog: { server?: { info?: Record<string, unknown> }, items?: Array<Record<string, unknown>> }, info: Record<string, unknown> | undefined, version: string, prefix: string, validator: import('./validation/validator.js').SchemaValidator, telemetry: ReturnType<typeof createTelemetry> | null, resolve: (request: import('fastify').FastifyRequest) => Promise<{ session: import('@mcp-layer/session').Session, breaker: import('opossum') | null }>, execute: (request: import('fastify').FastifyRequest, method: string, params: Record<string, unknown>) => Promise<Record<string, unknown>>, normalize: (error: Error & { code?: string | number }, instance: string, requestId?: string) => unknown }>, breakers: Map<string, import('opossum')>, normalize: (error: Error & { code?: string | number }, instance: string, requestId?: string) => unknown, close: () => Promise<void> }>}
|
|
5
|
+
* @returns {Promise<{ config: ReturnType<typeof validateRuntimeOptions>, contexts: Array<{ session: import('@mcp-layer/session').Session | undefined, catalog: { server?: { info?: Record<string, unknown> }, items?: Array<Record<string, unknown>> }, info: Record<string, unknown> | undefined, version: string, prefix: string, validator: import('./validation/validator.js').SchemaValidator, telemetry: ReturnType<typeof createTelemetry> | null, resolve: (request: import('fastify').FastifyRequest) => Promise<{ session: import('@mcp-layer/session').Session, breaker: import('opossum') | null }>, execute: (request: import('fastify').FastifyRequest, method: string, params: Record<string, unknown>) => Promise<Record<string, unknown>>, normalize: (error: Error & { code?: string | number }, instance: string, requestId?: string) => unknown }>, breakers: Map<string, import('opossum')>, normalize: (error: Error & { code?: string | number }, instance: string, requestId?: string) => unknown, close: () => Promise<void> }>}
|
|
6
6
|
*/
|
|
7
7
|
export function createRuntime(opts: Record<string, unknown>, meta?: {
|
|
8
8
|
name?: string;
|
|
@@ -10,7 +10,7 @@ export function createRuntime(opts: Record<string, unknown>, meta?: {
|
|
|
10
10
|
}): Promise<{
|
|
11
11
|
config: ReturnType<typeof validateRuntimeOptions>;
|
|
12
12
|
contexts: Array<{
|
|
13
|
-
session: any;
|
|
13
|
+
session: any | undefined;
|
|
14
14
|
catalog: {
|
|
15
15
|
server?: {
|
|
16
16
|
info?: Record<string, unknown>;
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Determine whether schemas from a session should be trusted.
|
|
3
|
-
*
|
|
3
|
+
* Missing bootstrap sessions are treated conservatively because catalog-only
|
|
4
|
+
* manager mode may be registering schemas sourced from a remote runtime.
|
|
5
|
+
*
|
|
6
|
+
* @param {import('@mcp-layer/session').Session | undefined} session - Optional MCP session to evaluate.
|
|
4
7
|
* @param {'auto' | true | false} policy - Trust policy.
|
|
5
8
|
* @returns {boolean}
|
|
6
9
|
*/
|
|
7
|
-
export function shouldTrustSchemas(session: any, policy: "auto" | true | false): boolean;
|
|
10
|
+
export function shouldTrustSchemas(session: any | undefined, policy: "auto" | true | false): boolean;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Create a schema validator instance.
|
|
3
3
|
* @param {{ trustSchemas: 'auto' | true | false, maxSchemaDepth: number, maxSchemaSize: number, maxPatternLength: number, maxToolNameLength: number, maxTemplateParamLength: number }} config - Validation configuration.
|
|
4
|
-
* @param {import('@mcp-layer/session').Session} session -
|
|
4
|
+
* @param {import('@mcp-layer/session').Session | undefined} session - Optional bootstrap session.
|
|
5
5
|
* @returns {SchemaValidator}
|
|
6
6
|
*/
|
|
7
7
|
export function createValidator(config: {
|
|
@@ -11,14 +11,14 @@ export function createValidator(config: {
|
|
|
11
11
|
maxPatternLength: number;
|
|
12
12
|
maxToolNameLength: number;
|
|
13
13
|
maxTemplateParamLength: number;
|
|
14
|
-
}, session: any): SchemaValidator;
|
|
14
|
+
}, session: any | undefined): SchemaValidator;
|
|
15
15
|
/**
|
|
16
16
|
* Schema validator for MCP tool and prompt inputs.
|
|
17
17
|
*/
|
|
18
18
|
export class SchemaValidator {
|
|
19
19
|
/**
|
|
20
20
|
* @param {{ trustSchemas: 'auto' | true | false, maxSchemaDepth: number, maxSchemaSize: number, maxPatternLength: number, maxToolNameLength: number, maxTemplateParamLength: number }} config - Validation configuration.
|
|
21
|
-
* @param {import('@mcp-layer/session').Session} session -
|
|
21
|
+
* @param {import('@mcp-layer/session').Session | undefined} session - Optional bootstrap session.
|
|
22
22
|
*/
|
|
23
23
|
constructor(config: {
|
|
24
24
|
trustSchemas: "auto" | true | false;
|
|
@@ -27,7 +27,7 @@ export class SchemaValidator {
|
|
|
27
27
|
maxPatternLength: number;
|
|
28
28
|
maxToolNameLength: number;
|
|
29
29
|
maxTemplateParamLength: number;
|
|
30
|
-
}, session: any);
|
|
30
|
+
}, session: any | undefined);
|
|
31
31
|
/**
|
|
32
32
|
* Register a tool schema for validation.
|
|
33
33
|
* @param {string} name - Tool name.
|