云计算百科
云计算领域专业知识百科平台

【随笔】MCP资源更新订阅:通知到达以后,Agent怎样刷新旧资料

上一篇随笔用URI串起了MCP Resources的发现、读取与来源记录。资料能够读出来之后,还会遇到一个很实际的问题:配置和文档发生变化,Agent手里的缓存怎样知道该刷新?

MCP的资源更新订阅为客户端提供变化线索。本文依据2026-10-02核对的2026-07-28协议版本及官方SDK文档,分析统一订阅流、缓存失效和断线后的重新读取。示例用Python教学模拟器验证缓存处理,没有连接真实MCP服务。

一、从主动读取走向按需监听

客户端可以定期读取资料,也可以订阅自己关心的资源变化。订阅能减少无目的的重复检查,但应用仍要决定收到通知以后读什么、何时读,以及旧结果还能不能继续使用。

2026-07-28版本引入subscriptions/listen统一通知订阅流。旧版代码常见的resources/subscribe属于旧协议路径,不能直接混进新版连接。TypeScript SDK v2也需要明确选择新版协议行为,升级包版本并不自动切换协议。版本差异见官方v2迁移说明。

这点对教程阅读尤其重要:先看资料使用的协议版本,再对照所安装SDK的API。把两套方法拼到一起,可能得到能通过类型检查却无法正确协作的客户端。

二、目录变化与内容变化是两件事

资源目录描述“当前有哪些可发现条目”,资源内容描述“这个URI现在能读到什么”。两种变化应该分别处理。

变化订阅过滤字段通知客户端常见动作
可发现目录变化 resourcesListChanged notifications/resources/list_changed 重新列出目录
已关注资源内容变化 resourceSubscriptions中的URI notifications/resources/updated 让对应缓存失效并重新读取

服务端的resources.listChanged和resources.subscribe能力可以分别声明。声明其中一项并不意味着另一项必然可用;请求了过滤条件也要核对服务端实际接受的范围。能力与资源消息见官方Resources规范。

MCP订阅流中目录变化与资源内容变化的两条刷新路径

图中目录变化走resources/list,内容变化走resources/read。两条路径服务不同的数据层次,URI更新通知不会替代目录查询。

三、先收到确认,再建立当前基线

下面是统一订阅请求的JSON-RPC结构示意,订阅目录变化和一个特定URI:

{
"jsonrpc": "2.0",
"id": "watch-1",
"method": "subscriptions/listen",
"params": {
"notifications": {
"resourcesListChanged": true,
"resourceSubscriptions": ["docs://service/config"]
},
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": {
"name": "resource-demo",
"version": "1.0.0"
},
"io.modelcontextprotocol/clientCapabilities": {}
}
}
}

这段只展示消息体;HTTP接入还需要传输层规定的请求头、身份验证与流式读取。它不是可以单独运行的完整客户端。

服务端先发送notifications/subscriptions/acknowledged,其中报告接受的过滤条件;后续流内通知通过_meta中的io.modelcontextprotocol/subscriptionId关联订阅。一个流只承载确认以后发生的变化,不能当作历史事件重放日志。官方Ruby SDK订阅文档展示了这套消息结构与交付语义。

应用可以先完成监听确认,再读取当前目录和关注的资源,建立缓存基线。读取期间如果又收到更新,应该保留失效状态,必要时再次读取,避免旧读取结果覆盖刚刚到达的变化线索。

四、通知表示需要刷新,正文仍然要读

资源内容变化通知可以只包含URI:

{
"jsonrpc": "2.0",
"method": "notifications/resources/updated",
"params": {
"uri": "docs://service/config",
"_meta": {
"io.modelcontextprotocol/subscriptionId": "watch-1"
}
}
}

客户端收到它以后,可以把对应缓存标记为过期,再调用resources/read取得当前内容。重复通知可以合并处理;不能把通知次数当作配置修改次数,也不能假定每次通知都有一份完整的新正文。

导师猫收到URI变化提示后标记旧缓存失效,学习猫重新读取最新资料

图中的提示卡只传递“这个URI有变化”。旧缓存随后进入待刷新状态,重新读取成功以后,客户端才更新可用资料。

五、可运行模拟器:拦住迟到的旧结果

下面的Python标准库示例用“本地失效计数”识别读取期间到达的通知。这个计数只是应用内部标记,不属于MCP协议字段,也不代表服务端内容版本。

class ResourceCache:
def __init__(self):
self.generation = {}
self.values = {}

def invalidate(self, uri):
self.generation[uri] = self.generation.get(uri, 0) + 1
self.values.pop(uri, None)

def begin_read(self, uri):
return self.generation.get(uri, 0)

def finish_read(self, uri, generation, text):
if generation != self.generation.get(uri, 0):
return False
self.values[uri] = text
return True

def reconnect(self):
for uri in set(self.generation) | set(self.values):
self.invalidate(uri)

uri = "docs://service/config"
cache = ResourceCache()

first = cache.begin_read(uri)
assert cache.finish_read(uri, first, "version 1")
print("initial:", cache.values[uri])

slow_read = cache.begin_read(uri)
cache.invalidate(uri) # 读取尚未返回时,收到updated通知。
accepted = cache.finish_read(uri, slow_read, "version 1")
print("late result accepted:", accepted)

fresh_read = cache.begin_read(uri)
assert cache.finish_read(uri, fresh_read, "version 2")
print("refreshed:", cache.values[uri])

cache.reconnect() # 断线期间可能漏掉变化,重新建立基线。
print("after reconnect:", cache.values.get(uri, "<needs read>"))

assert not accepted
assert uri not in cache.values

运行输出:

initial: version 1
late result accepted: False
refreshed: version 2
after reconnect: <needs read>

本例已在Python 3.12.14运行。示例先缓存version 1,再模拟一次读取尚未结束就收到更新通知的情况。通知使本地失效计数变化,迟到的旧结果被拒绝;重新读取才能写入version 2。

这里按顺序模拟事件,没有实现线程安全、网络连接或持久化。在异步客户端中,还应限制同一个URI的并行刷新,或增加读取任务序号,避免两个相同失效计数的读取结果乱序覆盖。刷新失败时保留失效状态,并明确区分“旧资料可降级使用”与“业务必须等待新资料”。

六、断线恢复与Agent上下文需要一起处理

断线重连之后,仅重新打开流还不够。遗漏的通知不会自动补齐,应用需要重新读取依赖的资源;官方Python SDK订阅说明也明确要求重新监听与重新获取依赖状态。

实际接入可以依次完成:重新检查可用能力、确认新的订阅范围、读取当前基线、处理建立基线期间的新通知。对于不支持订阅的服务端,再依据业务时效要求选择轮询或有期限的缓存。

客户端缓存刷新也不会自动改写已经提交给模型的上下文。应用要决定下一轮任务使用哪个版本,必要时重新组装上下文,并保存URI、读取时间与内容版本线索。长任务中可为同一阶段固定资料快照,减少中途混入不同版本的风险。

此外,权限变化、URI删除、刷新失败与大量重复通知都需要明确处理。缓存键应包含适当的租户或权限范围;刷新请求继续执行访问控制,订阅确认不能当作永久读取授权。应用可以合并同一URI的待刷新任务,再设置适当的重试与节流策略。

七、🧠 思维导图

#mermaid-svg-q1dYrH5P3ntHqWpL{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-q1dYrH5P3ntHqWpL .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-q1dYrH5P3ntHqWpL .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-q1dYrH5P3ntHqWpL .error-icon{fill:#552222;}#mermaid-svg-q1dYrH5P3ntHqWpL .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-q1dYrH5P3ntHqWpL .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-q1dYrH5P3ntHqWpL .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-q1dYrH5P3ntHqWpL .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-q1dYrH5P3ntHqWpL .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-q1dYrH5P3ntHqWpL .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-q1dYrH5P3ntHqWpL .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-q1dYrH5P3ntHqWpL .marker{fill:#333333;stroke:#333333;}#mermaid-svg-q1dYrH5P3ntHqWpL .marker.cross{stroke:#333333;}#mermaid-svg-q1dYrH5P3ntHqWpL svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-q1dYrH5P3ntHqWpL p{margin:0;}#mermaid-svg-q1dYrH5P3ntHqWpL .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-q1dYrH5P3ntHqWpL .cluster-label text{fill:#333;}#mermaid-svg-q1dYrH5P3ntHqWpL .cluster-label span{color:#333;}#mermaid-svg-q1dYrH5P3ntHqWpL .cluster-label span p{background-color:transparent;}#mermaid-svg-q1dYrH5P3ntHqWpL .label text,#mermaid-svg-q1dYrH5P3ntHqWpL span{fill:#333;color:#333;}#mermaid-svg-q1dYrH5P3ntHqWpL .node rect,#mermaid-svg-q1dYrH5P3ntHqWpL .node circle,#mermaid-svg-q1dYrH5P3ntHqWpL .node ellipse,#mermaid-svg-q1dYrH5P3ntHqWpL .node polygon,#mermaid-svg-q1dYrH5P3ntHqWpL .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-q1dYrH5P3ntHqWpL .rough-node .label text,#mermaid-svg-q1dYrH5P3ntHqWpL .node .label text,#mermaid-svg-q1dYrH5P3ntHqWpL .image-shape .label,#mermaid-svg-q1dYrH5P3ntHqWpL .icon-shape .label{text-anchor:middle;}#mermaid-svg-q1dYrH5P3ntHqWpL .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-q1dYrH5P3ntHqWpL .rough-node .label,#mermaid-svg-q1dYrH5P3ntHqWpL .node .label,#mermaid-svg-q1dYrH5P3ntHqWpL .image-shape .label,#mermaid-svg-q1dYrH5P3ntHqWpL .icon-shape .label{text-align:center;}#mermaid-svg-q1dYrH5P3ntHqWpL .node.clickable{cursor:pointer;}#mermaid-svg-q1dYrH5P3ntHqWpL .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-q1dYrH5P3ntHqWpL .arrowheadPath{fill:#333333;}#mermaid-svg-q1dYrH5P3ntHqWpL .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-q1dYrH5P3ntHqWpL .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-q1dYrH5P3ntHqWpL .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-q1dYrH5P3ntHqWpL .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-q1dYrH5P3ntHqWpL .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-q1dYrH5P3ntHqWpL .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-q1dYrH5P3ntHqWpL .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-q1dYrH5P3ntHqWpL .cluster text{fill:#333;}#mermaid-svg-q1dYrH5P3ntHqWpL .cluster span{color:#333;}#mermaid-svg-q1dYrH5P3ntHqWpL 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-q1dYrH5P3ntHqWpL .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-q1dYrH5P3ntHqWpL rect.text{fill:none;stroke-width:0;}#mermaid-svg-q1dYrH5P3ntHqWpL .icon-shape,#mermaid-svg-q1dYrH5P3ntHqWpL .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-q1dYrH5P3ntHqWpL .icon-shape p,#mermaid-svg-q1dYrH5P3ntHqWpL .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-q1dYrH5P3ntHqWpL .icon-shape .label rect,#mermaid-svg-q1dYrH5P3ntHqWpL .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-q1dYrH5P3ntHqWpL .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-q1dYrH5P3ntHqWpL .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-q1dYrH5P3ntHqWpL :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

MCP资源更新订阅

版本与能力

2026协议统一监听

目录与内容分别声明

消息流程

listen与确认

updated提示重新读取

缓存处理

URI对应缓存失效

拒绝迟到旧结果

恢复与上下文

断线后重建基线

重新组织Agent资料

八、总结

总结要点

统一订阅流让客户端明确表达想接收哪些变化。实际可用范围依赖协议版本、服务端能力与订阅确认结果。

更新通知与缓存失效构成刷新链路。通知提供变化线索,resources/read提供当前内容;迟到的旧读取结果需要额外防护。

断线与上下文更新由应用完成。重新监听以后重建基线,再决定怎样把新资料交给Agent,才能让协议消息落实为可用的资料管理流程。

下一篇随笔继续讨论MCP的缓存期限与作用范围,看看ttlMs和cacheScope怎样影响资料复用。

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

赞(0)
未经允许不得转载:网硕互联帮助中心 » 【随笔】MCP资源更新订阅:通知到达以后,Agent怎样刷新旧资料
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!