MCP 工具升级,如何避免旧调用失效
工具改名、字段变为必填或返回结构调整,都可能破坏缓存客户端和长时间运行的 Agent 流程。文章区分 MCP 协议版本与应用工具契约,强调升级应兼容、可感知且可回退。
AI 深度解读
昨天还能跑通的 Agent,今天突然报错,未必是模型退步了。把工具参数从 location 改成 query、新增必填字段,甚至调整一段工具描述,都可能让它继续按旧约定调用一个已经变化的接口。
作者给出的结论是:MCP 工具契约要按公共 API 迁移来管理,不能把服务部署成功当成兼容性验证。文章没有提供性能数据或团队实测结果,但给出了一套可落地的工程做法,适合正在给 Agent 接入内部服务、维护 Node 工具服务的开发者参考。
MCP 协议兼容,为什么工具调用还是会坏
先分清两个层次:MCP 协议版本决定客户端和服务端能否按同一套通信规则交流;工具 Schema 决定具体调用要传什么、返回什么,以及调用会产生什么结果。前者协商成功,不代表后者保持兼容。
原文用 find_customer 举例:原来只要求 email,后来新增必填的 tenant_id。两台服务可以支持相同的 MCP 协议版本,但旧 Agent 只传 email,仍会验证失败。
判断变更是否破坏兼容,可以从一个问题开始:昨天合法的调用,今天会不会被拒绝,或者执行出不同的含义?
检查范围也不能只盯着参数:
- 工具被删除、改名,字段被删除或改成必填,都会影响旧调用。
- 类型、数值范围、正则或枚举收窄,会让原来合法的值失效;默认值变化则可能悄悄改变行为。
- 输出字段消失、类型变化,会影响 Agent 对结果的理解。
- 描述、权限、副作用、幂等性发生变化,即使 JSON Schema 一字未动,也可能改变后续动作。
描述尤其容易被当作文案处理。对 Agent 来说,它还是选工具、构造参数的依据。同样,status: "complete" 如果从「已经执行」变成「已经排队」,结构检查完全看不出问题,业务结果却已经不同。
需要维护兼容性的,是工具对调用方作出的整套承诺。
动态发现工具,也应该算一次发布
Agent 每次连接都读取实时 tools/list,看起来很方便,却引入了一个隐蔽的发布入口:Agent 服务没有更新,上游工具定义变了,它的行为也可能跟着变。
作者把接入方式分成三种:
Pinned:使用当前版本批准过的工具清单,作者将其作为有写入能力工具的最安全默认选择。Monitored dynamic:读取实时定义,但指纹变化会触发告警,必要时禁用变化后的高风险工具。Fully dynamic:立即接受当前定义,留给低风险、只读的实验场景。
固定清单不等于复制工具实现,而是记录「这版 Agent 接受评估时,看到的接口是什么」。刷新清单应像升级 SDK 一样接受审查。动态发现也要把当时的指纹写进调用轨迹,否则出问题时,很难区分是提示词退化,还是上游契约变化。
工具集合本身也属于这个边界。新增十个无关工具,未必破坏任何 JSON 请求,却可能影响模型选择。因此,每个工作流应只暴露所需工具,清单变化后补做代表性的选工具测试。
落实到 CI,作者建议调用 tools/list,规范化结果,为每个受支持的服务和环境保存两份产物:供人审查的 JSON 快照,以及供部署快速检查的指纹清单。快照覆盖 name、description、inputSchema、outputSchema,不放凭据、租户样例和真实工具结果。
原文指纹示例使用 client.listTools()、createHash("sha256") 和 JSON.stringify,但也明确提醒:实际代码必须使用递归规范化的 JSON 序列化器,只排序顶层键不够。示例不能直接当作完整实现。
差异还要按风险分类。例如,find_customer 新增必填 tenant_id 属于 BREAKING;list_invoices 增加可选 page_size、默认值为 50,示例将其归为 COMPATIBLE;有副作用的 refund_payment 修改描述,则进入 REVIEW。涉及创建记录、资金、消息或基础设施的破坏性变化,应附带迁移方案再发布。
用适配器接住旧调用,再用测试证明没有变义
当接口从地点文本迁移到经纬度时,作者建议先接受两种输入,让旧调用经过适配层继续工作:
type WeatherArgs =
| { location: string; unit?: "c" | "f" }
| { lat: number; lng: number; unit?: "c" | "f" };
function normalizeWeatherArgs(args: WeatherArgs) {
if ("location" in args) {
return {
kind: "legacy" as const,
query: { place: args.location, unit: args.unit ?? "c" }
};
}
return {
kind: "current" as const,
query: { lat: args.lat, lng: args.lng, unit: args.unit ?? "c" }
};
}
要看的是 kind: "legacy":它让旧路径可以被统计,两条路径也都保留默认单位 "c"。不过,这段代码只做参数归一化,没有完成地点到坐标的转换,也不包含运行时校验。
适配器应有移除日期,并统计旧分支的使用量。旧流量可能来自定时任务、数据库里的提示词、缓存工具定义的客户端,或者外部集成。如果新旧含义已经不同,就该增加 get_weather_by_coordinates 这样明确的新工具名。
输出迁移更容易静默出错。原来返回 ok、ticket_id,后来换成 state: "accepted"、job_id,Agent 可能误报完成或重复重试。原文提出在过渡期保留两组字段:
{
"ok": true,
"ticket_id": "T-42",
"state": "accepted",
"job_id": "J-96",
"deprecation": { "fields": ["ok", "ticket_id"], "remove_after": "release-2027.03" }
}
这里要看 remove_after:它用明确的发布标识代替「以后删除」。但双写字段的前提是含义一致;如果旧 ok: true 表示已经完成,就不能拿它表达新接口的「已受理」。
作者建议用三层测试守住契约:快照检查发现定义变化;兼容性样例让旧、新请求都经过处理器;行为测试验证只读不写入、dry run 不执行、重试不重复扣款,以及 accepted 不等于 completed。Pydantic、Zod、OpenAPI 或 SDK 升级造成的 Schema 重生成,也应由快照检查捕获。测试使用合成租户和假的副作用适配器,才能安全地进入每次 PR。
发布则按顺序推进:盘点调用方,增加兼容支持,提交快照和样例,按租户或工作流灰度,观察验证失败、回退、重试和任务完成情况,再通知弃用。只有旧流量在约定窗口内持续为零,才删除适配器。每条轨迹同时记录服务构建标识和工具清单指纹,排障才有依据。
我的判断
我赞同把描述、输出含义和工具集合纳入版本管理。常见的请求参数校验只能覆盖其中一部分,HTTP 成功也不能证明 Agent 正确理解了结果。
但这是一套工程建议,不是经过对照实验验证的收益报告。原文没有提供事故减少比例、维护成本,也没有证明固定清单在所有团队里都优于动态发现。
固定清单还有一个前提:服务端仍然支持对应契约。客户端保存了旧定义,远端却已经删掉旧接口,调用照样失败。适配层也有边界,例如 tenant_id 涉及租户隔离,就不能为了兼容随便填一个默认值;无法安全推导时,明确的迁移错误更合适。
与「全部重命名成 v2 工具」相比,适配器能降低小改动的迁移成本;与「永远兼容」相比,基于旧流量的退出条件能控制维护负担。取舍的依据应是语义变化和调用方情况,而不是一律新增版本。
能拿来做什么
- 给现有 Node MCP 服务加入
tools/list快照检查,先把名称、描述、输入和输出 Schema 纳入 PR 审查,再逐步补充风险分类。 - 修改必填参数或返回状态前,整理已有请求样例,让旧调用经过真实处理器验证,并检查 Agent 是否会误报完成或重复操作。
- 对有写入能力的工具固定已评估的清单,在调用轨迹里记录服务构建标识和清单指纹,方便定位「Agent 当时看到了什么」。
- 给兼容分支添加使用计数、替代请求示例和移除条件,依据最长运行调用方与实际旧流量决定弃用窗口。