快速结论:该报错通常出现在有状态 StreamableHTTPServerTransport 处理通知时:服务端已经完成 202 Accepted 响应,但随后向已关闭的会话 writer 转发通知失败,外层异常处理又尝试发送 500,从而触发第二轮 http.response.start。优先排查 session writer 是否已关闭,以及异常路径是否在响应完成后再次写响应。
适用环境:Issue 中确认可复现于 mcp==2.3.0,并在生产环境的 mcp==2.2.0 观察过相同异常链;复现环境为 Python 3.11、anyio 4.14、starlette 1.0、uvicorn 0.46。发布线为 2.x 当前稳定版。
最快修复方案:暂无确认的一步修复方案。Issue 被关闭为 #3631 的重复项,尚未在本 Issue 中合并已验证的修复。可优先尝试社区建议的方向:在通知路径的 writer.send(session_message) 周围捕获 anyio.ClosedResourceError,或在 _handle_post_request 中跟踪响应是否已完成,已完成后不要再次构建并发送 500,仅记录异常。
注意事项:上述修复方向来自 Issue 评论中的建议和分支,并未在本 Issue 中确认为已合入上游的最终方案。对调用方而言 202 已经返回,实际可见影响主要是未处理的服务器异常和误导性的错误遥测,而不是 HTTP 响应本身变化。修改异常处理逻辑时需避免掩盖真正需要在响应未完成时返回 500 的错误。
问题场景
使用 MCP Python SDK 的 StreamableHTTPServerTransport 有状态模式下,服务端收到通知(例如 notifications/initialized)时,如果会话的 incoming writer 已关闭,_handle_post_request 会先发送并完成 202 Accepted,随后调用 writer.send(session_message) 转发通知。writer 关闭会抛出 anyio.ClosedResourceError,外层异常处理尝试用同一个 ASGI send 构造 500 响应,Uvicorn 因响应已完成而拒绝第二次 http.response.start。报告中说明可在不涉及 FastMCP 应用行为的情况下直接复现,并在生产环境观察到相同异常链。
报错原文
anyio.ClosedResourceError
at streamable_http.py:629
await writer.send(session_message)
During handling of the above exception:
RuntimeError: Unexpected ASGI message 'http.response.start' sent, after response already completed.
at streamable_http.py:724
await response(scope, receive, send)
原因分析
最可能的原因是通知转发发生在 202 Accepted 已经完成之后:_handle_post_request 先完成确认响应,再向会话 writer 发送通知。当 session writer 已关闭时,writer.send() 抛出 anyio.ClosedResourceError,外层异常处理没有识别“响应已完成”这一状态,仍然尝试发送 500,导致 Uvicorn 抛出第二个 http.response.start 相关的 RuntimeError。Issue 中也提到这可能与 #2741 的会话生命周期行为有关,但本报告聚焦于通知已被确认后再次发送 ASGI 响应的问题。
环境排查
- 确认 MCP Python SDK 发布线:2.x 当前稳定版;已在
mcp==2.3.0复现,并在mcp==2.2.0生产环境观察到相同异常链。 - 确认 Python 版本:复现记录为 Python 3.11。
- 确认 anyio 版本:复现记录为 anyio 4.14。
- 确认 Starlette 版本:复现记录为 starlette 1.0。
- 确认 Uvicorn 版本:复现记录为 uvicorn 0.46。
- 确认是否为有状态
StreamableHTTPServerTransport,并检查会话 incoming writer 是否在通知到达前已经关闭。 - 确认异常是否发生在通知路径:先完成
202,再调用writer.send(session_message),随后进入外层异常处理并尝试第二个响应。
解决步骤
- 先确认当前使用的 MCP Python SDK 版本和发布线,并核对是否与 Issue 中的
mcp==2.3.0或生产环境的mcp==2.2.0一致。 - 检查服务端异常栈中是否同时出现
anyio.ClosedResourceError和RuntimeError: Unexpected ASGI message 'http.response.start' sent, after response already completed.,并确认前者发生在writer.send(session_message),后者发生在外层异常处理尝试发送响应时。 - 确认触发路径为通知处理:
_handle_post_request已经完成202 Accepted,之后通知转发失败。 - 可优先尝试在通知路径的
writer.send(session_message)周围捕获anyio.ClosedResourceError,避免继续落入外层的500响应分支。 - 可优先尝试在
_handle_post_request中跟踪响应是否已完成;在 except 路径中如果响应已经完成,则跳过构建和发送500,仅使用logger.exception()记录异常,未完成路径保持原逻辑不变。 - 如果采用修复分支或补丁,建议补充回归测试:在
tests/server/test_streamable_http_router.py中使用内存 transport,关闭 session stream,发送notifications/initialized,断言只出现一次http.response.start。 - 关注上游 #3631,因为本 Issue 已被关闭为该问题的重复项;确认最终修复是否合入目标版本。
验证方法
复现相同通知路径时,服务端应只完成一次 202 Accepted 响应,不再出现第二次 http.response.start,也不再抛出 RuntimeError: Unexpected ASGI message 'http.response.start' sent, after response already completed.。如果采用“响应完成后仅记录异常”的方案,日志中可能仍会看到 anyio.ClosedResourceError 或相应异常记录,但不应再产生未处理的第二个响应异常。回归测试中应断言单次 http.response.start。
参考来源
modelcontextprotocol/python-sdk #3651
modelcontextprotocol/python-sdk #3631
modelcontextprotocol/python-sdk #2741
AI 工具推荐
想把多个 AI 模型放在一个入口?
GamsGo AI 集成 ChatGPT、DeepSeek、Gemini、Claude、Midjourney、Veo 等常用模型,适合写作、绘图、视频和日常 AI 工作流。
推广链接:通过此链接购买,我可能获得佣金,不影响你的价格。
这个方案解决了吗?
可以继续搜索完整报错,或查看同一工具的其他排查指南。
![[Bug] Agent custom user tools are permanently removed during cold start while the installed-plugin catalog is loading](https://www.chat-gpts.plus/wp-content/uploads/2026/10/18856-ce9d4c32-768x403.jpg)

