You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Low-level Server.setRequestHandler strips unknown tools/call params members before any author schema can preserve them #3008
TL;DR: On the maintained 2.x line, Server.setRequestHandler validates each request through its era request schema and passes only the validated value to the handler. Unknown members of tools/callparams (fields beside name and arguments) are silently stripped before any author-side schema can see them, so a server cannot preserve them even when its own tool schemas use passthrough(). Requesting a maintained way to receive the original params (or the stripped members) at request level.
Bug Type: behavior gap / API gap
Version:@modelcontextprotocol/server 2.3.1 + @modelcontextprotocol/core 2.3.1 (also observed on the v2 alpha line)
Steps to Reproduce: Register a handler for tools/call on a raw Server (not the high-level McpServer.registerTool path). Send a call whose params carry business fields beside name/arguments, e.g. {name: "delete_record", arguments: {...}, resource_type: "company", record_id: "..."}. Inside the handler, inspect the received params: the extra members are gone. Verified at the wire level with a raw stdio JSON client against the built CLI, in both modern and legacy protocol eras, reproduced twice; the same calls with every field inside arguments survive when the tool's registration schema uses z.object({...}).passthrough() (the workaround discussed in #147), which confirms the strip happens in the SDK's own request validation, below the author-schema layer.
Observed SDK behavior:Server.setRequestHandler passes dispatchCodec.validateRequest(...).value to the handler; the era request schemas for CallToolRequestParams retain name and arguments and strip unknown siblings. The documented three-argument schema overload is for custom non-spec methods, so there is currently no blessed path to see spec-request extras.
Expected: A maintained extension point to preserve unknown tools/call params - for example an option on request validation, a documented raw-params hook, or a documented guarantee that unknown members are dropped (so servers can tell clients to stop sending them). Any of the three resolves the ambiguity; today the behavior is neither documented nor controllable.
Impact: Servers migrating from older SDKs (or serving older clients) that accept loose call parameters lose those fields silently, with no error or hook to recover them. Concrete case: a long-lived server whose normalizer accepts resource_type/record_id beside name for legacy clients - under 2.3.1 those fields vanish before the server's own validation layer runs.
The high-level McpServer.registerTool path is unaffected (its argument schema is author-owned); this gap is specific to the low-level Server request-handler path that servers use for custom normalization.
I checked the current v2 source and think this is a real mismatch between the SDK's generic extension posture and the method-specific dispatch boundary.
The generated 2025 and 2026 schema sources deliberately type the base Request.params as open for unofficial extensions (2025 lines 65–69, 2026 lines 184–188). The SDK mirrors that at the generic boundary with BaseRequestParamsSchema.loose() (schemas.ts lines 50–77), but CallToolRequestSchema replaces params with the non-loose CallToolRequestParamsSchema (lines 1454–1471). Dispatch then gives the handler only outcome.value, not the original request (protocol.ts lines 1733–1751). So once that method schema strips a sibling, an author schema cannot recover it.
I would avoid fixing this only by making CallToolRequestParamsSchema globally loose. That would change the typed payload delivered to every spec handler, implicitly bless non-standard top-level fields, and create collision behavior when a future protocol revision assigns one of those names. The base-schema comment is compatibility intent, not a claim that resource_type or record_id are normative; _meta remains the standard extension space.
A bounded API could keep the validated spec request as argument 1 while exposing the untouched wire params through an explicit opt-in/context field, or add a handler-registration option that preserves unknown params. The key is that the preservation choice must happen before validateRequest(...).value replaces the input.
Suggested regression matrix for both maintained wire eras:
opt-in/raw path sees an exact sentinel sibling at params root;
name, arguments, _meta, and era-specific task/input-response members retain their existing validation;
an author schema may accept or reject the preserved sibling;
a future-known-field collision has a defined precedence rather than being silently shadowed.
Source review only; I did not run the TypeScript test suite. AI assistance was used to inspect the paths and draft this comment; I reviewed the cited source and conclusions before posting.
Could a maintainer confirm which bounded API direction you would accept: an opt-in context field exposing original wire params, or a handler-registration option preserving unknown params before validateRequest replaces the input?
The sketch above matches my downstream evidence on server/core 2.3.1. The installed dispatch path passes validateRequest(method, request).value to the handler. In my recorded raw-stdio tests, wrapped calls reach the handler with their business fields in both wire eras; loose calls in both eras lose resource_type and record_id before application normalization and fail with missing resource_type. The HTTP backend was mocked, and these tests made no live service writes. I have not run the SDK's own test suite.
One clarification to my original report: applying .passthrough() to an author schema preserves fields inside arguments, but cannot recover siblings already stripped by the method request schema. This low-level path uses Server.setRequestHandler, with no McpServer.registerTool schema to adjust.
I favor keeping the validated request as the first handler argument and exposing original params separately through an explicit opt-in context field. The five-item regression matrix in the linked comment covers the behavior I need in both eras: pinned defaults, exact sentinel preservation, unchanged validation of known fields, author acceptance/rejection, and defined precedence for future-known-field collisions. _meta remains the standard extension space; the loose business fields here are an existing downstream compatibility requirement.
Would that approach fit the SDK, or would you prefer the registration option? I will wait for that decision before implementing against either API.
I opened #3038 with an implementation of the opt-in raw-params direction. A note on what it covers and where the guarantee stops, since the thread discussed two options:
The PR captures rawRequest.params after transport parsing but before handler dispatch, and exposes it behind an opt-in flag. For my downstream case (unknown sibling fields beside name and arguments on tools/call), this preserves them through validation. That was the failure I filed, and the PR's tests cover it.
One boundary I found while testing: fields nested inside schema-parsed objects are normalized by transport schemas before capture. For example, _meta sub-fields run through RelatedTaskMetadataSchema, so they are not preserved. Rather than extend capture across the transport boundary in this PR, I documented that limit in the doc comments, the changeset, and the PR description, and added a stdio-level test asserting the narrowed contract. If maintainers want pre-parse preservation as the guarantee, that deserves its own review of the transport layer's parse order; I am happy to take it up here or in a follow-up if that direction is preferred.
The PR also carries a regression matrix covering both the legacy and current request-schema eras, since the original loose-params loss reproduced in both.
Activity
jarrettdustinqq commentedon Oct 13, 2026
I checked the current v2 source and think this is a real mismatch between the SDK's generic extension posture and the method-specific dispatch boundary.
The generated 2025 and 2026 schema sources deliberately type the base
Request.paramsas open for unofficial extensions (2025 lines 65–69, 2026 lines 184–188). The SDK mirrors that at the generic boundary withBaseRequestParamsSchema.loose()(schemas.ts lines 50–77), butCallToolRequestSchemareplacesparamswith the non-looseCallToolRequestParamsSchema(lines 1454–1471). Dispatch then gives the handler onlyoutcome.value, not the original request (protocol.ts lines 1733–1751). So once that method schema strips a sibling, an author schema cannot recover it.I would avoid fixing this only by making
CallToolRequestParamsSchemaglobally loose. That would change the typed payload delivered to every spec handler, implicitly bless non-standard top-level fields, and create collision behavior when a future protocol revision assigns one of those names. The base-schema comment is compatibility intent, not a claim thatresource_typeorrecord_idare normative;_metaremains the standard extension space.A bounded API could keep the validated spec request as argument 1 while exposing the untouched wire params through an explicit opt-in/context field, or add a handler-registration option that preserves unknown params. The key is that the preservation choice must happen before
validateRequest(...).valuereplaces the input.Suggested regression matrix for both maintained wire eras:
paramsroot;name,arguments,_meta, and era-specific task/input-response members retain their existing validation;Source review only; I did not run the TypeScript test suite. AI assistance was used to inspect the paths and draft this comment; I reviewed the cited source and conclusions before posting.
kesslerio commentedon Oct 14, 2026
Could a maintainer confirm which bounded API direction you would accept: an opt-in context field exposing original wire params, or a handler-registration option preserving unknown params before
validateRequestreplaces the input?The sketch above matches my downstream evidence on server/core 2.3.1. The installed dispatch path passes
validateRequest(method, request).valueto the handler. In my recorded raw-stdio tests, wrapped calls reach the handler with their business fields in both wire eras; loose calls in both eras loseresource_typeandrecord_idbefore application normalization and fail with missingresource_type. The HTTP backend was mocked, and these tests made no live service writes. I have not run the SDK's own test suite.One clarification to my original report: applying
.passthrough()to an author schema preserves fields insidearguments, but cannot recover siblings already stripped by the method request schema. This low-level path usesServer.setRequestHandler, with noMcpServer.registerToolschema to adjust.I favor keeping the validated request as the first handler argument and exposing original params separately through an explicit opt-in context field. The five-item regression matrix in the linked comment covers the behavior I need in both eras: pinned defaults, exact sentinel preservation, unchanged validation of known fields, author acceptance/rejection, and defined precedence for future-known-field collisions.
_metaremains the standard extension space; the loose business fields here are an existing downstream compatibility requirement.Would that approach fit the SDK, or would you prefer the registration option? I will wait for that decision before implementing against either API.
kesslerio commentedon Oct 15, 2026
I opened #3038 with an implementation of the opt-in raw-params direction. A note on what it covers and where the guarantee stops, since the thread discussed two options:
The PR captures
rawRequest.paramsafter transport parsing but before handler dispatch, and exposes it behind an opt-in flag. For my downstream case (unknown sibling fields besidenameandargumentsontools/call), this preserves them through validation. That was the failure I filed, and the PR's tests cover it.One boundary I found while testing: fields nested inside schema-parsed objects are normalized by transport schemas before capture. For example,
_metasub-fields run throughRelatedTaskMetadataSchema, so they are not preserved. Rather than extend capture across the transport boundary in this PR, I documented that limit in the doc comments, the changeset, and the PR description, and added a stdio-level test asserting the narrowed contract. If maintainers want pre-parse preservation as the guarantee, that deserves its own review of the transport layer's parse order; I am happy to take it up here or in a follow-up if that direction is preferred.The PR also carries a regression matrix covering both the legacy and current request-schema eras, since the original loose-params loss reproduced in both.