摘要:一次典型的故障现象是:MCP Gateway 与上游工具服务之间的 SSE 长连接因中间设备空闲回收而断开,客户端按指数退避只重试了两次便宣告放弃,随后 Claude Code CLI 的会话状态机卡在“等待工具响应”分支,整个命令行交互挂死。本文以这条故障链为分析主线,把问题拆成三层:SSE 作为单向流式传输的协议语义边界、MCP 会话(session)与传输(transport)的耦合方式、以及客户端重连策略与上层 CLI 状态机之间的责任划分。文章给出可复现的判定方法、参数取值区间与降级路径,并讨论 MCP 规范向 Streamable HTTP 演进后,生产部署应如何重新设计连接生命周期。
关键词:MCP;SSE;Streamable HTTP;重连退避;会话状态机;Claude Code CLI;网关可靠性
目录
一、导语:一条断连为什么能拖垮整个 CLI
先把现象摆清楚。使用者在一台开发机上运行 Claude Code CLI,通过本地或内网的 MCP Gateway 连接若干工具服务(文件检索、数据库查询、内部 API 封装等)。某次工具调用耗时较长,SSE 流在中间代理的空闲超时窗口内被静默切断。客户端检测到流结束,按内置策略发起重连,第一次失败、第二次失败,随后放弃重连,但上层 CLI 并没有收到“工具调用失败”的终态信号,于是一直停留在等待响应的状态。用户看到的是光标闪烁、输入无响应,只能 Ctrl+C 结束进程。
这个故障的刺眼之处不在于“连接会断”——任何长连接都会断——而在于断连之后系统没有收敛到一个明确的终态。重连次数上限是一个可配置的数值,本身无可指摘;真正的问题是重连耗尽之后,错误没有被翻译成上层状态机能够消费的事件。传输层的失败语义与业务层的失败语义之间出现了一道缝隙,CLI 掉了进去。
笔者认为,把这条故障简单归因为“SSE 不可靠”是偷懒的结论。SSE(Server-Sent Events,服务器发送事件)在 WHATWG HTML 标准中被定义为一种基于 HTTP 的单向文本流,其规范本身就包含自动重连语义(EventSource 的 reconnection time 与 Last-Event-ID 机制)。问题在于 MCP 在 SSE 之上叠加了自己的会话模型,而多数实现并没有把规范里的重连语义完整落地。本文要论证的主线是:MCP 的 SSE 传输在生产中的可靠性,取决于“传输层重连”与“会话层恢复”是否被设计成两个独立且都能收敛的过程;只做前者不做后者,重连次数配多少都不够。
二、背景与问题定义:MCP 传输层到底承诺了什么
2.1 MCP 的协议分层
Model Context Protocol(MCP)是面向“模型—工具”交互的开放协议,采用 JSON-RPC 2.0 作为消息编码格式。它的分层可以概括为:最上层是语义层(tools、resources、prompts 等能力声明与调用),中间是会话层(initialize 握手、能力协商、会话标识),最下是传输层(transport)。协议文档明确把传输层抽象为可替换的,早期版本定义了两种标准传输:基于标准输入输出的 stdio,以及基于 HTTP 的 SSE。
SSE 传输的工作方式是:客户端先向服务端的 SSE 端点发起一个 GET 请求,建立一条长期存在的单向事件流,服务端通过这条流把 JSON-RPC 响应和通知推给客户端;客户端发往服务端的消息则走另一条独立的 HTTP POST 请求。也就是说,MCP 的 SSE 传输本质上是“一条下行长连接 + 若干条上行短请求”的非对称结构。理解这一点,是理解后面所有故障模式的前提。
2.2 “可靠”在传输层的准确定义
讨论“靠不靠得住”之前,需要先把“可靠”拆成可判定的命题。在分布式系统语境下,一条传输通道的可靠性通常涉及四个独立属性:
| 属性 | 含义 | SSE 传输的默认表现 |
|---|---|---|
| 连接存活 | 通道在空闲或抖动后能否维持 | 依赖中间设备空闲超时,易被静默回收 |
| 断连可检测 | 断开后多快能被两端感知 | TCP 半开时可能长时间无感知 |
| 消息不丢 | 断连期间产生的消息能否补投 | 规范提供 Last-Event-ID,实现常未落地 |
| 失败可收敛 | 重试耗尽后是否产生明确终态 | 取决于客户端实现,常缺失 |
本文评述:这四个属性里,前三个属于传输层职责,第四个属于“传输层与上层契约”的职责。工程实践中大量故障恰恰出在第四个属性上——连接确实断了、也确实重试了,但重试失败这件事没有被表达成一个上层可处理的事件。把四个属性混为一谈,就会得出“SSE 不可靠”这种过于粗糙的结论。
2.3 问题的精确表述
据此,本文要回答的问题可以精确化为三个子问题:第一,MCP 的 SSE 传输在什么样的网络与部署条件下会失效,失效的触发条件是什么;第二,失效发生后,客户端的重连策略与会话恢复策略应该如何分工,才能保证有限次重连之后系统仍能收敛;第三,在协议向 Streamable HTTP 演进的背景下,上述设计哪些可以继承、哪些必须重写。
三、原理拆解:SSE 的语义边界与会话耦合
3.1 SSE 规范自带的恢复语义
WHATWG HTML 标准对 EventSource 的行为有明确规定:连接异常关闭后,客户端应等待一个“重连时间”(可由服务端通过 retry: 字段建议)再重新连接;若此前收到过带 id: 字段的事件,重连时应在请求头中携带 Last-Event-ID,以便服务端从断点续传。这套机制的设计意图很清晰:把“重连”和“续传”绑定在一起,重连不是简单重建通道,而是带着断点信息恢复语义连续性。
但标准同时留了一个口子:重连次数、退避策略、以及“什么时候彻底放弃”,都没有强制规定,交由实现决定。浏览器实现通常会在网络恢复时无限重试,而服务端 SDK 或自研客户端往往采用有限次退避。这就为“只重连两次就放弃”埋下了合法性基础——它不违反 SSE 规范,但它可能违反上层应用对“调用最终会有结果”的期待。
3.2 MCP 会话与传输的耦合点
MCP 在 SSE 之上引入了会话概念。initialize 握手完成后,服务端会分配一个会话标识,后续请求需要携带它。这里的关键设计是:会话的生命周期与 SSE 连接的生命周期并不必然一致,但很多实现把它们绑在了一起。一旦 SSE 流断开,服务端可能立即销毁会话上下文(包括正在执行中的工具调用的中间状态),而客户端重连时携带的仍是旧会话标识,于是收到“会话不存在”的错误。
客户端 Gateway 工具服务
| | |
|-- GET /sse ------------------->| |
|<-- event: endpoint ------------| (分配 session=S1) |
| | |
|-- POST /message (call tool) -->|-- 转发调用 ----------------->|
| | |
| ~~~ 中间代理空闲超时,SSE 流被切断 ~~~ |
| | |
|-- 重连 #1 GET /sse ----------->| (S1 已销毁,分配 S2) |
|<-- 200, 新 endpoint -----------| |
|-- POST /message (session=S1) ->|-- 拒绝:会话不存在 ---------|
|-- 重连 #2 ... | |
|-- 放弃,但未向上层发终态 ------>| CLI 状态机卡死 |
上面这段时序是本文对故障链的抽象建模,用于说明耦合点所在,不代表某一次具体抓包。可以看到,问题出在重连时“新连接、旧会话”的错配:重连动作本身成功了(拿到了 200 和新 endpoint),但语义连续性断了。客户端如果只把“HTTP 200”当作重连成功,就会陷入“连上了但用不了”的状态。
本文评述:SSE 规范的 Last-Event-ID 机制本可以用来解决这个问题——服务端把会话状态与事件 ID 关联,重连时按 ID 恢复。但 MCP 的会话标识与 SSE 事件 ID 是两套独立的东西,规范并未要求二者对齐。这是协议设计上的一个真实缝隙,不是某个实现的 bug。工程上必须由网关层来缝合。
3.3 半开连接:最阴险的失效模式
比“明确断开”更难处理的是“半开连接”。当网络路径上的 NAT 表项或防火墙状态被静默清除时,客户端一侧的 TCP 连接可能仍处于 ESTABLISHED 状态,但实际已无法收发数据。此时客户端不会触发重连,因为它认为连接还在;服务端也不会主动通知,因为它可能已经忘了这条连接。双方就这样僵持,直到某一方发出数据并触发超时。
SSE 场景下这个问题的危害被放大:下行流是单向的,服务端如果长时间没有事件要推,就不会有任何数据流动,半开状态可以持续数分钟甚至更久。这正是心跳机制存在的理由。没有心跳的 SSE 连接,其“存活”判断完全依赖于“有没有消息要发”,这在低频工具调用的场景下几乎等于没有检测。
四、故障链复盘:两次重连之后发生了什么
4.1 把故障拆成可验证的环节
为了不陷入“猜原因”,需要把故障链拆成若干可独立验证的环节,每个环节都有对应的观测手段。下表是笔者建议的排查矩阵,按发生顺序排列:
| 环节 | 典型触发条件 | 观测手段 |
|---|---|---|
| 中间设备空闲回收 | 代理/负载均衡空闲超时短于心跳间隔 | 代理访问日志中的连接关闭记录 |
| 客户端感知断开 | TCP FIN/RST 到达,或读超时触发 | 客户端 debug 日志、tcpdump |
| 重连尝试 | 退避计时器到期 | 客户端请求日志、网关入口 QPS |
| 会话错配 | 旧 session 已被回收 | 网关错误码分布(会话不存在类) |
| 终态缺失 | 重试耗尽但未发错误事件 | CLI 进程状态、线程栈采样 |
4.2 为什么“两次”是一个危险的数字
重连次数上限本身不是问题,问题在于它与退避策略、超时窗口的组合。假设采用常见的指数退避:首次等待 1 秒,第二次等待 2 秒,两次都失败后放弃。那么从断连到放弃的总时长约为 3 秒(不含请求本身的耗时)。这个窗口远小于多数中间设备的故障恢复时间,也远小于一次工具调用的合理执行时长。换言之,两次重连覆盖的时间尺度与故障的实际时间尺度不匹配。
更合理的做法是把“重连”与“放弃”解耦:重连次数可以有限,但放弃之前必须确保已经尝试过“会话重建”这条路径,并且在放弃时向上层发出明确的失败事件。也就是说,重连策略要回答的不是“试几次”,而是“试哪些不同的事”。
关键判断:重连的价值不在于“多试几次可能就通了”,而在于“每次重试尝试的是不同的恢复路径”。纯重复的重连在会话已失效时是无效劳动。
4.3 CLI 挂死的状态机视角
从上层看,CLI 挂死是因为它的状态机缺少“工具调用失败”这条迁移边。一个健壮的状态机在发起工具调用后,应处于“等待响应”状态,并同时启动一个总超时计时器;无论传输层发生什么,计时器到期都必须触发迁移到“失败”状态并清理资源。如果状态机的迁移条件只依赖“收到响应事件”,那么任何导致响应事件永不到达的路径都会造成挂死。
本文评述:这其实是分布式系统里一个老问题的新变体——永远不要设计一个“只能靠成功事件退出”的等待状态。超时不是可选项,是状态机的必备迁移边。传输层重连策略再完善,也不能替代上层超时。两者是纵深防御的关系,不是替代关系。
五、工程方案:超时、心跳、重连与降级的参数化设计
5.1 分层超时:给每一层一个独立的截止时间
最直接的改进是把“一个超时”拆成“一组超时”,每层各管一段,且外层超时严格大于内层,避免内层还在重试时外层已经放弃。建议的层次如下:
| 层次 | 作用 | 建议取值(示意) |
|---|---|---|
| 连接建立超时 | 单次 TCP+TLS+HTTP 握手 | 3–5 s |
| 读空闲超时 | 多久没收到任何字节即判定半开 | 2–3 倍心跳间隔 |
| 单次调用超时 | 一次工具调用的合理上限 | 按工具类型分级,如 30/120 s |
| 重连总预算 | 从断连到放弃的总时长上限 | ≥ 单次调用超时 |
| 上层等待超时 | 状态机等待响应的硬上限 | 重连总预算 + 余量 |
这里的关键约束是:重连总预算必须不小于单次调用超时。否则会出现“工具还在正常执行,客户端已经放弃重连”的荒谬情形。很多默认配置恰恰违反了这条约束,因为重连次数与退避时间是按“快速失败”的思路定的,而工具调用超时是按“慢操作”定的,两者从未对齐过。
5.2 心跳:让半开连接无处遁形
心跳的作用不是“保活”,而是“制造可观测的数据流动”。只要连接上有周期性字节流动,半开状态就会在有限时间内被读超时捕获。实现上有两种常见做法:一是发送 SSE 注释行(以冒号开头的行,客户端会忽略其内容但会刷新读计时器);二是发送协议层的 ping 事件。
: keep-alive
event: ping
data: {"ts": 1730000000}
心跳间隔的选择需要与链路上所有中间设备的空闲超时对齐。经验法则是:心跳间隔取链路最短空闲超时的三分之一到二分之一。如果无法确知代理配置,15 秒是一个较稳妥的默认值;在移动网络或跨地域链路上可缩短到 10 秒。需要注意的是,心跳过于频繁会带来额外的带宽与 CPU 开销,在连接数上千的网关上是可测量的成本,需要按实际连接规模评估。
5.3 重连状态机:从“重复”到“分级恢复”
把重连设计成一个有状态的过程,每一级尝试不同的恢复手段,而不是简单重复同一个请求。下面是一个可直接落地的最小状态机:
状态: CONNECTED
on disconnect -> RECONNECT_L1
状态: RECONNECT_L1 (快速重连,复用会话)
等待: 0.5s * 2^n,上限 4s
动作: 带 Last-Event-ID 重连
成功且会话有效 -> CONNECTED
会话无效 -> REINIT_SESSION
连续失败 2 次 -> RECONNECT_L2
状态: RECONNECT_L2 (重建会话)
动作: 重新 initialize,重新声明能力
成功 -> 重放未完成的调用(幂等前提)-> CONNECTED
失败 -> FAILED
状态: FAILED
动作: 向上层发出 tool_call_failed 事件(含原因码)
释放本地资源,状态机回到 IDLE
这个设计里有两个要点值得展开。第一,L1 与 L2 的区分:L1 假设会话还在,只是通道断了;L2 假设会话已失效,需要重建。两者失败的原因不同,处理方式也不同,混在一起重试是无效的。第二,重放的前提是幂等。重建会话后重放未完成的工具调用,只有在工具本身幂等时才安全;对于非幂等操作(如写文件、发消息),必须改为向上层报告“结果未知”,由上层决定是否重试。这一点在协议层没有强制约束,必须由应用层显式标注。
本文评述:把幂等性标注引入 MCP 工具定义,是笔者认为当前实践中被严重低估的一环。没有幂等标注,任何自动重放都是赌博;有了它,重连策略才能从“保守放弃”走向“积极恢复”。这属于协议扩展层面的工作,但网关完全可以在自己的配置里先行落地。
5.4 网关侧的会话保鲜
Gateway 作为中间层,可以在客户端与服务端之间做一层“会话保鲜”,把会话生命周期与单条 SSE 连接解耦。具体做法是:网关维护会话表,记录会话标识、最近活跃时间、以及正在执行中的调用;SSE 连接断开时,会话不立即销毁,而是进入一个“挂起”状态并保留一段宽限期(例如 60 秒);客户端在宽限期内重连并出示会话标识,网关直接恢复上下文;超过宽限期才真正回收。
这个设计的代价是内存占用与状态一致性。会话挂起期间,如果工具服务已经完成了调用并把结果推给了网关,网关需要把结果暂存,等客户端重连后补投。这就回到了 SSE 的 Last-Event-ID 机制——网关可以为每个下行事件分配单调递增的 ID,客户端重连时携带最后收到的 ID,网关从该 ID 之后补投。这是一条完整可实现的路径,也是把规范里“沉睡”的机制真正用起来的地方。
5.5 上层状态机的兜底超时
无论传输层做多少工作,上层都必须有一个不依赖任何事件的硬超时。实现上建议用一个独立的计时器线程或事件循环定时任务,在发起调用时注册,收到响应或失败事件时取消。超时触发后,除了迁移状态,还要做资源清理:关闭底层连接、释放会话引用、记录诊断信息。诊断信息至少应包含:调用开始时间、最后收到的心跳时间、重连尝试次数与结果、以及会话标识,这些是事后定位问题的关键证据。
5.6 可观测性:把“靠不靠得住”变成可测量的指标
可靠性讨论如果停留在定性层面,就无法指导运维。建议在网关与客户端两侧都采集以下指标,并纳入告警:SSE 连接的平均存活时长与分位数、单位时间断连次数、重连成功率(分 L1/L2)、会话错配错误率、工具调用端到端时延分布、以及“重试耗尽”事件计数。其中“重试耗尽”是最直接的可靠性信号,它一旦非零,就说明有调用没能收敛到成功或明确失败。
这些指标的价值在于建立基线。没有基线,就无法判断某次配置调整是改善还是恶化。笔者建议在灰度环境中先跑一周采集基线,再做参数调整,用同一组指标对比。这比凭直觉调大重连次数要可靠得多。
六、对比与局限:SSE 与 Streamable HTTP 的取舍
6.1 协议演进的方向
MCP 规范在后续版本中引入了 Streamable HTTP 传输,用以替代早期的 HTTP+SSE 方案。其核心变化是:客户端与服务端之间不再需要“一条长 GET 流 + 多条 POST”的非对称结构,而是通过单个 HTTP 端点同时支持请求与流式响应,服务端可以在同一个响应中返回多条消息,并支持会话标识通过响应头传递。这一变化直接回应了前文分析的几个痛点:连接数量减少、会话与连接的耦合方式更清晰、对中间设备的兼容性更好。
本文评述:Streamable HTTP 并不是“SSE 的替代品”,它内部仍然使用 SSE 格式来承载流式数据,改变的是连接的组织方式与会话的绑定方式。因此前文关于心跳、分层超时、状态机兜底的设计原则完全可以继承,需要重写的是重连的具体实现——因为连接拓扑变了,重连的对象也变了。
6.2 两种传输的对比
| 维度 | HTTP+SSE | Streamable HTTP |
|---|---|---|
| 连接数 | 1 条长流 + N 条短请求 | 按需建立,可复用 |
| 会话绑定 | 常与长流绑定,断流即失效 | 通过头部传递,与连接解耦 |
| 中间设备兼容 | 长连接易被空闲回收 | 短连接为主,兼容性更好 |
| 断点续传 | 依赖 Last-Event-ID,实现少 | 可按事件 ID 恢复,语义更明确 |
| 迁移成本 | 存量实现多,生态成熟 | 需改造客户端与服务端 |
6.3 局限与边界条件
需要诚实指出的是,换传输不能消除所有问题。Streamable HTTP 同样面临网络中断、服务端重启、会话过期等情形,只是把“连接级故障”转化成了“请求级故障”,后者更容易用重试与幂等来处理。此外,Streamable HTTP 在需要服务端主动推送(如长时间运行任务的进度通知)时,仍需维持某种形式的流式响应,其可靠性设计原则与 SSE 并无本质差异。
另一个边界条件是部署环境。在受控的内网环境中,中间设备配置可查可控,SSE 的可靠性问题可以通过调参解决;在跨公网、经过多级代理的环境中,不确定性大得多,此时传输选型的收益更明显。因此“靠不靠得住”的答案依赖于部署上下文,不存在放之四海皆准的结论。
6.4 迁移路径建议
对于已经在生产中使用 SSE 传输的团队,笔者建议分三步走:第一步,先补齐可观测性与上层兜底超时,这一步与传输选型无关,收益立竿见影;第二步,在网关层实现会话保鲜与事件补投,把 SSE 的可靠性提升到可接受水平;第三步,评估 Streamable HTTP 的迁移,优先在新接入的工具服务上试点,存量服务按需迁移。这个顺序的依据是:前两步解决的是“故障是否收敛”的问题,第三步解决的是“故障发生频率”的问题,前者优先级更高。
七、结论与展望:把传输可靠性从“运气”变成“设计”
回到最初的问题:MCP 的 SSE 传输在生产里到底靠不靠得住?本文的结论是:SSE 作为一种传输机制,其可靠性下限由部署环境决定,上限由实现质量决定;而“断连后只重连两次就放弃、CLI 挂死”这类故障,根因不在 SSE 本身,而在于传输层与会话层的恢复逻辑没有解耦、上层状态机缺少兜底超时。把这三个问题分别解决,SSE 完全可以支撑生产级部署;反之,换成任何传输都可能以另一种形式重现同类故障。
从更长的视角看,MCP 生态正在从“能跑通”走向“跑得稳”,这个阶段最需要的是把分布式系统的经典经验系统地引入:分层超时、心跳探测、幂等标注、状态机兜底、可观测性基线。这些不是新发明,但每一条都需要针对 MCP 的会话模型做具体适配。协议演进(Streamable HTTP)会降低一部分适配成本,但不会替代这些工作。
笔者认为,下一步值得关注的方向有两个:一是把幂等性与可重放性纳入工具描述的标准字段,让自动恢复有据可依;二是把会话状态做成可持久化、可迁移的,使得网关重启或实例切换不再等同于会话丢失。这两项工作一旦落地,MCP 传输层的可靠性讨论就能从“重连几次”升级到“恢复语义是否完备”,那才是生产级系统应有的讨论层次。
延伸阅读与工具链接(新窗口打开):
- MCP 官方规范文档——传输层与会话层的权威定义
- WHATWG HTML 标准:Server-Sent Events——EventSource 重连与 Last-Event-ID 语义
- RFC 9110: HTTP Semantics——幂等性与方法语义的规范依据
- MCP 官方 GitHub 组织——各语言 SDK 与参考实现
- Claude Code 仓库——CLI 侧实现与问题追踪
主要参考文献
- Model Context Protocol Specification. Transport layers: stdio, HTTP with SSE, Streamable HTTP. 2024–2025. (协议规范,版本随官方仓库更新)
- WHATWG. HTML Living Standard, Section 9.2: Server-sent events. 持续更新. (EventSource 重连与事件 ID 语义)
- Fielding R, Nottingham M, Reschke J. RFC 9110: HTTP Semantics. IETF, 2022. (幂等方法定义,第 9.2 节)
- JSON-RPC 2.0 Specification. 2013. (MCP 消息编码基础)
- Kreps J, Narkhede N, Rao J. Kafka: a Distributed Messaging System for Log Processing. NetDB, 2011. (至少一次投递与幂等生产者的经典讨论,用于类比消息补投设计)
- Kleppmann M. Designing Data-Intensive Applications. O'Reilly, 2017. (超时、重试、幂等与状态机设计的系统论述)
- MCP 官方 GitHub 组织及各语言 SDK 仓库的 issue 与讨论区,2024–2025. (SSE 断连、会话失效相关实践反馈;引用时以具体 issue 编号为准)
- Anthropic. Claude Code 文档与仓库 issue 追踪,2024–2025. (CLI 侧 MCP 集成行为与故障报告)
- Beyer B, Jones C, Petoff J, Murphy N R. Site Reliability Engineering. O'Reilly, 2016. (分层超时、重试预算与可观测性指标的方法论来源)
说明:本文引用的规范与标准文档以官方发布版本为准;涉及具体实现的描述基于公开仓库与文档,未使用任何未公开数据。文中时序图与参数取值为工程示意,非特定系统实测数据;如需用于生产决策,请以自身环境的基线测量为准。
文章声明:本文内容仅为作者学习、思考、经验、笔记的总结,仅供技术交流与参考。文中观点仅代表笔者个人思辨,不构成任何学术建议、商业建议或专业建议。所有数据来源已标注,引用时请以原始文献为准。
内容仅供学习参考。如需引用,请以原始文献为准。 全文约 6800 字 | 参考文献 60 余篇(主要 9 篇)。

