@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.
Files changed (119) hide show
  1. package/AGENTS.md +36 -13
  2. package/CLAUDE.md +36 -13
  3. package/README.md +9 -9
  4. package/changelog/0.13.x/0.13.10.md +118 -0
  5. package/dist/config/index.d.ts +3 -0
  6. package/dist/config/index.d.ts.map +1 -1
  7. package/dist/config/index.js +11 -0
  8. package/dist/config/index.js.map +1 -1
  9. package/dist/core/app.d.ts.map +1 -1
  10. package/dist/core/app.js +15 -1
  11. package/dist/core/app.js.map +1 -1
  12. package/dist/core/context.d.ts +89 -20
  13. package/dist/core/context.d.ts.map +1 -1
  14. package/dist/core/context.js +40 -0
  15. package/dist/core/context.js.map +1 -1
  16. package/dist/core/index.d.ts +1 -1
  17. package/dist/core/index.d.ts.map +1 -1
  18. package/dist/core/index.js.map +1 -1
  19. package/dist/core/worker.d.ts +6 -0
  20. package/dist/core/worker.d.ts.map +1 -1
  21. package/dist/core/worker.js +1 -0
  22. package/dist/core/worker.js.map +1 -1
  23. package/dist/linter/rules/error-contract-rules.d.ts +3 -44
  24. package/dist/linter/rules/error-contract-rules.d.ts.map +1 -1
  25. package/dist/linter/rules/error-contract-rules.js +8 -144
  26. package/dist/linter/rules/error-contract-rules.js.map +1 -1
  27. package/dist/linter/rules/index.d.ts +1 -1
  28. package/dist/linter/rules/index.d.ts.map +1 -1
  29. package/dist/linter/rules/index.js +1 -1
  30. package/dist/linter/rules/index.js.map +1 -1
  31. package/dist/linter/rules/resource-rules.d.ts.map +1 -1
  32. package/dist/linter/rules/resource-rules.js +1 -2
  33. package/dist/linter/rules/resource-rules.js.map +1 -1
  34. package/dist/linter/rules/tool-rules.d.ts.map +1 -1
  35. package/dist/linter/rules/tool-rules.js +1 -2
  36. package/dist/linter/rules/tool-rules.js.map +1 -1
  37. package/dist/mcp-server/handlerContext.d.ts +26 -13
  38. package/dist/mcp-server/handlerContext.d.ts.map +1 -1
  39. package/dist/mcp-server/handlerContext.js +32 -17
  40. package/dist/mcp-server/handlerContext.js.map +1 -1
  41. package/dist/mcp-server/inputRequired.d.ts +120 -8
  42. package/dist/mcp-server/inputRequired.d.ts.map +1 -1
  43. package/dist/mcp-server/inputRequired.js +177 -12
  44. package/dist/mcp-server/inputRequired.js.map +1 -1
  45. package/dist/mcp-server/prompts/prompt-registration.d.ts +10 -2
  46. package/dist/mcp-server/prompts/prompt-registration.d.ts.map +1 -1
  47. package/dist/mcp-server/prompts/prompt-registration.js +49 -11
  48. package/dist/mcp-server/prompts/prompt-registration.js.map +1 -1
  49. package/dist/mcp-server/resources/resource-registration.d.ts +4 -2
  50. package/dist/mcp-server/resources/resource-registration.d.ts.map +1 -1
  51. package/dist/mcp-server/resources/resource-registration.js +6 -4
  52. package/dist/mcp-server/resources/resource-registration.js.map +1 -1
  53. package/dist/mcp-server/resources/utils/resourceHandlerFactory.d.ts +4 -3
  54. package/dist/mcp-server/resources/utils/resourceHandlerFactory.d.ts.map +1 -1
  55. package/dist/mcp-server/resources/utils/resourceHandlerFactory.js +42 -14
  56. package/dist/mcp-server/resources/utils/resourceHandlerFactory.js.map +1 -1
  57. package/dist/mcp-server/server.d.ts +9 -0
  58. package/dist/mcp-server/server.d.ts.map +1 -1
  59. package/dist/mcp-server/server.js +14 -13
  60. package/dist/mcp-server/server.js.map +1 -1
  61. package/dist/mcp-server/tools/tool-registration.d.ts +7 -3
  62. package/dist/mcp-server/tools/tool-registration.d.ts.map +1 -1
  63. package/dist/mcp-server/tools/tool-registration.js +9 -5
  64. package/dist/mcp-server/tools/tool-registration.js.map +1 -1
  65. package/dist/mcp-server/tools/utils/toolHandlerFactory.d.ts +17 -9
  66. package/dist/mcp-server/tools/utils/toolHandlerFactory.d.ts.map +1 -1
  67. package/dist/mcp-server/tools/utils/toolHandlerFactory.js +91 -50
  68. package/dist/mcp-server/tools/utils/toolHandlerFactory.js.map +1 -1
  69. package/dist/mcp-server/transports/http/httpErrorHandler.d.ts.map +1 -1
  70. package/dist/mcp-server/transports/http/httpErrorHandler.js +7 -1
  71. package/dist/mcp-server/transports/http/httpErrorHandler.js.map +1 -1
  72. package/dist/testing/index.d.ts +17 -2
  73. package/dist/testing/index.d.ts.map +1 -1
  74. package/dist/testing/index.js +21 -7
  75. package/dist/testing/index.js.map +1 -1
  76. package/dist/types-global/errors.d.ts +18 -15
  77. package/dist/types-global/errors.d.ts.map +1 -1
  78. package/dist/utils/internal/error-handler/errorHandler.d.ts +5 -4
  79. package/dist/utils/internal/error-handler/errorHandler.d.ts.map +1 -1
  80. package/dist/utils/internal/error-handler/errorHandler.js +9 -7
  81. package/dist/utils/internal/error-handler/errorHandler.js.map +1 -1
  82. package/dist/utils/internal/error-handler/types.d.ts +3 -1
  83. package/dist/utils/internal/error-handler/types.d.ts.map +1 -1
  84. package/dist/utils/internal/performance.d.ts +4 -2
  85. package/dist/utils/internal/performance.d.ts.map +1 -1
  86. package/dist/utils/internal/performance.js +8 -6
  87. package/dist/utils/internal/performance.js.map +1 -1
  88. package/dist/utils/internal/telemetryMessages.d.ts +0 -1
  89. package/dist/utils/internal/telemetryMessages.d.ts.map +1 -1
  90. package/dist/utils/internal/telemetryMessages.js +0 -1
  91. package/dist/utils/internal/telemetryMessages.js.map +1 -1
  92. package/dist/utils/telemetry/attributes.d.ts +5 -4
  93. package/dist/utils/telemetry/attributes.d.ts.map +1 -1
  94. package/dist/utils/telemetry/attributes.js +5 -4
  95. package/dist/utils/telemetry/attributes.js.map +1 -1
  96. package/framework-skills/add-service/SKILL.md +3 -12
  97. package/framework-skills/add-test/SKILL.md +6 -3
  98. package/framework-skills/add-tool/SKILL.md +29 -33
  99. package/framework-skills/api-config/SKILL.md +2 -1
  100. package/framework-skills/api-context/SKILL.md +155 -40
  101. package/framework-skills/api-errors/SKILL.md +43 -47
  102. package/framework-skills/api-linter/SKILL.md +5 -29
  103. package/framework-skills/api-telemetry/SKILL.md +11 -7
  104. package/framework-skills/api-testing/SKILL.md +43 -11
  105. package/framework-skills/api-workers/SKILL.md +3 -1
  106. package/framework-skills/design-mcp-server/SKILL.md +5 -5
  107. package/framework-skills/field-test/SKILL.md +2 -2
  108. package/framework-skills/polish-docs-meta/SKILL.md +2 -2
  109. package/framework-skills/release-and-publish/SKILL.md +3 -3
  110. package/framework-skills/release-pr-review/SKILL.md +2 -2
  111. package/framework-skills/security-pass/SKILL.md +8 -7
  112. package/package.json +4 -3
  113. package/scripts/install-otel.ts +84 -0
  114. package/scripts/lint-packaging.ts +188 -27
  115. package/templates/.env.example +2 -0
  116. package/templates/AGENTS.md +5 -4
  117. package/templates/CLAUDE.md +5 -4
  118. package/templates/Dockerfile +67 -50
  119. 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 InputRequiredGate } from './inputRequired.js';
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: the SDK request id and
65
- * the raw session id when present. Raw handler input is deliberately not part
66
- * of it — the context spreads into the completion log and can carry caller
67
- * PII or secrets; sizes and parameter names are recorded as metric attributes
68
- * by the execution measurement instead.
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(request: HandlerRequest): Partial<Pick<RequestContext, 'requestId' | 'sessionId'>>;
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. `inputGate` is the
77
- * connection's client-capability check, which `ctx.requestInput` runs on the
78
- * result it builds (#379); the context is unchanged without one.
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, inputGate?: InputRequiredGate, uri?: URL): Context;
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,EAGL,KAAK,iBAAiB,EACvB,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,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,yEAAyE;IACzE,YAAY,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC;;;;;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,CAiBhB;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,IAAI,CAAC,cAAc,EAAE,WAAW,GAAG,WAAW,CAAC,CAAC,CAM1D;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,cAAc,EACvB,QAAQ,EAAE,eAAe,EACzB,WAAW,EAAE,cAAc,EAC3B,MAAM,EAAE,SAAS,aAAa,EAAE,GAAG,SAAS,EAC5C,SAAS,CAAC,EAAE,iBAAiB,EAC7B,GAAG,CAAC,EAAE,GAAG,GACR,OAAO,CAqBT"}
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 = typeof serverContext?.sessionId === 'string' ? serverContext.sessionId : undefined;
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: the SDK request id and
36
- * the raw session id when present. Raw handler input is deliberately not part
37
- * of it — the context spreads into the completion log and can carry caller
38
- * PII or secrets; sizes and parameter names are recorded as metric attributes
39
- * by the execution measurement instead.
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(request) {
42
- const requestId = request.mcpReq?.id;
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
- ...(request.sdkSessionId && { sessionId: request.sdkSessionId }),
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. `inputGate` is the
54
- * connection's client-capability check, which `ctx.requestInput` runs on the
55
- * result it builds (#379); the context is unchanged without one.
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, inputGate, uri) {
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(inputGate),
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,EACL,mBAAmB,EACnB,kBAAkB,GAEnB,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAGL,eAAe,GAChB,MAAM,+BAA+B,CAAC;AAEvC,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAgD3D;;;;;GAKG;AACH,MAAM,UAAU,qBAAqB,CACnC,aAAwC,EACxC,QAAyB,EACzB,SAA0B;IAE1B,MAAM,MAAM,GAAG,aAAa,EAAE,MAAM,CAAC;IACrC,MAAM,YAAY,GAChB,OAAO,aAAa,EAAE,SAAS,KAAK,QAAQ,CAAC,CAAC,CAAC,aAAa,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC;IACrF,MAAM,cAAc,GAAG,kBAAkB,CAAC,MAAM,CAAC,cAAc,CAAC,KAAK,UAAU,CAAC;IAChF,MAAM,iBAAiB,GAAG,MAAM,CAAC,gBAAgB,KAAK,OAAO,IAAI,MAAM,CAAC,WAAW,KAAK,MAAM,CAAC;IAC/F,OAAO;QACL,eAAe,EAAE,iBAAiB,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS;QAC1D,MAAM;QACN,SAAS,EAAE,eAAe,CAAC,SAAS,EAAE,MAAM,CAAC;QAC7C,YAAY;QACZ,SAAS,EACP,YAAY,IAAI,CAAC,cAAc,IAAI,QAAQ,CAAC,wBAAwB,KAAK,IAAI,CAAC;YAC5E,CAAC,CAAC,YAAY;YACd,CAAC,CAAC,SAAS;QACf,MAAM,EAAE,MAAM,EAAE,MAAM,IAAI,IAAI,eAAe,EAAE,CAAC,MAAM;KACvD,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,oBAAoB,CAClC,OAAuB;IAEvB,MAAM,SAAS,GAAG,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC;IACrC,OAAO;QACL,GAAG,CAAC,OAAO,SAAS,KAAK,QAAQ,IAAI,EAAE,SAAS,EAAE,CAAC;QACnD,GAAG,CAAC,OAAO,CAAC,YAAY,IAAI,EAAE,SAAS,EAAE,OAAO,CAAC,YAAY,EAAE,CAAC;KACjE,CAAC;AACJ,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,mBAAmB,CACjC,OAAuB,EACvB,QAAyB,EACzB,WAA2B,EAC3B,MAA4C,EAC5C,SAA6B,EAC7B,GAAS;IAET,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,GAAG,OAAO,CAAC;IACtC,OAAO,eAAe,CACpB,aAAa,CAAC;QACZ,UAAU,EAAE,WAAW;QACvB,eAAe,EAAE,OAAO,CAAC,eAAe;QACxC,MAAM,EAAE,QAAQ,CAAC,MAAM;QACvB,OAAO,EAAE,QAAQ,CAAC,OAAO;QACzB,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,SAAS,EAAE,OAAO,CAAC,SAAS;QAC5B,MAAM,EAAE,mBAAmB,CAAC,MAAM,CAAC;QACnC,YAAY,EAAE,kBAAkB,CAAC,SAAS,CAAC;QAC3C,GAAG,CAAC,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,MAAM,CAAC,GAAG,EAAE,CAAC;QAC3C,uBAAuB,EAAE,SAAS,CAAC,uBAAuB;QAC1D,yBAAyB,EAAE,SAAS,CAAC,yBAAyB;QAC9D,qBAAqB,EAAE,SAAS,CAAC,qBAAqB;QACtD,qBAAqB,EAAE,SAAS,CAAC,qBAAqB;QACtD,GAAG,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,CAAC;KACpB,CAAC,EACF,MAAM,CACP,CAAC;AACJ,CAAC"}
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 connection from its declared client capabilities.
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
- * `clientCapabilities()` returning `undefined` is the per-request legacy case:
61
- * an instance that never saw an `initialize` holds no capability view, so every
62
- * embedded request is refused, and the message and hint say so.
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(clientCapabilities: () => ClientCapabilities | undefined): InputRequiredGate;
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
- * Builds the `ctx.inputs` reader over a retried request's `inputResponses`.
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;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EACL,eAAe,EACf,KAAK,kBAAkB,EACvB,KAAK,mBAAmB,EAIxB,KAAK,aAAa,EAEnB,MAAM,8BAA8B,CAAC;AAEtC,OAAO,KAAK,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACvE,OAAO,EAAoB,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AAEtE;;;;;;;GAOG;AACH,qBAAa,mBAAoB,SAAQ,KAAK;IAIhC,QAAQ,CAAC,MAAM,EAAE,mBAAmB;IAHhD,wEAAwE;IACxE,QAAQ,CAAC,qBAAqB,OAAiB;IAE/C,YAAqB,MAAM,EAAE,mBAAmB,EAG/C;CACF;AAED,iFAAiF;AACjF,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,mBAAmB,CAOlF;AAwED;;;;;GAKG;AACH,MAAM,MAAM,iBAAiB,GAAG,CAC9B,MAAM,EAAE,mBAAmB,EAC3B,YAAY,CAAC,EAAE,MAAM,KAClB,QAAQ,GAAG,SAAS,CAAC;AAE1B;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,uBAAuB,CACrC,kBAAkB,EAAE,MAAM,kBAAkB,GAAG,SAAS,GACvD,iBAAiB,CA8BnB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,CAAC,EAAE,iBAAiB,GAAG,cAAc,CAO3E;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,aAAa,CAAC,QAAQ,CAAC,GAAG,SAAS,GAAG,aAAa,CAM9F;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC,CAAC,CAAC,EAChD,OAAO,EAAE,MAAM,EAAE,EACjB,KAAK,EAAE,aAAa,CAAC,OAAO,CAAC,GAC5B,aAAa,CAaf"}
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 connection from its declared client capabilities.
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
- * `clientCapabilities()` returning `undefined` is the per-request legacy case:
119
- * an instance that never saw an `initialize` holds no capability view, so every
120
- * embedded request is refused, and the message and hint say so.
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(clientCapabilities) {
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