用故障注入工具 mcp-drill 扫描 31 个主流 MCP 服务器(265 个工具),发现 56% 未声明输出契约,42% 声明的契约无法拒绝错误响应,Agent 无法区分好坏结果。
MCP 有 outputSchema,所以 Agent 可以验证工具的返回结果。但这个 schema 真的能拒绝一个错误答案吗?我写了一个 mcp-drill——一个能说 MCP 协议的错误注入测试框架——扫描了 31 个流行的服务器(265 个工具),包括 Microsoft Learn、Hugging Face、Cloudflare、DeepWiki。结果:只有 3% 声明了一个能拒绝腐化响应的契约。56% 什么都没声明,42% 声明的 schema 快乐地验证着垃圾。你的 Agent 根本分不清好结果和坏结果。
实时记分牌:timurrakhmatullullin86.github.io/mcp-drill
安装:pip install mcp-drill[scan]
这和安全扫描的区别:vs mcp-scan
MCP 是基于 stdio / Streamable HTTP 的 JSON-RPC,带双向通知。普通的 HTTP 混沌测试工具根本不认识它。而且即使你测试一个 MCP 服务器,通常也是在测试你的 Agent,而不是服务器契约是否能保护你。
我不断碰到的真实失败场景:
一个工具在流式返回中途返回一个格式正确但被截断的 payload——Agent 基于半个 JSON 行动了。
一个 fetch 工具在工具名错误时也返回 {"result": "ok"} 且状态码 200——Agent 怎么知道它失败了?
一个服务器声明了 outputSchema: {type: "object"}——太好了,它能验证任何对象,包括腐化的对象。零保护。
我想要一个命令来回答:如果我破坏响应但保持其类型,你的 schema 能catch到吗?以及:如果我发送垃圾输入,你会用正确的错误来告诉我吗?
所以我写了 mcp-drill:
故障注入代理——mcp-drill wrap --faults timeout,corrupt,truncate,malformed -- npx ...——坐在客户端和服务器之间,确定性地扰动响应(可种子化)。
无模型记分牌——mcp-drill scan -- npx ... 或 mcp-drill scan --url https://...——无 LLM,确定性,可复现。每个数字都是服务器的固有属性。
我测了什么(不涉及 LLM)
对于每个服务器,mcp-drill 完成 MCP 握手,列出工具,然后运行固定探测:
Output-contract coverage——声明了 outputSchema 的工具占比。
Output-contract enforceability——在声明了 schema 的工具中,schema 能否拒绝一个腐化但类型正确的 payload。腐化方式:保持结构和类型,将每个叶子节点替换为 mcp-drill-corruption / -999999999 / 越界值。如果仍能通过验证 → 空约。如果拒绝 → 可执行。这是一个结果测试,不是风格审查。
Error conformance——3 个探测:未知方法、未知工具、缺少必需参数。分类为 jsonrpc_error / tool_error(好)vs accepted / timeout / crash(坏)。
复现:pip install -e ".[scan]" && python studies/pilot/run_pilot.py studies/pilot/servers.json ——提交服务器列表 + raw results.json。
完整方法论:METHODOLOGY.md
31 个服务器,265 个工具——3% 可执行。
SDK 自动包装(x-fastmcp-wrap-result):10 个工具——主流 MCP Python SDK(FastMCP)的默认行为是把返回值包装成 {"result": string} 并称之为契约。它在构造上就是空约。
错误处理:30/31 个服务器正确处理了错误输入——错误路径是健康的。成功路径不是。
这个数字是稳定的:18 服务器时 3% → 22 时 3% → 26 时 2% → 31 时 3%(包括远程 marquee 服务器)。不是小样本 artefacts。
为什么这 对 Agent 很重要
Agent 越来越不需要人类介入就基于工具结果行动:工具 A 的输出成为工具 B 的输入。唯一的自动守卫是:传输是否成功 + payload 是否匹配 outputSchema?如果 schema 是空约,没有任何东西守卫一个类型正确但值错误的结果,Agent 会在坏数据上继续。
这不是安全扫描器(如 mcp-scan)能 catch 的。那些问的是"这个服务器能被滥用做坏事吗?"我们问的是"当它返回一个结果时,这个服务器值得信任吗?"见 VS 页面。
而且覆盖率在这里是个虚荣指标。自动生成的 schema(FastMCP 从返回类型提示推断)把覆盖率推向 100%,而可执行性保持在 0% 附近——这是每个没有覆盖它的服务器的默认空约。随着工具改进,差距会扩大,除非 schema 加入值级约束(enum、pattern、format、bounds)。
在你自己的服务器上试试
# install
pip install "mcp-drill[scan]"
# or without install
uvx mcp-drill scan -- --help
# local stdio server
mcp-drill scan -- npx -y @modelcontextprotocol/server-filesystem /tmp
# remote Streamable HTTP
mcp-drill scan --url https://mcp.deepwiki.com/mcp
# JSON output for CI
mcp-drill scan --json -- npx -y @modelcontextprotocol/server-filesystem /tmp > mcp-drill.json
# badge (shields.io endpoint)
mcp-drill scan --badge --url https://mcp.deepwiki.com/mcp > badge.json
# fault injection proxy
mcp-drill wrap --faults timeout,truncate -- npx -y @modelcontextprotocol/server-everything
CI 门禁——GitHub Action(无 LLM,无 API key):
- uses: TimurRakhmatullin86/mcp-drill@v0
with:
server: 'npx -y @modelcontextprotocol/server-filesystem /tmp'
min-error-handling: '0.9'
如果你在构建一个 MCP 服务器:声明约束值的 outputSchema,而不只是约束形状。在语义允许的地方加上 enum / pattern / format / 数值边界。additionalProperties: false 有帮助,但单靠它不够——一个腐化的字符串仍然是一个字符串。在 CI 中用 mcp-drill scan --json 测试并用它做门禁。
如果你在消费 MCP 工具:不要把 outputSchema 的存在当作安全依据。在下游做语义验证,或用 mcp-drill wrap 在 prod 前锻炼你 Agent 的失败路径。
如果你在评审 MCP 提案:覆盖率会随着生成器扩散而趋向 100%。要可执行性。
GitHub 仓库——Apache-2.0,遥测关闭,Python 3.10+。实时记分牌。欢迎 PR 和 issues——尤其是当你的服务器得分不同且你认为测试框架有问题时。
方法是无模型且确定性的——每个数字都是服务器的固有属性,而不是调用它的任何 Agent 的属性。这个工具就是方法论,而且它已经发布了。