MCP Tool execution hangs indefinitely in stdio mode when calling external Python scripts

在 Windows 系统上使用 FastMCP 的 stdio 传输模式时,当 MCP 工具调用外部 Python 脚本,会因子进程继承了服务端的 stdin(协议管道)而挂起无限等待。优先排查子进程创建时是否设置了 stdin=asyncio.subprocess.DEVNULL 。

快速结论:在 Windows 系统上使用 FastMCP 的 stdio 传输模式时,当 MCP 工具调用外部 Python 脚本,会因子进程继承了服务端的 stdin(协议管道)而挂起无限等待。优先排查子进程创建时是否设置了 stdin=asyncio.subprocess.DEVNULL

适用环境:Windows 11 24H2 26100.3775,Python 3.10.0,mcp 1.7.1,asyncio 3.4.3(评论区确认是 Windows 专用问题,且 mcp v1.x 中存在此 Bug,v2 已修复)。

最快修复方案:在调用 asyncio.create_subprocess_execasyncio.create_subprocess_shell 时,增加参数 stdin=asyncio.subprocess.DEVNULL,使子进程的 stdin 指向空设备,避免它读取父进程的协议管道。示例代码片段:process = await asyncio.create_subprocess_exec(*cmd, stdin=asyncio.subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.PIPE)

注意事项:该方案为已验证的工作区(workaround),并非官方根本修复。官方已在 #3117 中修复,确认 mcp v2 起不再需要此修改。若你正在使用 v1.x,请仅将其视为临时绕过方法,并计划升级到 v2。

问题场景

用户使用 FastMCP 在 stdio 传输模式下运行 MCP 服务器,并定义了一个工具(tool)通过 asyncio.create_subprocess_execsubprocess.run 调用外部 Python 脚本(例如 minimal_script.py)。当通过 MCP Inspector 调用该工具时,请求挂起直至超时,且不返回任何输出或错误。相同的代码在 SSE 传输模式下工作正常。

报错原文

MCP Tool execution hangs indefinitely in stdio mode when calling external Python scripts

实际运行时用户观察不到任何错误输出,工具调用一直卡住直到达到客户端超时。

原因分析

可能原因:在 Windows 系统上,通过 asyncio.create_subprocess_execsubprocess.run 启动的子进程会继承父进程的 stdin 文件句柄。由于 FastMCP 的 stdio 传输模式下,父进程的 stdin 实际上是 MCP 协议的通信管道。子进程(外部 Python 脚本)在启动时尝试读取 stdin 来初始化(CPython 存在一个已知问题 gh-78961,会导致子进程在 Windows 上读取 stdin 时挂起),造成了死锁:父进程等待子进程输出,子进程等待从 stdin 中读到数据,而 stdin 却被 MCP 协议占用。

官方在 #3117 (PR) 中确认了根本原因:stdio_server() 在建立连接后并未将协议管道移到私有描述符上,导致子进程继承了服务端 stdin 句柄。v2 修复中通过将 fd 0 指向 EOF 解决。

环境排查

  • 操作系统是否 Windows(Issue 确认仅 Windows 重现)
  • Python 版本(Issue 测试于 Python 3.10.0,但可能影响其他版本)
  • mcp Python SDK 版本(v1.7.1 及之前版本都可能受影响)
  • 确认子进程创建方式:asyncio.create_subprocess_execasyncio.create_subprocess_shellsubprocess.run 均会触发
  • 确认在 SSE 传输模式下能否正常运行(可作为对比)

解决步骤

  1. 临时解决方案(已验证):在创建子进程时显式设置 stdin=asyncio.subprocess.DEVNULL。修改工具函数中的调用代码,例如:
    process = await asyncio.create_subprocess_exec(
        *cmd,
        stdin=asyncio.subprocess.DEVNULL,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE
    )

    或使用 shell 方式时一样处理。完整示例见 Issue 中 @DanielAvdar 提供的代码。

  2. 替代方案:暂时切换为 SSE 传输模式运行服务器(mcp.run(transport="sse")),但需注意 SSE 模式可能需要额外配置。
  3. 最终修复:升级 mcp Python SDK 至 v2 或更高版本(已在 main 分支修复,合并 commit 629ca29,随 v2 发布)。v2 中 stdio_server() 已将协议管道移到私有描述符,子进程读取 stdin 会直接获得 EOF,不会挂起。

验证方法

完成修改后,重新以 stdio 模式启动 MCP 服务器,并通过 MCP Inspector 调用之前挂起的工具。正常情况下应当立即返回外部脚本的输出(如 “Successfully Call!”),不再超时。也可同时使用 SSE 模式对比确保结果一致。

参考来源

modelcontextprotocol/python-sdk #671

GamsGo AI

AI 工具推荐

想把多个 AI 模型放在一个入口?

GamsGo AI 集成 ChatGPT、DeepSeek、Gemini、Claude、Midjourney、Veo 等常用模型,适合写作、绘图、视频和日常 AI 工作流。

了解 GamsGo AI

推广链接:通过此链接购买,我可能获得佣金,不影响你的价格。

这个方案解决了吗?

celebrityanime
celebrityanime
文章: 15158

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注