MCP Server 最烦人的故障不是「进程起不来」------那种很吵,你会立刻发现。真正危险的是 静默损坏:升级 SDK 或依赖后,tool 改名、必填字段消失、枚举变松,客户端要到运行时才炸,而且不一定好猜。
动态加载成为常见能力之后(见同日快报),你装的 Server 可能更多,升级更频繁------契约测试就该成为标配。本文给最小集成测试骨架:拉起或 mock stdio、断言 schema、抽样调用,并可推进 AtomGit。

目标
- CI 无 GUI 可跑。
- tool 名集合与必填字段有硬断言。
- 故意破坏 schema 时测试变红。
- 夹具无真实密钥。
测试流水线

- 启动:子进程跑 Server(stdio)或使用内存/mock 传输。
- 握手:按 MCP 生命周期做 initialize(细节以当前规范/SDK 为准)。
- 列表 :
list_tools(或等价)拿到名称与 schema。 - 调用:happy path + 预期错误形状各至少一条。
契约测试优先于「打开 Cursor 点一点」的端到端------后者慢、脆、难进 CI。
该断言什么

| 断言 | 为何 | 失败意味 |
|---|---|---|
| tool 名集合 | 防改名/删除 | 调用方直接崩 |
| 必填参数 | 防破坏性变更 | 参数对不上 |
| 类型/枚举 | 防悄悄放宽或收紧 | 脏数据或误拒 |
| 错误形状 | 防乱码回包 | 难排障 |
把 schema 当 公开 API:破坏性变更必须红灯,而不是靠用户群提问发现。
实现五步

1. 夹具 Server
最小 Server:两个 tools 即可(例如 echo 与 add),回包固定,便于测。生产 Server 也可在测试里用「只读 list」模式。
2. 客户端与传输
两条路选一(或都做):
- 真 stdio:spawn 进程,连标准输入输出------更接近真实。
- mock 传输:单测里注入固定 JSON-RPC 帧------更快、更稳。
团队初期可先 mock 锁 schema,再加一条 stdio 冒烟。
3. 快照
将 list_tools 规范化 JSON 存为快照(审阅后入库)。CI 对比快照,防止无意漂移。快照变更必须走 PR 说明。
4. 用例
- 名称集合相等。
- 某 tool 的
inputSchema.required包含预期字段。 - 调用 happy path 返回约定结构。
- 缺必填时错误可解析(不要只判断「有报错」)。
5. CI
依赖升级(Dependabot/Renovate 或手工)必须跑该套件。红了就挡住合并。
示例断言(伪代码)
语言可换,意思是硬相等:
python
tools = {t["name"]: t for t in list_tools()}
assert set(tools) == {"echo", "add"}
req = tools["add"]["inputSchema"].get("required", [])
assert set(req) >= {"a", "b"}
result = call_tool("add", {"a": 1, "b": 2})
assert result["content"][0]["text"] == "3" # 依真实回包结构调整
schema 字段路径以你使用的 SDK 序列化为准;断言要对着真实回包写,不要抄过期博客。
mock stdio 注意点
- 行缓冲/粘包:按 SDK 推荐方式读帧,勿假设每次
readline一条完整消息。 - 超时:子进程挂死要有 timeout,避免 CI 卡死。
- 环境:测试用临时目录与假密钥;禁止读开发者真实
.env。 - 清理:
finally里杀子进程。
与 Cursor / 动态加载的关系
即使客户端动态加载工具,Server 端 schema 仍是契约。客户端少加载只能省前缀,不能发现 Server 改坏了名字。测试站在 Server 仓或「工具适配层」仓,两端都受益。
验收清单

- CI 无 GUI 跑通
- tool 名与必填字段硬断言
- 故意改 schema 时变红
- 密钥不出现在夹具
- README 写明如何跑测试
- 示例已去密钥可推 AtomGit
推进 AtomGit 的目录建议
text
mcp-server-min/
src/
tests/test_tools_contract.py
snapshots/tools.json
README.md
.env.example
秋季活动里,「带契约测试的 MCP 示例」比「又能聊天的 Demo」更耐看。
快照审阅清单
合并变更 snapshots/tools.json 时,审阅者应问:
- 是否有工具被删或改名?调用方是否同步?
- 必填字段是否减少(可能导致静默用默认值)或增加(可能导致旧客户端失败)?
- 描述是否暴涨(前缀税)?
- 是否出现疑似密钥的默认值?
快照不是「让测试通过的金文件」,是 API 评审附件。
mock 与真 stdio 的分工
| 手段 | 优点 | 缺点 |
|---|---|---|
| mock 传输 | 快、稳、易测错误形状 | 可能漏掉进程/缓冲问题 |
| 真 stdio | 更接近生产 | 慢、易抖、要清理进程 |
推荐:契约断言以 mock 或进程内为主;CI 夜跑或预发加一条 stdio 冒烟。
破坏性实验(请在分支做)
- 改 tool 名 → 期望红。
- 删除 required 中的字段 → 期望红。
- 把返回从结构化改成随意字符串 → 期望红。
若实验不红,说明断言太弱,先修测试再修功能。
版本钉扎策略
在清单文件中钉 MCP SDK 与运行时版本。升级用单独 PR:先跑契约测试,再更新快照(若有意变更),最后改 Changelog。禁止「顺便升级」混在业务 PR------静默损坏最爱混战。
客户端侧的补充测试(可选)
若你维护 Cursor 用的包装配置,可增加:启用分组后期望出现的工具名子集测试(读配置 + 对 Server list 做交集断言)。这把「动态加载习惯」也固化进 CI。
文档模板段落(可复制到 README)
text
## 测试
- 契约:pytest tests/test_tools_contract.py
- 更新快照:......(命令)
- 故意改名应失败:......(说明)
没有这段的「开源 MCP 示例」,活动场上容易被当成不可维护 Demo。
最小测试文件结构说明
test_tools_contract.py 建议分区:
test_tool_names:集合相等。test_required_fields:关键 tool 的 required。test_call_echo_ok:happy path。test_call_missing_param:错误可解析。
不要一上来写五十个用例;先锁契约,再按故障补行为测试。
持续集成伪配置(示意)
yaml
# 示意:字段以你所用 CI 为准
test:
script:
- pip install -r requirements-dev.txt
- pytest tests/test_tools_contract.py -q
AtomGit Actions / 其他 CI 同理:关键是 每 PR 必跑,不是本地偶发。
与资源(resources)相关的测试
若 Server 暴露 resources:同样 list/read 断言 URI 前缀与「不可逃逸到仓库外路径」。只测 tools 不够,资源 URI 也是攻击面与误配面。保持只读与路径沙箱声明,在测试里用故意错误的 URI 期望被拒绝。
发布前检查单(维护者)
- SDK 版本钉扎
- 契约测试绿
- 快照已审
- CHANGELOG 记录工具变更
- 示例配置去密钥
- README 测试段落仍准确
维护者比作者更需要这张单------升级依赖的人往往不是当初写 Demo 的人。
把测试当作活动故事
秋季活动介绍里可写:「本示例的 CI 会在工具改名时失败。」这比「支持 AI」六个字更像工程。读者克隆后看到红灯可复现,是信任的来源。
从坏掉中学习
生产一旦出现静默损坏:先补契约断言再现,再修 Server,最后才谈客户端兼容层。顺序反了,会修成「兼容所有历史错误」的泥球。
手写一版「伪客户端」思路
若官方测试工具尚未接入,可用最小客户端:
- 启动 Server 子进程;
- 发送 initialize 与 initialized 通知(按规范);
- 发送 tools/list;
- 解析 JSON,断言;
- 发送 tools/call;
- 关闭。
伪代码级别即可起步;重要的是进 CI。等 SDK 测试辅助成熟再替换内部实现,契约断言可保留。
错误注入表
| 注入 | 期望 |
|---|---|
| 未知 tool 名 | 明确错误,非空响应 |
| 缺必填 | 明确错误 |
| 类型错误(字符串当数字) | 明确错误或按文档拒绝 |
| 超大参数 | 超时或尺寸拒绝,不崩溃留尸 |
错误注入保证「坏输入」时可观测,避免客户端挂死。
多 Server 仓库的单体测试策略
单体仓含多个 MCP Server 时:每个 Server 独立测试目录与快照,CI 矩阵按目录跑。禁止一个巨型测试文件断言所有 Server------失败定位会地狱化。
文档中的「破坏性变更」声明
当有意改 tool 名或必填项:Changelog 用醒目标记,给调用方迁移期,并同步更新快照与示例。测试保证你自己先痛,而不是用户先痛。
系列咬合
上接手写最小 MCP、resources/prompts、动态加载习惯;下接鉴权与密钥托管实战。契约测试是中间的保险丝。收官与秋季活动里,带保险丝的示例比会闪光的 Demo 更像可维护开源。
进阶:schema 兼容性策略
对外部已有调用方的 Server,采用:
- 加字段:可选字段可增;必填增属破坏性。
- 改名:破坏性,需新 tool + 弃用期或主版本。
- 删 tool:破坏性,需公告。
- 放宽类型:可能藏脏数据,谨慎并补测试。
把策略写进 docs/compatibility.md,契约测试负责执行红线。动态加载再流行,也救不了你擅自改名的调用方。
FAQ
Q:只做快照对比够不够?
A:不够。快照防漂移,行为用例防「结构对但逻辑坏」。
Q:要不要测 Cursor 本身?
A:一般测 Server 契约即可;客户端升级另说。
Q:mock 会不会假绿?
A:会,若 mock 回包过时。定期用真 stdio 冒烟对拍。
Q:测试失败但功能「看起来能用」?
A:以契约为准先红灯,再决定是放宽断言还是修 Server------禁用「先合并再说」。
保险丝的意义是让合并变吵;吵比静默好。把这句话写进维护者指南,升级依赖的人会感谢你。
附录:最小断言清单(打印级)
- tools 名称集合是否与快照一致?
- 关键 tool 的 required 是否仍包含约定字段?
- happy path 调用是否返回约定结构?
- 缺参错误是否可解析?
- 资源 URI(若有)是否拒绝路径逃逸?
- CI 是否在无 GUI 环境绿?
- 故意改名是否变红?
- 夹具是否无密钥?
八条全勾,升级才敢点合并。动态加载省前缀,契约测试省事故;一个管成本,一个管信任。将本清单与示例仓 README 对齐后,AtomGit 秋季展示就有了可验收故事:不是「我们接了 MCP」,而是「MCP 改坏会被红灯抓住」。这才是开源工具链该有的成年人味道。
实践节奏与责任人
指定 Server 「契约责任人」不一定是作者:谁合并依赖升级,谁先看契约测试。责任人轮值也可,但要在 README 写明当前联系方式或团队频道。升级 PR 模板强制勾选:「契约测试已本地绿 / CI 绿 / 快照变更已说明」。没有勾选就请评审直接请求修改。
对示例仓而言,责任人可以是文档里的维护者小节。秋季活动过后若无人维护,至少 CI 仍会在破坏时尖叫------这比华丽却失修的 README 更负责任。记住:mock stdio、断言 schema、防止静默坏掉,这三件事做实了,MCP 才从「能 Demo」变成「能养」。养得起的开源,才值得别人 star 与二次开发。
与排障、动态加载的三角关系
动态加载管「默认少注入」,契约测试管「升级不静默」,排障剧本当「坏了怎么问」。三角缺一:只会装、不会测、坏了只会骂模型。把三角写进团队 AI 工程页,MCP 生态再吵,你的仓库仍有自己的节奏。今晚先让故意改名变红;那盏红灯,就是信任的开始。
收尾提醒
测试是保险丝,不是装饰。缺断言的 MCP 示例仓,升级一次就可能静默坏掉;有断言的仓,坏掉时至少会吵。把吵声留给 CI,把安静留给生产变更窗口------这就是本文想逼近的工程秩序。
边界声明
- MCP 规范与 SDK API 持续演进:以官方规范与你钉扎的版本为准。
- 本文不覆盖攻击性 fuzz、越权利用或破解鉴权。
- draft 未发布。
今晚可执行
- 给现有最小 MCP 加
list_tools名称断言。 - 加一条必填字段断言。
- 故意改名看 CI 是否红。
- 写 README「如何跑测试」并推 AtomGit。
静默损坏的对面,是嘈杂但诚实的红灯。把 schema 测红,比在 IDE 里猜一天便宜。