modelcontextprotocol / modelcontextprotocol/python-sdk

FuncMetadata.pre_parse_json mis-detects str | None as non-string and corrupts JSON-looking string arguments

Aperta Adatta ai principianti
#3,055 2 commenti 0 reazioni 0 assegnatari Vedi su GitHub

Nessuno ha ancora preso questa issue.

bug P2 v1 v2
Lingua principale
Python
Stelle
24.3k
Fork
4k
Merge medio
1g 1h
PR unite (30g)
31

Descrizione

Title: FuncMetadata.pre_parse_json mis-detects str | None as non-string and corrupts JSON-looking string arguments

Environment

  • mcp version: 1.27.0
  • Python: 3.10.12
  • Transport: streamable-http (also affects stdio — the bug is transport-agnostic)

Summary

FuncMetadata.pre_parse_json() in mcp/server/fastmcp/utilities/func_metadata.py decides whether to json.loads() a string argument based on:

if isinstance(data_value, str) and field_info.annotation is not str:

This check is meant to catch cases where a client (e.g. Claude Desktop) stringifies a list/dict argument that should really be a Python object. But field_info.annotation is not str is True for Optional[str] / str | None as well, since that annotation is not literally str. So any optional string parameter gets the same treatment as a list/dict/model parameter.

If the caller passes a valid string value for such a parameter that also happens to parse as a JSON object or array — e.g. a JSON-serialized template body like '{"blocks": [...]}' — the value silently gets replaced with a dict/list before the pydantic argument model is validated. Validation then fails with something like:

1 validation error for my_tool_nameArguments
body
  Input should be a valid string [type=string_type, input_value={'blocks': [...]}, input_type=dict]

...even though the caller sent a perfectly valid string and the tool signature explicitly declares body: str | None.

Minimal repro

from typing import Any
import json
from mcp.server.fastmcp.utilities.func_metadata import func_metadata

async def my_tool(body: str | None = None) -> dict[str, Any]:
    return {"body": body}

meta = func_metadata(my_tool)
data = {"body": json.dumps({"blocks": ["a", "b"]})}
new_data = meta.pre_parse_json(data)
print(type(new_data["body"]))          # <class 'dict'>  -- should be <class 'str'>
meta.arg_model.model_validate(new_data)  # raises: Input should be a valid string

Expected behavior

A parameter typed str | None (or any Union that includes str) should not have its string value re-interpreted as JSON, since the raw string is already a valid value for that field. Pre-parsing should only kick in when a plain str could never satisfy the annotation (e.g. list[str], dict[str, Any], a Pydantic model, int, etc.).

Suggested fix

Replace the identity check with one that walks Union/X | Y members:

def _annotation_accepts_str(annotation: Any) -> bool:
    origin = typing.get_origin(annotation)
    if origin is typing.Union or origin is types.UnionType:
        return any(_annotation_accepts_str(arg) for arg in typing.get_args(annotation))
    return annotation is str

# in pre_parse_json:
if isinstance(data_value, str) and not _annotation_accepts_str(field_info.annotation):
    ...

Impact

Any FastMCP tool with an Optional[str] (or str | None) parameter breaks whenever a caller passes a string value that happens to be valid JSON for an object/array (JSON-in-a-string payloads: template bodies, block-based editor content, serialized configs, etc.). We hit this in production with a Unisender email-template MCP server where body: str | None holds a JSON block structure — every update_email_template / create_email_template call with a block-based template failed validation until we monkey-patched FuncMetadata.pre_parse_json locally with the fix above.

Guida per i contributori

Apri la guida per i contributori

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Direzione di ricerca

Iniziate in mcp/server/fastmcp/utilities/func_metadata.py, in corrispondenza di FuncMetadata.pre_parse_json, e riproducete il problema con l'esempio str | None riportato nel report. Verificate la gestione delle annotazioni per le unioni, quindi assicuratevi che le stringhe che sembrano JSON rimangano stringhe, mentre le annotazioni che non sono stringhe continuino a essere pre-analizzate e il modello degli argomenti venga validato correttamente.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Valutazione

Stack tecnologico
python
Ambito
backend-api-design
Tipo di issue
Bug
Difficoltà
2/5
Tempo stimato
1-3 ore
Stato di attività
Tranquilla
Chiarezza
Specificata chiaramente
Idoneità per principianti
72/100

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.