qinghuan
← 回到知识探索

AI 直觉 / 动手实验

说明书该做成 MCP 资源,还是查询工具?

从资料目录、工具返回的链接到本轮真正送入模型的正文,分清每一步由谁选择、读取和更新。

展开本文目录
  1. 下载完整练习包
  2. 先问它是一份材料,还是一次按参数办事
  3. 看见书名,还不等于读过书
  4. 工具也能交回一张“去这里读”的纸条
  5. “资料更新了”是一声提醒,不是新正文
  6. 离线重放:目录、两轮正文和一次更新
  7. 再加一种:用户挑选的工作模板
  8. 验收时盯住正文去了哪里
  9. 记住这三点
  10. 别把例子用过头
  11. 自己试一小步
  12. 查原始资料
开始前,会这些就够了

你给学习助手准备了课程说明书,又做了一个“按条件查待办”的接口。两者都能带回文字,于是想统统包装成工具。结果模型每问一次课程规则都重新选工具,界面却明明已经知道用户选了哪本说明书。另一种设计把说明书列成资源,但只显示了名字,正文根本没有交给模型。

已有 MCP 课程用同名 search 演示服务路由;这里关心的是资料如何进入本轮上下文。本文固定依据 MCP 2025-11-25,产物是能力选择表与上下文快照实验。读取、缓存和组装由自写替身完成,不运行真正的 MCP 连接或模型。

把例子带到自己的电脑上

下载完整练习包

需要 Python 3.10 或更新版本;制作时实测 3.14.0。使用标准库,不需要安装第三方包。下载 ZIP 后选择“全部提取”,进入能看到 README.md 的文件夹。

下载 ZIP · 5 个文件
包里有什么
  • mcp_context_snapshot.py:完整示例
  • README.md:环境、范围与独立练习
  • observed.stdout.txt:制作时的观察记录
  • verification.json:对应源码的验证范围
  • SHA256SUMS.txt:文件内容摘要

在这个文件夹打开 PowerShell,逐行运行

python --version
python -B mcp_context_snapshot.py

解压后在含示例文件的文件夹打开 PowerShell,按命令运行。先预测结果,再与当次输出对照。 离线固定数据与应用规则,不代表真实模型、完整协议或服务联调。

找不到命令或文件时,先按 README 检查环境和所在目录。文中标出的阅读时间只估算文章篇幅,安装环境和动手练习可按自己的节奏进行。

先问它是一份材料,还是一次按参数办事

课程说明书可以用一个 URI 指定,应用根据用户所选课程决定带入哪份内容,适合考虑资源。查待办则需要筛选条件并执行查询,适合考虑工具。这个区分服务于交互设计:谁决定取哪份数据,谁给参数,以及何时发生一次调用。

规范把资源设计为由应用主导使用,把工具设计为可由模型选择调用;同时允许实现采用合适的界面形式。因此,这不是“资源只能给人读、工具只能给模型读”的硬隔离。只读查询同样可以是工具,参数化资料也可以是资源模板,不能只按是否写数据库分类。

在自己的产品设计表里,可以依次填:用户当前选择、内容标识、参数、期望返回与触发者。如果工具只是“把当前选中的文件全文读出来”,而应用已经掌握文件标识,就有理由比较直接读取资源的方案。是否更好要看宿主支持能力和实际任务,不由名称决定。

看见书名,还不等于读过书

resources/list 返回可发现的资源定义;取得正文使用 resources/read。资源模板通过 resources/templates/list 暴露可参数化的 URI 模式;得到模板仍需按参数构造具体资源标识并读取。拿到目录里“课程说明书”的名字,并没有因此拿到说明书内容。

目录、已取回正文、实际模型上下文应分别记录。假设目录有十份资料,应用只读了两份,最后因任务相关性只把一份交给模型,那么在这十份资料中,只有实际传入的那份正文能算作本轮已经提供给模型的材料。用“已连接资源库”暗示模型读过全部资料,会让排错走错方向。

对较大的材料,应用可以按明确规则节选并保留段落定位;不应只留下一个无法回到原文的概括。实验选用短文本,直接复制完整正文,目的就是让读者先看清三份清单如何变化。它没有实现分块、向量检索和模型 Token 预算。

“资料更新了”是一声提醒,不是新正文

固定版本规范中,资源能力的 subscribe 与 listChanged 都是可选项:前者关联单份资源更新订阅,后者关联资源目录变化通知。单份更新通知标出 URI,不能把这个提示本身当成新正文。应用应根据实际能力和策略处理后续读取。

这与手机收到“文件有更新”很像。提示到达时,屏幕上的旧内容仍是旧内容;重新下载之后,本地文件才变了;下一次把内容发给模型时,才可能使用新版本。正在进行的一轮是否取消、重算或标记旧依据,是应用政策,需要明确设计。

实验选择保守规则:收到更新提醒就把该缓存标成待重读,阻止它进入新一轮上下文;旧一轮快照保持原样。没有订阅能力或漏掉提醒时,这套通知驱动策略不够,仍需另行安排版本核验。单次演示不会证明真实连接下通知一定送达。

离线重放:目录、两轮正文和一次更新

下面先列出说明书,再模拟工具返回一个不在目录里的报告链接。读取两份正文后形成 turn1。随后把服务替身中的说明书从 v1 改成 v2,发送只有 URI 的更新提醒;直到重新读取并组装,turn2 才出现 v2。版本是本题业务字段,不是这里宣称 MCP 必带的字段。

观察 turn1 是否仍保留 v1,这是审计的关键。若组装时只保存可变字典引用,后续缓存修改可能把旧记录也“改成新版本”,让你误以为模型当时看过更新。深复制只解决本题的内存快照问题,真实系统还需要持久化、权限与记录生命周期设计。

展开收起代码 · 小实验python · 76 行

长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。

小实验 · python
"""Offline fixtures inspired by MCP 2025-11-25; no MCP server or LLM."""
from copy import deepcopy

guide = "study://guide"
report = "study://report/7"
store = {
    guide: {"uri": guide, "version": "v1", "text": "Bring a notebook."},
    report: {"uri": report, "version": "v1", "text": "There are two open tasks."},
}
catalog = [{"uri": guide, "name": "Study guide"}]
# Model a tool result containing a link that is NOT in resources/list.
tool_result = {"content": [{"type": "resource_link", "uri": report,
                            "name": "Task report"}]}
allowed = {guide, report}  # Teaching policy, not a real authorization service.
cache, stale = {}, set()


def read(uri):
    if uri not in allowed:
        raise PermissionError("uri denied")
    cache[uri] = deepcopy(store[uri])
    stale.discard(uri)


def assemble(uris, byte_budget=200):
    if any(uri not in allowed for uri in uris):
        raise PermissionError("uri denied")
    if any(uri not in cache or uri in stale for uri in uris):
        raise ValueError("read current content first")
    rows = [cache[uri] for uri in uris]
    if sum(len(row["text"].encode("utf-8")) for row in rows) > byte_budget:
        raise ValueError("context budget exceeded")
    return deepcopy(rows)  # Preserve what THIS simulated turn actually received.


assert not cache  # Discovery alone has not read any contents.
assert report not in {item["uri"] for item in catalog}
read(guide)
read(tool_result["content"][0]["uri"])
turn1 = assemble([guide, report])
print("turn1:", ",".join(row["version"] for row in turn1))

store[guide] = {"uri": guide, "version": "v2", "text": "Bring a laptop."}
notification = {"method": "notifications/resources/updated",
                "params": {"uri": guide}}
stale.add(notification["params"]["uri"])
assert "text" not in notification["params"]
assert turn1[0]["version"] == "v1"
try:
    assemble([guide])
except ValueError:
    print("after_notice: needs_read")
else:
    raise AssertionError("stale cached content was accepted")

read(guide)
turn2 = assemble([guide, report])
assert turn1[0]["version"] == "v1" and turn2[0]["version"] == "v2"
print("turn2:", ",".join(row["version"] for row in turn2))
cache[report]["text"] = "Changed only inside the local cache."
assert turn1[1]["text"] == "There are two open tasks."
assert turn2[1]["text"] == "There are two open tasks."
for uri in ["study://private", "https://untrusted.invalid/data"]:
    try:
        read(uri)
    except PermissionError:
        pass
    else:
        raise AssertionError("unknown URI must not be read")
try:
    assemble([guide], byte_budget=0)
except ValueError:
    pass
else:
    raise AssertionError("budget must be enforced")
print("PASS: discovery, unlisted link, snapshot, notice, permission, budget")

怎样运行:保存为 mcp_context_snapshot.py,用 Python 3.10 或更新版本运行:python mcp_context_snapshot.py。仅用标准库,不联网、不安装依赖。 2026-10-13 已用 3.14.0 执行这份示例并通过自带断言;下方为当次输出。离线固定数据与应用规则,不代表真实模型、完整协议或服务联调。

运行后对照结果
turn1: v1,v1
after_notice: needs_read
turn2: v2,v1
PASS: discovery, unlisted link, snapshot, notice, permission, budget

再加一种:用户挑选的工作模板

如果需求是“按固定问题检查一份读书笔记”,可以考虑 MCP Prompts 提供的可复用消息模板。规范将其设计为用户选择使用;prompts/list 用来发现,prompts/get 用来取得具体模板内容。模板可以帮助组织交互,但拿到模板不表示任务已经执行,更不表示模板中的每句要求都获得授权。

在本场景中,说明书、按条件查询待办、笔记检查模板可以分别承担资料、操作和交互起点。也可以组合:用户选模板,应用加入说明书,模型在任务范围内选择查询工具。真正发给模型的内容仍由宿主汇总,不能把三个名词当成三套互不相干的系统。

宿主是把这些连接、权限和模型输入组织起来的应用;规范的 Architecture 将上下文汇总列为它的职责。我们据此提出三份清单的观察方法,但清单字段和快照实现是本篇工程设计,不是协议强制规定的数据库表。

验收时盯住正文去了哪里

设计接口前,先确定用户如何选择资料、工具什么时候调用、资源链接怎样读取。验收时逐项看:目录发现成功了吗,正文真的取回了吗,实际输入中是哪份内容,更新发生后下一轮换了吗。这样能够定位“明明接了文档,回答却不知道”的具体断点。

离线断言还检查未知 URI 被拒绝与正文超过字节预算被拒绝。字节预算只是本题容量限制,不等于模型 Token 上限;允许集合也不等于真实身份授权。接入真实客户端后,需要另外验收协商、读取错误、不同 MIME 类型、分页以及通知行为,不能拿这份脚本的 PASS 冒充 MCP 互通认证。

记住这三点

  • 资源、工具和提示模板帮助表达不同交互职责,选择应围绕任务与宿主能力。
  • 目录可见、正文已读、内容已进入本轮上下文,必须分开证明。
  • 更新通知与重新读取是两步,旧一轮证据快照应保持可追溯。

别把例子用过头

  • 本文固定 MCP 2025-11-25;不同客户端支持情况应按实际协商核对。
  • resource_link 不保证列在资源目录中,也不自动授予访问权。
  • 模拟没有 MCP 服务器、网络、真实模型或完整授权系统;version 是自定义字段。

自己试一小步

为一个学习助手设计“教材目录、临时作业报告、复习模板”,怎样证明每轮实际用了哪份材料?

  1. 为三类能力各写一张选择表,说明内容、参数、触发者和返回方式。
  2. 独立增加一个正文读取失败场景,决定是否允许下一轮继续。
  3. 加入两份材料中只有一份更新的场景,保存前后两轮的实际内容清单。

做到这些,就算完成

  • 是否区分目录、已读取缓存和实际上下文,而非只记录工具成功?
  • 是否在能力不支持订阅时保留可说明的刷新政策?
  • 是否保留旧轮快照,并避免把外部链接当成访问授权?

查原始资料

想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。

  • MCP Resources:目录、读取、模板与更新 (新标签页)↗Model Context Protocol · 英文

    阅读 User Interaction Model、Capabilities、Listing/Reading Resources、Resource Templates、Subscriptions;核对应用主导资源使用及可选订阅、目录变化能力。

    2026-10-13 已打开并核对2025-11-25 固定规范中的所列段落;阅读不需要账号。未据此执行真实模型或服务。

  • MCP Tools:工具调用与资源链接 (新标签页)↗Model Context Protocol · 英文

    阅读 User Interaction Model、Listing/Calling Tools、Tool Result 中 Resource Links 与 Embedded Resources;支持工具可返回链接且链接不保证出现在资源目录。

    2026-10-13 已打开并核对2025-11-25 固定规范中的所列段落;阅读不需要账号。未据此执行真实模型或服务。

  • MCP Architecture:宿主的上下文职责 (新标签页)↗Model Context Protocol · 英文

    阅读 Core Components 的 Host、Clients、Servers;支持宿主协调权限、连接与上下文汇总。三份清单和快照是本文自定设计。

    2026-10-13 已打开并核对2025-11-25 固定规范中的所列段落;阅读不需要账号。未据此执行真实模型或服务。

  • MCP Prompts:用户选择的交互模板 (新标签页)↗Model Context Protocol · 英文

    阅读 User Interaction Model、Listing Prompts、Getting a Prompt;支持模板发现与取得的职责,协议不强制特定界面形式。

    2026-10-13 已打开并核对2025-11-25 固定规范中的所列段落;阅读不需要账号。未据此执行真实模型或服务。