Fix pre_parse_json corrupting str | None values that look like JSON

VI
19d ago
+79-1

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_parsed covering dict-like,
    list-like, plain-string, and None values for a str | None param.
  • 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 --check on changed files