Race condition in StreamableHTTP: zero-buffer memory streams cause deadlock with concurrent SSE responses

此问题出现在使用 MCP Python SDK 的 StreamableHTTPServerTransport 时,常见场景包括: 使用 FastMCP 构建的 MCP 服务器(stateless_http=True)返回包含数组(3+ 元素)的工具响应 Claude Code 等客户端通过互联网连

Race condition in StreamableHTTP: zero-buffer memory streams cause deadlock with concurrent SSE responses

Race condition in StreamableHTTP: zero-buffer memory streams cause deadlock with concurrent SSE responses

快速结论:当使用 StreamableHTTP 传输(stateless 或 stateful 模式)且响应中包含 3 个及以上条目(或大量工具列表)时,SSE 连接可能永久挂起。优先排查 mcp/server/streamable_http.pycreate_memory_object_stream(0) 缓冲区大小,增大缓冲区可解决。

问题场景

此问题出现在使用 MCP Python SDK 的 StreamableHTTPServerTransport 时,常见场景包括:

  • 使用 FastMCP 构建的 MCP 服务器(stateless_http=True)返回包含数组(3+ 元素)的工具响应
  • Claude Code 等客户端通过互联网连接 MCP 服务器(网络延迟可能加剧问题)
  • stateful 模式下(使用 Mcp-Session-Id)发送大量工具列表请求(如返回 73 个工具的 mcp-atlassian 服务器)
  • 客户端中途 TCP 中止 SSE 流后,后续请求永久挂起,仅心跳(: ping)正常

报错原文

SSE connections hang indefinitely when using StreamableHTTPServerTransport in stateless mode with responses containing 3+ items.

Expected: All tool responses should complete regardless of response size.
Actual: 1-2 items: Response returns immediately (~150ms). 3+ items: Request hangs indefinitely (deadlock).

HTTP 200, Content-Type: text/event-stream, Transfer-Encoding: chunked, 0 bytes of body, just `: ping` heartbeats. 
/healthz keeps returning 200, so docker / k8s liveness probes don't recover the container.

原因分析

根本原因在 mcp/server/streamable_http.py 中使用了零缓冲区的内存流:

第 412 行:self._request_streams[request_id] = anyio.create_memory_object_stream[EventMessage](0)
第 460 行:sse_stream_writer, sse_stream_reader = anyio.create_memory_object_stream[dict[str, str]](0)

零缓冲区意味着 send() 会阻塞直到 receive() 被调用。竞态条件流程如下:

  1. 通过 tg.start_soon(response, ...) 启动 SSE 响应任务(非阻塞)
  2. 主循环调用 await writer.send(session_message) 将请求发送给 MCP 服务器
  3. MCP 服务器处理速度较快,调用 message_router 并尝试 await request_streams[id][0].send(EventMessage(...))
  4. 如果此时 SSE writer 任务尚未开始迭代(即尚未调用 receive()),send() 会永久阻塞

小响应(1-2 项)正常工作是因为 SSE writer 任务在 MCP 响应到达前已启动;而大响应(3+ 项)中 MCP 处理更快,响应在 SSE 迭代器准备就绪前到达,导致死锁。

另一种可能原因(推测):src/mcp/shared/session.pyrequest_id 使用自增方式,在高并发或客户端中断后可能出现重复 ID,加剧竞态条件。

环境排查

  • MCP Python SDK 版本:1.23.3 及以上(包括 1.26.0)
  • Python 版本:3.11
  • FastMCP 版本(如使用):2.13.1(但问题出在官方 SDK 的 streamable_http.py,非 FastMCP 代码)
  • 传输模式:StreamableHTTP(stateless_http=True 或 stateful 模式均可能触发)
  • 网络环境:互联网连接(可能加剧)或局域网连接(未解决根本问题)
  • 客户端:Claude Code、Cursor、自定义 httpx 脚本等

解决步骤

  1. 增大缓冲区大小(已验证方案):将 mcp/server/streamable_http.py 中所有 create_memory_object_stream(0) 改为 create_memory_object_stream(10)。具体涉及两处:
    • 第 412 行:self._request_streams[request_id] = anyio.create_memory_object_stream[EventMessage](10)
    • 第 460 行:sse_stream_writer, sse_stream_reader = anyio.create_memory_object_stream[dict[str, str]](10)
  2. Docker/生产环境快捷修复(sed 方案):可在 Dockerfile 中添加:
    
    RUN sed -i 's/create_memory_object_stream\[EventMessage\](0)/create_memory_object_stream[EventMessage](10)/g' \
        /usr/local/lib/python3.11/site-packages/mcp/server/streamable_http.py && \
        sed -i 's/create_memory_object_stream\[dict\[str, str\]\](0)/create_memory_object_stream[dict[str, str]](10)/g' \
        /usr/local/lib/python3.11/site-packages/mcp/server/streamable_http.py
  3. 替代方案(可优先尝试):改用 await tg.start() 替换 tg.start_soon() 来启动 SSE 响应任务,确保 SSE writer 在发送请求前完全就绪(需要 EventSourceResponse 支持任务状态协议)。
  4. 注意 stateless 和 stateful 路径的区别:PR #2145 修复了 stateless 模式下的任务累积问题,但未触及 stateful 模式下的缓冲区大小。如果使用 stateful 模式(Mcp-Session-Id),仅应用 #2145 的修补可能不够,仍需修改缓冲区大小。

验证方法

修复后,使用包含 3 个或以上元素的数组响应的工具进行测试:

  • 对于 stateless 模式:调用返回数组的工具,确认所有响应完整返回(不应挂起)
  • 对于 stateful 模式:模拟客户端中断后发送新请求,确认新请求正常返回(HTTP 200 含完整 SSE body)
  • 在生产环境:监控文件描述符(FD)数量,确认无持续增长;检查 liveness probe(/healthz)是否正常

验证标准:所有工具响应应在几百毫秒内完成,无永久挂起,无仅发送心跳的 HTTP 200 响应。

参考来源

modelcontextprotocol/python-sdk #1764

GamsGo AI

AI 工具推荐

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

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

了解 GamsGo AI

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

celebrityanime
celebrityanime
文章: 9616

发表回复

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