快速结论:这个报错通常出现在 MCP Python SDK 的 streamable-http(stateful)transport 下,服务器刚为一条通知(notification)发出 202 Accepted 后,会话流被关闭,转发消息到 writer 时抛出 ClosedResourceError;随后 catch-all 错误处理又试图在已完成的 ASGI 响应上补发响应,触发 RuntimeError: Unexpected ASGI message 'http.response.start' sent, after response already completed。优先排查通知分支发出 202 之后的写操作是否被正确保护。
适用环境:Issue 已确认:mcp 2.1.1、starlette 1.6.0、uvicorn 0.52.4、anyio 4.14.2、Python 3.13.14、macOS 27.0.0 (Darwin, arm64);服务端为 FastMCP-based 的 basic-memory 0.23.2,使用 streamable-http transport、stateful(非 stateless_http)、单 uvicorn worker、作为 launchd 长期本地服务运行在 127.0.0.1。
最快修复方案:Issue 中被维护者确认为与 #3631 重复,修复集中在 #3631;本 Issue 本身的已验证处理方式是:在 _handle_post_request 的通知分支中,对 202 之后向 writer 的发送做保护——一旦最终响应体已发出,捕获并吞掉 ClosedResourceError(记录 debug 日志即可),不要再尝试发送第二次响应。该修复已带有回归测试(修复前失败、修复后通过)。
注意事项:该修复只针对“通知分支 202 之后写关闭流”这一竞态,不能解释 Issue 作者偶尔看到的“进程存活但不再响应、CPU 处于 runnable 状态”的 hang;维护者明确要求如果再次出现挂死,另行开新 Issue 并尽量提供 py-spy dump。此外,修复方案在评论中由贡献者提出并实现,官方合并状态以 #3631 为准。
问题场景
用户运行基于 FastMCP 的 basic-memory 服务,接入 MCP Python SDK 的 streamable-http transport(stateful 模式,单 uvicorn worker),作为长期本地服务(launchd,监听 127.0.0.1)。在多个本地 MCP 客户端并发对同一长连接会话发起工具调用的正常使用下,每 1–2 小时会出现一对异常:先在 _handle_post_request 的通知/响应分支(约 streamable_http.py:588,await writer.send(session_message))抛出 ClosedResourceError,紧接着在同一个请求上因向已完成的 ASGI 响应再次发送 http.response.start 而抛出 RuntimeError。多数情况是瞬时错误,服务毫秒级内继续正常响应;但有一次与“任务组卡死、进程存活但不再响应”同时出现。
报错原文
Error handling POST request
Traceback (most recent call last):
File ".../mcp/server/streamable_http.py", line 588, in _handle_post_request
await writer.send(session_message)
...
raise ClosedResourceError
anyio.ClosedResourceError
ERROR: Exception in ASGI application
Traceback (most recent call last):
File ".../mcp/server/streamable_http.py", line 588, in _handle_post_request
await writer.send(session_message)
...
raise RuntimeError(f"Unexpected ASGI message '{message['type']}' sent, after response already completed.")
RuntimeError: Unexpected ASGI message 'http.response.start' sent, after response already completed.
原因分析
根据维护者与复现者的结论,这两个异常来自同一个请求,而不是两个 handler 在竞争。对 notification 消息,transport 会先发出 202 Accepted,然后把消息转发给 session 的 message router。如果这个窗口期内 session 流已经关闭,writer.send(session_message) 就会抛出 ClosedResourceError;这个异常冒泡到外层的 catch-all handler 后,handler 又尝试在“已经完成的 ASGI 响应”上发送 500,于是触发第二个 Unexpected ASGI message 'http.response.start'。更糟的是,错误处理路径中的 writer.send(Exception(err)) 会再次在已关闭的流上抛错,使这对异常以未处理 ASGI 异常的形式逃出 handle_request。
与已知 Issue 的区别:#1363/#1384 是消息路由早期校验失败导致的 ClosedResourceError(已修复,2.1.1 中已有 guard);#2064/#2072 是客户端断开后错误恢复路径中 send_log_message() 的问题;#2328/#2334 是取消后的 respond() 断言竞态(症状是 AssertionError,不是双 ASGI 响应)。本 Issue 发生在 notification/response 分支的 success 路径上。
环境排查
- 确认
mcp版本(Issue 报告为 2.1.1)以及是否已包含 #3631 的修复。 - 确认
starlette(1.6.0)、uvicorn(0.52.4)、anyio(4.14.2)版本。 - 确认 Python 版本(3.13.14)与操作系统(macOS 27.0.0, arm64)。
- 确认 transport 配置:
streamable-http、stateful(非stateless_http)、单 uvicorn worker。 - 确认服务端框架:FastMCP-based 的
basic-memory0.23.2,是否作为 launchd 长期服务运行。 - 检查日志中两个异常是否总是成对、按“
ClosedResourceError→Unexpected ASGI message”顺序出现。 - 如遇到进程卡死(存活但不响应、CPU runnable),准备
py-spy dump以便另行报告。
解决步骤
- 先确认这是否为 #3631 的重复问题——维护者已明确本 Issue 与 #3631 为同一 bug,修复集中在 #3631,本 Issue 已作为重复关闭。因此优先跟踪并升级到包含 #3631 修复的
mcp版本。 - 若需自行打补丁:在
_handle_post_request的通知分支中,对202发出之后向writer的转发(即await writer.send(session_message))进行保护:一旦最终响应体已发出,捕获ClosedResourceError(以及评论提到的BrokenResourceError)并吞掉,只写 debug 日志,不再进入发送第二次响应的错误分支。 - 避免在已完成的 ASGI scope 上再次调用
response(...);即错误处理路径不应在 202 已发出后尝试补发 500。 - 用回归测试验证:复现者提供的测试在修复前失败、修复后通过(
tests/server/test_streamable_http_router.py中 7/7 通过),并保持ruff、pyright干净。 - 如果升级/打补丁后仍出现进程卡死(过期 Issue 作者报告的场景),不要在本 Issue 下追,按维护者建议新开 Issue,并附上
py-spy dump等现场信息。
验证方法
在修复后的代码上复现原场景:发送 notification 并在 202 body 输出后立即关闭 read stream,确认 writer.send(session_message) 抛出的 ClosedResourceError 被吞掉,且不再产生第二个 Unexpected ASGI message 'http.response.start' sent, after response already completed.。运行 tests/server/test_streamable_http_router.py,回归测试应全部通过;同时观察长时间多客户端并发使用下,日志中不再成对出现这两个异常。
参考来源
modelcontextprotocol/python-sdk #3641
AI 工具推荐
想把多个 AI 模型放在一个入口?
GamsGo AI 集成 ChatGPT、DeepSeek、Gemini、Claude、Midjourney、Veo 等常用模型,适合写作、绘图、视频和日常 AI 工作流。
推广链接:通过此链接购买,我可能获得佣金,不影响你的价格。
这个方案解决了吗?
可以继续搜索完整报错,或查看同一工具的其他排查指南。


