【随笔】MCP缓存期限与共享范围:让Agent复用资料时记住边界

上一篇讨论了资源更新通知:资料变了,Agent需要让旧缓存失效。还有一个常见问题------通知尚未到达时,刚读过的资料能否复用?多人共用一个网关时,这份缓存又能给谁看?

MCP 在2026-07-28协议中提供了ttlMs 与cacheScope,分别描述新鲜度与共享范围。本文依据2026年10月3日可查的官方规范及TypeScript SDK v2文档,使用本地教学模型解释缓存判断;不假定旧版MCP服务已经支持这些字段。

一、把缓存放在Host读取资料的路径上

Agent使用资料时,Host先确定要读取的资源,MCP Client发起resources/read,Server返回内容。缓存可以保留这个响应,让后续相同请求在条件合适时复用。官方Resources规范给出了携带缓存提示的读取响应。

下面只展示教学用的result片段,不是完整JSON-RPC请求或服务端实现:

json 复制代码
{
  "resultType": "complete",
  "contents": [
    {"uri": "docs://manual", "mimeType": "text/plain", "text": "使用手册第一版"}
  ],
  "ttlMs": 60000,
  "cacheScope": "public"
}

这里的60000单位是毫秒,表示收到响应后的新鲜度期限。public用于各调用者得到相同资料的情形;带个人配置或按权限筛选的资料要选择private。

二、ttlMs描述新鲜度,不承诺内容冻结

客户端以收到响应的本地时刻为起点,判断当前时间是否仍小于"接收时间+ttlMs"。到达边界时,缓存过期,下次需要资料时再取。ttlMs为0表示立即过期;面对旧服务缺失字段的情形,保守策略也可以按0处理。

时间轴用于说明边界,节点间距不代表真实时间比例。

资料可能在期限内提前变化,因此TTL不能替代更新通知。收到相关通知后,应立即使对应缓存失效。TTL也无需变成后台轮询计时器:没有新的读取需求,就不必为过期本身自动发请求。这些含义来自2026-07-28缓存规范。

三、cacheScope约束共享,身份隔离还要落到缓存键

public 允许不同调用者共享不含用户专属数据的响应;private要求在同一授权上下文内复用。网关若只用URI当键,Alice与Bob读取同一个config://settings,就可能碰到彼此的内容。

缓存键需要包含服务端命名空间、请求方法、影响结果的参数,以及private响应对应的授权分区。授权分区要能反映权限上下文变化,不能只凭显示名称判断;也不要把原始访问令牌直接写入日志。

public/private是缓存范围提示。服务端仍需要执行访问控制,Host也应在读取缓存前确认当前调用者有权访问资源。图中的分区盒表示隔离关系,不能替代权限检查。

TypeScript SDK v2提供cachePartition ,用来给共享responseCacheStore中的private响应设置分区。其ClientOptions说明还指出默认空分区适合单一授权主体;多个主体共享存储时需要明确设置。

四、一个可运行的本地缓存模型

下面的Python程序只演示正常complete响应的缓存存放与查询,没有连接真实MCP服务,也没有实现并发、持久化、授权验证或通知处理。假设每次查询前已经完成授权检查;alice与bob代表两个不同授权上下文,now_ms使用显式模拟时间,方便复现边界。

python 复制代码
from copy import deepcopy
import json


class DemoCache:
    def __init__(self):
        self.entries = {}

    @staticmethod
    def key(server, method, params, partition):
        arguments = json.dumps(params, sort_keys=True, separators=(",", ":"))
        return server, method, arguments, partition

    def put(self, server, uri, principal, result, now_ms):
        ttl = result.get("ttlMs", 0)
        scope = result.get("cacheScope", "private")
        if (result.get("resultType") != "complete"
                or type(ttl) is not int or ttl <= 0
                or scope not in ("public", "private")):
            return False
        partition = "shared" if scope == "public" else "auth:" + principal
        key = self.key(server, "resources/read", {"uri": uri}, partition)
        self.entries[key] = (now_ms + ttl, deepcopy(result))
        return True

    def get(self, server, uri, principal, now_ms):
        for partition in ("auth:" + principal, "shared"):
            key = self.key(server, "resources/read", {"uri": uri}, partition)
            entry = self.entries.get(key)
            if entry is None:
                continue
            expires_at, value = entry
            if now_ms >= expires_at:
                del self.entries[key]
                continue
            return deepcopy(value)
        return None


cache = DemoCache()
private = {"resultType": "complete", "ttlMs": 1000,
           "cacheScope": "private", "contents": [{"text": "alice settings"}]}
cache.put("catalog-v1", "config://settings", "alice", private, 0)
print("alice before expiry:", cache.get("catalog-v1", "config://settings", "alice", 999) is not None)
print("bob same URI:", cache.get("catalog-v1", "config://settings", "bob", 999) is not None)
print("alice at expiry:", cache.get("catalog-v1", "config://settings", "alice", 1000) is not None)

public = {"resultType": "complete", "ttlMs": 1000,
          "cacheScope": "public", "contents": [{"text": "shared manual"}]}
cache.put("catalog-v1", "docs://manual", "alice", public, 0)
print("bob shared manual:", cache.get("catalog-v1", "docs://manual", "bob", 999) is not None)
print("other server:", cache.get("another-server", "docs://manual", "bob", 999) is not None)
print("missing TTL stored:", cache.put("catalog-v1", "docs://other", "alice",
      {"resultType": "complete", "cacheScope": "public"}, 0))

保存为cache_hints_demo.py,在Python3.12.14实际运行输出:

text 复制代码
alice before expiry: True
bob same URI: False
alice at expiry: False
bob shared manual: True
other server: False
missing TTL stored: False

三个对比解释了结果:999毫秒时Alice自己的条目仍新鲜;Bob使用另一分区,读不到Alice的private内容;1000毫秒恰好达到过期边界,Alice也需要重新读取。共享手册可以复用,但another-server的同名URI仍属于另一个命名空间。

这个模型采用了保守选择:过期就返回未命中,缺失TTL不存入缓存。生产系统若允许在读取失败时继续展示旧资料,需要显式说明过期状态与适用场景,避免旧配置静默参与决策。

五、SDK上手与上线前需要确认的细节

在已经建立连接的TypeScript SDK v2客户端上,可以按每次调用选择缓存模式:

typescript 复制代码
await client.readResource({ uri: 'docs://manual' });
await client.readResource({ uri: 'docs://manual' }, { cacheMode: 'refresh' });
await client.readResource({ uri: 'docs://manual' }, { cacheMode: 'bypass' });

默认模式可使用新鲜响应;refresh强制读取并更新缓存;bypass读取时不读写缓存。这是SDK v2缓存文档中的调用语义,代码片段没有在本文连接真实服务执行。

检查点 应当确认的内容
协议与SDK版本 确认实际连接的协议版本,旧服务缺少提示时采用明确策略
缓存键 方法、URI、cursor等影响结果的参数不能混用
授权分区 权限上下文变化时隔离或清理private条目
分页列表 每页有独立接收时间,分页面缓存不等于整体快照
更新通知 即便TTL未到,也要使对应条目失效
输入往返 input_required以及携带inputResponses/requestState的重试结果不纳入普通缓存

缓存收益要通过实际请求次数、命中率与可接受的资料时效评估。本文只验证模型的隔离与过期行为,没有给出延迟改善或节省Token的测量结论。

六、🧠 思维导图

#mermaid-svg-V8Rq0IICfbD0sXtQ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-V8Rq0IICfbD0sXtQ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-V8Rq0IICfbD0sXtQ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-V8Rq0IICfbD0sXtQ .error-icon{fill:#552222;}#mermaid-svg-V8Rq0IICfbD0sXtQ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-V8Rq0IICfbD0sXtQ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-V8Rq0IICfbD0sXtQ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-V8Rq0IICfbD0sXtQ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-V8Rq0IICfbD0sXtQ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-V8Rq0IICfbD0sXtQ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-V8Rq0IICfbD0sXtQ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-V8Rq0IICfbD0sXtQ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-V8Rq0IICfbD0sXtQ .marker.cross{stroke:#333333;}#mermaid-svg-V8Rq0IICfbD0sXtQ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-V8Rq0IICfbD0sXtQ p{margin:0;}#mermaid-svg-V8Rq0IICfbD0sXtQ .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-V8Rq0IICfbD0sXtQ .cluster-label text{fill:#333;}#mermaid-svg-V8Rq0IICfbD0sXtQ .cluster-label span{color:#333;}#mermaid-svg-V8Rq0IICfbD0sXtQ .cluster-label span p{background-color:transparent;}#mermaid-svg-V8Rq0IICfbD0sXtQ .label text,#mermaid-svg-V8Rq0IICfbD0sXtQ span{fill:#333;color:#333;}#mermaid-svg-V8Rq0IICfbD0sXtQ .node rect,#mermaid-svg-V8Rq0IICfbD0sXtQ .node circle,#mermaid-svg-V8Rq0IICfbD0sXtQ .node ellipse,#mermaid-svg-V8Rq0IICfbD0sXtQ .node polygon,#mermaid-svg-V8Rq0IICfbD0sXtQ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-V8Rq0IICfbD0sXtQ .rough-node .label text,#mermaid-svg-V8Rq0IICfbD0sXtQ .node .label text,#mermaid-svg-V8Rq0IICfbD0sXtQ .image-shape .label,#mermaid-svg-V8Rq0IICfbD0sXtQ .icon-shape .label{text-anchor:middle;}#mermaid-svg-V8Rq0IICfbD0sXtQ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-V8Rq0IICfbD0sXtQ .rough-node .label,#mermaid-svg-V8Rq0IICfbD0sXtQ .node .label,#mermaid-svg-V8Rq0IICfbD0sXtQ .image-shape .label,#mermaid-svg-V8Rq0IICfbD0sXtQ .icon-shape .label{text-align:center;}#mermaid-svg-V8Rq0IICfbD0sXtQ .node.clickable{cursor:pointer;}#mermaid-svg-V8Rq0IICfbD0sXtQ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-V8Rq0IICfbD0sXtQ .arrowheadPath{fill:#333333;}#mermaid-svg-V8Rq0IICfbD0sXtQ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-V8Rq0IICfbD0sXtQ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-V8Rq0IICfbD0sXtQ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-V8Rq0IICfbD0sXtQ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-V8Rq0IICfbD0sXtQ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-V8Rq0IICfbD0sXtQ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-V8Rq0IICfbD0sXtQ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-V8Rq0IICfbD0sXtQ .cluster text{fill:#333;}#mermaid-svg-V8Rq0IICfbD0sXtQ .cluster span{color:#333;}#mermaid-svg-V8Rq0IICfbD0sXtQ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-V8Rq0IICfbD0sXtQ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-V8Rq0IICfbD0sXtQ rect.text{fill:none;stroke-width:0;}#mermaid-svg-V8Rq0IICfbD0sXtQ .icon-shape,#mermaid-svg-V8Rq0IICfbD0sXtQ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-V8Rq0IICfbD0sXtQ .icon-shape p,#mermaid-svg-V8Rq0IICfbD0sXtQ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-V8Rq0IICfbD0sXtQ .icon-shape .label rect,#mermaid-svg-V8Rq0IICfbD0sXtQ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-V8Rq0IICfbD0sXtQ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-V8Rq0IICfbD0sXtQ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-V8Rq0IICfbD0sXtQ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} MCP缓存边界
ttlMs
接收时刻起算
过期后按需重取
cacheScope
public共享资料
private授权分区
缓存键
服务方法与参数
身份上下文隔离
更新与边界
通知立即失效
缓存不替代授权

七、总结

总结要点

时效与共享是两个判断。ttlMs帮助决定是否重新取资料,cacheScope帮助决定能在哪个范围复用,两者需要同时落实。

缓存键承载隔离。相同URI并不代表相同响应,服务来源、请求参数与授权上下文都可能影响结果。共享存储尤其需要明确分区。

及时失效保留主动刷新能力。更新通知与TTL可以配合使用,权限检查仍由服务端与Host执行。资料复用的前提,是内容、来源和适用范围都明确。

下一篇继续看看MCP分页目录的cursor,讨论资料持续变化时怎样完成一次可靠的目录读取。

👉 如果你觉得这篇文章对你有所帮助,欢迎点赞、收藏、分享!😊

相关推荐
llqbzllll43 分钟前
线程池里的“幽灵数据”:ThreadLocal 用完不 remove,为什么下个请求还能看到?
后端
量化分析码农1 小时前
【Python量化系统工程实战 #02】每天手动拉数据太烦?用 APScheduler 搭一条「自动采集 + 增量去重」的流水线
后端
YYYing.2 小时前
【设计模式系列 (九) 】装饰器模式
c++·后端·设计模式·装饰器模式·c/c++
逃逸线LOF2 小时前
工具类文件头文件
java
Wang's Blog2 小时前
Java 项目实战: 外卖平台优化-Nginx目录结构与conf配置文件体系
java·nginx
蜗牛互联网2 小时前
Gemini 4 Argon的1M输出窗口与长程Agent工程边界
java·人工智能·后端
elseif1232 小时前
【2026 CSP-J】【逐题精讲】超详细(15道选择,3道阅读,2道完善)
java·开发语言·c++·算法·csp
智鸟科技GemeOpen开发者智能设备2 小时前
MQTT智能插座GSPM1B2 · 开发者实战指南
java·开发语言·python·物联网
Thinker QAQ2 小时前
并发编程(七):volatile——从语言规则到 CPU
java·python·go·并发编程·volatile·memory model