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
Server.call_tool()'s input-validation error result discards jsonschema.ValidationError's structured fields, leaving only free-text prose #3351
_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.
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.
Activity
Ben-Home commentedon Aug 21, 2026
Lubaoshuai commentedon Aug 30, 2026
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 resultingToolErrorand returnsCallToolResult(content=[TextContent(text=str(exc))], is_error=True). TheValidationError'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 onmaintoday.One additional datapoint for the design: making this safe is easier than it might look, because clients only revalidate
structured_contentfor successful results —Client.call_toolgatesvalidate_tool_resultonnot result.is_error(mcp/client/session.py). So attaching something likestructured_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
mainand am happy to open a PR if/when the issue gets assigned.