Cleanly terminated streamable-HTTP sessions are never deregistered: the DELETE path skips its own cleanup

当 MCP Python SDK 走到握手时代(legacy)的 streamable-HTTP 路径,客户端用 DELETE 干净地结束会话时,会话不会从 StreamableHTTPSessionManager._server_instances 注销,而是被 run_server 的 fina

快速结论:当 MCP Python SDK 走到握手时代(legacy)的 streamable-HTTP 路径,客户端用 DELETE 干净地结束会话时,会话不会从 StreamableHTTPSessionManager._server_instances 注销,而是被 run_server 的 finally 清理守卫跳过,导致注册表只增不减。优先排查 DELETE 处理链与 is_terminated 判断的先后关系。

适用环境:MCP Python SDK(Issue 标注 v1/v2、handshake-era 路径,协议版本 2024-11-05 至 2025-11-25);使用真实 uvicorn 监听真实 socket、SDK 自带客户端;客户端 mode="legacy" 或 2.x 默认 mode="auto" 协商到旧协议时;terminate_on_close=True(legacy 下的默认值)。stateless_http=True 不受影响。Issue 未提供操作系统、Python、CUDA、显卡信息。

最快修复方案:暂无确认的一步修复方案(截至关闭时尚未合入已验证修复;社区提出的 PR #3302 需要维护者评审)。

注意事项:配置 session_idle_timeout 不能绕过此问题——Issue 的复现中,显式 DELETE 的会话在超时后仍留在注册表,反而是客户端直接消失的会话会被回收。该问题只出现在已被标记 removed_in="2026-07-28" 的握手时代路径上,属于“废弃但仍支持”路径上的缺陷;modern 路径(如 2026-07-28)不涉及 _server_instances,不会泄漏。

问题场景

在 MCP Python SDK 的 streamable-HTTP 服务端(握手时代协议路径)上,客户端先通过真实的 initialize 握手建立会话(返回 200),随后主动发送 DELETE 结束会话。服务端返回正常,但 StreamableHTTPSessionManager._server_instances 中的条目不会被移除,长时间运行的服务器会持续累积已终止会话及其 transport、reader 和缓冲状态。复现中对比了三种场景:legacy + DELETE(有/无 idle timeout)均泄漏,legacy + 客户端直接断开可被超时回收,mode="auto" 协商到 modern 协议则不泄漏。

报错原文

Cleanly terminated streamable-HTTP sessions are never deregistered: the DELETE path skips its own cleanup

A leaked : sessions=3 mode=legacy delete=True  idle_timeout=None | registry immediate=3 after_checkpoints=3 after_2.0s=3 is_terminated=[True, True, True]
B leaked : sessions=3 mode=legacy delete=True  idle_timeout=0.3  | registry immediate=3 after_checkpoints=3 after_3.0s=3 is_terminated=[True, True, True]
C reaped : sessions=3 mode=legacy delete=False idle_timeout=0.3  | registry immediate=3 after_checkpoints=3 after_3.0s=0 is_terminated=[]
D modern : sessions=3 mode=auto   delete=True  idle_timeout=None | registry immediate=0 after_checkpoints=0 after_1.0s=0 is_terminated=[]

原因分析

核心原因是 StreamableHTTPServerTransport.terminate() 在关闭流之前就把 self._terminated 置为 True。等 run_server 的 finally 块执行时,条件中的 and not http_transport.is_terminated 已经为假,于是注销会话的 del 被跳过——恰好跳过了那些正常关闭的会话。该布尔标记实际含义是“拆除已开始”,却被当作“会话已结束”来用,导致干净终止路径反而绕过了自身的清理。除管理器的 _server_instances.clear() 外,没有其他移除途径。

可能的次级影响:后续请求若复用同一 session id,可能拿到一个已终止的旧 transport,表现为看似无关的 404 或流已关闭错误。另外,该问题与仅在建立请求返回 ≥400 时丢弃会话的 #3229 修复正交,因为成功握手返回 200,不会进入那条分支。

环境排查

  • 确认客户端使用的协议模式:mode="legacy" 或 mode="auto" 协商结果是否落在 2024-11-05 至 2025-11-25 区间。
  • 确认是否使用 stateless_http=True,该模式下不受影响。
  • 确认是否配置了 session_idle_timeout,并注意它不是该问题的规避手段。
  • 确认客户端是否以 DELETE 主动结束会话(terminate_on_close=True 时 legacy 默认会发出 DELETE),还是直接断开连接。
  • 确认服务端是否经过 StreamableHTTPSessionManager,因为泄漏体现在其 _server_instances 长度上。
  • Issue 未给出 Python、CUDA、PyTorch、显卡及依赖版本,无需据此推断。

解决步骤

  1. 在复现环境(真实 uvicorn + 真实 socket + SDK 自带客户端)建立 N 个会话,逐个 DELETE,再统计 StreamableHTTPSessionManager._server_instances 的长度,横跨多个事件循环检查点和一段静置窗口后再读取,确认是否为稳定泄漏。
  2. 对照关闭 delete、保留 session_idle_timeout 运行一次,确认“礼貌 DELETE 泄漏、直接消失被回收”的不对称现象是否复现,以确认问题路径。
  3. 若自行修改源码,可优先尝试让 DELETE 与其他清理路径收敛:在 DELETE 处理中、调用 terminate() 之前直接从会话注册表中注销,使 terminate() 只负责关闭流;或记录终止原因,仅在非客户端 DELETE 时跳过注销。此为社区讨论中的建议方向,不是已合入的修复。
  4. 检查是否存在与断开/中止处理共用的清理逻辑,避免 DELETE 路径重复进入清理导致二次释放;同时保证无效 session id 的 DELETE 是干净的 no-op 或明确的 4xx。
  5. 关注 PR #3302 的评审与合入状态;该 PR 声称在干净 DELETE 后同步从两个注册表移除已终止会话并保持原有 404 行为,同时附带回归测试,但在 Issue 关闭时仍待维护者确认。

验证方法

用真实握手建立 N 个会话后逐个 DELETE,等待事件循环检查点与静置窗口耗尽,断言会话注册表长度回到基线(理想情况下为 0 或与请求前一致),而不是仅观察泄漏现象。可同时观察服务器是否报告仍打开的 socket。若改动了 DELETE 路径,需一并验证无效/未知 session id 的 DELETE 不会触发重复清理,且原有 404 行为保持不变。

参考来源

modelcontextprotocol/python-sdk #3300

GamsGo AI

AI 工具推荐

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

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

了解 GamsGo AI

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

这个方案解决了吗?

celebrityanime
celebrityanime
文章: 32314

发表回复

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