把几个 MCP Server 放在一起检查,我改掉了最初的工具设计
从工具名冲突、描述相似度,到一次把评分算成 0 分的经历。聊聊我为什么改变检查范围,以及实现时遇到的几个具体问题。
最开始我想写一个 MCP 工具描述的 linter。
检查的东西并不复杂:工具名有没有歧义,描述是不是太短,参数有没有说明,配置里有没有明显的风险项。第一版做出了 32 条规则,我又去看了看已有的扫描工具和官方的协议一致性测试。越看越觉得,如果只是把单个 Server 的检查规则再堆几十条,很难说明这个项目为什么值得装。
后来我换了检查对象:不只看某一个 Server,而是看同一份 Agent 配置里,多个 Server 的工具放到一起之后是什么样子。
这个差别很小,却直接改变了实现方式。
单独没问题,放在一起呢?
假设一个文件 Server 暴露了 read_file,另一个 Git 相关 Server 也暴露了 read_file。它们各自的工具定义可能都合法,但对于使用这两个 Server 的客户端来说,需要区分的是两个来源、两组参数和两种执行语义。
具体客户端可能会对工具名做重命名或命名空间处理,行为并不统一。单从工具清单出发,这已经是一组值得提醒维护者确认的定义:两个名字相同,实际用途可能完全不同。
另一个例子更难用精确规则抓出来:
search_public
Search public repositories by keyword and return matching repository names.
search_private
Search private repositories by keyword and return matching repository names.
这两个名字不同,描述却高度重叠。它们不一定有设计问题,也不代表模型必然会选错;但如果 Agent 同时看到这些定义,我至少希望诊断报告把它们列为“需要人工看一眼”的候选项。
还有一笔很容易被忽略的账:工具清单本身也占上下文。agentcheckup 会给出一个粗略的 token 体量估算,供使用者检查工具暴露规模。它不是 API 实际扣费数字,是否进入每次请求、有没有缓存,还得看具体客户端。
于是我把检查拆成了三块:跨 Server 的工具冲突、配置与描述风险、工具清单的上下文开销。下面主要讲第一块,以及后来重做的评分逻辑。
冲突检测没有上 embedding
这一块我最后选了很普通的词集合相似度,没接模型,也没引入 embedding 依赖。
原因是工程上的:这是一个本地诊断 CLI。如果检查一次配置还需要下载模型或调用远程接口,使用门槛就偏高了。词集合算法当然没有语义模型聪明,但运行快、容易解释,而且每次命中都能追溯到具体的名称和描述。
当前实现只比较不同 Server之间的工具对,按顺序处理三种情况:
| 规则 | 触发条件 | 处理方式 |
|---|---|---|
AGT201 | 工具名忽略大小写后完全一致 | 直接报告同名冲突 |
AGT202 | 名字不完全相同,但分词归一化后的集合一致 | 报告名称近重复 |
AGT203 | 名字不同,但共享名称词,且文本相似度满足阈值 | 报告疑似选择歧义 |
判断顺序是互斥的。例如两个工具都叫 search,会先命中 AGT201,不会再进入 AGT203。
名字先按小写字母、数字分词,处理简单的英文复数;描述则在分词之后再去掉部分停用词。然后分别计算 Jaccard 相似度:
J(A, B) = |A ∩ B| / |A ∪ B|
name_sim = J(name_tokens(a), name_tokens(b))
desc_sim = J(desc_tokens(a), desc_tokens(b))
blended = 0.45 × name_sim + 0.55 × desc_sim
AGT203 还有一个前置条件:两个工具名必须至少共享一个词。满足这个条件之后,只要 desc_sim ≥ 0.65 或者 blended ≥ 0.55,就会被标记。
这里有个实现上的取舍。只看描述,get_file 和一些描述相似但处理对象不同的工具可能被大量报出来;给名字加一个共同词的限制,可以收窄候选范围。代价也很明显:find_document 和 lookup_file 即使表达相似,也可能漏掉。这是启发式过滤,不是“理解了工具语义”。
一个 0.543 的漏报
上面那组 search_public / search_private 是刻意构造的测试输入,不是从某个真实 Server 抓来的。跑出来的数值是:
name_sim = 0.3333
desc_sim = 0.7143
blended = 0.543
如果只判断 blended ≥ 0.55,这组描述重叠超过七成的工具反而不会被提示。原因很直接:名字的差异把合并分数拉低了。
所以我没有继续反复调整一个总阈值,而是给描述重叠加了一条独立条件:desc_sim ≥ 0.65 也可以触发 AGT203。这组输入现在会产生一条 warning。
这个案例说明的是规则触发机制,不是模型有多大概率会选错工具。后者需要单独设计模型调用实验,不能拿 Jaccard 分数代替。
另一个更不起眼的坑是词形处理。之前一版简单地删除 -es 后缀,会把 files 处理成 fil,于是 read_file 和 read_files 反而匹配不上。现在对 -ies、-ches、-xes 等结尾分情况处理,并把复数变体加进回归测试。
这种问题写算法说明时几乎看不见,拿具体词跑一次就暴露了。
算法之外,麻烦在于把输入收齐
如果只能手动喂两份工具 JSON,这个检查器用起来意义不大。
现在的 CLI 会尝试从 Claude Desktop、Claude Code、Cursor、VS Code、Codex CLI 等常见客户端的配置位置发现 Server;兼容若干 JSON 配置形状,也处理部分 TOML 配置。对于本地 stdio Server,会尝试启动进程、完成握手、收集工具定义,再统一检查。
这要求程序不能因为某个 Server 启动失败就把整次诊断中断。当前做法是把启动或工具清单获取失败记为一条发现,继续检查其余 Server。
结果可以输出成人看的文本,也能输出 JSON、SARIF,方便放进 CI。运行时依赖为零,安装包只要求 Python 3.10+。冲突判定本身不需要网络;但默认扫描可能启动用户配置中的 Server 进程,因此不能据此推断被启动的 Server 一定不会联网。
工程上还有个小要求:同一条发现必须知道自己属于哪个 Server、哪个工具。否则报出“两个 read_file 冲突”也没什么用,用户还得自己翻配置。报告保留了类似 files/read_file 和 git/read_file 的来源标识。
一次把评分算成 0 分的测试
真正迫使我重新设计评分的,是用三个现成的 MCP Server 跑了一次:mcp-server-fetch、mcp-server-git、mcp-server-time,版本均为 2026.11.4。
它们一共暴露 18 个工具。当前 agentcheckup 0.4.0 的实测结果如下:
agentcheckup 0.4.0 | 3 server(s) | 18 tool(s)
~3,336 tokens per request [工具清单粗略估算]
Grade B Score 87/100
12 warning 77 info
89 条发现听起来不少,拆开就清楚了:
| 规则 | 数量 | 主要检查什么 |
|---|---|---|
AGT022 | 18 | Schema 是否限制额外属性 |
AGT023 | 17 | 字符串参数缺少格式或取值约束 |
AGT033 | 14 | 文件路径参数的作用域说明 |
AGT013 | 12 | 参数缺少描述 |
| 其他提示 | 28 | 描述长度、返回结构、数组边界等 |
| 合计 | 89 | 12 条 warning、77 条 info |
一开始我的评分很直接:从 100 分开始,error、warning、info 分别扣固定分数。但这样有个问题:工具越多,越容易因为同一种低严重度提醒被重复扣分。
按旧规则和当时的严重度配置复算,这组输入有 21 条 warning、68 条 info:
100 - (21 × 5 + 68 × 1) = -73
截断后 = 0 分
零分并不意味着这些 Server 无法使用,更不能说明它们存在严重安全问题。它只能说明我的旧评分公式没处理好规模和重复项。这组算式是按旧权重和规则严重度进行的复盘,而不是新的性能测量。
后来我做了三处调整:
- 扣分预算随工具数量增加:每个工具提供 20 分预算。
info的总扣分上限设为 10,避免轻量建议把结果淹没。- 同一规则命中多个工具时,报告合并展示,不再逐条刷屏。
现在这组结果的计算过程可以完整列出来:
warning penalty = 12 × 3 = 36
info penalty = min(77, 10) = 10
budget = 18 × 20 = 360
score = round(100 × (1 - 46 / 360)) = 87
87 分不是“官方 Server 的客观质量分”,而是这套规则下便于比较和定位问题的工程指标。 对我来说,更重要的变化是:看到报告时能先找到真正值得处理的 warning,而不是翻几十条相似的提示。
这组真实 Server 的工具之间没有检测到冲突,因此它主要验证的是启动、收集、检查、生成报告这条链路。冲突判定的正向样本则由构造输入和 mock Server 测试覆盖。
做成一个能直接跑的工具
目前发布的是 agentcheckup 0.4.0,PyPI 已验证可以在干净虚拟环境安装:
pip install agentcheckup
agentcheckup
不希望自动发现并启动本机配置中的所有 Server,也可以指定一个配置文件:
agentcheckup audit --no-discover --config .mcp.json
如果只想先检查配置而不启动 Server,可以加 --no-spawn。此外还支持 JSON、SARIF 输出以及 CI 的失败阈值。项目的 85 项测试已通过;CI 覆盖 Linux、Windows、macOS 下的多个 Python 版本,GitHub Action 也在本仓库的真实 runner 上完成了自测。
代码和使用说明放在这里:
GitHub:github.com/jiaqinq/age…
PyPI:pypi.org/project/age…
接下来想验证的事
工具现在能筛出名称冲突和文本高度相似的候选工具对。下一步值得做的是在真实客户端里对比这些候选项的暴露与调用行为,再用人工标注样本评估误报、漏报。目前的分词规则主要针对英文,中文描述和跨词同义表达也还有扩展空间。
所以我更想收集两类反馈:一类是实际多 Server 配置中确实会撞到的工具名,另一类是明显不该报警却被标记的工具对。能复现的案例比单纯加几条规则更有用。
如果你也在 Claude Code、Cursor 或其他 Agent 客户端里配置了多个 MCP Server,欢迎试一下。提交 Issue 时可以只贴脱敏后的工具名、描述和报告,不需要公开 Token 或完整私人配置。