静态资源定义对于保持不变的数据(如配置文件或历史记录)运作良好。但是,现代应用程序经常处理状态快速变化的数据,例如应用程序日志、股票行情或协作文档。依赖客户端重复请求同一资源以检查更新(这种技术称为轮询)会产生不必要的网络流量并增加延迟。模型上下文协议通过订阅和通知机制解决此问题,允许服务器仅在相关数据发生变化时通知客户端。MCP中的发布-订阅模式MCP架构采用一种修改过的发布-订阅模式。与传统持续推送数据的流式传输不同,MCP将变更通知与数据获取分离。当资源发生变化时,服务器会向客户端发送一个轻量级通知。然后,如果客户端需要更新的内容,它需自行发起新的读取请求。这种分离避免了服务器在高频更新期间用大量数据负载使客户端负担过重。客户端可自行决定何时占用带宽来获取新上下文。下图演示了MCP客户端和服务器在订阅生命周期中的消息流。digraph G { rankdir=LR; node [shape=box, style=filled, fontname="Helvetica", fontsize=10]; edge [fontname="Helvetica", fontsize=9, color="#adb5bd"]; subgraph cluster_0 { label = "初始化"; color = "#e9ecef"; style = filled; Server [label="MCP Server", fillcolor="#4dabf7", fontcolor="white", color="#4dabf7"]; Client [label="MCP 客户端\n(LLM 主机)", fillcolor="#37b24d", fontcolor="white", color="#37b24d"]; } subgraph cluster_1 { label = "事件循环"; color = "#f8f9fa"; style = filled; Resource [label="数据源\n(例如,日志文件)", fillcolor="#ff6b6b", fontcolor="white", color="#ff6b6b"]; } Client -> Server [label="1. resources/subscribe (uri)"]; Server -> Client [label="2. 确认"]; Resource -> Server [label="3. 数据已修改"]; Server -> Client [label="4. notifications/resources/updated (uri)"]; Client -> Server [label="5. resources/read (uri)"]; Server -> Client [label="6. 更新后的内容负载"]; }该序列演示了通知与数据获取的分离。服务器通知客户端发生变化,从而触发后续的读取请求。声明订阅能力在客户端订阅资源之前,服务器必须明确声明它支持资源通知。这发生在初始化握手期间。在能力协商阶段,服务器会包含一个 resources 对象,其中 subscribe 布尔值设置为 true。如果使用标准Python SDK,此配置通常在您注册资源处理器时自动处理。然而,理解底层的JSON-RPC结构对调试很有帮助。服务器初始化响应包含:$$ \text{能力} = { \text{"资源"}: { \text{"订阅"}: \text{true}, \text{"列表已更改"}: \text{true} } } $$listChanged 能力与资源订阅不同。它通知客户端可用资源列表(清单)已更改,而 subscribe 关联的是特定资源URI的内容。处理订阅请求当用户或LLM代理认为特定资源与当前对话相关时,客户端会发送一个 resources/subscribe 请求,其中包含目标资源的 uri。收到此请求后,服务器不需要立即返回数据。其主要任务是将连接注册为该URI的活动观察者。在Python实现中,服务器必须维护资源URI与活动客户端会话之间的映射。考虑一个开放动态系统日志的服务器。您首先定义资源读取逻辑:from mcp.server.fastmcp import FastMCP mcp = FastMCP("SystemMonitor") # 模拟日志文件的内部状态 log_state = ["Server started", "Initialization complete"] @mcp.resource("system://logs/recent") def get_recent_logs() -> str: """返回最新的日志条目。""" return "\n".join(log_state)在此状态下,资源是被动的。客户端可以读取它,但将不知道 log_state 是否更改。触发通知要使此资源具有响应性,您必须在内部状态更改时触发通知。MCP SDK提供了广播此更新的方法。该方法通常接受被修改资源的URI。当数据源更新时,服务器执行通知逻辑。这通常与外部事件循环、文件系统监视器或数据库触发器关联。import asyncio async def append_log(message: str): """更新状态并通知订阅者。""" log_state.append(message) # 通知客户端此特定资源已更改 # 客户端很可能会在此之后立即发出新的读取请求 await mcp.server.request_context.session.send_resource_updated( "system://logs/recent" ) # 模拟系统活动的示例后台任务 async def generate_traffic(): while True: await asyncio.sleep(5) await append_log("Heartbeat check: OK")在此示例中,send_resource_updated 构建JSON-RPC通知。传输层发送的负载如下所示:{ "method": "notifications/resources/updated", "params": { "uri": "system://logs/recent" } }请注意,负载不包含文本“Heartbeat check: OK”。它只包含URI。此设计最大限度地减少了带宽使用。如果客户端当前忙碌或用户已暂停代理,客户端可以选择忽略通知或将读取请求排队以供稍后处理。管理并发和粒度在实现订阅时,您必须考虑URI的粒度。如果资源代表一个大型数据集,例如一个10MB的CSV文件,为每次单行编辑触发通知将强制客户端重复重新下载整个文件。这会造成性能瓶颈。对于快速变化的数据,请考虑两种策略:细粒度资源: 将数据分解成更小、可寻址的块(例如,data://rows/50-100),以便更新只触发少量读取。防抖: 在服务器端累积更改,并每隔几秒发送一次通知,而不是在每次微小更新时都发送。取消订阅该协议也支持 resources/unsubscribe。当上下文窗口被清除或用户从上下文中明确移除资源时,标准客户端将取消订阅。您的服务器实现通常依靠SDK来处理从订阅列表中移除会话,从而防止服务器持续尝试通知已断开连接或不感兴趣的客户端而导致的内存泄漏。通过检查器验证您可以使用MCP检查器验证您的订阅逻辑。连接您的服务器后:导航到资源选项卡。找到您的目标资源(例如,system://logs/recent)。点击订阅按钮。在您的服务器上触发一个修改数据的操作。观察检查器日志。您应该会看到一条 notifications/resources/updated 消息,紧接着是检查器自动发起的 resources/read 请求。这个循环确认您的服务器正确地连接了内部数据变化和外部LLM上下文之间的关系。