在 CI 中实测 MCP 服务行为并输出发现
开源命令行工具 protocolprobe-selftest 会启动自有 MCP 服务,以客户端身份实际调用并报告观察结果。工具采用 ISC 许可证,支持通过 stdio 启动 Node 或 Python 服务,结果保留在本机。
AI 深度解读
MCP 工具描述看起来安全,不代表调用之后也安全。protocolprobe-selftest 选择直接启动服务器、作为 MCP 客户端连接并调用工具,再报告实际行为。它想补上的,是静态文本检查看不到的那一层。
对正在写 Node MCP server、准备把检查接进 GitHub Actions 的开发者,这个方案值得了解:它把运行时发现转成 CI 退出码和 GitHub 安全告警。但检查通过不等于安全认证,作者给出的工作流也没有完整跑通验证。
为什么要真的调用一次 MCP 工具
读取工具描述,可以发现可疑文本,却无法回答:这个工具是否会直接修改数据,运行时会不会泄露秘密,客户端不要求确认时服务器会怎么处理。
protocolprobe-selftest 是一个采用 ISC 许可证的开源命令行工具,源码仓库为 protocolprobe/protocolprobe-tools。作者也在维护 ProtocolProbe,记录官方 MCP registry 中服务器的带日期事实;selftest 则运行在使用者自己的机器上,结果留在本机。按作者的使用约定,它用于自己拥有或获准测试的服务器。
Node 服务的入口是 npx protocolprobe-selftest --i-own-this-server --stdio node ./dist/server.js。--stdio 后面全部属于服务器的启动命令,--i-own-this-server 每次都必须提供,用来声明测试授权。
Python 服务同样适用,但需要 uv,命令为 npx protocolprobe-selftest --i-own-this-server --stdio uv run --directory . server.py。服务器会在临时目录启动,因此按原文要求要保留 --directory .;直接使用 uv run server.py 会找不到项目。
作者对五个广泛使用的参考 MCP server 跑过同一组检查。重复出现的现象是:只要服务器提供修改数据的工具,调用通常一两毫秒就完成,没有报错,也没有确认步骤。
这符合协议的设计,许多客户端会替用户请求确认。问题在于,换一个不要求确认的客户端,服务器这一侧可能什么也不拦。
客户端的确认弹窗,不能自动成为服务器的安全边界。
这项观察能说明检查运行行为的价值,但原文没有列出五个服务器的名称、版本和完整结果,不能据此推断所有 MCP server 都存在同样的问题。
CI 要拦住发现,也要把报告送出去
输出采用「N of M checks passed」这样的计数,不给字母评级,也不做加权。无法执行或不适用的检查会单独列出,不算通过;有检查没跑完,结果就标为 partial。
这比一个总分更容易判断。尤其是两类可能改变数据或发送畸形输入的检查,默认关闭,需要显式开启。
下面的命令启用这两类检查,并写出 JSON 报告:
npx protocolprobe-selftest --i-own-this-server --allow-mutating --allow-fuzzing --out report.json --stdio node ./dist/server.js
要注意 --allow-mutating 和 --allow-fuzzing:作者要求在开发副本上使用,避免碰有重要数据的服务。
接入 CI,还需要 --fail-on-finding。出现 VULNERABLE 或 FOUND 时,它让进程以退出码 5 结束;不加这个参数,无论结果如何,退出码都是 0。只把命令塞进流水线,未必会拦住问题。
--sarif <file> 则额外生成 GitHub code scanning 能读取的 SARIF 2.1.0。只有需要处理的发现会成为 SARIF results:VULNERABLE 对应 error,FOUND 对应 warning,后者只是需要人工复核的启发式命中。计分和未运行检查保存在 runs[0].properties。
下面是原文给出的 GitHub Actions 示例,需要按项目调整构建和启动命令:
name: mcp-selftest
on: [push, pull_request]
jobs:
selftest:
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci && npm run build
- run: npx protocolprobe-selftest --i-own-this-server --fail-on-finding --sarif selftest.sarif --stdio node ./dist/server.js
- uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: selftest.sarif
最容易漏的是 if: always():检查步骤退出 5 后,后续步骤通常不会继续,恰好有发现时报告反而传不上去。
另一个细节是文件定位。发现针对运行中的服务器,无法精确对应源码,所以全部挂在某个文件的第 1 行。可以通过 --sarif-anchor <file> 指定文件;默认选择 --stdio 命令中位于当前目录内的第一个文件。没有可用文件时,SARIF 仍然有效,GitHub 却不展示结果,因此应从仓库根目录运行。
作者明确承认:这份工作流没有端到端实跑,命令和退出码行为来自工具测试及文档。示例可以作为起点,不能当成已经验证过的接入结果。
测线上 URL,覆盖范围会少一块
从 0.2.0 开始,工具支持测试自己运营的 Streamable HTTP 服务,但先要证明对目标 URL 的控制权。
第一次带 --url 运行时,工具不发起连接,而是输出类似 protocolprobe-proof=pp1-... 的随机证明,并以退出码 3 结束。把这一行单独放进服务器的 instructions 文本并部署,再于 30 分钟内执行相同命令。工具读取到证明后,在同一连接上运行检查。
证明只能使用一次,只对生成时的精确 URL 有效,过期失效,并且不跟随重定向。需要凭据时,通过 --bearer-env 指定环境变量,不把凭据写在命令行里;凭据只发给已证明的 URL,在工具输出和写入内容中替换成 [redacted]。
这些规则用于减少误操作和随意滥用。作者没有把它包装成强制隔离:工具开源,别人可以修改代码跳过证明。
HTTP 模式还有明确的覆盖缺口。两项泄漏检查依赖工具向启动的进程植入假秘密,再观察输出和日志。远端 URL 无法这样植入,因此这两项被标为不适用。
通过 mcp-remote 等 stdio-to-HTTP 代理启动也有类似问题:真正接受测试的进程可能是代理。工具只能识别一小部分常见代理,未识别的代理或包装脚本可能让这两项看似通过,实际没有证明力。容器、虚拟机场景也存在同类边界,作者明确说没有测试。
我的判断
我认可它对结果的克制:不把未执行算成通过,不用一个等级掩盖失败项,也区分明确发现与启发式命中。这些设计有助于团队判断报告到底覆盖了什么。
但原文没有给出与静态扫描、其他 MCP 安全工具的对照数据,也没说明误报率、漏报率和完整检查集合。它更适合作为补充证据:描述检查关注可疑文本,运行时检查验证特定调用下的行为,两者覆盖的问题不同。
对于修改数据无需确认的现象,还要区分「观察到的行为」与「已经确认的漏洞」。是否需要服务器额外授权,取决于客户端是否可信、调用者权限和工具的影响范围。
一次干净结果,只能说明这次配置下的这些检查没有发现问题。
能拿来做什么
- 如果正在开发 Node MCP server,可以先用
--stdio在本地跑一次,检查实际执行了哪些项目,再决定是否接入 CI。 - 接入 GitHub Actions 时,同时验证退出码、SARIF 上传和告警展示。首次行为不符预期时,按作者建议记录工具版本、Node 版本及实际命令。
- 对会修改数据的工具,在开发副本启用
--allow-mutating,观察不弹确认的客户端调用时会发生什么,再检查服务器权限是否符合预期。 - 测试远端服务或代理链路时,把未执行项目纳入评审。不要用远端报告中的干净结果,替代原本需要直接启动服务才能完成的泄漏检查。