@cyanheads/mcp-ts-core 0.13.9 → 0.13.10
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/AGENTS.md +36 -13
- package/CLAUDE.md +36 -13
- package/README.md +9 -9
- package/changelog/0.13.x/0.13.10.md +118 -0
- package/dist/config/index.d.ts +3 -0
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +11 -0
- package/dist/config/index.js.map +1 -1
- package/dist/core/app.d.ts.map +1 -1
- package/dist/core/app.js +15 -1
- package/dist/core/app.js.map +1 -1
- package/dist/core/context.d.ts +89 -20
- package/dist/core/context.d.ts.map +1 -1
- package/dist/core/context.js +40 -0
- package/dist/core/context.js.map +1 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/worker.d.ts +6 -0
- package/dist/core/worker.d.ts.map +1 -1
- package/dist/core/worker.js +1 -0
- package/dist/core/worker.js.map +1 -1
- package/dist/linter/rules/error-contract-rules.d.ts +3 -44
- package/dist/linter/rules/error-contract-rules.d.ts.map +1 -1
- package/dist/linter/rules/error-contract-rules.js +8 -144
- package/dist/linter/rules/error-contract-rules.js.map +1 -1
- package/dist/linter/rules/index.d.ts +1 -1
- package/dist/linter/rules/index.d.ts.map +1 -1
- package/dist/linter/rules/index.js +1 -1
- package/dist/linter/rules/index.js.map +1 -1
- package/dist/linter/rules/resource-rules.d.ts.map +1 -1
- package/dist/linter/rules/resource-rules.js +1 -2
- package/dist/linter/rules/resource-rules.js.map +1 -1
- package/dist/linter/rules/tool-rules.d.ts.map +1 -1
- package/dist/linter/rules/tool-rules.js +1 -2
- package/dist/linter/rules/tool-rules.js.map +1 -1
- package/dist/mcp-server/handlerContext.d.ts +26 -13
- package/dist/mcp-server/handlerContext.d.ts.map +1 -1
- package/dist/mcp-server/handlerContext.js +32 -17
- package/dist/mcp-server/handlerContext.js.map +1 -1
- package/dist/mcp-server/inputRequired.d.ts +120 -8
- package/dist/mcp-server/inputRequired.d.ts.map +1 -1
- package/dist/mcp-server/inputRequired.js +177 -12
- package/dist/mcp-server/inputRequired.js.map +1 -1
- package/dist/mcp-server/prompts/prompt-registration.d.ts +10 -2
- package/dist/mcp-server/prompts/prompt-registration.d.ts.map +1 -1
- package/dist/mcp-server/prompts/prompt-registration.js +49 -11
- package/dist/mcp-server/prompts/prompt-registration.js.map +1 -1
- package/dist/mcp-server/resources/resource-registration.d.ts +4 -2
- package/dist/mcp-server/resources/resource-registration.d.ts.map +1 -1
- package/dist/mcp-server/resources/resource-registration.js +6 -4
- package/dist/mcp-server/resources/resource-registration.js.map +1 -1
- package/dist/mcp-server/resources/utils/resourceHandlerFactory.d.ts +4 -3
- package/dist/mcp-server/resources/utils/resourceHandlerFactory.d.ts.map +1 -1
- package/dist/mcp-server/resources/utils/resourceHandlerFactory.js +42 -14
- package/dist/mcp-server/resources/utils/resourceHandlerFactory.js.map +1 -1
- package/dist/mcp-server/server.d.ts +9 -0
- package/dist/mcp-server/server.d.ts.map +1 -1
- package/dist/mcp-server/server.js +14 -13
- package/dist/mcp-server/server.js.map +1 -1
- package/dist/mcp-server/tools/tool-registration.d.ts +7 -3
- package/dist/mcp-server/tools/tool-registration.d.ts.map +1 -1
- package/dist/mcp-server/tools/tool-registration.js +9 -5
- package/dist/mcp-server/tools/tool-registration.js.map +1 -1
- package/dist/mcp-server/tools/utils/toolHandlerFactory.d.ts +17 -9
- package/dist/mcp-server/tools/utils/toolHandlerFactory.d.ts.map +1 -1
- package/dist/mcp-server/tools/utils/toolHandlerFactory.js +91 -50
- package/dist/mcp-server/tools/utils/toolHandlerFactory.js.map +1 -1
- package/dist/mcp-server/transports/http/httpErrorHandler.d.ts.map +1 -1
- package/dist/mcp-server/transports/http/httpErrorHandler.js +7 -1
- package/dist/mcp-server/transports/http/httpErrorHandler.js.map +1 -1
- package/dist/testing/index.d.ts +17 -2
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +21 -7
- package/dist/testing/index.js.map +1 -1
- package/dist/types-global/errors.d.ts +18 -15
- package/dist/types-global/errors.d.ts.map +1 -1
- package/dist/utils/internal/error-handler/errorHandler.d.ts +5 -4
- package/dist/utils/internal/error-handler/errorHandler.d.ts.map +1 -1
- package/dist/utils/internal/error-handler/errorHandler.js +9 -7
- package/dist/utils/internal/error-handler/errorHandler.js.map +1 -1
- package/dist/utils/internal/error-handler/types.d.ts +3 -1
- package/dist/utils/internal/error-handler/types.d.ts.map +1 -1
- package/dist/utils/internal/performance.d.ts +4 -2
- package/dist/utils/internal/performance.d.ts.map +1 -1
- package/dist/utils/internal/performance.js +8 -6
- package/dist/utils/internal/performance.js.map +1 -1
- package/dist/utils/internal/telemetryMessages.d.ts +0 -1
- package/dist/utils/internal/telemetryMessages.d.ts.map +1 -1
- package/dist/utils/internal/telemetryMessages.js +0 -1
- package/dist/utils/internal/telemetryMessages.js.map +1 -1
- package/dist/utils/telemetry/attributes.d.ts +5 -4
- package/dist/utils/telemetry/attributes.d.ts.map +1 -1
- package/dist/utils/telemetry/attributes.js +5 -4
- package/dist/utils/telemetry/attributes.js.map +1 -1
- package/framework-skills/add-service/SKILL.md +3 -12
- package/framework-skills/add-test/SKILL.md +6 -3
- package/framework-skills/add-tool/SKILL.md +29 -33
- package/framework-skills/api-config/SKILL.md +2 -1
- package/framework-skills/api-context/SKILL.md +155 -40
- package/framework-skills/api-errors/SKILL.md +43 -47
- package/framework-skills/api-linter/SKILL.md +5 -29
- package/framework-skills/api-telemetry/SKILL.md +11 -7
- package/framework-skills/api-testing/SKILL.md +43 -11
- package/framework-skills/api-workers/SKILL.md +3 -1
- package/framework-skills/design-mcp-server/SKILL.md +5 -5
- package/framework-skills/field-test/SKILL.md +2 -2
- package/framework-skills/polish-docs-meta/SKILL.md +2 -2
- package/framework-skills/release-and-publish/SKILL.md +3 -3
- package/framework-skills/release-pr-review/SKILL.md +2 -2
- package/framework-skills/security-pass/SKILL.md +8 -7
- package/package.json +4 -3
- package/scripts/install-otel.ts +84 -0
- package/scripts/lint-packaging.ts +188 -27
- package/templates/.env.example +2 -0
- package/templates/AGENTS.md +5 -4
- package/templates/CLAUDE.md +5 -4
- package/templates/Dockerfile +67 -50
- package/templates/src/mcp-server/tools/definitions/echo.tool.ts +3 -8
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import type { ServerContext } from '@modelcontextprotocol/server';
|
|
8
8
|
import { type Context } from '../core/context.js';
|
|
9
|
-
import { type
|
|
9
|
+
import { type ClientCapabilityView, type RequestStateSealer } from './inputRequired.js';
|
|
10
10
|
import { type NotifierSources, type OptionalNotifiers } from './notifications.js';
|
|
11
11
|
import type { InputHandlingOptions } from './tools/utils/inputPrevalidation.js';
|
|
12
12
|
import type { StorageService } from '../storage/core/StorageService.js';
|
|
@@ -28,6 +28,12 @@ export interface HandlerServices {
|
|
|
28
28
|
*/
|
|
29
29
|
input?: InputHandlingOptions;
|
|
30
30
|
logger: Logger;
|
|
31
|
+
/**
|
|
32
|
+
* Seals the `requestState` a handler returns with `ctx.requestInput(...)`,
|
|
33
|
+
* built from `MCP_REQUEST_STATE_KEY`. Absent when no key is configured, and
|
|
34
|
+
* the state then leaves as the handler wrote it.
|
|
35
|
+
*/
|
|
36
|
+
requestState?: RequestStateSealer;
|
|
31
37
|
storage: StorageService;
|
|
32
38
|
}
|
|
33
39
|
/** What a factory derives once per request from the SDK's `ServerContext`. */
|
|
@@ -41,8 +47,6 @@ export interface HandlerRequest {
|
|
|
41
47
|
mcpReq: ServerContext['mcpReq'] | undefined;
|
|
42
48
|
/** The delivery path `ctx.notify*` takes for this request's era. */
|
|
43
49
|
notifiers: OptionalNotifiers;
|
|
44
|
-
/** The raw SDK session token — log correlation uses it in every mode. */
|
|
45
|
-
sdkSessionId: string | undefined;
|
|
46
50
|
/**
|
|
47
51
|
* What `ctx.sessionId` surfaces: a session with request-spanning lifetime
|
|
48
52
|
* (stateful HTTP, or `auto` resolving to it), or the SDK's per-request token
|
|
@@ -61,21 +65,30 @@ export interface HandlerRequest {
|
|
|
61
65
|
*/
|
|
62
66
|
export declare function resolveHandlerRequest(serverContext: ServerContext | undefined, services: HandlerServices, notifiers: NotifierSources): HandlerRequest;
|
|
63
67
|
/**
|
|
64
|
-
* The `parentContext` of a request's tracing context
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
68
|
+
* The `parentContext` of a request's tracing context — for a tool call, a
|
|
69
|
+
* resource read, and a `prompts/get` alike: the SDK request id when the client
|
|
70
|
+
* sent a string id (otherwise the context generates one) and the raw session
|
|
71
|
+
* id when present. That id is the one the call's log records carry and its
|
|
72
|
+
* error `data.requestId` returns (#576). Raw handler input is deliberately not
|
|
73
|
+
* part of it — the context spreads into the completion log and can carry
|
|
74
|
+
* caller PII or secrets; sizes and parameter names are recorded as metric
|
|
75
|
+
* attributes by the execution measurement instead.
|
|
69
76
|
*/
|
|
70
|
-
export declare function handlerParentContext(
|
|
77
|
+
export declare function handlerParentContext(serverContext: ServerContext | undefined): Partial<Pick<RequestContext, 'requestId' | 'sessionId'>>;
|
|
71
78
|
/**
|
|
72
79
|
* Builds the handler `Context`. Called from inside the execution span so
|
|
73
80
|
* `ctx.traceId` / `ctx.spanId` — and the child logger built from them — name
|
|
74
81
|
* the span the handler runs in rather than the enclosing request span (#296).
|
|
75
82
|
* `attachTypedFail` adds `ctx.fail` when the definition declares an error
|
|
76
|
-
* contract; otherwise the context is unchanged.
|
|
77
|
-
*
|
|
78
|
-
*
|
|
83
|
+
* contract; otherwise the context is unchanged.
|
|
84
|
+
*
|
|
85
|
+
* `capabilityView` is the instance's view of its client's declared
|
|
86
|
+
* capabilities (#580), resolved once here into the value
|
|
87
|
+
* `ctx.clientCapabilities` carries, the filter over `ctx.inputs` applies
|
|
88
|
+
* (#496), and — on a 2025-era instance — the gate `ctx.requestInput` runs on
|
|
89
|
+
* the result it builds (#379). Without a view the request has none:
|
|
90
|
+
* `ctx.clientCapabilities` is `undefined`, no response reaches `ctx.inputs`,
|
|
91
|
+
* and nothing gates `ctx.requestInput`.
|
|
79
92
|
*/
|
|
80
|
-
export declare function buildHandlerContext(request: HandlerRequest, services: HandlerServices, spanContext: RequestContext, errors: readonly ErrorContract[] | undefined,
|
|
93
|
+
export declare function buildHandlerContext(request: HandlerRequest, services: HandlerServices, spanContext: RequestContext, errors: readonly ErrorContract[] | undefined, capabilityView?: ClientCapabilityView, uri?: URL): Context;
|
|
81
94
|
//# sourceMappingURL=handlerContext.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"handlerContext.d.ts","sourceRoot":"","sources":["../../src/mcp-server/handlerContext.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAGlE,OAAO,EAAmB,KAAK,OAAO,EAAiB,MAAM,mBAAmB,CAAC;AACjF,OAAO,
|
|
1
|
+
{"version":3,"file":"handlerContext.d.ts","sourceRoot":"","sources":["../../src/mcp-server/handlerContext.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAGlE,OAAO,EAAmB,KAAK,OAAO,EAAiB,MAAM,mBAAmB,CAAC;AACjF,OAAO,EACL,KAAK,oBAAoB,EAIzB,KAAK,kBAAkB,EACxB,MAAM,+BAA+B,CAAC;AACvC,OAAO,EACL,KAAK,eAAe,EACpB,KAAK,iBAAiB,EAEvB,MAAM,+BAA+B,CAAC;AACvC,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,gDAAgD,CAAC;AAE3F,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kCAAkC,CAAC;AACvE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAC9D,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,4BAA4B,CAAC;AACzD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAC;AAEzE,wDAAwD;AACxD,MAAM,WAAW,eAAe;IAC9B;;;;;OAKG;IACH,wBAAwB,CAAC,EAAE,OAAO,CAAC;IACnC;;;OAGG;IACH,KAAK,CAAC,EAAE,oBAAoB,CAAC;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,YAAY,CAAC,EAAE,kBAAkB,CAAC;IAClC,OAAO,EAAE,cAAc,CAAC;CACzB;AAED,8EAA8E;AAC9E,MAAM,WAAW,cAAc;IAC7B;;;;OAIG;IACH,eAAe,EAAE,MAAM,GAAG,SAAS,CAAC;IACpC,MAAM,EAAE,aAAa,CAAC,QAAQ,CAAC,GAAG,SAAS,CAAC;IAC5C,oEAAoE;IACpE,SAAS,EAAE,iBAAiB,CAAC;IAC7B;;;;;OAKG;IACH,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,qFAAqF;IACrF,MAAM,EAAE,WAAW,CAAC;CACrB;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CACnC,aAAa,EAAE,aAAa,GAAG,SAAS,EACxC,QAAQ,EAAE,eAAe,EACzB,SAAS,EAAE,eAAe,GACzB,cAAc,CAehB;AAOD;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAClC,aAAa,EAAE,aAAa,GAAG,SAAS,GACvC,OAAO,CAAC,IAAI,CAAC,cAAc,EAAE,WAAW,GAAG,WAAW,CAAC,CAAC,CAO1D;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,cAAc,EACvB,QAAQ,EAAE,eAAe,EACzB,WAAW,EAAE,cAAc,EAC3B,MAAM,EAAE,SAAS,aAAa,EAAE,GAAG,SAAS,EAC5C,cAAc,CAAC,EAAE,oBAAoB,EACrC,GAAG,CAAC,EAAE,GAAG,GACR,OAAO,CAyBT"}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { config } from '../config/index.js';
|
|
8
8
|
import { attachTypedFail, createContext } from '../core/context.js';
|
|
9
|
-
import { createContextInputs, createRequestInput, } from './inputRequired.js';
|
|
9
|
+
import { createContextInputs, createInputRequiredGate, createRequestInput, } from './inputRequired.js';
|
|
10
10
|
import { selectNotifiers, } from './notifications.js';
|
|
11
11
|
import { resolveSessionMode } from './types.js';
|
|
12
12
|
/**
|
|
@@ -17,32 +17,39 @@ import { resolveSessionMode } from './types.js';
|
|
|
17
17
|
*/
|
|
18
18
|
export function resolveHandlerRequest(serverContext, services, notifiers) {
|
|
19
19
|
const mcpReq = serverContext?.mcpReq;
|
|
20
|
-
const sdkSessionId =
|
|
20
|
+
const sdkSessionId = sdkSessionIdOf(serverContext);
|
|
21
21
|
const isStatefulMode = resolveSessionMode(config.mcpSessionMode) === 'stateful';
|
|
22
22
|
const hasNoAuthPipeline = config.mcpTransportType === 'stdio' || config.mcpAuthMode === 'none';
|
|
23
23
|
return {
|
|
24
24
|
defaultTenantId: hasNoAuthPipeline ? 'default' : undefined,
|
|
25
25
|
mcpReq,
|
|
26
26
|
notifiers: selectNotifiers(notifiers, mcpReq),
|
|
27
|
-
sdkSessionId,
|
|
28
27
|
sessionId: sdkSessionId && (isStatefulMode || services.exposeStatelessSessionId === true)
|
|
29
28
|
? sdkSessionId
|
|
30
29
|
: undefined,
|
|
31
30
|
signal: mcpReq?.signal ?? new AbortController().signal,
|
|
32
31
|
};
|
|
33
32
|
}
|
|
33
|
+
/** The raw SDK session token of a request, when it carries one. */
|
|
34
|
+
function sdkSessionIdOf(serverContext) {
|
|
35
|
+
return typeof serverContext?.sessionId === 'string' ? serverContext.sessionId : undefined;
|
|
36
|
+
}
|
|
34
37
|
/**
|
|
35
|
-
* The `parentContext` of a request's tracing context
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
38
|
+
* The `parentContext` of a request's tracing context — for a tool call, a
|
|
39
|
+
* resource read, and a `prompts/get` alike: the SDK request id when the client
|
|
40
|
+
* sent a string id (otherwise the context generates one) and the raw session
|
|
41
|
+
* id when present. That id is the one the call's log records carry and its
|
|
42
|
+
* error `data.requestId` returns (#576). Raw handler input is deliberately not
|
|
43
|
+
* part of it — the context spreads into the completion log and can carry
|
|
44
|
+
* caller PII or secrets; sizes and parameter names are recorded as metric
|
|
45
|
+
* attributes by the execution measurement instead.
|
|
40
46
|
*/
|
|
41
|
-
export function handlerParentContext(
|
|
42
|
-
const requestId =
|
|
47
|
+
export function handlerParentContext(serverContext) {
|
|
48
|
+
const requestId = serverContext?.mcpReq?.id;
|
|
49
|
+
const sessionId = sdkSessionIdOf(serverContext);
|
|
43
50
|
return {
|
|
44
51
|
...(typeof requestId === 'string' && { requestId }),
|
|
45
|
-
...(
|
|
52
|
+
...(sessionId && { sessionId }),
|
|
46
53
|
};
|
|
47
54
|
}
|
|
48
55
|
/**
|
|
@@ -50,21 +57,29 @@ export function handlerParentContext(request) {
|
|
|
50
57
|
* `ctx.traceId` / `ctx.spanId` — and the child logger built from them — name
|
|
51
58
|
* the span the handler runs in rather than the enclosing request span (#296).
|
|
52
59
|
* `attachTypedFail` adds `ctx.fail` when the definition declares an error
|
|
53
|
-
* contract; otherwise the context is unchanged.
|
|
54
|
-
*
|
|
55
|
-
*
|
|
60
|
+
* contract; otherwise the context is unchanged.
|
|
61
|
+
*
|
|
62
|
+
* `capabilityView` is the instance's view of its client's declared
|
|
63
|
+
* capabilities (#580), resolved once here into the value
|
|
64
|
+
* `ctx.clientCapabilities` carries, the filter over `ctx.inputs` applies
|
|
65
|
+
* (#496), and — on a 2025-era instance — the gate `ctx.requestInput` runs on
|
|
66
|
+
* the result it builds (#379). Without a view the request has none:
|
|
67
|
+
* `ctx.clientCapabilities` is `undefined`, no response reaches `ctx.inputs`,
|
|
68
|
+
* and nothing gates `ctx.requestInput`.
|
|
56
69
|
*/
|
|
57
|
-
export function buildHandlerContext(request, services, spanContext, errors,
|
|
70
|
+
export function buildHandlerContext(request, services, spanContext, errors, capabilityView, uri) {
|
|
58
71
|
const { mcpReq, notifiers } = request;
|
|
72
|
+
const clientCapabilities = capabilityView?.capabilities(mcpReq);
|
|
59
73
|
return attachTypedFail(createContext({
|
|
60
74
|
appContext: spanContext,
|
|
75
|
+
clientCapabilities,
|
|
61
76
|
defaultTenantId: request.defaultTenantId,
|
|
62
77
|
logger: services.logger,
|
|
63
78
|
storage: services.storage,
|
|
64
79
|
signal: request.signal,
|
|
65
80
|
sessionId: request.sessionId,
|
|
66
|
-
inputs: createContextInputs(mcpReq),
|
|
67
|
-
requestInput: createRequestInput(
|
|
81
|
+
inputs: createContextInputs(mcpReq, clientCapabilities),
|
|
82
|
+
requestInput: createRequestInput(capabilityView?.era === 'legacy' ? createInputRequiredGate(clientCapabilities) : undefined),
|
|
68
83
|
...(mcpReq?.log && { wireLog: mcpReq.log }),
|
|
69
84
|
notifyPromptListChanged: notifiers.notifyPromptListChanged,
|
|
70
85
|
notifyResourceListChanged: notifiers.notifyResourceListChanged,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"handlerContext.js","sourceRoot":"","sources":["../../src/mcp-server/handlerContext.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAC3C,OAAO,EAAE,eAAe,EAAgB,aAAa,EAAE,MAAM,mBAAmB,CAAC;AACjF,OAAO,
|
|
1
|
+
{"version":3,"file":"handlerContext.js","sourceRoot":"","sources":["../../src/mcp-server/handlerContext.ts"],"names":[],"mappings":"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"}
|
|
@@ -15,10 +15,15 @@
|
|
|
15
15
|
* returns against 2025-era clients by issuing real `elicitation/create` /
|
|
16
16
|
* `sampling/createMessage` / `roots/list` requests and re-entering the handler.
|
|
17
17
|
*
|
|
18
|
+
* What a handler reads back is bounded by what its client declared: the
|
|
19
|
+
* per-request capability view (#580) decides which responses reach
|
|
20
|
+
* `ctx.inputs` (#496), and an opt-in key seals the `requestState` a handler
|
|
21
|
+
* returns so a retry can only echo what this server minted.
|
|
22
|
+
*
|
|
18
23
|
* @see {@link https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/mrtr | MCP Multi-Round-Trip Requests}
|
|
19
24
|
* @module src/mcp-server/inputRequired
|
|
20
25
|
*/
|
|
21
|
-
import { acceptedContent, type ClientCapabilities, type InputRequiredResult, type ServerContext } from '@modelcontextprotocol/server';
|
|
26
|
+
import { acceptedContent, type ClientCapabilities, type InputRequiredResult, type InputResponses, type McpServer, type ServerContext } from '@modelcontextprotocol/server';
|
|
22
27
|
import type { ContextInputs, RequestInputFn } from '../core/context.js';
|
|
23
28
|
import { McpError } from '../types-global/errors.js';
|
|
24
29
|
/**
|
|
@@ -37,6 +42,46 @@ export declare class InputRequiredSignal extends Error {
|
|
|
37
42
|
}
|
|
38
43
|
/** Narrows an unknown thrown value to the input-required control-flow signal. */
|
|
39
44
|
export declare function isInputRequiredSignal(error: unknown): error is InputRequiredSignal;
|
|
45
|
+
/**
|
|
46
|
+
* What one server instance can see of its client's declared capabilities,
|
|
47
|
+
* resolved per request. One view is built per instance, from the era the
|
|
48
|
+
* instance serves — never from whether a request happens to carry an envelope
|
|
49
|
+
* key, since the SDK lifts reserved `_meta` keys off 2025-era messages too.
|
|
50
|
+
*
|
|
51
|
+
* `ctx.clientCapabilities`, the 2025-era refusal `ctx.requestInput` raises
|
|
52
|
+
* (#379), and the filter over `ctx.inputs` (#496) all read the one value this
|
|
53
|
+
* resolves for a request, so the three can never disagree.
|
|
54
|
+
*/
|
|
55
|
+
export interface ClientCapabilityView {
|
|
56
|
+
/**
|
|
57
|
+
* The capabilities the request's client declared: `{}` when it declared
|
|
58
|
+
* none, `undefined` when the instance holds no view at all — a 2025-era
|
|
59
|
+
* request served per-request, whose instance never processed `initialize`.
|
|
60
|
+
*/
|
|
61
|
+
capabilities(mcpReq: ServerContext['mcpReq'] | undefined): ClientCapabilities | undefined;
|
|
62
|
+
/**
|
|
63
|
+
* The era the instance serves. Only a `legacy` instance gates
|
|
64
|
+
* `ctx.requestInput` in the framework; a `modern` one is gated by the SDK
|
|
65
|
+
* after the handler returns (`-32021`).
|
|
66
|
+
*/
|
|
67
|
+
readonly era: 'legacy' | 'modern';
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The view for one instance: on a 2025-era instance the `initialize`-declared
|
|
71
|
+
* value, read through the instance on every call so an `initialize` that lands
|
|
72
|
+
* after registration is seen; on a 2026-07-28 instance the request's own
|
|
73
|
+
* `io.modelcontextprotocol/clientCapabilities` envelope key, which the SDK has
|
|
74
|
+
* validated before dispatch. `getClientCapabilities()` is not used there — a
|
|
75
|
+
* 2026-07-28 stdio instance never has it backfilled.
|
|
76
|
+
*/
|
|
77
|
+
export declare function clientCapabilityView(era: 'legacy' | 'modern', server: McpServer): ClientCapabilityView;
|
|
78
|
+
/**
|
|
79
|
+
* The framework-owned `data.reason` on an input request this connection cannot
|
|
80
|
+
* serve. It joins `invalid_arguments` as a reserved reason: a definition cannot
|
|
81
|
+
* declare it in `errors[]`, because it names a property of the connection
|
|
82
|
+
* rather than a domain outcome.
|
|
83
|
+
*/
|
|
84
|
+
export declare const CLIENT_CAPABILITY_MISSING_REASON = "client_capability_missing";
|
|
40
85
|
/**
|
|
41
86
|
* Decides whether a handler's `input_required` return can be served on this
|
|
42
87
|
* connection: the `McpError` the caller receives instead, or `undefined` to
|
|
@@ -45,7 +90,9 @@ export declare function isInputRequiredSignal(error: unknown): error is InputReq
|
|
|
45
90
|
*/
|
|
46
91
|
export type InputRequiredGate = (result: InputRequiredResult, fallbackHint?: string) => McpError | undefined;
|
|
47
92
|
/**
|
|
48
|
-
* Builds the gate for one
|
|
93
|
+
* Builds the gate for one request from the capabilities its client declared —
|
|
94
|
+
* the value the instance's {@link ClientCapabilityView} resolved, which
|
|
95
|
+
* `ctx.clientCapabilities` carries too.
|
|
49
96
|
*
|
|
50
97
|
* Only the 2025-era arm needs it. There the refusal is produced by the SDK's
|
|
51
98
|
* legacy shim, which runs *above* the handler callback on the result that
|
|
@@ -57,16 +104,16 @@ export type InputRequiredGate = (result: InputRequiredResult, fallbackHint?: str
|
|
|
57
104
|
* the SDK itself, which raises `MissingRequiredClientCapabilityError`
|
|
58
105
|
* (`-32021`) — that arm takes no gate.
|
|
59
106
|
*
|
|
60
|
-
* `
|
|
61
|
-
*
|
|
62
|
-
*
|
|
107
|
+
* `declared` being `undefined` is the per-request legacy case: an instance that
|
|
108
|
+
* never saw an `initialize` holds no capability view, so every embedded
|
|
109
|
+
* request is refused, and the message and hint say so.
|
|
63
110
|
*
|
|
64
111
|
* The hint ends at reconnecting (#495). Whether the caller could instead
|
|
65
112
|
* supply the answer as an argument is the handler's knowledge, not the gate's:
|
|
66
113
|
* a consent gate deliberately has no such field, so the gate appends a
|
|
67
114
|
* fallback only when the handler passed one.
|
|
68
115
|
*/
|
|
69
|
-
export declare function createInputRequiredGate(
|
|
116
|
+
export declare function createInputRequiredGate(declared: ClientCapabilities | undefined): InputRequiredGate;
|
|
70
117
|
/**
|
|
71
118
|
* Builds `ctx.requestInput`. Always present — a handler may request input on
|
|
72
119
|
* any transport and either era; whether the round trip is served by the client
|
|
@@ -85,16 +132,81 @@ export declare function createInputRequiredGate(clientCapabilities: () => Client
|
|
|
85
132
|
*/
|
|
86
133
|
export declare function createRequestInput(gate?: InputRequiredGate): RequestInputFn;
|
|
87
134
|
/**
|
|
88
|
-
*
|
|
135
|
+
* The entries of `responses` the client's declared capabilities cover, at the
|
|
136
|
+
* same mode level the gate applies to requests ({@link responseRequirement},
|
|
137
|
+
* {@link isDeclared}), or `undefined` when none survives.
|
|
138
|
+
*
|
|
139
|
+
* The SDK lifts `inputResponses` off every client request, a first call and a
|
|
140
|
+
* 2025-era request included, so a client can arrive pre-answered with no
|
|
141
|
+
* `input_required` result ever sent. An answer the client's modes do not cover
|
|
142
|
+
* cannot have come from a round this server asked for: on the 2025 arm the
|
|
143
|
+
* legacy shim only issues requests the connection declared, and on 2026-07-28
|
|
144
|
+
* the SDK refuses an embedded request the envelope does not cover. Dropping
|
|
145
|
+
* those closes the pre-answered consent-gate bypass — a URL-only client's form
|
|
146
|
+
* answer included — without touching a legitimate round. With no view
|
|
147
|
+
* (`declared` undefined) nothing survives, and an entry the SDK's classifier
|
|
148
|
+
* cannot read as any kind has no capability to justify it either.
|
|
149
|
+
*/
|
|
150
|
+
export declare function declaredResponses(responses: InputResponses | Record<string, unknown> | undefined, declared: ClientCapabilities | undefined): Record<string, unknown> | undefined;
|
|
151
|
+
/**
|
|
152
|
+
* Builds the `ctx.inputs` reader over a retried request's `inputResponses`,
|
|
153
|
+
* keeping only the answers `declared` covers ({@link declaredResponses}).
|
|
89
154
|
*
|
|
90
155
|
* Values arrive from the client and are never re-validated by the SDK — pass a
|
|
91
156
|
* schema to `accepted()` wherever the content matters.
|
|
92
157
|
*/
|
|
93
|
-
export declare function createContextInputs(mcpReq: ServerContext['mcpReq'] | undefined): ContextInputs;
|
|
158
|
+
export declare function createContextInputs(mcpReq: ServerContext['mcpReq'] | undefined, declared: ClientCapabilities | undefined): ContextInputs;
|
|
94
159
|
/**
|
|
95
160
|
* The `ctx.inputs` reader over an explicit set of responses — what
|
|
96
161
|
* {@link createContextInputs} builds from the SDK request, and what the test
|
|
97
162
|
* kit builds from seeded `inputResponses` / `requestState`.
|
|
98
163
|
*/
|
|
99
164
|
export declare function contextInputsFrom(responses: Parameters<typeof acceptedContent>[0], dropped: string[], state: ContextInputs['state']): ContextInputs;
|
|
165
|
+
/**
|
|
166
|
+
* Seals the `requestState` handlers return and verifies it when a retry
|
|
167
|
+
* echoes it back. Built once per process from `MCP_REQUEST_STATE_KEY`; the
|
|
168
|
+
* handler factories seal each input-required signal through
|
|
169
|
+
* {@link sealThrown}, and every `McpServer` gets
|
|
170
|
+
* {@link RequestStateSealer.verify} as `ServerOptions.requestState.verify`.
|
|
171
|
+
*/
|
|
172
|
+
export interface RequestStateSealer {
|
|
173
|
+
/**
|
|
174
|
+
* The `input_required` result a factory returns, its string `requestState`
|
|
175
|
+
* minted into the codec's signed envelope, bound to the request's
|
|
176
|
+
* authenticated principal. A result carrying no state is returned as is.
|
|
177
|
+
*/
|
|
178
|
+
seal(result: InputRequiredResult, ctx: ServerContext): Promise<InputRequiredResult>;
|
|
179
|
+
/**
|
|
180
|
+
* Resolves with the handler's original string, or throws — a forged,
|
|
181
|
+
* tampered, expired, other-principal, or other-key state. The SDK answers a
|
|
182
|
+
* throw as `-32602` with `data.reason: 'invalid_request_state'` before the
|
|
183
|
+
* handler runs, and reports only the codec's opaque reason to `onerror`.
|
|
184
|
+
*/
|
|
185
|
+
verify(state: string, ctx: ServerContext): Promise<unknown>;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* The process's sealer, or `undefined` when no key is configured — in which
|
|
189
|
+
* case nothing reaches the SDK and handlers read the raw wire string, exactly
|
|
190
|
+
* as before the option existed. There is deliberately no per-process random
|
|
191
|
+
* key: a 2026-07-28 retry can land on another instance or outlive a restart,
|
|
192
|
+
* and every instance that may receive an echoed state needs the same key.
|
|
193
|
+
*
|
|
194
|
+
* @throws {McpError} `ConfigurationError` naming `MCP_REQUEST_STATE_KEY` when
|
|
195
|
+
* the key is shorter than 32 UTF-8 bytes. The key itself is never included.
|
|
196
|
+
*/
|
|
197
|
+
export declare function createRequestStateSealer(key: string | undefined): RequestStateSealer | undefined;
|
|
198
|
+
/**
|
|
199
|
+
* What a handler factory rethrows for a value its handler threw: an
|
|
200
|
+
* input-required signal carrying its `requestState` sealed when `sealer` is
|
|
201
|
+
* configured, anything else unchanged.
|
|
202
|
+
*
|
|
203
|
+
* The factories call it inside the measured region, where the signal is
|
|
204
|
+
* rethrown. Minting can reject — WebCrypto failing, or no request context for
|
|
205
|
+
* the principal binding — and a rejection there would escape the factory with
|
|
206
|
+
* no error envelope, no failure record, and no request id. It resolves to an
|
|
207
|
+
* `InternalError` instead, so the call fails through the family's ordinary
|
|
208
|
+
* error path; the codec's error rides as `cause` into the server log only, and
|
|
209
|
+
* no part of the key reaches it.
|
|
210
|
+
*/
|
|
211
|
+
export declare function sealThrown(thrown: unknown, sealer: RequestStateSealer | undefined, ctx: ServerContext): Promise<unknown>;
|
|
100
212
|
//# sourceMappingURL=inputRequired.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"inputRequired.d.ts","sourceRoot":"","sources":["../../src/mcp-server/inputRequired.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"inputRequired.d.ts","sourceRoot":"","sources":["../../src/mcp-server/inputRequired.ts"],"names":[],"mappings":"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"}
|
|
@@ -15,11 +15,16 @@
|
|
|
15
15
|
* returns against 2025-era clients by issuing real `elicitation/create` /
|
|
16
16
|
* `sampling/createMessage` / `roots/list` requests and re-entering the handler.
|
|
17
17
|
*
|
|
18
|
+
* What a handler reads back is bounded by what its client declared: the
|
|
19
|
+
* per-request capability view (#580) decides which responses reach
|
|
20
|
+
* `ctx.inputs` (#496), and an opt-in key seals the `requestState` a handler
|
|
21
|
+
* returns so a retry can only echo what this server minted.
|
|
22
|
+
*
|
|
18
23
|
* @see {@link https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/mrtr | MCP Multi-Round-Trip Requests}
|
|
19
24
|
* @module src/mcp-server/inputRequired
|
|
20
25
|
*/
|
|
21
|
-
import { acceptedContent, inputRequired, inputResponse, } from '@modelcontextprotocol/server';
|
|
22
|
-
import { JsonRpcErrorCode, McpError } from '../types-global/errors.js';
|
|
26
|
+
import { acceptedContent, CLIENT_CAPABILITIES_META_KEY, createRequestStateCodec, inputRequired, inputResponse, } from '@modelcontextprotocol/server';
|
|
27
|
+
import { configurationError, internalError, JsonRpcErrorCode, McpError, } from '../types-global/errors.js';
|
|
23
28
|
/**
|
|
24
29
|
* Thrown by `ctx.requestInput(...)` and caught by the handler factories, which
|
|
25
30
|
* return the carried `input_required` result to the SDK instead of a normal
|
|
@@ -45,6 +50,23 @@ export function isInputRequiredSignal(error) {
|
|
|
45
50
|
error !== null &&
|
|
46
51
|
error.isInputRequiredSignal === true));
|
|
47
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* The view for one instance: on a 2025-era instance the `initialize`-declared
|
|
55
|
+
* value, read through the instance on every call so an `initialize` that lands
|
|
56
|
+
* after registration is seen; on a 2026-07-28 instance the request's own
|
|
57
|
+
* `io.modelcontextprotocol/clientCapabilities` envelope key, which the SDK has
|
|
58
|
+
* validated before dispatch. `getClientCapabilities()` is not used there — a
|
|
59
|
+
* 2026-07-28 stdio instance never has it backfilled.
|
|
60
|
+
*/
|
|
61
|
+
export function clientCapabilityView(era, server) {
|
|
62
|
+
if (era === 'modern') {
|
|
63
|
+
return {
|
|
64
|
+
era,
|
|
65
|
+
capabilities: (mcpReq) => mcpReq?.envelope?.[CLIENT_CAPABILITIES_META_KEY],
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
return { era, capabilities: () => server.server.getClientCapabilities() };
|
|
69
|
+
}
|
|
48
70
|
// ---------------------------------------------------------------------------
|
|
49
71
|
// Client-capability gate (#379)
|
|
50
72
|
// ---------------------------------------------------------------------------
|
|
@@ -54,7 +76,7 @@ export function isInputRequiredSignal(error) {
|
|
|
54
76
|
* declare it in `errors[]`, because it names a property of the connection
|
|
55
77
|
* rather than a domain outcome.
|
|
56
78
|
*/
|
|
57
|
-
const CLIENT_CAPABILITY_MISSING_REASON = 'client_capability_missing';
|
|
79
|
+
export const CLIENT_CAPABILITY_MISSING_REASON = 'client_capability_missing';
|
|
58
80
|
/**
|
|
59
81
|
* What an embedded request kind requires, mode-aware where the capability is:
|
|
60
82
|
* URL-mode elicitation needs `elicitation.url`, form-mode (or mode-omitted)
|
|
@@ -103,7 +125,9 @@ function capabilityPath({ capability, member }) {
|
|
|
103
125
|
return member === undefined ? capability : `${capability}.${member}`;
|
|
104
126
|
}
|
|
105
127
|
/**
|
|
106
|
-
* Builds the gate for one
|
|
128
|
+
* Builds the gate for one request from the capabilities its client declared —
|
|
129
|
+
* the value the instance's {@link ClientCapabilityView} resolved, which
|
|
130
|
+
* `ctx.clientCapabilities` carries too.
|
|
107
131
|
*
|
|
108
132
|
* Only the 2025-era arm needs it. There the refusal is produced by the SDK's
|
|
109
133
|
* legacy shim, which runs *above* the handler callback on the result that
|
|
@@ -115,21 +139,20 @@ function capabilityPath({ capability, member }) {
|
|
|
115
139
|
* the SDK itself, which raises `MissingRequiredClientCapabilityError`
|
|
116
140
|
* (`-32021`) — that arm takes no gate.
|
|
117
141
|
*
|
|
118
|
-
* `
|
|
119
|
-
*
|
|
120
|
-
*
|
|
142
|
+
* `declared` being `undefined` is the per-request legacy case: an instance that
|
|
143
|
+
* never saw an `initialize` holds no capability view, so every embedded
|
|
144
|
+
* request is refused, and the message and hint say so.
|
|
121
145
|
*
|
|
122
146
|
* The hint ends at reconnecting (#495). Whether the caller could instead
|
|
123
147
|
* supply the answer as an argument is the handler's knowledge, not the gate's:
|
|
124
148
|
* a consent gate deliberately has no such field, so the gate appends a
|
|
125
149
|
* fallback only when the handler passed one.
|
|
126
150
|
*/
|
|
127
|
-
export function createInputRequiredGate(
|
|
151
|
+
export function createInputRequiredGate(declared) {
|
|
128
152
|
return (result, fallbackHint) => {
|
|
129
153
|
const requests = result.inputRequests;
|
|
130
154
|
if (requests === undefined)
|
|
131
155
|
return;
|
|
132
|
-
const declared = clientCapabilities();
|
|
133
156
|
for (const [key, entry] of Object.entries(requests)) {
|
|
134
157
|
const requirement = capabilityRequirement(entry);
|
|
135
158
|
if (requirement === undefined || isDeclared(requirement, declared))
|
|
@@ -175,14 +198,81 @@ export function createRequestInput(gate) {
|
|
|
175
198
|
throw new InputRequiredSignal(result);
|
|
176
199
|
};
|
|
177
200
|
}
|
|
201
|
+
// ---------------------------------------------------------------------------
|
|
202
|
+
// ctx.inputs — responses filtered by declared capability (#496)
|
|
203
|
+
// ---------------------------------------------------------------------------
|
|
204
|
+
/** Content block types only a tool-enabled sampling result carries. */
|
|
205
|
+
const TOOL_BLOCK_TYPES = new Set(['tool_use', 'tool_result']);
|
|
206
|
+
/** Whether sampling content — one block or an array — holds a tool block. */
|
|
207
|
+
function carriesToolBlock(content) {
|
|
208
|
+
return (Array.isArray(content) ? content : [content]).some((block) => typeof block === 'object' &&
|
|
209
|
+
block !== null &&
|
|
210
|
+
TOOL_BLOCK_TYPES.has(block.type));
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* What one response requires — the response-side twin of
|
|
214
|
+
* {@link capabilityRequirement}, so an answer needs whatever the request it
|
|
215
|
+
* answers would have needed. An elicit result carrying `content` is a
|
|
216
|
+
* form-mode answer and needs `elicitation.form`; one without (a URL-mode
|
|
217
|
+
* accept, a decline, a cancel) needs `elicitation` in any mode. A sampling
|
|
218
|
+
* result holding a `tool_use` or `tool_result` block answers a tool-enabled
|
|
219
|
+
* request and needs `sampling.tools`; any other needs `sampling`. A roots
|
|
220
|
+
* result needs `roots`. `undefined` for an entry the SDK's classifier reads as
|
|
221
|
+
* no kind.
|
|
222
|
+
*
|
|
223
|
+
* `content` is read off the raw `entry`, not the view: the view keeps it only
|
|
224
|
+
* when it is an object, while `ctx.inputs.responses` exposes the entry as sent.
|
|
225
|
+
*/
|
|
226
|
+
function responseRequirement(view, entry) {
|
|
227
|
+
switch (view.kind) {
|
|
228
|
+
case 'elicit':
|
|
229
|
+
return entry.content === undefined
|
|
230
|
+
? { capability: 'elicitation' }
|
|
231
|
+
: { capability: 'elicitation', member: 'form' };
|
|
232
|
+
case 'sampling':
|
|
233
|
+
return carriesToolBlock(view.result.content)
|
|
234
|
+
? { capability: 'sampling', member: 'tools' }
|
|
235
|
+
: { capability: 'sampling' };
|
|
236
|
+
case 'roots':
|
|
237
|
+
return { capability: 'roots' };
|
|
238
|
+
default:
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* The entries of `responses` the client's declared capabilities cover, at the
|
|
244
|
+
* same mode level the gate applies to requests ({@link responseRequirement},
|
|
245
|
+
* {@link isDeclared}), or `undefined` when none survives.
|
|
246
|
+
*
|
|
247
|
+
* The SDK lifts `inputResponses` off every client request, a first call and a
|
|
248
|
+
* 2025-era request included, so a client can arrive pre-answered with no
|
|
249
|
+
* `input_required` result ever sent. An answer the client's modes do not cover
|
|
250
|
+
* cannot have come from a round this server asked for: on the 2025 arm the
|
|
251
|
+
* legacy shim only issues requests the connection declared, and on 2026-07-28
|
|
252
|
+
* the SDK refuses an embedded request the envelope does not cover. Dropping
|
|
253
|
+
* those closes the pre-answered consent-gate bypass — a URL-only client's form
|
|
254
|
+
* answer included — without touching a legitimate round. With no view
|
|
255
|
+
* (`declared` undefined) nothing survives, and an entry the SDK's classifier
|
|
256
|
+
* cannot read as any kind has no capability to justify it either.
|
|
257
|
+
*/
|
|
258
|
+
export function declaredResponses(responses, declared) {
|
|
259
|
+
if (responses === undefined || declared === undefined)
|
|
260
|
+
return;
|
|
261
|
+
const kept = Object.entries(responses).filter(([key, entry]) => {
|
|
262
|
+
const requirement = responseRequirement(inputResponse(responses, key), entry);
|
|
263
|
+
return requirement !== undefined && isDeclared(requirement, declared);
|
|
264
|
+
});
|
|
265
|
+
return kept.length > 0 ? Object.fromEntries(kept) : undefined;
|
|
266
|
+
}
|
|
178
267
|
/**
|
|
179
|
-
* Builds the `ctx.inputs` reader over a retried request's `inputResponses
|
|
268
|
+
* Builds the `ctx.inputs` reader over a retried request's `inputResponses`,
|
|
269
|
+
* keeping only the answers `declared` covers ({@link declaredResponses}).
|
|
180
270
|
*
|
|
181
271
|
* Values arrive from the client and are never re-validated by the SDK — pass a
|
|
182
272
|
* schema to `accepted()` wherever the content matters.
|
|
183
273
|
*/
|
|
184
|
-
export function createContextInputs(mcpReq) {
|
|
185
|
-
return contextInputsFrom(mcpReq?.inputResponses, mcpReq?.droppedInputResponseKeys ?? [], () => mcpReq?.requestState());
|
|
274
|
+
export function createContextInputs(mcpReq, declared) {
|
|
275
|
+
return contextInputsFrom(declaredResponses(mcpReq?.inputResponses, declared), mcpReq?.droppedInputResponseKeys ?? [], () => mcpReq?.requestState());
|
|
186
276
|
}
|
|
187
277
|
/**
|
|
188
278
|
* The `ctx.inputs` reader over an explicit set of responses — what
|
|
@@ -201,4 +291,79 @@ export function contextInputsFrom(responses, dropped, state) {
|
|
|
201
291
|
view: (key) => inputResponse(responses, key),
|
|
202
292
|
};
|
|
203
293
|
}
|
|
294
|
+
// ---------------------------------------------------------------------------
|
|
295
|
+
// requestState sealing (MCP_REQUEST_STATE_KEY)
|
|
296
|
+
// ---------------------------------------------------------------------------
|
|
297
|
+
/**
|
|
298
|
+
* How long a sealed `requestState` verifies, in seconds. Longer than the SDK
|
|
299
|
+
* legacy shim's 600 s per-round timeout, so a 2025-era round the shim holds
|
|
300
|
+
* open never outlives its own state; the codec's 600 s default leaves no
|
|
301
|
+
* margin.
|
|
302
|
+
*/
|
|
303
|
+
const REQUEST_STATE_TTL_SECONDS = 900;
|
|
304
|
+
/** The shortest key the SDK codec accepts, in UTF-8 bytes. */
|
|
305
|
+
const REQUEST_STATE_KEY_MIN_BYTES = 32;
|
|
306
|
+
/**
|
|
307
|
+
* The principal a sealed state is bound to: the request's authenticated
|
|
308
|
+
* `clientId`, `subject`, and `tenantId`, each empty when absent — as they all
|
|
309
|
+
* are on stdio and under `MCP_AUTH_MODE=none`. The codec stores an HMAC tag of
|
|
310
|
+
* it, never the value.
|
|
311
|
+
*/
|
|
312
|
+
function principalOf(ctx) {
|
|
313
|
+
const auth = ctx.http?.authInfo;
|
|
314
|
+
return JSON.stringify([auth?.clientId ?? '', auth?.subject ?? '', auth?.tenantId ?? '']);
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* The process's sealer, or `undefined` when no key is configured — in which
|
|
318
|
+
* case nothing reaches the SDK and handlers read the raw wire string, exactly
|
|
319
|
+
* as before the option existed. There is deliberately no per-process random
|
|
320
|
+
* key: a 2026-07-28 retry can land on another instance or outlive a restart,
|
|
321
|
+
* and every instance that may receive an echoed state needs the same key.
|
|
322
|
+
*
|
|
323
|
+
* @throws {McpError} `ConfigurationError` naming `MCP_REQUEST_STATE_KEY` when
|
|
324
|
+
* the key is shorter than 32 UTF-8 bytes. The key itself is never included.
|
|
325
|
+
*/
|
|
326
|
+
export function createRequestStateSealer(key) {
|
|
327
|
+
if (key === undefined)
|
|
328
|
+
return;
|
|
329
|
+
if (new TextEncoder().encode(key).byteLength < REQUEST_STATE_KEY_MIN_BYTES) {
|
|
330
|
+
throw configurationError(`MCP_REQUEST_STATE_KEY must be at least ${REQUEST_STATE_KEY_MIN_BYTES} bytes. Set a longer random secret, shared by every instance that serves this server, or unset it.`, { variable: 'MCP_REQUEST_STATE_KEY', minimumBytes: REQUEST_STATE_KEY_MIN_BYTES });
|
|
331
|
+
}
|
|
332
|
+
const codec = createRequestStateCodec({
|
|
333
|
+
key,
|
|
334
|
+
ttlSeconds: REQUEST_STATE_TTL_SECONDS,
|
|
335
|
+
bind: principalOf,
|
|
336
|
+
});
|
|
337
|
+
return {
|
|
338
|
+
async seal(result, ctx) {
|
|
339
|
+
if (typeof result.requestState !== 'string')
|
|
340
|
+
return result;
|
|
341
|
+
return { ...result, requestState: await codec.mint(result.requestState, ctx) };
|
|
342
|
+
},
|
|
343
|
+
verify: (state, ctx) => codec.verify(state, ctx),
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* What a handler factory rethrows for a value its handler threw: an
|
|
348
|
+
* input-required signal carrying its `requestState` sealed when `sealer` is
|
|
349
|
+
* configured, anything else unchanged.
|
|
350
|
+
*
|
|
351
|
+
* The factories call it inside the measured region, where the signal is
|
|
352
|
+
* rethrown. Minting can reject — WebCrypto failing, or no request context for
|
|
353
|
+
* the principal binding — and a rejection there would escape the factory with
|
|
354
|
+
* no error envelope, no failure record, and no request id. It resolves to an
|
|
355
|
+
* `InternalError` instead, so the call fails through the family's ordinary
|
|
356
|
+
* error path; the codec's error rides as `cause` into the server log only, and
|
|
357
|
+
* no part of the key reaches it.
|
|
358
|
+
*/
|
|
359
|
+
export async function sealThrown(thrown, sealer, ctx) {
|
|
360
|
+
if (sealer === undefined || !isInputRequiredSignal(thrown))
|
|
361
|
+
return thrown;
|
|
362
|
+
try {
|
|
363
|
+
return new InputRequiredSignal(await sealer.seal(thrown.result, ctx));
|
|
364
|
+
}
|
|
365
|
+
catch (error) {
|
|
366
|
+
return internalError('Could not seal the requestState of an input_required result.', undefined, { cause: error });
|
|
367
|
+
}
|
|
368
|
+
}
|
|
204
369
|
//# sourceMappingURL=inputRequired.js.map
|