MCP 资源模板与订阅:URI 模板、ListChanged 通知与上下文注入

系统讲解 MCP Resources 的进阶机制:Resource Template(URI 模板)与动态资源、resources/listChanged 订阅通知、资源读取与内容协商、资源如何注入 LLM 上下文、与 Tools 和 Prompts 的协同,以及资源缓存与失效策略。

1. 从静态资源到资源模板

基础 MCP 资源是「固定的 URI → 固定内容」。但真实世界的资源是动态的:配置文件随环境变、文档按 ID 取、用户数据按参数查。

Resource Template(资源模板) 用 URI 模板表达「一类可寻址资源」,让客户端按需取任意子资源。

一句话:静态资源是「文件名」,资源模板是「路径模式」——模板让资源的寻址从枚举升级为参数化。

1.1 两种资源形态

形态URI 示例说明
静态资源config://app/settings一个固定资源
资源模板doc://{id}一类资源,id 可变

1.2 模板定义(服务器端)

server.registerResourceTemplate(
  {
    uriTemplate: "doc://{id}.{ext}",
    name: "文档资源",
    description: "按 id 访问文档",
  },
  async (uri) => {
    // uri = doc://42.md
    const doc = await loadDocument(uri);
    return {
      contents: [{
        uri: uri,
        mimeType: "text/markdown",
        text: doc,
      }],
    };
  }
);

1.3 客户端读取

const res = await client.readResource({ uri: "doc://42.md" });
// 服务器按模板匹配 doc://{id}.{ext} → 解析 id=42, ext=md

一句话:模板的语法是 RFC 6570 URI 模板({var}),服务器解析匹配后动态生成内容——资源从「枚举」变成「按需计算」。

2. 资源模板语法与匹配

2.1 常用模板模式

模式示例说明
路径参数doc://{id}单参数
多参数org://{org}/user/{user}复合路径
扩展名doc://{id}.{ext}后缀分离
前缀通配db://{env}/*子资源

2.2 服务器匹配流程

客户端读 doc://42.md
  服务器匹配模板 doc://{id}.{ext}
  → 解析变量:id=42, ext=md
  → 调用 handler(可用变量拼 SQL/路径/API 参数)
  → 返回资源内容

2.3 变量校验

// 解析后校验参数合法性
const match = template.match(uri);
if (!match) return { contents: [] };
const id = match.id;
if (!isValidId(id)) {
  return { contents: [], isError: true, error: "invalid id" };
}

一句话:模板匹配 = 解析变量 → 校验 → 按变量取数——参数的合法性与安全性由服务器 handler 负责,不要信任 URI 中的任意值。

3. 订阅:ListChanged 通知

静态内容可以「客户端自己刷新」,动态资源需要服务器主动通知。

3.1 订阅机制

客户端请求订阅某资源列表
  → 服务器在资源集合变化时发 resources/listChanged 通知
  → 客户端收到通知 → 重新 listResources
  → 更新本地展示/上下文

3.2 服务器端实现

// 服务器:资源变化时推送通知
server.sendNotification(ResourceListChangedNotificationSchema, {
  _meta: { version: ++resourceVersion },
});

3.3 客户端实现

// 客户端:注册 listChanged 处理
client.setRequestHandler(ResourceListChangedNotificationSchema, async () => {
  const res = await client.listResources();
  refreshContext(res.resources);  // 更新上下文
});

3.4 订阅 vs 轮询

方式实时性成本适用
轮询有延迟持续请求低频变化
订阅通知即时事件推送高频/事件驱动

一句话:订阅把「客户端问有没有新东西」变成「服务器有变化就告诉你」——这是资源从静态快照走向实时数据的关键一跳。

4. 内容协商与读取细节

4.1 读取响应结构

// readResource 响应
{
  contents: [
    {
      uri: "doc://42.md",
      mimeType: "text/markdown",      // 内容类型
      text: "...",                     // 文本内容
      // 或 binary + encoding: "base64"
    }
  ]
}

4.2 二进制资源

- 图片/音频等二进制资源用 base64 编码传输
- 客户端按 mimeType 决定如何呈现
- 超大资源需考虑「是否值得进 LLM 上下文」

4.3 资源注入上下文

资源的用途:
  1. 直接注入 → 让 LLM 阅读该文件/数据
  2. 作为参考 → 工具调用时的参数依据
  3. 触发操作 → 资源的获取/变更驱动工作流

注入时注意 token 预算:只注入相关的部分

一句话:读取的语义由 mimeType 决定,注入的价值由「相关性」决定——资源进上下文前,先问「LLM 真的需要看全部吗」。

5. 与 Tools / Prompts 的协同

5.1 三种能力的分工

能力语义场景
Resources读数据(只读)配置、文档、快照
Tools执行操作(读写/副作用)查询、写入、计算
Prompts模板化指令结构化引导、复用流程

5.2 协同模式

例:代码分析场景
  Resources:read 代码文件(src://main.ts)
  Prompts :按「代码评审模板」引导
  Tools   :执行格式化/lint/搜索

例:运维场景
  Resources:read 服务状态(svc://api/status)
  Tools   :执行重启/扩容

5.3 何时用哪种

只读数据 → Resources(甚至用模板按需取)
需要动作 → Tools(副作用明确)
需要引导 → Prompts(流程复用)

一句话:三者不是并列的「三选一」,而是一套协作语言——读用 Resources、动用 Tools、引导用 Prompts,组合起来才覆盖完整场景。

6. 缓存与失效策略

6.1 为什么需要缓存

LLM 上下文昂贵 → 重复读同一资源不应反复注入
  → 客户端缓存资源内容(按 URI)
  → 结合订阅:变化时失效缓存

6.2 缓存策略

- 静态资源:长 TTL / 不失效
- 动态资源:短 TTL + listChanged 主动失效
- 大资源:缓存但摘要后注入
- 秘密/敏感:缓存不进日志

失效优先级:
  通知 > TTL > 客户端主动刷新

6.3 示例

const cache = new Map<string, { content, expiresAt }>();

async function readResourceCached(client, uri, ttlMs) {
  const hit = cache.get(uri);
  if (hit && hit.expiresAt > Date.now()) return hit.content;
  const res = await client.readResource({ uri });
  cache.set(uri, { content: res, expiresAt: Date.now() + ttlMs });
  return res;
}

一句话:缓存是「资源进上下文」的成本控制器——订阅通知失效最及时,TTL 兜底,敏感资源特殊处理。

7. 常见陷阱

陷阱症状解决
模板与 URI 不匹配返回空仔细核对模板语法
未校验模板参数注入攻击/错误handler 内严格校验
无订阅就等通知资源不更新需先订阅 + 注册 handler
全量注入大资源上下文爆炸摘要 / 只注入相关部分
缓存无失效读到旧数据TTL + listChanged 失效
二进制当文本乱码按 mimeType + base64

8. 总结

MCP 资源进阶机制可以概括为「模板寻址、订阅联动、注入克制」:

层面要点
资源模板URI 模板让资源按需参数化
订阅通知listChanged 让资源实时联动
内容协商mimeType 决定读取与呈现
三能力协同读用 Resource、动用 Tool、引导用 Prompt
缓存失效通知 > TTL > 手动刷新

Resources 从静态走向动态,靠模板与订阅两件事;从「能读」走向「有用」,靠上下文注入的克制与三能力协同。把模板、订阅、注入三件事做对,MCP 的资源能力才真正为 Agent 所用。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「AI工程」更多文章

  1. MCP 的 OAuth 鉴权与会话:动态注册、PKCE 与令牌轮换
  2. MCP 服务器测试框架:in-memory 传输、协议断言与端到端测试
  3. MCP 客户端 SDK 深入:TypeScript 客户端 API、传输状态机与错误处理