streamable_http: early response.aclose() poisons keepalive connection, causes ~260ms latency on every subsequent tool call

用户在使用 MCP Python SDK (mcp==1.27.1) 的 streamable_http 传输模式时,通过 ClientSession.call_tool() 或 send_ping 、 list_tools 等调用,在 Windows 11 环境下发现问题。每次串行请求在单一连接上

streamable_http: early response.aclose() poisons keepalive connection, causes ~260ms latency on every subsequent tool call

streamable_http: early response.aclose() poisons keepalive connection, causes ~260ms latency on every subsequent tool call

快速结论:此问题发生在使用 MCP Python SDK 的 streamable HTTP 传输模式,客户端在收到首个 SSE 事件后过早调用 response.aclose() 关闭响应流,导致 HTTP/1.1 keepalive 连接被“污染”,后续每次请求额外增加约 260ms 延迟。优先排查是否调用了 aclose() 提前关闭连接。

问题场景

用户在使用 MCP Python SDK (mcp==1.27.1) 的 streamable_http 传输模式时,通过 ClientSession.call_tool()send_pinglist_tools 等调用,在 Windows 11 环境下发现问题。每次串行请求在单一连接上固定增加约 260ms 延迟,而使用裸 httpx.AsyncClient 只消耗约 5ms。

需要注意:该问题在部分环境(如 Linux 下的 uvicorn+sse-starlette)无法复现,可能取决于底层 HTTP 服务器实现。

报错原文

# Symptom: sequential call_tool() requests average ~265ms with early aclose()
# After removing early aclose(), avg drops to ~7ms
# Measured latency from: session.call_tool(TOOL_NAME, TOOL_ARGS)

原因分析

根本原因在于 src/mcp/client/streamable_http.py 中的 _handle_sse_response 方法,它在接收到第一个 JSON-RPC 响应事件后立即调用 await response.aclose(),强制关闭 SSE 流而未能等到 EOF。这导致该 HTTP/1.1 keepalive 连接在放回连接池时处于“未完全排空”状态。后续复用该连接的 POST 请求会在服务器发送状态码之前阻塞约 260ms(可能是服务器端 SSE 空闲/重连窗口所致——sse_starlette.EventSourceResponse 在发送唯一事件后仍会保持写入器任务存活)。

代码中一共存在三个类似的提前关闭站点:

  • _handle_sse_response(第 364 行附近)
  • _handle_resumption_request(第 251 行附近)
  • _handle_reconnection(第 421 行附近)

环境排查

  • 确认 MCP Python SDK 版本(mcp==1.27.1,但问题可能在多个版本存在)
  • 确认 Python 版本(用户使用 3.12.8)
  • 确认操作系统(Windows 11 上可复现,Linux 上部分环境不可复现)
  • 确认服务器端实现(用户使用 mcp.server.streamable_http,其他服务器实现也可能受影响)
  • 确认使用 httpx.AsyncClient 且启用 keepalive

解决步骤

  1. (可优先尝试)移除 aclose(),改为排空流至 EOF。_handle_sse_response 中,当 _handle_sse_event() 返回 complete 时,设置本地标记 saw_terminal_event = True,继续迭代直到服务器自然关闭流,而不是立即 aclose()
  2. 仅在取消、显式关闭会话或超时/错误路径时使用 aclose()保持正常完成路径排空至 EOF,以便连接能正常返回 keepalive 池。
  3. _handle_resumption_request_handle_reconnection 采用相同策略。PR #2712 覆盖了所有三个提前关闭站点。
  4. 修复后添加回归测试:通过同一 httpx.AsyncClient 执行多次串行 call_tool(),验证预热后每次调用保持在个位数毫秒级别,无固定每次调用延迟。
  5. 添加取消测试:确保修复后不会导致挂起的 SSE 流无法关闭,即取消/超时路径仍正常关闭连接。

验证方法

运行用户提供的复现脚本(参见 Issue 正文),比较修复前后的平均延迟:

  • 修复前:avg ≈ 265 ms
  • 修复后:avg ≈ 7 ms(37 倍加速)

注意:该验证方法在部分环境(Linux + uvicorn + sse-starlette)可能无法观察到 260ms 差异(已测试仅 ~7ms),但仍应确保代码修改不会引入新的取消/超时相关问题。

参考来源

modelcontextprotocol/python-sdk #2707

GamsGo AI

AI 工具推荐

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

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

了解 GamsGo AI

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

celebrityanime
celebrityanime
文章: 7722

发表回复

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