MCP Resource 订阅一致性:列表游标、更新通知与版本对账
resources/updated 只表示某 URI 可能变化,不携带内容版本。本文用发现代次、订阅租约与读取摘要对账通知丢失、乱序更新和分页期间的资源集合变化。
本文要点
- 主题聚焦:AI Agent、MCP、RAG
- 适合人群:独立开发者、全栈工程师、AI 应用开发者
- 核心价值:快速理解背景、实践路径、常见误区和工程建议
Client 用 resources/list 扫到三页文件,随后订阅其中一个 URI。分页期间资源被删除又重建,更新通知还在断线时丢失;Client 仍把旧正文加入提示词,因为 notifications/resources/updated 没有版本号,它也没有保存上次读取摘要。通知到达不等于内容已经同步,列表扫完也未必代表同一时刻的快照。
RAG 增量索引一致性说明数据进入索引后的更新策略,RAG 文档新鲜度 SLO跟踪来源健康。本篇把边界放在 MCP Resource 协议本身:如何从 URI 发现、订阅、通知和 read 生成一个可证明的新本地版本。
协议事件不是资源版本
Server 声明 resources capability 后才可提供资源;subscribe 与 listChanged 是两个独立可选子能力。resources/list 和 resources/templates/list 使用 opaque cursor,页大小由 Server 决定,Client 不得解析、修改或跨 session 持久化 cursor。规范建议稳定 cursor,却没有承诺多页是事务快照;即使声明 listChanged,Server 也只是 SHOULD 通知,且通知没有 sequence 或水位。因此通知只能触发失效,通知未出现绝不是完整性证明。
控制面保存 resourceDiscoveryScan.v1:session generation、首请求时间、每页 cursor 摘要、URI/descriptor 集合摘要、通知观察和结束时间。若 Server 提供已协商的 snapshot ID 扩展,所有页必须绑定同一 ID 才能发布 SNAPSHOT_PROVEN。标准 MCP 没有该证明时至少连续完成 A/B 两轮全扫描;任一轮中收到 list-changed、两轮摘要不同或 cursor 失效都重新对账。两轮相同也只能发布带 measuredAt、陈旧上限和下一次复核时间的 SNAPSHOT_UNPROVEN,不能向需要完整枚举的授权撤销或清理任务声称它是权威快照。
订阅账本 resourceSubscription.v1 绑定主体、Server、规范化 URI、session、capability receipt、subscription generation、开始/结束时间、最后通知序号和最后已接受 read digest。官方 schema 定义 resources/subscribe 与 resources/unsubscribe;unsubscribe 成功只停止后续更新通知,不删除已缓存内容。没有 subscribe 能力时使用带抖动的周期 read,并公开陈旧上限,不能假装实时。Agent 工具能力目录可用同样的 generation 隔离不同会话。
DISCOVERED -> SUBSCRIBING -> ACTIVE
ACTIVE -> UPDATE_SEEN -> READ_PENDING -> VERSION_ACCEPTED -> ACTIVE
ACTIVE -> UNSUBSCRIBING -> CLOSED | UNSUBSCRIBE_UNKNOWN
任一读写身份不明 -> RECONCILIATION_REQUIRED
updated notification 只携带 URI,收到后应合并同 URI 的并发脉冲并调用 resources/read。每次派发先为 (session, subscriptionGeneration, URI) 以 CAS 分配单调 readOrdinal,保存 request ID 与触发原因。Client 为响应计算 resourceVersion.v1,包含 URI、MIME type、内容摘要、read ordinal、授权 subject、Server 实现版本和本地 generation;lastModified 只是 hint,不能替代摘要。
唯一接受点 acceptResourceVersion 同时要求订阅仍为 ACTIVE,匹配 generation、URI、已签发 request 与 ordinal,并满足 readOrdinal > lastAcceptedOrdinal,再原子提升水位、写 version receipt 与 invalidation outbox。B=6 先返回后,迟到 A=5 只能留下 stale receipt;A 先返回则 5、6 可依次接受。缓存 Revalidation 单飞控制可复用同一 fence。
beginUnsubscribe 以 CAS 把 ACTIVE 改为 UNSUBSCRIBING,同时冻结 closeOrdinal=lastIssuedOrdinal 与 close epoch;此后不再分配 read。无论 read 是否早于 close 发出,其迟到内容或 404 都只写 STALE_AFTER_CLOSE 审计,不更新版本、tombstone 或 outbox。unsubscribe ACK 丢失进入 UNSUBSCRIBE_UNKNOWN,本地仍保持静默并按同一 session 对账或最终关闭 session,不能重新开放。若仍 ACTIVE 的 read 返回 not found,才可通过 ordinal fence 标 TOMBSTONE_PENDING 并用列表对账确认删除。内容进入模型前还要经过Agent 工具输出防线。
故障注入包括多页变更且通知丢失、循环 cursor、伪造 snapshot ID、更新通知乱序、unsubscribe ACK 丢失、close 与 read 接受竞争、CLOSED 后迟到内容/404、旧 session 通知、read A 慢于 B、URI 编码混淆、权限撤销后缓存命中。断言旧 generation 不覆盖新版本,单次扫描不产生完整目录,非 ACTIVE 订阅的版本/tombstone/invalidation 接受为 0,权限撤销可追到每份派生索引。
发布门要求未对账目录接受、SNAPSHOT_UNPROVEN 被完整性任务消费均为 0;十万次通知/read/close 乱序中版本倒退、迟到 tombstone 和 CLOSED 后 outbox 均为 0;重复通知 read 放大低于 1.2 倍;活跃资源 60 秒内收敛 99.9%,权限撤销 p99 小于 30 秒。上线先 shadow 双扫描、ordinal 和 close fence,再启用 5% 订阅刷新;回滚保留水位与 close receipt,不能让旧 read 失去 fence。
Related