接口契约漂移(Schema Drift)问题
在敏捷迭代中,后端工程师向 createUser 接口新增了一个必填字段 tenant_id。如果本地 MCP Server 依然沿用旧的 Schema 定义,大模型发起调用时就会反复报错“缺少必填参数”。
自动化发布闭环
将 MCP 工具的类型定义与后端核心源码放在同一个 Monorepo 中:
- 类型导出触发器:每当核心数据结构发生变更并合并至主干时,GitHub Actions 触发类型提取脚本;
- 自动重新编译与发版:自动生成最新的 MCP 工具包并打上语义化版本 Tag,通知内部客户端热重载更新,杜绝任何人工不同步风险。
先定单一事实来源
自动同步的前提是只有一处手写。三种常见起点各有代价:
| 事实来源 | 生成方式 | 优点 | 风险 |
|---|---|---|---|
| 后端 OpenAPI / Swagger 注解 | 导出 spec 再生成工具定义 | 与实现最贴近,前端与 AI 共用一份 | 注解漏写等于工具缺字段 |
| 前端共享类型(TS interface / schema 库) | 类型提取脚本转 JSON Schema | 适合同仓 Monorepo,改动即刻可见 | 只覆盖有类型的那部分接口 |
| IDL(Protobuf / GraphQL Schema) | 从服务定义生成 | 跨语言一致性好 | 与 HTTP 语义映射需要额外约定 |
选完就把「手写 MCP 工具描述」这件事从流程里删掉,任何字段都不允许只存在于工具定义中。
流水线设计
一条可复现的流水线大致是这样:变更合并主干 -> 触发提取任务 -> 生成新的工具定义并做 JSON Schema 校验 -> 与上一版做 diff -> 打包发布并带版本号 -> 通知客户端重载。几个关键设计点值得单独说:
- diff 是守门员。提取结果与上一版逐字段比对:新增可选字段可以直接发;新增必填字段、删除字段、改类型都属于破坏性变更,必须让流水线失败并交人确认,而不是悄悄发出去。
- 失败要响。提取脚本报错时保留上一个可用版本继续服务,同时把失败原因发到值班渠道,避免出现「工具静默消失」。
- 重载要有节奏。客户端拉取工具清单通常有缓存,热重载的实际生效延迟取决于缓存策略,把这一点写进变更说明,否则使用者会以为同步没生效。
以 GitHub Actions 为例的骨架如下(步骤名与语法以官方文档为准):
steps:
- run: npm run extract:mcp-schema
- run: npm run verify:mcp-schema # JSON Schema 合法性 + 与线上版本 diff
- run: npm run publish:mcp-tools # 产出带版本号的工具包
版本与兼容策略
工具定义的版本号建议用「主版本随破坏性变更、次版本随新增能力」的语义化规则,并在工具描述里保留简短的变更说明,让客户端能提示用户。更实用的一条是双版本共存:新旧工具定义并行一段时间,旧版标注为待下线,等调用量归零再摘除;这比强制所有客户端同一天升级现实得多。
验证方法
三类检查缺一不可。第一是契约测试:用生成的工具定义对真实接口发起只读调用,确认参数被接受、响应能被解析。第二是漂移检测:定期反向比对线上接口与工具定义,把不一致项列成报表——这一步能发现「注解根本没写全」的存量问题。第三是失败观测:上线后专门盯「缺少必填参数」「未知字段」这类错误的数量变化,它是契约漂移最直接的信号。
常见故障速查
| 现象 | 原因 | 处理 |
|---|---|---|
| 模型反复报缺少必填参数 | 新增字段未同步到工具定义 | 把提取步骤接进主干合并流程 |
| 工具凭空消失 | 提取失败但流水线未阻断 | 保留上一版本 + 失败告警 |
| 生成大量无用工具 | 全量导出了内部接口 | 加端点白名单/标签过滤 |
| 客户端仍用旧定义 | 清单缓存未刷新 | 明确 TTL 与主动通知机制 |
| 新旧字段混在同一工具 | 破坏性变更未走双版本 | 共存期 + 调用量归零再下线 |
小结
判断这条流水线是否真的生效,方法很直接:在接口上新增一个可选字段并合并,不改任何 MCP 相关代码,观察工具定义是否在无人介入的情况下更新、契约测试是否通过、客户端是否在缓存周期内拿到新版。做不到就回到「人工同步 + 漂移报表」的半自动形态,也比放任契约漂移安全。