Skip to content

Server.call_tool()'s input-validation error result discards jsonschema.ValidationError's structured fields, leaving only free-text prose #3351

Open

Description

@allableaidev

Summary

Server.call_tool()'s low-level dispatch validates tool arguments against tool.inputSchema and, on failure, builds the error result like this:

try:
    jsonschema.validate(instance=arguments, schema=tool.inputSchema)
except jsonschema.ValidationError as e:
    return self._make_error_result(f"Input validation error: {e.message}")

_make_error_result produces CallToolResult(content=[TextContent(text=error_message)], isError=True) — no code, no structured data, nothing beyond the interpolated message string. jsonschema.ValidationError carries several structured attributes that would make a good stable identifier — e.validator (e.g. "type", "required", "enum", "additionalProperties"), e.schema_path, e.json_path — but all of them are discarded before the text crosses the transport (stdio, in our case).

Why this matters

A client that wants to programmatically distinguish why a tool call was rejected (missing required field vs. wrong type vs. enum mismatch, etc.) currently has no option but to regex e.message. That's brittle by construction: jsonschema's message wording already varies per validator keyword ("'X' is not of type 'Y'", "'X' is a required property", "'X' is not one of [...]", ...) with no shared machine-readable code across them, and nothing upstream commits to keeping that wording stable across jsonschema versions.

We hit this trying to classify tool-call failures from an MCP server (google-analytics-mcp, built on this SDK's low-level Server class) into "agent-fixable bad input" vs. "genuine server fault" for log-severity purposes, and had to give up — there's no code or structured field anywhere in the CallToolResult to key on, only the interpolated message. Confirmed this isn't something a well-behaved MCP client (or our own client's JSON-RPC handling) is dropping — the schema-validation error result is a normal JSON-RPC success envelope wrapping isError: true, and _make_error_result simply never puts anything but a message string into it.

Suggested fix

Forward e.validator (and ideally e.schema_path/e.json_path) into a structured data field on the error result, alongside the existing human-readable message — e.g. _make_error_result(message, data={"validator": e.validator, "schema_path": list(e.schema_path)}) — so any server built on the low-level Server class gets locale-stable, machine-readable input-validation errors "for free," without every server author having to reimplement schema validation themselves to get one.

Happy to provide a full repro (raw JSON-RPC request/response) if useful.

Activity

  1. added
    v2Affects the v2 line (2.x on main)
    v1Affects the v1.x maintenance line
    on Aug 21, 2026
  2. Ben-Home commented on Aug 21, 2026

    @Ben-Home
  3. Lubaoshuai commented on Aug 30, 2026

    @Lubaoshuai

    I hit the same gap and can confirm it still reproduces on current main (post-mcpserver refactor) — the mechanics have moved, but the outcome is the same.

    On the v2 code path, argument validation against the input schema happens via pydantic in Tool.run (mcp/server/mcpserver/tools/base.py), and _handle_call_tool (mcp/server/mcpserver/server.py) catches the resulting ToolError and returns CallToolResult(content=[TextContent(text=str(exc))], is_error=True). The ValidationError's structured detail (errors() → loc/type/msg) is used only for the server-side INFO log (field names), and never crosses the transport — so the classification problem described above stands on main today.

    One additional datapoint for the design: making this safe is easier than it might look, because clients only revalidate structured_content for successful results — Client.call_tool gates validate_tool_result on not result.is_error (mcp/client/session.py). So attaching something like structured_content={"type": "input_validation", "errors": [{"path": ..., "type": ..., "message": ...}]} to the error result does not trip any output-schema check. Rejected values should stay out of the payload (mirroring the existing field-names-only logging policy). The human-readable text content can stay byte-identical, so nothing changes for the model.

    I have a working branch with regression tests against main and am happy to open a PR if/when the issue gets assigned.

  4. added
    enhancementRequest for a new feature that's not currently supported
    on Oct 8, 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

    enhancementRequest for a new feature that's not currently supportedv1Affects the v1.x maintenance linev2Affects the v2 line (2.x on main)

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions