Skip to content

Low-level Server.setRequestHandler strips unknown tools/call params members before any author schema can preserve them #3008

Open

Description

@kesslerio

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/call params (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.

Activity

  1. jarrettdustinqq commented on Oct 13, 2026

    @jarrettdustinqq

    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:

    • default spec handler behavior remains explicitly pinned;
    • 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.

  2. added
    v2Ideas, requests and plans for v2 of the SDK which will incorporate major changes and fixes
    on Oct 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    v2Ideas, requests and plans for v2 of the SDK which will incorporate major changes and fixes

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions