Skip to main content
最终标准跟踪

摘要

本 SEP 提议为 tools/listprompts/listresources/listresources/readresources/templates/list 返回的结果对象添加字段以支持缓存。将新增两个字段:ttlMscacheScope。TTL 告诉客户端在重新获取之前,这个响应可以被视为新鲜的时长。这使客户端能够缓存功能列表,并减少对服务器推送通知的依赖,同时保持完全向后兼容。cacheScope 字段控制谁可以缓存响应。TTL 是对现有通知机制的补充,而不是替代——两者可以共存。

动机

目前,MCP 客户端通过调用服务器上的方法来发现服务器功能。这些调用返回当前的功能集合。为了了解变化,客户端依赖服务器推送通知。下表将服务器方法映射到通知类型。 这种方法有若干局限性:
  1. 基于 HTTP 的传输需要 SSE 流:许多客户端和服务器在支持长连接 SSE 流时存在困难,而通知正是需要这些流的。目标是让 SSE 流成为一种可选优化,同时无需它们也能支持协议功能。TTL 允许客户端按可预测的时间表轮询,而无需依赖服务器推送通知。
  2. 实现复杂性:客户端和服务器都必须实现通知订阅与投递基础设施。许多简单服务器的功能列表很少变化(甚至从不变化),但如果希望客户端保持最新,仍然必须支持通知机制。
  3. 缺少新鲜度信号:即使可以接收通知的客户端,也无法知道一个列表有多“稳定”。一个工具列表每天变化一次的服务器,与一个每秒变化一次的服务器,对客户端来说看起来是一样的——两者都只是在发生变化时发送通知。TTL 提供了明确的新鲜度提示。
  4. 与 Web 标准一致:HTTP 缓存(Cache-Control: max-age)和 DNS TTL 长期以来都证明了,基于时间的新鲜度提示是一种简单且广为理解的减少不必要重新获取的机制。MCP 也可以从同样的模式中受益。
为列表响应添加 TTL 字段,以最小且向后兼容的协议变更解决了上述所有问题。

规范

新接口:CacheableResult

引入一个新的 CacheableResult 接口,作为扩展 Result 的独立类型。它拥有 ttlMscacheScope 字段。

架构变更(TypeScript)

语义

TTL 是新鲜度估计,而不是保证。服务器 MAY 在 TTL 到期前更改底层列表;如果服务器这样做并且已声明 listChanged,SHOULD 发送相应通知。 服务器 MUST 为 tools/listprompts/listresources/listresources/readresources/templates/list 返回的 Results 提供 ttlMs ttlMs MUST 大于等于 0。如果服务器返回负值,客户端 SHOULD 忽略它,并将其视为 0(立即过期)。

新鲜度计算

客户端记录接收响应时的本地时间(t_received)。当 now < t_received + ttlMs 时,响应被视为新鲜。一旦 TTL 过期,响应即为过期,客户端 SHOULD 在下次访问时重新获取。 客户端 SHOULD NOT 将 TTL 视为会触发自动后台重新获取的轮询间隔。TTL 是一个新鲜度提示:客户端在需要列表时检查其新鲜度,仅在过期时才重新获取。若实现选择轮询,SHOULD 使用抖动和退避。 即使 TTL 尚未到期,如果客户端有理由相信数据已发生变化,MAY 重新获取。例如,在工具调用中收到意外错误,提示该方法未找到或参数无效。 如果在重新获取结果时发生错误(例如网络问题、服务器宕机),客户端 MAY 提供过期响应。TTL 只是一个提示,说明客户端可以在多长时间内安全地依赖该数据,但现实条件可能需要灵活处理。

缓存作用域

cacheScope 字段控制谁可以缓存响应:
  • "public":响应不包含用户特定数据。任何客户端、共享网关或缓存代理 MAY 存储该缓存响应,并将其提供给任何用户。这适用于对所有用户都相同的工具、提示和资源模板列表。
  • "private":响应包含用户特定数据。只有请求该响应的用户的客户端 MAY 缓存它。共享缓存(例如多租户 API 网关)MUST NOT 将 "private" 缓存响应提供给不同的用户。这适用于依赖已认证用户的 resources/read 结果,或按用户变化的过滤列表结果。
此设计仿照 HTTP 的 Cache-Control: publicCache-Control: private,在 MCP 协议层应用同样广为理解的语义。

与通知的交互

TTL 与服务器推送通知是互补的:
  • 服务器 MAY 提供 ttlMs,而不在其能力声明中声明 listChanged: true。在这种情况下,客户端完全依赖 TTL。
  • 服务器 MAY 同时声明 listChanged: true 提供 ttlMs。在这种情况下,客户端可以使用 TTL 避免在通知之间进行不必要的重新获取,而通知则充当立即失效信号。

与分页的交互

当列表结果分页时(包含 nextCursor),每一页都是独立可缓存的响应——这与 HTTP Cache-Control 处理分页资源的方式一致。具体而言:
  • 每个页面响应都带有自己的 ttlMs 值。每一页的新鲜度时钟从该页被接收时开始。
  • 服务器 MAY 在不同页面上返回不同的 ttlMs 值(例如:稳定列表的前几页使用更长 TTL,最后一页使用更短 TTL)。
  • 不保证跨页一致性。如果底层数据在分页获取之间发生变化,客户端可能观察到重复或缺失——这与 HTTP 分页 API 面临的是同样的权衡。
  • 需要完整列表一致快照的客户端 SHOULD 从头重新获取(不带 cursor)。
  • 如果 cursor 变为无效(例如,服务器对先前有效的 cursor 返回错误),客户端 SHOULD 丢弃所有已缓存页面,并从头重新获取。
服务器 MUST 对同一列表请求的所有响应页面应用相同的 cacheScope。例如,如果 tools/list 响应的第一页具有 cacheScope: "private",则该请求的所有后续页面也 MUST 被视为 "private"

错误处理

  • 如果 ttlMs 存在但为负整数,客户端 SHOULD 忽略它,并按 0(立即过期)处理。

原因

为什么不替换 list_changed 通知?

通知提供即时失效,这对于长连接很有价值。TTL 提供了一种互补机制,针对无状态传输进行了优化,并用于减少不必要的轮询。这两种机制服务于不同的用例,并且可以自然共存。

为什么 TTL 使用整数毫秒?

我们选择整数毫秒而不是秒,因为我们希望在整个 MCP 协议中为 ttl 使用一个统一单位。Tasks 有亚秒级 TTL 的用例,而使用毫秒可以在 MCP 中为所有 TTL 提供一致的表示。 许多现有系统将整数秒用于 TTL,但也有一些(例如 gRPC retry pushback)使用毫秒。关键是为 MCP 中所有 TTL 选择一个单一且一致的单位。整数毫秒在保持实现和理解简单的同时,提供了所需的精度。

为什么不直接使用 HTTP 缓存?

MCP 与传输无关。虽然基于 HTTP 的传输理论上可以使用 Cache-Control 头,但 MCP 也可以通过 stdio 运行,并支持可插拔传输,在这些场景下 HTTP 头可能不可用。将 TTL 嵌入 JSON 响应体可确保其在所有传输方式下都能一致工作。

向后兼容性

  • 现有不提供它的服务器将继续按原样工作。如果 ttlMs 字段缺失,客户端 SHOULD 假定默认 ttlMs 为 0(立即过期),并依赖其自身的缓存启发式规则或通知,这也是当前行为。
  • 不理解该字段的现有客户端将忽略它,因为 MCP 的结果对象通过基础类型 Result 上的 [key: string]: unknown 允许额外属性。
  • cacheScope 是必需的,因为对于旧服务器没有安全的默认值。服务器必须显式声明预期的缓存范围,以防止意外缓存用户特定数据。
  • 不修改或移除任何现有字段或行为。
  • 不需要能力协商。
  • SDK 维护者可以选择在其 SDK 中为 ttl 和 cacheScope 添加默认值以简化采用,但这不是合规所必需的。

参考实现

尚无参考实现。

安全影响

配置错误或恶意的服务器可能会设置过长的 TTL,导致客户端比预期更久地缓存过期数据。然而,由于 TTL 只是一个提示,客户端可以选择忽略它,或在怀疑有变化时重新获取,因此安全风险很小。客户端应设计为能够优雅地处理意外的 TTL 值。