Skip to content

The maxToolInputElements refusal lacks the Input validation error: prefix that the schema refusal carries #3111

Open

Description

@mihai-r-lupu

Version: @modelcontextprotocol/sdk 1.32.2 (the same text is in packages/server/src/server/mcp.ts on main).

McpServer.validateToolInput refuses a tool call in two ways. Both are McpError(ErrorCode.InvalidParams, …), and the tools/call handler returns either one as an isError: true result:

  • arguments over maxToolInputElements:
    Invalid arguments for tool start_run: arguments contain more than the maximum of 50000 elements
  • arguments that fail the input schema:
    Input validation error: Invalid arguments for tool get_run_state: Invalid input: expected string, received number at run_id

A client or a log filter that recognises an input refusal by its Input validation error: prefix misses the first one. Yet both are the same kind of refusal: bad arguments, same error code.

Suggestion: give the size refusal the same prefix:
Input validation error: Invalid arguments for tool start_run: arguments contain more than the maximum of 50000 elements

Context: we set maxToolInputElements: 50_000 in Realm's MCP server (thank you for #2926/#2927), and we document both texts for our users.

Related: #2879 asks for a hook to shape the schema refusal's result; if it lands, the size refusal could go through the same hook.

Activity

  1. barry166 commented on Oct 27, 2026

    @barry166

    I can take this. I’ll add the consistent input-validation prefix and a focused regression test.

  2. mihai-r-lupu commented on Oct 27, 2026

    @mihai-r-lupu
    Author

    Thanks!

  3. added
    v2Ideas, requests and plans for v2 of the SDK which will incorporate major changes and fixes
    v1Issues / PRs related to v1.x
    on Oct 28, 2026
  4. Ben-Home commented on Oct 28, 2026

    @Ben-Home

    You are right that they are the same kind of refusal, and the durable version of this fix is to make that sameness structural rather than textual. A shared prefix that two call sites both spell by hand will drift again the moment a third refusal is added, which is exactly how this one arrived.

    Two cheap moves make it stick:

    1. Hoist the prefix to a module constant, for example INPUT_REFUSAL_PREFIX = "Input validation error: ", and have both paths format through one helper: refuse(tool_name, detail) returning the McpError(ErrorCode.InvalidParams, ...). Then a grep for string literals starting with "Invalid arguments for tool" finds one producer instead of N.
    2. Put the payload in data as well as the prose. The size refusal already knows the count and the limit, and a client that wants to render "send fewer fields" or auto-chunk should not parse them back out of a sentence. McpError carries a data field, so {"tool": name, "elements": n, "maxElements": limit} alongside the message costs nothing and survives any future rewording.

    On the test side, the one that would have caught this is not a per-case assertion but an enumeration: a test that walks every refusal path validateToolInput can take and asserts each message begins with the constant. Written that way, the assertion is about the contract rather than about the two strings, and a new refusal path fails it until it is routed through the helper.

    One naming caveat worth settling before the constant lands: "Input validation error" is accurate for schema validation and slightly off for a size cap, since the arguments may be perfectly well-formed and merely large. If you would rather the prefix describe the response class than the cause, Invalid arguments for tool <name>: is already common to both and is the substring a client can rely on today without any server change. Either way, pick the one prefix and make both paths emit it, so the choice is a decision rather than an accident of which branch was written first.

    We maintain a remote MCP server whose tool arguments include large structured payloads, and the size-cap-before-schema order means our own clients see the size refusal far more often than the schema one, which is the practical argument for making the recognisable prefix cover both.

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

    v1Issues / PRs related to v1.xv2Ideas, 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