modelcontextprotocol / modelcontextprotocol/python-sdk

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

未关闭 适合新手
#3,055 2 条评论 0 个 reaction 已指派 0 人 在 GitHub 查看

还没有人认领这个 Issue。

bug P2 v1 v2
主要语言
Python
星标
24.3k
派生
4k
平均合并
1 天 1 小时
30 天内合并 PR
31

描述

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.

贡献指南

打开贡献指南

从这里开始

  1. 先读完整个 Issue,再读项目的贡献指南。
  2. 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
  3. Fork 仓库,在一个分支上完成修改。
  4. 提交 Pull Request,并在描述里引用这个 Issue 编号。

调研方向

从 mcp/server/fastmcp/utilities/func_metadata.py 中的 FuncMetadata.pre_parse_json 开始,使用报告中的 str | None 示例重现该问题。检查对 union 的注解处理,然后验证看起来像 JSON 的字符串仍保持为字符串,而非字符串注解仍会被预解析,并且参数模型能够成功完成验证。

由索引模型根据 Issue 内容生成。

评估

技术栈
python
领域
backend-api-design
Issue 类型
缺陷
难度
2/5
预计耗时
1-3 小时
活跃度
冷清
描述清晰度
描述清楚
新手友好度
72/100

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。