Fix pre_parse_json corrupting str | None values that look like JSON
Summary
FuncMetadata.pre_parse_json() decided whether to json.loads() a string
argument based on field_info.annotation is not str. That check is True
for str | None as well (it's Optional[str], not literally str), so any
optional string parameter got the same "maybe this is JSON" treatment as a
list/dict parameter.
If a caller passes a valid string that also happens to parse as a JSON
object/array — e.g. a JSON-serialized payload passed as a plain string field
(block-based template body, serialized config, etc.) — the value silently
got replaced with a dict/list before the pydantic argument model was
validated, and validation then failed with Input should be a valid string
even though the caller's string was perfectly valid for that field.
Fixes #3055.
Fix
Added _is_optional_str(), which recognizes str | None (any union whose
only non-None member is str) and skips JSON pre-parsing for it, since no
member of such a union other than str could ever be produced by decoding
JSON — so pre-parsing that annotation can only corrupt, never help.
str | list[str] (and other unions with a real non-str member) keep the
existing behavior, since a JSON array/object there is meaningfully different
from the raw string and should still be pre-parsed — covered by the existing
test_str_vs_list_str test, which still passes.
Test plan
- Added
test_optional_str_is_never_json_pre_parsedcovering dict-like,
list-like, plain-string, andNonevalues for astr | Noneparam. -
uv run pytest tests/server/mcpserver/test_func_metadata.py— 43 passed -
uv run pytest tests/server/mcpserver(full suite) — 526 passed -
uv run ruff check/ruff format --checkon changed files