上一篇讨论了资源更新通知:资料变了,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,讨论资料持续变化时怎样完成一次可靠的目录读取。
👉 如果你觉得这篇文章对你有所帮助,欢迎点赞、收藏、分享!😊