最后更新于 2026 年 9 月 6 日,内容核实自 MCP 2026-07-28 正式规范、官方 SDK 文档与运行时文档。

Python 官方 SDK 当前把 subscriptions/listen 的返回事件分成 4 类:工具列表变化、提示词列表变化、资源列表变化和指定资源更新。这个数字说明了一个关键边界:它是“变化信号流”,不是把完整列表或历史事件搬给客户端的消息日志。(Python SDK 订阅文档)

症状:单机能收到通知,多 Worker 或多节点却偶尔无声。
最快解法:同一运行时先用内存通知;只要发布者与持有订阅流的进程可能不同,就接入跨进程通知总线,并让 Client 断线后重新订阅、重新读取数据。

谁应该看这篇

如果你正在实现动态 tools、resources 或 prompts 列表,这篇文章可以帮你避免过早引入复杂基础设施。
如果你负责多实例 MCP Server、代理配置、缓存失效或 Agent 断线恢复,重点看后半部分的测试步骤和部署矩阵。

先把三个概念分开:信号、事实与日志

subscriptions/listen 推送的是“有变化了”的信号。比如服务端发送工具列表变化通知,Client 接着调用 tools/list 获取新的完整列表;资源更新通知也只说明某个 URI 发生变化,不应该把通知帧当成资源内容本身。官方 SDK 的服务端文档明确展示了这种“通知后重新读取”的模式。(Python SDK 服务端订阅文档)

这与持久事件日志是两回事:

  • 变化信号:提醒在线订阅者重新读取。
  • 事实数据:由 tools/list、resources/list、resources/read 或 prompts/list 返回。
  • 持久事件日志:记录过去发生过什么,并允许消费者按位置补读。

MCP 2026-07-28 并没有规定 subscriptions/listen 必须保存历史事件。正式变更说明还明确移除了 Streamable HTTP 的 SSE 恢复与消息重新投递语义,因此你不能因为客户端重新连上,就推断服务器会补发断线期间的每一条变化。(MCP 2026-07-28 变更说明)

单进程:先把最小通知链路跑通

单进程部署的判断条件不是“机器只有一台”,而是事件发布者和订阅流是否处于同一个运行时实例。

例如,MCP Server 在进程内维护工具注册表,某个请求触发工具更新,同时 subscriptions/listen 连接也由同一进程持有,那么内存里的订阅表和事件分发器就能完成链路。此时直接引入外部消息系统,通常只会增加连接管理、序列化、权限和故障排查成本,却没有解决真实问题。

单进程开发阶段应优先验证以下 4 项:

  1. Client 发起 subscriptions/listen 后,是否先收到订阅确认。
  2. 只订阅工具变化时,是否不会收到资源或提示词变化。
  3. Client 关闭连接后,服务端是否移除订阅者,不再继续向失效连接写数据。
  4. 收到通知后,Client 是否真的重新调用列表或资源读取接口,而不是只刷新本地标志位。

Python SDK 的客户端实现把订阅封装为上下文管理器,进入时等待服务器确认,流异常结束时抛出订阅丢失错误,并要求调用方重新监听和重新读取。这个实现细节不能自动代表所有语言 SDK,但它很好地说明了客户端恢复责任通常落在哪里。

第一阶段验收:不要先建设外部总线

你可以先建立一条最小链路:

变更发生
  → 进程内发布 ServerEvent
  → 匹配订阅过滤条件
  → 发送 subscriptions/listen 通知
  → Client 重新读取当前数据

如果这条链路在单元测试和本机 Streamable HTTP 测试中都稳定,再决定是否需要扩大部署。对于本地原型、单机工具服务和临时演示环境,这通常比直接接入共享组件更容易发现协议层错误。

多 Worker:先证明事件能否跨进程到达

Web Worker、进程池、ASGI 多 Worker 或请求型运行时会引入一个容易忽略的分离:

  • 进程 A 接收了工具变更,负责发布事件;
  • 进程 B 持有 Client 的长连接;
  • 进程 A 的内存订阅表里没有进程 B 的连接;
  • 事件没有跨进程传递,Client 最终看起来像“没有收到通知”。

这时不要马上把问题归咎于负载均衡。你需要给每条链路加上可关联的实例标识,例如:

event_id:本次变更测试编号
publisher_instance:发布事件的进程
stream_instance:持有 subscriptions/listen 的进程
subscription_id:订阅确认中的标识
client_refetch:Client 是否重新读取

然后执行一次定向测试:先让 Client 建立订阅,再固定让另一个 Worker 发布工具变化。如果 publisher_instance 和 stream_instance 不同,而通知没有抵达,就已经证明进程内事件总线不够。

Python SDK 的服务端文档把跨副本扩展点描述为 SubscriptionBus:总线传递的是类型化的服务器事件,SDK 仍然负责订阅确认、过滤、订阅标识和流生命周期。换句话说,跨进程组件只负责“把事件送到其他进程”,不应被误写成协议层的历史消息仓库。

多实例:共享的是通知路径,不是协议会话

2026-07-28 的一个重要变化是协议核心趋向无状态:请求携带自身所需的版本与元数据,普通请求可以由负载均衡后的任意实例处理。官方发布说明指出,新版本移除了协议级会话与 Mcp-Session-Id,并允许请求落到普通轮询负载均衡后的不同实例。(MCP 规范发布说明)

但这不等于通知流不需要共享组件。subscriptions/listen 本身仍然是一条由某个实例持有的长连接:

工具变更在实例 A 发生
  → 共享通知路径分发事件
  → 实例 B 找到自己的本地订阅者
  → 实例 B 向 Client 的长流发送通知
  → Client 在任意实例重新读取最新事实数据

因此,多实例部署通常需要共享通知路径,但不一定需要共享协议会话。你可以把它理解为两条不同的设计轴:

  • 请求路由轴:普通 MCP 请求能否被任意实例处理。
  • 通知分发轴:变化事件能否抵达持有订阅流的实例。

跨副本总线只移动事件,事件编码、接收任务和实例内监听器仍需要由部署方实现。不同 SDK 的辅助 API、默认能力和兼容方式可能不同,不能只看 Python 文档就推断 TypeScript、PHP 或 Ruby 的完整行为。

Client 恢复:重订阅与重读必须成对出现

断线恢复不能只做“重新打开流”。如果你只重新发送 subscriptions/listen,却没有重新读取当前列表,那么断线期间发生的变化仍可能留在缓存里。

推荐把恢复流程固定成:

  1. 记录当前订阅过滤器,包括工具、资源、提示词和资源 URI。
  2. 监听流断开时记录断开原因、请求标识和最后一次成功读取时间。
  3. 创建新的 Streamable HTTP 请求,重新发送 subscriptions/listen。
  4. 等待新的确认帧,确认服务器实际接受了哪些过滤条件。
  5. 重新调用 tools/list、resources/list、prompts/list 或 resources/read。
  6. 用新读取结果替换旧缓存,而不是尝试在本地拼接丢失事件。
  7. 如果新连接再次失败,使用退避和上限,避免代理故障时形成重连风暴。

Python SDK 的实现中,订阅流异常结束会被区分为非正常丢失,并提示重新监听和重新获取数据;其客户端还设置了 1024 个待消费事件的内存上限,超出后会结束订阅并要求恢复。这个上限是该 SDK 的实现保护,不是 MCP 协议统一规定,其他语言不能直接照抄。

旧版 Client 则要走兼容路径。2026-07-28 将新的 subscriptions/listen 作为统一通知流,而旧版客户端仍可能使用早期的订阅与通知机制;因此 Server 需要根据协商版本选择对应处理逻辑,并分别测试两条路径。官方 Python SDK 的迁移说明明确表示,同一个服务可以同时应答旧版和 2026-07-28 客户端,但具体 SDK 版本仍然需要单独核验。(Python SDK 版本迁移说明)

FAQ:把最容易误判的边界一次说清

多实例环境下,为什么只有部分订阅者能收到变化?

通常不是协议过滤出了问题,而是变更事件只发布到了某个实例的内存,而长连接却由另一实例持有。先记录发布实例、持流实例和订阅标识,再分别验证应用总线、代理转发和客户端重读,才能定位丢失环节。

MCP 通知流一定要接入消息队列吗?

不一定。单进程或发布者与订阅流位于同一运行时时,内存通知机制通常足够;只有事件可能跨进程、跨容器或跨节点传递时,才需要共享通知路径。这个组件负责即时分发,不等于协议规定的持久事件日志。

监听连接断开后,服务器会把遗漏变化重新发送吗?

不要这样假设。2026-07-28 的变更移除了 Streamable HTTP 的 SSE 可恢复与消息重投语义,断线期间的事件可能直接错过。客户端应重新建立监听,并重新读取 tools、resources 或 prompts 的当前状态。

旧版 MCP Client 还能接收列表变化吗?

可以,但走的是旧版本能力与旧通知路径,不能把旧版 resources/subscribe 的行为当成 subscriptions/listen。服务端最好保留兼容分支,并在测试中分别验证 2025 版本与 2026-07-28 版本的订阅确认、通知过滤和断线结果。

长连接经过反向代理后,哪些设置最容易造成断流?

响应缓冲、空闲超时、连接数限制、滚动发布和优雅关闭都可能让长流提前结束。你需要确认代理是否立即转发 text/event-stream 数据,运行时是否允许长响应持续存在,并用真实断线测试验证客户端能否重订阅和重读。

运维验收:代理、运行时和滚动更新要分开测

Streamable HTTP 的长流问题,不能只用接口返回状态码判断。至少要检查下面 5 个边界:

  • 响应缓冲:代理是否把多条事件攒在缓冲区后才发送。
  • 空闲超时:长时间没有业务事件时,连接是否被代理或服务器关闭。
  • 优雅关闭:实例收到终止信号后,是否给 Client 足够时间感知断开并重连。
  • 连接上限:每个 Worker、实例和代理允许的长连接数量是否足够。
  • 滚动发布:旧实例退出时,订阅是否正常结束,而不是让 Client 长时间等待。

SSE 使用 text/event-stream 响应,服务器也可以发送注释行维持连接活跃;但这属于传输和运行时层面的保活策略,不会让 MCP 自动拥有事件重放能力。(MDN 的 SSE 传输说明)

以常见运行时文档为例,Uvicorn 的 Keep-Alive 默认值是 5 秒,优雅关闭等待时间则由 --timeout-graceful-shutdown 控制;Gunicorn 的 Worker 默认超时是 30 秒,优雅重启默认等待时间也是 30 秒。这些数字只是运行时默认值,不能直接当成 MCP 推荐配置;你的代理、容器编排和网络路径必须一起验证。(Uvicorn 官方配置文档)

Ruby 官方传输实现还提供了更贴近订阅流的保护:其文档列出默认最多 1000 条并发 subscriptions/listen 流,并以 15 秒 的保活间隔帮助发现已断开的连接。这同样是 SDK 实现层面的资源保护,不是协议统一上限。(Ruby SDK Streamable HTTP 传输文档)

在测试环境中,你可以先通过 SSHMac 帮助中心确认远程连接和运行方式,再用一台可重复重启的 Mac 环境模拟实例滚动更新;如果需要持续验证 Streamable HTTP 长连接,也可以结合 Mac mini 远程测试环境和 VNC 连接指南完成代理、进程和 Client 三端同时观察。

三张表确定你的部署方案

架构选择表

运行形态 事件发布者与订阅流关系 是否需要通知总线 主要风险 推荐做法 评分
本地原型 同一进程 否 只验证了协议,未验证断线 内存总线,重点测确认、过滤、取消和重读 9/10
单机生产 同一运行时或固定单 Worker 通常否 进程重启导致订阅全部消失 保留内存通知,完善 Client 重订阅 8/10
多 Worker 发布者和持流 Worker 可能不同 是 事件停留在错误进程 用实例标识测试跨进程送达,再接共享总线 7/10
多节点平台 任意节点可能发布或持流 是 负载均衡、滚动更新、代理超时 共享通知路径+节点级监控+客户端重读 8/10

故障定位表

现象 最先检查的位置 能确认什么 不要直接下的结论
订阅确认都没有 Client 版本、请求格式、协议协商 是否真的进入 2026-07-28 路径 不要先改消息系统
确认有,但只有同进程能收到 应用总线和 Worker 标识 是否发生跨进程丢失 不要先怪代理
服务端日志显示已发送,Client 无感知 代理缓冲、空闲超时、连接关闭 长流是否被网络层截断 不要假设事件已持久化
断线后列表仍旧 Client 恢复流程和缓存失效 是否完成重订阅+重读 不要把通知当成完整数据
只有旧版 Client 失败 版本兼容分支和旧通知路径 是否错误使用了新方法 不要把单一 SDK 行为当成协议规则

测试环境与恢复责任表

团队形态 测试节点 兼容路径 故障恢复责任 适合的通知设计
个人开发者 单进程本机 先测 2026-07-28 Client 重新订阅与重读 内存通知
单机服务团队 单机、单 Worker、代理 同时测新旧 Client Server 正确关闭流,Client 重建流 内存通知,必要时预留总线接口
平台工程团队 多 Worker、固定实例和滚动重启 新旧协议分开回归 Server 负责跨进程分发,Client 负责一致性恢复 共享通知总线
多节点平台 至少覆盖发布节点与持流节点 多版本 Client、代理和运行时 平台负责路由、保活、监控与滚动策略 跨节点通知路径,不把它当事件日志

如果你现在的方案是单进程,直接上消息队列会带来额外的序列化、认证、部署和故障面;如果你已经使用多 Worker 或多节点,却仍依赖每个进程自己的内存通知,那么工具列表变化会在特定路由下静默丢失。传统的固定长连接方案还常常把会话粘性、代理超时和实例重启绑在一起,故障恢复责任不清晰,也不适合长期扩展。

更稳妥的路径是先按单进程、多 Worker 或多节点定位架构,再在真实远程环境中验证代理断线、滚动更新和 Client 重读。如果你需要临时算力、可重复的 MCP 测试节点或多实例状态验证,SSHMac 的 Mac 环境比临时改造现有办公 Mac 更容易隔离变量;但长期稳定重负载、必须接入本地物理设备或需要完全掌控硬件的项目,仍应优先评估自购 Mac 或自建基础设施。