Resources、URI 与订阅
学习目标
- 区分 Resource、Resource Template、Resource Contents 和 Tool 返回的 resource link。
- 正确使用 resources/list、resources/templates/list、resources/read 的数据流。
- 理解 URI 是协议标识,不自动等于本地文件路径或可访问权限。
- 按 2026-07-28 的 subscriptions/listen、缓存、分页和更新通知模型设计读取流程。
前置知识
需要阅读 Tools 与 JSON Schema 2020-12 和 server/discover 与能力发现。代码用 Python 标准库做 URI 检查。
核心知识点
Resource 是上下文,不是动作
Resource 是 Server 暴露给 Host/模型使用的上下文数据,例如文档、数据库 schema、代码文件或应用状态快照。它有唯一 URI 和描述信息,内容可以是:
- TextResourceContents:text 字符串;
- BlobResourceContents:base64 编码的 blob;
- 结果中也可出现 ResourceLink,让 Client 以后按 URI 读取。
与 Tool 的区别是:Resource 主要回答“读什么”,Tool 主要回答“做什么”。Server 可以根据当前授权列出不同 Resource,但不能仅凭 URI 猜测调用方拥有文件系统权限。
URI、URI Template 和读取
resources/list 返回已知 URI,支持分页和缓存。resources/templates/list 返回 URI Template,例如 file:///{path},客户端可以对变量请求 completion/complete。resources/read 以完整 URI 读取内容。下面每个现代 Request 都要在 params._meta 带协议版本和 Client capabilities:
{
"jsonrpc": "2.0",
"id": 8,
"method": "resources/read",
"params": {
"uri": "memo://team/project/readme",
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": {}
}
}
}URI scheme 可以是 https、file、git 或自定义 scheme。自定义 scheme 仍需符合 RFC 3986。https 资源只有在 Client 能自行安全读取时才适合直接使用,否则应由 Server 处理权限后通过 resources/read 返回。
订阅替代旧的 Subscribe
2026-07-28 用 subscriptions/listen 统一长连接通知:
- Client 在 notifications 过滤器中声明 toolsListChanged、promptsListChanged、resourcesListChanged 或 resourceSubscriptions;
- Server 必须先发 notifications/subscriptions/acknowledged;
- ack 和后续通知的 params._meta 携带 io.modelcontextprotocol/subscriptionId;
- HTTP 通过长 SSE 响应流传递;stdio 在同一 stdout 通道中用 subscriptionId 复用;
- HTTP GET stream 和 resources/subscribe 不再是现代核心路径。
订阅只表示“发生变化时提醒我”,不把资源内容永久推送给模型。收到 resources/updated 后,Client 应按 URI 再发 resources/read,并重新做权限和大小检查。
缓存和分页
Resource 目录和 read 结果可以带 ttlMs、cacheScope。private 结果不能跨授权上下文共享;public 也只是缓存提示,不是访问许可。分页 cursor 是不透明字符串,Client 不能解析成页码或 offset。
资源数据流
用标准库拒绝危险的 file URI
用途:这不是完整的文件沙箱,只演示在进入文件读取器之前做 URI scheme、主机和路径规范化。真实实现还需要根目录策略、符号链接检查、租户权限和文件大小上限。
from pathlib import PurePosixPath
from urllib.parse import unquote, urlsplit
def safe_file_uri(uri: str, allowed_root: str) -> bool:
parsed = urlsplit(uri)
if parsed.scheme != "file" or parsed.netloc not in ("", "localhost"):
return False
path = PurePosixPath(unquote(parsed.path))
root = PurePosixPath(allowed_root)
try:
path.relative_to(root)
except ValueError:
return False
return ".." not in path.parts
for uri in (
"file:///srv/project/readme.md",
"file:///srv/project/../secrets.env",
"https://example.com/readme",
):
print(uri, safe_file_uri(uri, "/srv/project"))输出:
file:///srv/project/readme.md True
file:///srv/project/../secrets.env False
https://example.com/readme False注意:URL 解码、Windows 驱动器、符号链接、大小和 MIME 解析都要纳入生产级读取器;不要把这个小函数直接当作完整安全方案。
术语与白话
Resource
术语:由 Server 标识并可读取的上下文对象。 白话:图书馆里可借阅的一本书,URI 是索书号。
Resource Template
术语:含变量的 URI 模板,用于生成一批相关资源。 白话:书架上的“按作者和年份查书”的检索规则。
URI
术语:资源的协议级唯一标识。 白话:地址标签,不自动代表你能进那栋楼。
subscriptions/listen
术语:Client 按过滤器打开的长生命周期通知请求。 白话:订阅菜单变更提醒;提醒到了还要自己重新取菜单。
易混点
Resource URI 不等于本地路径
file:// 可能映射到虚拟文件系统,custom scheme 甚至可能来自数据库或 API。Server 仍应把 URI 当不可信输入,明确解析和允许范围。
资源列表变化与资源内容变化不同
resources/list_changed 表示目录集合变化;resources/updated 表示某个已订阅 URI 内容变化。收到任一种通知后都应按相应范围重新列表或读取。
订阅不是协议 Session
subscriptions/listen 是一个长生命周期 Request 的响应流,状态属于这个请求;它不让其他请求获得隐含 Session,也不允许 Server 发独立 JSON-RPC Request。
缓存 public 不是放开权限
cacheScope 只影响缓存是否能跨授权上下文共享。每次读取仍要验证访问权限,不能通过一个 public 缓存绕过 Server 的策略。
课后小问(含解析)
问题 1:为什么 Server 不直接把整个文件内容塞进 tools/list?
解析:工具目录的职责是描述可执行动作;Resource 有 URI、MIME、缓存、分页和订阅语义,能按需读取和缓存,避免把大上下文一次放入模型。
问题 2:收到 resources/updated 后可以直接把旧缓存继续给模型吗?
解析:通知表示旧结果应视为可能过期。Client 应重新 resources/read,并在当前身份下检查权限、大小和内容,再决定是否更新上下文。
小结
Resources 用 URI 描述可读取上下文,templates 让 URI 参数化,read 返回 text 或 blob。2026-07-28 用 subscriptions/listen 统一变化通知,并用 subscriptionId 做关联;缓存和分页提高效率,但不能取代 URI 校验和访问控制。
快速回顾
- Resource 读上下文,Tool 做动作。
- resources/list、resources/templates/list、resources/read 都是独立的现代请求。
- URI 是标识,不是权限;file URI 必须防目录穿越。
- subscriptions/listen 替代现代 resources/subscribe 和 GET stream。
- ttlMs、cacheScope、cursor 都要按规范解释,不能当安全边界。
阿源的知识库