快速结论:当 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、显卡及依赖版本,无需据此推断。
解决步骤
- 在复现环境(真实 uvicorn + 真实 socket + SDK 自带客户端)建立 N 个会话,逐个 DELETE,再统计
StreamableHTTPSessionManager._server_instances的长度,横跨多个事件循环检查点和一段静置窗口后再读取,确认是否为稳定泄漏。 - 对照关闭
delete、保留session_idle_timeout运行一次,确认“礼貌 DELETE 泄漏、直接消失被回收”的不对称现象是否复现,以确认问题路径。 - 若自行修改源码,可优先尝试让 DELETE 与其他清理路径收敛:在 DELETE 处理中、调用
terminate()之前直接从会话注册表中注销,使terminate()只负责关闭流;或记录终止原因,仅在非客户端 DELETE 时跳过注销。此为社区讨论中的建议方向,不是已合入的修复。 - 检查是否存在与断开/中止处理共用的清理逻辑,避免 DELETE 路径重复进入清理导致二次释放;同时保证无效 session id 的 DELETE 是干净的 no-op 或明确的 4xx。
- 关注 PR #3302 的评审与合入状态;该 PR 声称在干净 DELETE 后同步从两个注册表移除已终止会话并保持原有 404 行为,同时附带回归测试,但在 Issue 关闭时仍待维护者确认。
验证方法
用真实握手建立 N 个会话后逐个 DELETE,等待事件循环检查点与静置窗口耗尽,断言会话注册表长度回到基线(理想情况下为 0 或与请求前一致),而不是仅观察泄漏现象。可同时观察服务器是否报告仍打开的 socket。若改动了 DELETE 路径,需一并验证无效/未知 session id 的 DELETE 不会触发重复清理,且原有 404 行为保持不变。
参考来源
modelcontextprotocol/python-sdk #3300
AI 工具推荐
想把多个 AI 模型放在一个入口?
GamsGo AI 集成 ChatGPT、DeepSeek、Gemini、Claude、Midjourney、Veo 等常用模型,适合写作、绘图、视频和日常 AI 工作流。
推广链接:通过此链接购买,我可能获得佣金,不影响你的价格。
这个方案解决了吗?
可以继续搜索完整报错,或查看同一工具的其他排查指南。

![[bug]: Windows model import crashes in torch_cpu.dll after repeated Safetensors mmap loads; pread avoids the crash](https://www.chat-gpts.plus/wp-content/uploads/2026/11/9957-ca8cc217-768x403.jpg)
![[Bug]: Go: auto/semi_auto meta_data_filter reports "no match" for an empty filter, and callers handle it in opposite ways](https://www.chat-gpts.plus/wp-content/uploads/2026/11/20533-5bd3ae8d-768x403.jpg)