Repository navigation
Tool validation errors echo the rejected input value (input_value) — add a masking option or a public validation-error hook #3572
Description
Activity
+1 for option 3 (drop
input_valueby default), with option 2 as the escape hatch.Pydantic already has both switches, so the change may be small:
e.errors(include_input=False)returns the error list without the rejected values, so theToolErrormessage can be built from that instead ofstr(e)- or create the generated arg model with
ConfigDict(hide_input_in_errors=True), thenstr(e)prints[type=string_type]withoutinput_value=...
Checked both on pydantic 2.13.5.
It is worth checking the other validation paths too (structured output validation, resource template params). They probably format the error the same way.
While looking at this, I found the same leak in the validation-error path of my own guard library, so this pattern is easy to miss.
- added a commit that references this issue
on Sep 25, 2026 I've implemented a fix and opened PR #3582. The approach: set
hide_input_in_errors=TrueonArgModelBase.model_config— a one-line change that suppressesinput_valuefrom all pydantic validation errors for tool arguments. All 59 existing tests pass. Happy to address the Copilot review comments (nested model coverage,ValidationErrorinstead ofException, end-to-end test) if a maintainer assigns me.Repro confirmed on
main, plus one data point on the design.Where it leaks
src/mcp/server/mcpserver/tools/base.py:150-153:except ValidationError as exc: # The caller's arguments don't match the input schema: the model's mistake # to read and correct, so it is reported like a deliberate ToolError. raise ToolError(f"Error executing tool {self.name}: {exc}") from exc
str(ValidationError)includesinput_value, so whatever the client sent comes back in the
error result. With pydantic 2.13.5, a tool takingpatient_id: intreturns:1 validation error for Args patient_id Input should be a valid integer, unable to parse string as an integer [type=int_parsing, input_value='MRN-8837-Jane-Doe-DOB-1974-03-02', input_type=str] For further information visit https://errors.pydantic.dev/2.13/v/int_parsingWhy the validation path is the odd one out
The SDK already treats "do not echo the original back" as a rule rather than a nicety.
UnexpectedToolError(src/mcp/server/mcpserver/exceptions.py:61-72) documents that its message
is onlyError executing tool <name>"so nothing from the original reaches the client", while
__cause__keeps the detail for the server log.run()'s own docstring repeats it: "A crash
does not, so nothing from an unexpected exception reaches the client." Argument validation is the
one branch that forwards pydantic's full text.The option I would take
Dropping
input_valueby default is the right call. The open question is whether it needs a new
public setting, and I would rather not add one. The repository's own guidance is to prefer
maintainability and consistency over new capabilities, andUnexpectedToolErroralready shows the
shape to copy: a short message to the client, the detail kept server-side on__cause__. Applying
that here -- sanitised by default, original exception preserved for the server log, no new
configuration surface -- extends a rule the SDK already follows instead of introducing a switch
that has to be maintained.hide_input_in_errors=TrueonArgModelBase.model_configis one line and covers the common case.
The reason I would not stop there is nested models: a field whose type is itself aBaseModel
carries its own model config, so a nested validation error can still render the input value.
Worth confirming against whichever approach is chosen.Disclosure: AI-assisted. The pydantic snippet above was run locally against 2.13.5, and both
files were read at the line numbers quoted.I've updated the branch with fixes for all three Copilot review comments:
1. Nested model coverage (High) — switched from relying solely on
hide_input_in_errors=TrueonArgModelBaseto formatting the error viaexc.errors(include_input=False)inTool.run(). This guarantees nested user-definedBaseModelfields are also sanitized, regardless of their own config.2. End-to-end test (Medium) — added
test_validation_error_does_not_echo_input_via_tool_runwhich drives the full stack throughTool.run()and asserts the sensitive value is absent from theToolErrormessage.3.
ValidationErrorvsException(Low) — already addressed in the previous commit.All 61 tests pass. Branch:
MohammadaminAlbooyeh:fix/hide-input-in-validation-errorsCan I be assigned to this issue? I've already implemented a fix on my fork.
Correction to my comment above — I got the nested-model point wrong, and I checked it properly
before anyone builds on it.I wrote that a nested
BaseModelfield "carries its own model config, so a nested validation
error can still render the input value". That is backwards. Measured on pydantic 2.13.5, with the
flag set on the outer model only, i.e. the one handed tomodel_validate:shape input_valuein the final messageflat field absent nested BaseModelabsent list[Model],dict[str, Model]absent Optional[Model],Union[Model, int]absent list[list[Model]],dict[str, list[Model]]absent nested field_validatorraisingabsent three levels deep absent Two controls show the flag is what does the work: the same shapes with no flag set do echo
input_value, and setting it on the nested model only — leaving the outer model at its default —
still echoes. The flag is read from the model you callmodel_validate()on and applies to
everything below it. A nested model's own setting is not consulted: one that sets the flag to
Truewhile the outer model leaves it at the default still echoes the value.So
ConfigDict(hide_input_in_errors=True)on the generated arguments model does cover
def fn(payload: Payload): there is no nested gap, and the two approaches in the thread are
equivalent in coverage. What is left is shape rather than safety — one line on the model config,
versus formatting where the message is built witherrors(include_input=False). I have no strong
preference between them; the second does make the masking visible at the point it happens.One thing neither switch covers, worth noting once: both remove the
input_value/input_type
that pydantic attaches. Neither can remove a value that a field or model validator wrote into its
own exception message. Different problem, same feature area.Disclosure: AI-assisted. Every row above was run locally against pydantic 2.13.5.
Thanks @liwenjie200543 for the thorough verification. Good to know the flag on the outer model covers all nested shapes with no gap — that clears up the original concern that prompted the switch to
exc.errors(include_input=False).Both approaches are now confirmed equivalent in coverage. I'm happy to revert to the one-liner (
ConfigDict(hide_input_in_errors=True)onArgModelBase) for a smaller diff if maintainers prefer that shape. Just let me know which direction to go.- addedenhancementRequest for a new feature that's not currently supportedRequest for a new feature that's not currently supported
on Oct 8, 2026
What happens
When a tool call's arguments fail pydantic validation,
Tool.runwraps theValidationErrorinto aToolErrorwhose message includes the pydantic rendering — andstr(ValidationError)carries the offending value:That message is sent to the client as an
isErrorresult, so the rejected value is echoed back to the caller (and into the model's context).Why it matters
Servers that handle sensitive input (PII/PHI, credentials, member identifiers) must not let a validation failure repeat the value; the error should describe the rule, not the data. We hit this building a healthcare-platform MCP server whose tool inputs can be PHI.
Workaround we use today (reaches private API)
We replace the SDK-generated arguments model with a subclass that raises
MCPError(-32602, "<field>: <rule>")instead of the defaultToolError:This depends on
_tool_managerandfn_metadata.arg_model(both private) and on validation continuing to flow throughmodel_validate, so it is fragile across minor releases.Asks (any one would remove the private-API dependency)
mask_error_details-style flag), orarg_modelas an extensible point), orinput_valuefrom the argument-validation error text by default.Version:
mcp2.2.0 (Python). Happy to open a PR if you can point at the preferred shape.