AI 技术
#MCP#Streamable HTTP#SSE#断线重连#会话恢复

MCP Streamable HTTP 断线重连:工具调用结果为什么会丢

移动网络下远程 MCP Server 断线重连后,工具调用结果可能凭空消失。文章从会话 ID、SSE 事件 ID 与 Last-Event-ID 重放机制入手,说明恢复成立的前提、服务端事件缓冲的边界,以及无状态模式为何无法重放,并给出可观测指标与部署取舍。

一个丢结果的现场

设想这样的场景:手机上的 AI 助手通过远程 MCP Server 调用一个耗时工具,比如在沙箱里跑一段代码或查询一个慢接口。工具已经执行完了,服务端也把结果写进了 SSE 流,但就在这几十毫秒里,手机从 Wi-Fi 切到蜂窝网络,TCP 连接断了。客户端重连后继续对话,却发现那次工具调用没有返回结果,模型只能重新发起一次调用,或者干脆告诉用户“工具没有响应”。

直觉上,重连就该接着上次的位置继续。但 MCP 的 Streamable HTTP 传输把“连接”和“会话”拆成了两个概念:HTTP 连接断了不代表请求被取消,而能不能补回丢失的消息,取决于服务端有没有为这条流保留可重放的事件。结果丢失不是网络问题,而是恢复前提没有满足。

下面用一个贯穿全文的场景来展开:一台部署在公网、通过 Streamable HTTP 暴露的远程 MCP Server,客户端是移动端 AI 助手,工具调用耗时较长且会持续推送进度。

基线方案为什么在弱网下失效

要理解恢复机制,先看它替换掉的旧方案。旧版 MCP 使用 HTTP+SSE 传输:客户端先连一个专门的 /sse 端点建立长连接,服务端通过这条 SSE 通道推送消息,客户端再用另一个端点发请求。

这个结构在弱网下有三个硬伤。服务端必须一直维持 SSE 长连接,连接一断,通道就没了。消息只能从这条 SSE 通道下发,客户端没有别的途径取回。最关键的是,这条流没有恢复语义,断线后无法“从断点继续”,只能重新建立一条新连接,断线前正在传输的消息就永久丢失了。

移动网络的切换、休眠、基站重选都会造成连接中断,而旧方案把连接本身当成了会话载体,连接一断,会话上下文随之消失。Streamable HTTP 的改动正是把这两者解耦。

会话 ID、事件 ID 与重放三件套

Streamable HTTP 规定服务端提供一个统一的 MCP 端点,同时支持 POST 和 GET。客户端发送的每条 JSON-RPC 消息都是一次独立的 HTTP POST;服务端可以选择用普通 JSON 响应,也可以把响应升级为 SSE 流。

恢复能力由三个要素组成。

会话 ID 用来标识一次逻辑会话。服务端在初始化响应里通过 Mcp-Session-Id 头返回一个会话标识,客户端在后续所有请求中带上它。会话 ID 让服务端在连接断开后仍能认出“这还是同一个客户端”,从而找到对应的服务端状态。

事件 ID 是 SSE 标准里的 id 字段。服务端可以给每个 SSE 事件附加一个 ID,规范要求这个 ID 在会话内(或未启用会话管理时对该客户端)全局唯一。它相当于给流里的每条消息编了号。

Last-Event-ID 是客户端重连时携带的请求头。客户端记下自己最后成功收到的事件 ID,重连时通过 HTTP GET 把它发回服务端,表示“我已经收到这里,请从这里之后继续发”。

规范对这三者的关系有明确约束:服务端可以给 SSE 事件附加 ID;客户端断线后应发起 GET 并带上 Last-Event-ID;服务端可以用这个值重放丢失的消息。注意规范用的是“MAY”和“SHOULD”,即这些能力是可选的,不是强制实现。这一点直接决定了结果会不会丢。

一次工具调用的完整数据流

把上面的机制放进贯穿场景,看一次长耗时工具调用在正常与断线两种情况下如何流动。

flowchart TD
    A[客户端 POST 工具调用请求] --> B[服务端建立会话并返回会话 ID]
    B --> C[服务端升级为 SSE 流]
    C --> D[推送进度事件 带事件 ID]
    D --> E{连接是否中断}
    E -->|否| F[推送最终结果事件]
    F --> G[客户端收到结果 关闭流]
    E -->|是| H[客户端记录最后事件 ID]
    H --> I[客户端 GET 重连 带 Last-Event-ID]
    I --> J{服务端是否保留事件缓冲}
    J -->|是| K[重放缺失事件 含最终结果]
    K --> G
    J -->|否| L[无法重放 结果丢失]
    L --> M[客户端只能重新发起调用]

正常路径里,客户端 POST 工具调用,服务端返回会话 ID 并把响应升级为 SSE 流,随后依次推送进度事件和最终结果事件,每个事件都带唯一 ID。客户端收到结果后关闭流。

断线路径里,连接在进度事件之后、结果事件之前中断。客户端已经收到了带 ID 的进度事件,于是记下最后一个 ID。重连时它发起 GET 并带上 Last-Event-ID。此时流程走到一个关键分叉:服务端是否保留了断线点之后的事件。

如果服务端把事件写进了可重放的缓冲,它就能把结果事件补发出来,客户端拿到完整结果。如果服务端没有缓冲,或者缓冲已经被清理,重连只能建立一条新流,断线期间的事件无法补回,那次工具调用的结果就永久丢失了。

这个分叉就是“结果为什么会丢”的直接答案:丢失发生在服务端没有可重放事件的时候,而不是发生在网络中断的那一刻。

恢复边界:哪些情况补不回来

Last-Event-ID 能恢复,但恢复有明确边界,越过边界就会退化。

服务端未实现重放。 规范里事件 ID 和重放都是可选项。一个只返回普通 JSON、不生成事件 ID 的服务端,客户端根本没有 ID 可记,重连时也无从续传。这是最常见的丢失原因。

事件缓冲被清理。 即使服务端支持重放,缓冲也有生命周期。如果断线时间超过缓冲保留窗口,或者缓冲容量被新事件挤满,旧事件已被丢弃,Last-Event-ID 指向的位置之后没有可补的内容,重放失败。移动网络的断线可能持续数秒到数分钟,这个窗口很容易被超过。

会话已过期。 会话 ID 有有效期。规范提到服务端在会话过期时可能关闭流。如果重连时会话已经过期,服务端无法把请求关联回原会话,只能要求客户端重新初始化,此时原工具调用的上下文已经不在。

无状态模式。 如果服务端选择不建立会话,就没有会话 ID,也没有跨连接的服务端状态。规范允许事件 ID 在“未使用会话管理时对该客户端唯一”,但无状态服务端通常不会为某个客户端保留事件缓冲,因为那本身就是状态。无状态模式下的重连基本等同于重新开始。

客户端没有记录事件 ID。 恢复是双向的。客户端必须在收到每个 SSE 事件时持久化其 ID,并在重连时正确带上 Last-Event-ID。如果客户端只把事件 ID 放在内存里,进程重启或页面刷新后同样无法续传。

还有一个容易被忽略的点:规范明确说明断线不应被解释为客户端取消了请求。要真正取消,客户端应显式发送取消通知。这意味着服务端在断线后可能仍在继续执行工具,只是结果没有通道送回。这既可能造成结果丢失,也可能造成重复执行。

无状态与有状态、stdio 与 HTTP 的取舍

不同传输和状态模式适用于不同场景,恢复能力只是其中一个维度。

维度stdio旧 HTTP+SSEStreamable HTTP 有状态Streamable HTTP 无状态
连接模型本地子进程标准输入输出专用 SSE 长连接统一端点,按需升级 SSE统一端点,普通 HTTP 响应
断线恢复不适用(本地进程)不支持,只能重建依赖事件 ID 与缓冲,可重放基本不支持
服务端状态进程内长连接即状态会话 ID 关联状态无
水平扩展不涉及长连接难迁移需共享或粘性会话天然易扩展
基础设施兼容本地防火墙易断长连接较好最好
适用场景本地工具、单机遗留实现弱网、长任务、需上下文简单无状态工具 API

stdio 传输把服务端作为客户端启动的子进程,通过标准输入输出交换消息。它没有网络断线问题,但只能本地使用,无法支撑远程多客户端。

旧 HTTP+SSE 的恢复能力最弱,长连接一断就重建,且服务端消息只能走 SSE 通道,高并发下长连接消耗资源。资料中的对比测试显示,在 1000 个并发用户场景下,Streamable HTTP 的 TCP 连接数明显低于 HTTP+SSE,后者连接数随时间持续增长;并发接近进程连接上限时,HTTP+SSE 的成功率会快速下降。这些数据来自特定部署环境的测试,具体数值不应直接外推到其他实现。

Streamable HTTP 有状态模式提供了恢复能力,代价是服务端要维护会话状态和事件缓冲,水平扩展时需要共享状态或粘性路由,否则重连可能落到没有该会话的实例上。

无状态模式部署最简单,兼容现有 HTTP 基础设施,适合数学计算、文本处理这类无状态工具。但它放弃了恢复能力,长任务和弱网场景下结果丢失的概率显著上升。

选择的关键在于:这个工具调用是否可重试、是否幂等、是否耗时。可重试的短工具可以走无状态;不可重试的长任务必须走有状态加事件缓冲。

工程实现中的关键点

如果要在服务端实现可恢复的流,几个组件边界需要明确。

事件缓冲是核心数据结构。服务端需要为每个会话维护一个按事件 ID 有序的缓冲,支持按 Last-Event-ID 查找后续事件。缓冲要有容量上限和过期时间,否则内存会随会话增长。常见做法是环形缓冲加 TTL,两者取先到者。

会话存储决定扩展性。单实例内存存储最简单,但无法水平扩展。多实例部署需要把会话和事件缓冲放到共享存储,或者用粘性路由把同一会话的请求固定到同一实例。粘性路由实现简单,但实例故障时会话仍会丢失。

事件 ID 的生成要保证唯一且有序。规范要求会话内全局唯一。用单调递增的序号最简单,也便于按 ID 范围查找。如果用随机 ID,就需要额外的顺序信息才能确定“之后”的事件。

幂等性是防重复的兜底。由于断线不等于取消,服务端可能重复执行工具。工具实现应尽量幂等,或让客户端在重连后先用某种方式确认上次调用的状态,而不是盲目重发。

超时设置要分层。规范建议为所有请求设置超时,并允许在收到进度通知时重置超时时钟,但仍应强制一个最大超时。对长任务来说,进度事件既是给用户的反馈,也是给超时逻辑的“还活着”信号。

部署与观测

恢复能力是否真正生效,要靠指标验证,而不是靠配置声明。

服务端应暴露这些信号:重连请求数,以及其中带 Last-Event-ID 的比例;重放成功与重放失败(找不到对应事件)的计数;事件缓冲的当前条目数、命中率与因容量或 TTL 被淘汰的计数;会话过期计数;同一会话的并发流数量。

客户端侧应记录:收到事件 ID 的持久化成功率;重连后是否拿到最终结果;同一工具调用被重发的次数。

一个实用的诊断路径是:当用户报告“工具没返回结果”时,先看服务端有没有该会话的重连记录。如果没有重连请求,问题在客户端没有触发恢复;如果有重连但重放失败,问题在事件缓冲被清理或会话过期;如果重放成功但客户端仍没结果,问题在客户端没有正确关联事件与请求。

部署边界上,有状态加缓冲的方案在会话量很大时会推高内存和存储成本,需要根据会话平均时长和事件速率估算缓冲规模。无状态方案成本低但恢复能力弱,适合可以接受重试的场景。两者不是替代关系,同一套 MCP 端点可以按工具类型选择是否启用会话。

仍未解决的问题

规范把事件 ID 和重放定义为可选能力,这给实现留下了空间,也留下了不一致。不同服务端对缓冲窗口、会话过期时间、重放范围的处理各不相同,客户端很难在连接前知道对端是否支持恢复。

断线不等于取消这一约定,在服务端继续执行、客户端却已重连的情况下,如何避免重复副作用,规范没有给出统一的确认机制,目前主要靠工具自身的幂等性。跨实例会话迁移、缓冲的一致性和故障恢复,也依赖具体部署方案,超出了传输层规范的范围。这些问题决定了:会话恢复能覆盖大部分弱网断线,但把它当作绝对可靠的结果保证,仍然不成立。

资料来源

  1. MCP Specification: Transports
  2. MCP Specification: Lifecycle
  3. MCP协议Streamable HTTP - LanternOps
  4. MCP 协议:为什么 Streamable HTTP 是最佳选择?