MCP 旧会话收到 404,Python 客户端报 Session terminated
Python mcp 1.22.0 在旧会话请求收到 404 后报 Session terminated。区分路由错误与会话失效,结束旧连接后重新初始化,并限制工具动作重放。
Agent 已经完成 MCP 初始化,也调用过工具。服务器重启或会话失效后,下一次 Streamable HTTP 请求却返回 404,Python 客户端随即抛出 Session terminated,没有重新初始化连接。
这个判断需要一个关键条件:失败请求确实携带了先前建立的 MCP-Session-Id。如果第一次 initialize 就是 404,应先查地址与路由,不能直接按会话过期恢复。
下面针对 Python mcp 1.22.0 的客户端实现。该版本初始化时请求的协议版本为 2025-06-18;MCP 2025-06-18 与 2025-11-25 对“旧会话 ID 请求收到 404 后重新初始化”的要求一致。后续 SDK 可能调整恢复行为,迁移时应重新核对实际版本,不要保留已经多余的外层重连。
先确认 404 出现在什么阶段
有状态服务器可以在初始化响应的 MCP-Session-Id 头中分配会话 ID。客户端随后应把它带到后续请求里;服务器终止这份会话后,会对携带该旧 ID 的请求返回 404。
从应用与服务器日志中核对这几个事实:是否完成过 initialize,初始化响应是否分配了 ID,失败请求是否继续携带这个 ID,以及 404 来自 MCP 端点还是中间代理。记录请求时间、方法与脱敏的会话关联即可,不需要把完整令牌和会话 ID 贴进公开日志。
如果没有建立过会话,或者路径本身返回的是代理的 HTML 404 页面,检查端点路径、挂载前缀和反向代理规则。对这些错误反复创建新会话,不会修复路由。
若确认是旧 ID 被服务器拒绝,再检查会话为什么失效。服务器重启、会话淘汰策略、请求落到另一个没有该会话的工作进程,都可能解释这种情况,但必须用服务日志和部署方式逐项判断。不能仅凭 404 就断定是多进程负载均衡。
1.22.0 的异常不代表已经重新初始化
这份版本的 Streamable HTTP POST 处理遇到 404 时,会给对应请求发送 Session terminated 错误。这个分支没有清除旧 ID 并发送新的 initialize。只捕获异常后再次使用原来的 session,下一次请求仍可能携带已经失效的 ID。
已确认旧会话失效时,恢复顺序应是:停止向旧连接提交新请求,完成旧 ClientSession 与 transport 上下文的退出,再创建新的 transport 和 session,重新初始化。新的 initialize 不应携带旧会话 ID;初始化成功后,由新 transport 管理后续请求头。
也不要只手动删掉某个请求里的 ID 再重发 tools/call。要求会话的服务器通常会拒绝没有 ID 的普通请求,重新初始化才是建立新协议会话的步骤。
用新连接读取工具列表
下面是 1.22.0 API 形态的一份完整客户端示例。它每次调用函数都创建新连接,只发送初始化和 tools/list,不执行工具:
import asyncio
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client
async def list_tools_in_new_session(url: str) -> list[str]:
async with streamablehttp_client(url) as (
read_stream,
write_stream,
get_session_id,
):
async with ClientSession(read_stream, write_stream) as session:
await session.initialize()
result = await session.list_tools()
print("Server assigned a session:", get_session_id() is not None)
return [tool.name for tool in result.tools]
async def main() -> None:
names = await list_tools_in_new_session("http://127.0.0.1:8000/mcp")
print(names)
if __name__ == "__main__":
asyncio.run(main())
本机地址是占位端点,需要替换为自己有权访问的 Streamable HTTP MCP 服务。示例假定这个本机服务不要求额外认证;远程服务要沿用原有 HTTPS、认证和授权配置,不要为了重连取消认证。返回值是本次 tools/list 响应中的工具名,存在分页时还需按服务的游标协议读取后续页。
把函数用于恢复时,应由外层连接管理器先确认“携带旧 ID 的请求收到 404”,并在拥有旧连接的同一个 Task 中退出上下文,再调用它。示例没有按 Session terminated 字符串自动重试所有请求,因为 1.22.0 的 404 分支本身不足以证明是哪一种 404。
对已确认的旧会话失效,可以安排一次新建连接和初始化尝试。新连接仍然失败时,把新的状态码、异常和请求阶段交给上层处理,不能用无限循环隐藏持续故障。恢复过程中也应避免多个请求同时为同一旧连接各自启动重连。
新会话不能证明旧工具动作可以重做
读取工具列表通常不修改业务数据,适合用来确认新会话可用。创建文件、写数据库、发送消息或提交订单的工具调用,需要单独判断是否已经执行,是否提供幂等键,以及失败结果如何查询。不要把建立新会话和重放业务动作绑成无条件的一步。
SSE 的 Last-Event-ID 用于在服务支持时续传某条事件流。它不能让服务器重新接受一个已终止的 MCP 会话 ID。类似地,Agent 的对话记忆或 LangGraph 的 thread_id 属于应用状态,也不能代替传输会话的初始化。
恢复成功应能观察到:新的 initialize 没有带旧 ID,服务器完成初始化,后续请求遵循新会话的请求头约定,并收到有效工具列表。新连接可以工作,不代表旧会话内的业务状态已经回来。若需要保留对话或任务进度,应由应用自己的状态存储恢复,而不是把旧 ID 硬塞回请求头。


