MCP 工具描述变更如何签名核验
MCP 服务即使保持版本号、工具名和参数结构不变,也能修改影响模型行为的工具描述。文章介绍为工具清单签名,并让客户端对照实际发现的定义,以识别描述漂移。
AI 深度解读
MCP 服务的版本号没变,工具名和参数也没变,只把描述从「搜索文档」改成「搜索私有文档」,客户端还会重新确认吗?原文讨论的就是这个容易漏掉的信任边界:交给模型的工具说明,可能已经不是上次批准的那一份。
作者给 HISTOR 增加了发布者签名机制,让客户端能够核对「眼前的工具定义是否由这个发布者签过」。它能证明定义的来源和一致性,但不能证明工具安全,也不能保证实际执行行为没有变化。
版本号为什么管不住工具描述的变化
MCP 客户端通过 tools/list 获取工具描述和参数模式,宿主应用可以把这些信息传给模型。模型还没调用工具,就已经开始依据描述判断它能做什么、什么时候该用。
因此,描述并非无关紧要的说明文字。控制这些字符串的人,也能影响模型的工具选择。原文引用了 Invariant Labs 的工具投毒演示,以及 Farooq Shaikh、Biagio Montaruli 在 2026 年 11 月的预印本 v2;后者跨代理框架测试投毒,并检查攻击是否导致实际工具执行。原文没有提供这些研究的具体成功率,不能据此推算风险大小。
问题在于,软件包版本与工具定义不是同一个身份标识。服务继续报告 1.4.2,并不代表 description 没变。
HISTOR 原先已经能记录观察到的工具定义。新增代码补上发布者这一侧:由发布流程签署工具清单,再让客户端把收到的定义与签名版本比较。其格式遵循 OWASP MCP Security Testing Guide 的草案测试项 MCPSTG-SUPP-06。
描述只改一句,怎样让客户端发现
发布者先抓取完整的 tools/list,有分页就沿游标继续取,直到结束。HISTOR 修订格式纳入七个字段:name、title、description、inputSchema、outputSchema、annotations、execution。
工具以名称作为对象键;遇到重名直接拒绝,避免后一项悄悄覆盖前一项。每个工具分别计算 SHA-256,整个按名称组织的清单再计算一个 SHA-256。
计算之前,JSON 按 RFC 8785 规范化。这样,工具排列顺序、对象字段顺序变化不会凭空制造新摘要,定义内容变化才会反映出来。软件包版本也进入签名声明,但清单摘要由工具定义决定。

下面的例子验证「版本相同、描述不同,摘要仍然不同」。它需要已安装项目依赖的 HISTOR 源码检出,固定提交为 ba957e4;保存为 compare.py,在项目环境运行 python compare.py。作者使用 Python 3.12.13 检查过。
from histor.revision import build_statement, digests
tool = {
"name": "search",
"description": "Search the docs.",
"inputSchema": {
"type": "object",
"properties": {"query": {"type": "string"}},
"required": ["query"],
},
}
changed = {**tool, "description": "Search the private docs."}
release = {
"server": "io.github.example/docs",
"identity": (
"https://github.com/example/docs/"
".github/workflows/sign.yml@refs/heads/main"
),
"server_version": "1.4.2",
}
before = build_statement(tools=[tool], **release)
after = build_statement(tools=[changed], previous=before, **release)
assert before["serverVersion"] == after["serverVersion"] == "1.4.2"
assert after["previousManifestDigest"] == before["manifestDigest"]
assert before["manifestDigest"] != after["manifestDigest"]
assert before["toolDigests"]["search"] != after["toolDigests"]["search"]
for snapshot in ([tool], [changed]):
manifest, per_tool = digests(snapshot)
print(per_tool["search"][7:15], manifest[7:15])
重点看连续的四条断言:版本保持不变,新修订关联旧清单,但两个层级的摘要都变了。打印结果第一列是工具摘要前缀,第二列是清单摘要前缀:
896aa19c 4f97f46c
1d051183 cdf20eeb
签名通过,为什么还要再比较一次
CI 签署的是一份声明,包含服务名称、修订号、清单摘要、各工具摘要,以及上一份清单摘要。histor revision build 输出规范化字节;验证时从封装中移除 signer.signature 和 transparencyLog,重新构造相同字节。因此,外层 JSON 可以排版成人类易读的格式。
发布流程通过 GitHub Actions 的 OIDC,从 Sigstore 的 Fulcio 获取短期签名证书。对于 io.github.<owner>/... 名称,HISTOR 检查 GitHub Actions 签发方,以及证书中的仓库所有者是否符合该命名空间;CLI 还可以固定精确的工作流身份。
Rekor 包含证明另行验证,并核对 Sigstore bundle 中的日志 ID、索引是否与封装副本一致。Rekor 记录发布者签名,HISTOR 修订树记录自身接受该修订,这两件事各有用途。
签名有效,只能证明这份声明通过了身份与完整性验证;客户端拿到的定义仍需单独比较。
对于已收录且已摄入签名修订的服务,客户端向 POST /api/v1/check 提交有效的原始 tools 数组,HISTOR 才能重新计算摘要。定义不一致时,signedRevision.match 返回 different。如果只提交 MTL/1 摘要,就省略这项比较,因为 MTL/1 用于观察者快照,采用另一套摘要格式。

作者报告,11 月 11 日首个发布者签名覆盖 io.github.alexar76/histor 的修订 1,Rekor 索引为 3803861704。本地验证器检查了公开 bundle 的预期工作流身份,并与已发布的 aimarket-histor 0.7.0 包新列出的 3 个工具比较,两项均通过;修改封装中的版本则导致验签失败。
本地专项测试通过 13 个用例,覆盖规范化字节、修订链,以及 SQLite、PostgreSQL 上的比较响应。不过,摄入测试夹具绕过了密码学验证,密码学路径由上述公开 bundle 检查补充。截至 11 月 11 日,修订同步和 signedRevision 响应已在源码实现,生产上线仍待完成。
我的判断
我认同它把检查对象从版本号推进到了工具定义本身。相较于只固定包版本或人工浏览描述,这种方式能给发布者身份和定义变化留下可核验的证据。
但摘要一致只是边界之一。服务实现、工具返回内容仍可能变化,发布者也可能主动签署有害描述。它无法替代权限限制、执行前确认和返回内容检查。
原文给出了一个合成例子、一份真实签名验证和局部测试,没有提供对照实验来证明攻击成功率降低,也没有交代定义变化后客户端必须怎样重新审批。把它视为可检查的供应链证据比较准确,把它当成完整防护方案则超出了证据。
能拿来做什么
- 开发 MCP 客户端时,把
tools/list的变化纳入信任判断,尤其关注描述和参数模式;版本号相同不能直接复用上次结论。 - 维护 Node 编写的 MCP 服务时,可以参考这套发布流程:取全分页、拒绝重名、规范化 JSON,再签署完整清单。
- 接入这项 HISTOR 比较能力时,提交原始
tools数组,并分别处理验签结果与定义匹配结果;只上传MTL/1摘要不会得到同一项核对。 - 制定 AI 编程工具的审批策略时,明确清单变化后是提示差异、重新确认还是暂停使用;原文提供了检测依据,具体处置仍需宿主决定。