SEP-2549:列表结果的 TTL

SEP-2549:列表结果的 TTL

社区译文;来源: MCP 4fa48191

本 SEP 已达到 Final 状态,并作为已接受设计的历史记录保留。 定稿后对协议所做的更改不会反映在此处。权威要求请参阅当前规范 及其变更日志。
字段值
SEP2549
标题列表结果的 TTL
状态Final
类型标准跟踪
创建日期2026-04-09
作者Caitie McCaffrey (@CaitieM20)
发起人@CaitieM20
PR#2549

摘要

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

动机

目前,MCP 客户端通过调用服务器上的方法来发现服务器功能。这些调用返回当前的功能集合。 为了获知变化,客户端依赖服务器的推送通知。下表列出了服务器方法与通知类型之间的对应关系。

服务器方法通知类型
tools/listnotifications/tools/list_changed
prompts/listnotifications/prompts/list_changed
resources/listnotifications/resources/list_changed
resources/templates/listnotifications/resources/list_changed
resources/readnotifications/resources/updated

这种方式存在若干限制:

  1. 基于 HTTP 的传输需要 SSE stream:许多客户端和服务器难以支持通知所需的长连接 SSE stream。 目标是让 SSE stream 成为可选优化,同时在没有它的情况下也能支持协议功能。TTL 允许客户端按可预测的 计划轮询,而无需依赖服务器推送通知。

  2. 实现复杂度:客户端和服务器都必须实现通知订阅与传递基础设施。许多简单服务器的功能列表很少变化 (甚至从不变化),但如果希望客户端保持最新,仍必须支持通知机制。

  3. 缺少新鲜度信号:即使客户端能够接收通知,也无法知道列表有多“稳定”。工具列表每天变化一次的服务器 与每秒变化一次的服务器,在客户端看来完全相同;两者都只是在发生变化时发送通知。TTL 提供了明确的新鲜度提示。

  4. 与 Web 标准保持一致:HTTP 缓存(Cache-Control: max-age)和 DNS TTL 早已证明,基于时间的 新鲜度提示是一种简单、易于理解的方式,可以减少不必要的重新获取。MCP 可以从相同模式中受益。

向列表响应添加 TTL 字段,能够以最小且向后兼容的协议变更解决所有这些问题。

规范

新接口:CacheableResult

引入新的 CacheableResult 接口,作为扩展 Result 的独立类型。它拥有 ttlMs 和 cacheScope 字段。

Schema 变更(TypeScript)

/**
 * A result that supports a time-to-live (TTL) hint for client-side caching.
 *
 * @internal
 */
export interface CacheableResult extends Result {
  /**
   * A hint from the server indicating how long (in milliseconds) the
   * client MAY cache this response before re-fetching. Semantics are
   * analogous to HTTP Cache-Control max-age.
   *
   * - If 0, The response SHOULD be considered immediately stale, The client
   *   MAY re-fetch every time the result is needed.
   * - If positive, the client SHOULD consider the result fresh for this many
   *   milliseconds after receiving the response.
   */
  ttlMs: number & { readonly minimum: 0 };

  /**
   * Indicates the intended scope of the cached response, analogous to HTTP
   * Cache-Control: public vs Cache-Control: private.
   *
   * - "public": Any client or intermediary (e.g., shared gateway, proxy)
   *   MAY cache the response and serve it to any user.
   * - "private": Only the requesting user's client MAY cache the response.
   *   Shared caches (e.g., multi-tenant gateways) MUST NOT serve a cached
   *   copy to a different user.
   *
   * Defaults to "public" if absent.
   */
  cacheScope: "public" | "private";
}

语义

TTL 是新鲜度估计,而不是保证。服务器**可以(MAY)**在 TTL 到期前更改底层列表;已公布 listChanged 的服务器如果这样做,**应该(SHOULD)**发送相应通知。

服务器在 tools/list、prompts/list、resources/list、resources/read 和 resources/templates/list 返回的 Results 中**必须(MUST)**提供 ttlMs。

ttlMs **必须(MUST)大于或等于 0。如果服务器返回负值,客户端应该(SHOULD)**忽略该值, 并将其按 0(立即过期)处理。

条件客户端行为
ttlMs = 0响应**应该(SHOULD)被视为立即过期。客户端可以(MAY)**在每次需要结果时重新获取。
ttlMs > 0客户端**应该(SHOULD)**从收到响应时起,在 ttlMs 毫秒内将响应视为新鲜状态。
TTL 有效期间收到相关通知该通知使缓存响应失效。无论 TTL 还剩多少,客户端都**应该(SHOULD)**重新获取。
cacheScope = "public"任何客户端或共享中间层(网关、代理)都**可以(MAY)**缓存响应,并将其提供给任何用户。
cacheScope = "private"只有发起请求的用户客户端**可以(MAY)缓存。共享缓存不得(MUST NOT)**将缓存副本提供给其他用户。

新鲜度计算

客户端记录收到响应时的本地时间(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: public 与 Cache-Control: private,在 MCP 协议层应用同样 广为理解的语义。

与通知的交互

TTL 与服务器推送通知互为补充:

  • 服务器**可以(MAY)**提供 ttlMs,而不在其 capabilities 中公布 listChanged: true。在这种情况下, 客户端完全依赖 TTL。
  • 服务器**可以(MAY)**公布 listChanged: true,同时提供 ttlMs。在这种情况下,客户端可以使用 TTL 避免在通知之间进行不必要的重新获取,而通知则充当立即失效信号。
  sequenceDiagram
    participant C as Client
    participant S as Server

    C->>S: tools/list
    S-->>C: { tools: [...], ttlMs: 300000 }
    Note over C: Cache response, fresh for 5 min

    Note over C: 2 minutes later...
    C->>C: Need tools list → cache still fresh, use cached

    Note over C: 3 minutes later (TTL expired)...
    C->>C: Need tools list → cache stale
    C->>S: tools/list
    S-->>C: { tools: [...], ttlMs: 300000 }

    Note over S: Tools change before TTL expires
    S-->>C: notifications/tools/list_changed
    Note over C: Invalidate cache immediately
    C->>S: tools/list
    S-->>C: { tools: [...], ttlMs: 300000 }

与分页的交互

当列表结果分页(包含 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)**假定默认 ttlMs 为 0(立即过期), 并依赖自身的缓存启发式策略或通知。
  • 如果存在 ttlMs,但它是负整数,客户端**应该(SHOULD)**忽略该值,并按 0(立即过期)处理。

设计理由

为什么不替代 list_changed 通知?

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

为什么 TTL 使用整数毫秒?

我们选择整数毫秒而不是秒,是因为希望 MCP 协议中的 TTL 统一使用一种单位。Tasks 存在亚秒 TTL 的使用场景, 使用毫秒可以让 MCP 中所有 TTL 采用一致表示。

许多现有系统的 TTL 使用整数秒,但有些系统(例如 gRPC retry pushback)使用毫秒。关键在于为 MCP 中的所有 TTL 选择一种一致的单位。整数毫秒既提供了所需精度,又保持实现和理解的简单性。

系统机制说明
HTTP Cache-Control: max-age整数秒Web 基础设施中部署最广泛的新鲜度提示
DNS TTL整数秒控制解析器缓存 DNS 记录的时长
GraphQL @cacheControlmaxAge 整数秒GraphQL 响应中的逐字段缓存提示
gRPC grpc-retry-pushback-ms毫秒服务器提供的重试提示(使用场景不同,模式类似)

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

MCP 与传输无关。虽然基于 HTTP 的传输理论上可以使用 Cache-Control 标头,但 MCP 也通过 stdio 运行, 并支持可能无法使用 HTTP 标头的可插拔传输。将 TTL 嵌入 JSON 响应正文,可以确保它在所有传输上统一工作。

向后兼容性

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

参考实现

目前尚无参考实现。


安全影响

配置错误或恶意服务器可能设置过长的 TTL,导致客户端缓存过期数据的时间超出预期。不过,由于 TTL 是提示, 客户端在怀疑数据变化时可以选择忽略它或重新获取,因此安全风险很小。客户端应能妥善处理异常 TTL 值。