快速结论:在 MCP Python SDK 2.x 的 MCPServer 中,如果工具函数返回值注解用 typing.Dict[str, Any] 而不是 dict[str, Any],生成的 outputSchema 和 structuredContent 会被额外包一层 {"result": ...}。优先排查 func_metadata.py 里的 _create_output_model 对泛型注解的判定分支。
适用环境:Issue 已确认的环境为 Python 3.12、mcp 2.3.0,并注明 main 分支的 func_metadata.py 仍使用 isinstance(type_expr, GenericAlias)。操作系统、CUDA、显卡型号在 Issue 中未提及。
最快修复方案:暂无确认的一步修复方案。Issue 中提出的可优先尝试改法是把 elif isinstance(type_expr, GenericAlias): 改为 elif get_origin(type_expr) is not None:,但评论仅表示已准备该修复,Issue 文本中没有合并或验证记录。
注意事项:该改法会影响所有经 get_origin 可识别的泛型注解;提出者认为 Union、Optional、Literal 等 origin 不是 dict,仍会走原有包裹逻辑,但这一行为尚未在 Issue 中给出测试结论。修改线上工具的输出结构后,已缓存旧 outputSchema 的客户端可能仍需刷新缓存。
问题场景
用户在使用 MCP Python SDK 2.x 的 MCPServer 注册工具时,如果返回值注解写成 typing.Dict[str, Any],工具调用结果会被包成 {"result": {"ok": true}};而写成 dict[str, Any] 时结果直接是 {"ok": true}。Issue 描述的实际触发渠道是 Ruff UP006 或 pyupgrade 这类工具把 Dict 自动改写成 dict,导致某个工具的输出形状静默变化,缓存了旧输出 schema 的客户端随后拒绝了它的结果。
报错原文
typing.Dict[str, X] return annotation is wrapped in {"result": ...}, unlike dict[str, X]
typing_dict {'properties': {'result': {'additionalProperties': True, 'title': 'Result', 'type': 'object'}}, 'required': ['result'], 'title': 'typing_dictOutput', 'type': 'object'} {'result': {'ok': True}}
builtin_dict {'additionalProperties': True, 'title': 'builtin_dictDictOutput', 'type': 'object'} {'ok': True}
原因分析
Issue 已定位到 src/mcp/server/mcpserver/utilities/func_metadata.py 里的 _create_output_model。该函数用 elif isinstance(type_expr, GenericAlias): 判断泛型,其中 GenericAlias 来自 types;而 typing.Dict[str, Any] 实际是 typing._GenericAlias,不是 types.GenericAlias,因此跳过了 origin is dict 的分支,落入最后的 else 并被包裹。内置 dict[str, Any] 能命中该分支,所以两种写法输出不同。评论也确认根因正是 typing.Dict[str, Any] 与 types.GenericAlias 的类型判断不匹配。
环境排查
- 确认 mcp Python SDK 版本:Issue 复现于 2.3.0,且指出 main 分支
func_metadata.py仍有同样的GenericAlias判断。 - 确认 Python 版本:Issue 使用 Python 3.12。
- 检查工具函数返回值注解是否使用
typing.Dict、typing.List、typing.Tuple等 typing 泛型写法,而运行时仍按内置泛型形式生成 schema。 - 确认是否使用 Ruff UP006 或 pyupgrade 自动重写注解,这是 Issue 中描述的输出形状静默变化的来源。
- CUDA、PyTorch、显卡、具体节点版本在 Issue 中未提供,无需作为本次排查前提。
解决步骤
- 先复现差异:用一个返回
typing.Dict[str, Any]的工具和另一个返回dict[str, Any]的工具,分别查看output_schema与structured_content,确认只有前者被{"result": ...}包裹。 - 检查当前安装的 SDK 中
func_metadata.py的_create_output_model,定位elif isinstance(type_expr, GenericAlias):这一行。 - 可优先尝试 Issue 中提出的修复:把该分支条件改为
elif get_origin(type_expr) is not None:,让typing.Dict也能依据 origin 命中dict分支。 - 修改后运行 Issue 示例代码,对比两种注解输出是否一致;如用于生产,建议先覆盖
typing.Dict、typing.List、typing.Tuple与对应内置写法的组合测试。 - 如果无法修改 SDK,可先统一工具返回值注解写法,避免混用
typing.Dict与dict,并暂缓自动重写注解,避免输出 schema 变化。
验证方法
运行 Issue 中的示例代码,检查 typing_dict 与 builtin_dict 的 output_schema 和 structured_content 是否一致:两者都应输出对象本身,而不是 {"result": ...} 包裹结构。同时确认已缓存旧 schema 的客户端重新拉取工具定义后能正常接受结果。
参考来源
modelcontextprotocol/python-sdk #3745
AI 工具推荐
想把多个 AI 模型放在一个入口?
GamsGo AI 集成 ChatGPT、DeepSeek、Gemini、Claude、Midjourney、Veo 等常用模型,适合写作、绘图、视频和日常 AI 工作流。
推广链接:通过此链接购买,我可能获得佣金,不影响你的价格。
这个方案解决了吗?
可以继续搜索完整报错,或查看同一工具的其他排查指南。
![[BUG] LanceDBStorage.list_records() returns oldest records instead of newest first due to premature query truncation](https://www.chat-gpts.plus/wp-content/uploads/2026/10/7394-ea476651-768x403.jpg)
![[Bug]: decisions (typesafe): missing usage becomes zero cost, a 200 with an invalid body becomes a 500, and the error body is only in the me](https://www.chat-gpts.plus/wp-content/uploads/2026/10/48147-5054fca8-768x403.jpg)
