你的 AI 编程工具跑着你从未锁定过版本的软件,这个 lockfile 补上了这个缺口

blog.devgenius.io 2026-10-31T15:52:56.455497

MCP 服务器在每次启动时都会重新解析版本,没有任何记录说明你批准的是哪个版本,CI 也无从发现它何时发生了变化。mcp-audit v0.18.0 补上了这个缺失的文件。

打开你所用 AI 编程工具的 MCP 配置,Cursor、Claude Code、VS Code、Windsurf,哪个都一样。你会看到类似这样的配置:

"filesystem": {
  "command": "npx",
  "args": ["-y", "@modelcontextprotocol/server-filesystem", "."]
}

用包管理器的方式读这行配置:工具每次启动时,npx 都会去仓库拉取那个包当前被标记为 latest 的版本,运行它,并把你当前所在目录的读写权限交给它。没有版本号,没有哈希,也没有任何记录能告诉你上周跑的是哪个版本,或者它什么时候变了。

你绝不会用这种方式发布 Node 或 Python 项目。package-lock.json 和 poetry.lock 之所以存在,就是因为「装最新的那个」既带来供应链问题,又带来可复现性问题。而你机器上的 MCP 服务器,恰恰是唯一还在用老办法的依赖,偏偏又是权限最大的那个依赖。

这就是我做 mcp-audit lock 想解决的问题。mcp-audit 是一个免费、开源的 MCP 服务器安全扫描工具。MCP 服务器让 Claude 这类 AI 代理能够访问你的文件、数据库和 API。0.18.0 版本为它们加上了 lockfile。

出问题的时候是什么样子

下面两个帖子来自官方 MCP servers 仓库,就在维护者自己的 issue 追踪区里:

今年 7 月,有用户报告说,某个组件自动升级到 2026.7.10 版本之后,「桌面聊天里所有 Filesystem 工具调用都报错了,只有一句光秃秃的 [error]」(modelcontextprotocol/servers#4545)。去年 11 月,另一位用户描述说,某个服务器「在升级之后」就无法配合某个 agent SDK 使用了(servers#3051)。这两个人都没动过任何东西。是他们下面的某个东西变了,而他们第一次得知这件事,就是代理挂掉的时候。

这些都还算良性。恶意版本用的是同一套机制,只不过换了个作者。

今年 8 月,Hacker News 上有个帖子聊到数万台暴露在外的 MCP 服务器。有位评论者照着 npm 的样子,列出 MCP 缺什么:「lockfile、审计命令、签名溯源」(原帖)。这是我看过的、对这块空缺最简短也最准确的概括。

再补一个数字,是我写这篇时顺手查的。npm 上 15 个官方 @modelcontextprotocol/server-* 包,有 11 个已经废弃(deprecated)。它们的 latest 标签照样能解析,npx -y 照样能跑。配置文件里没有任何提示,而且直到这周之前,我自己的工具也没提示过。

那如果一个包都没被废弃呢?Prachet Poddar 正在为同一个 lockfile 做安装树生成器。他从 npm 的 mcp-server 关键词搜索里取了 250 个 MCP 服务器根包(这个范围本身就排除了废弃包),然后用同一个解析器把每个包解析了两次:一次对着 9 月 7 日那天的 registry,按发布时间戳重建出来;一次对着 9 月 17 日的 registry。250 个里有 184 个解析出的包集合变了。语料库几乎没长,去重后的「名称 + 版本」对从 2,867 个变成 2,882 个。没有新增任何东西。变的是底下的版本被换掉了:大部分来自 zod、hono、proxy-addr、fast-uri、ip-address 这些包的例行发版,而每个发生变化的依赖树里,至少有一个真正会运行的包版本不同。

这两个前提都是他自己提的,而且都指向同一个方向:这些集合来自他的解析器,不是真实安装——只是这个解析器在他跑过的每个测试样例上都和 npm 的选择规则吻合;另外,重建的 registry 看不到 7 号之后被撤回发布的版本,所以 184 更像是下限。他的测量、他的范围、他自己标注的前提,都写在仓库的 issue #88 里,也是我见过的、支持这份文件最有力的论据。

MCP 服务器的 lock 文件该记什么

我在这个问题上花了点时间,因为一个记错东西的 lockfile 比没有还糟:它给你掌控感,却没有实质。下面是我的结论。

它会逐个服务器记录:属于哪个客户端、服务器名称、包名、浮动版本在锁定那一刻解析出的确切版本、注册表提供的话就记下该包的完整性哈希、服务器声明的能力,以及它用到的环境变量和请求头名称。

它绝不记录:这些环境变量或请求头的值、绝对文件路径、你的主机名或用户名。锁文件是要提交到仓库的,里面的任何内容都会永远留在 Git 历史里。mcp-audit 有一个测试,会读取它写出的每一个锁文件,一旦发现家目录路径、当前用户名,或任何命中其密钥模式的内容,就判定构建失败。

该文件是规范格式(RFC 8785)的纯 JSON,所以两台机器锁定同一份配置会产生逐字节一致的输出,git diff 只显示真正的变更。

三条命令

mcp-audit lock              # write mcp-lock.json from your project's MCP configs
mcp-audit lock --verify     # exit 1 if anything drifted, or could not be verified
mcp-audit lock --accept     # "I reviewed this": rewrite the lock from the current state

--verify 离线运行。它把磁盘上的配置与锁文件比对,出现以下情况时以非零状态退出:已锁定的服务器发生了变化(LOCK-001)、冒出一个从未锁定过的服务器(LOCK-002),或锁文件本身被手动改过(LOCK-005)。服务器消失会被报告,但不会单独导致构建失败;删除并不天然可疑。加上 --resolve,它还会向注册表查询:latest 现在指向的版本是否已不同于你锁定的那个。

在 CI 里只是一个步骤:

- run: mcp-audit lock --verify

这一步正是关键所在。改动 MCP 服务器的 Pull Request 现在会直接失败,直到有人运行 --accept 并提交新的锁文件。变更就出现在评审里、紧挨着代码,而它本来就该在那儿。

它不会做的事,以及还有谁在做这个

我宁愿你在这里就看到这些局限,而不是到 issue 跟踪器里去发现。

锁文件拦不住客户端自我更新。如果你的桌面应用自动更新了某个捆绑组件,下次校验时锁会告诉你:你批准的版本和实际存在的版本对不上。但它无法拦截这次更新。运行时强制管控是另一款产品的事;锁做的是评审阶段的管控。

它还没有记录服务器工具的实际行为。锁文件里留了一个 tools 段,专门放每个工具契约(contract)的哈希值,这样服务器描述一旦被悄悄改动,校验就会失败。这个版本里该段是空的。今年 7 月 mcpindex.ai 上那份关于单工具契约哈希的提案,正好就是它该有的形态。

它不构建安装树。package-lock.json 会记录所有传递依赖,mcp-audit 则刻意从不执行包管理器——正因如此,你才敢拿它去扫描一个不信任的仓库。所以锁文件只覆盖根包及其解析出的版本,不含背后的依赖树。Prachet 的安装树生成器之所以做成独立的、需要主动启用的步骤,原因就在这里:要解析 2,882 个名称加版本对,得有一套解析器、缓存和自己的威胁模型,这些都不该塞进一个扫描器里——扫描器最大的优点就是从不执行你的代码。

上手试试

npm install -g mcp-audit
mcp-audit lock
git add mcp-lock.json

然后把 --verify 步骤加进 CI,就一直留着。在已有仓库上第一次跑,生成的文件多半和现状一致,这正是关键所在:价值不在第一次提交,而在第十一次——那时某个浮动标签(floating tag)发生了位移,差异会出现在代码评审里,而不是周二早上崩掉的 agent 里。

如果你发现锁记录得不对,或者某种配置格式它读不出来,请提一个 issue,附上配置(脱敏后)和锁的输出。Cursor、Claude Code、VS Code 和 Windsurf 的格式并没有统一标准,版本之间还会漂移,所以解析器 bug 报告是眼下你能提供的最有用的东西。

问题从来不是 MCP 服务器不安全。真正的问题是,它们是你技术栈里唯一一种没有“批准记录”的依赖:你批准了什么,没有任何记录;而它们手里的访问权限,比整个 node_modules 加起来还大。一个文件,写清楚你当时要的是哪个版本,和代码一起提交,就是补上这个缺口的最小动作。

文件里的 trees 部分由另一个生成器负责,Prachet Poddar 正在做这个生成器。两个独立工具写同一个文件,但谁也不碰对方的部分。这条规则我们在公开的 issue 跟踪器里争论过,最后他的版本比我的更好。

PyPI 的完整性哈希还没补上,npm tarball 的哈希已经填了。现在,如果它锁定的版本在对应软件源里已被弃用,它会给出提示。SC-005 这条发现项就是这么来的:我自己 README 里的演示样例用了 @modelcontextprotocol/server-github,它的最新版本从 2025 年 4 月起就已弃用,而锁文件当时一声不吭地记了下来。这个问题是我在文章发布前三天,从演示录像里抽帧时发现的;当天就随 v0.18.1 发布。现在这个样例换成了仍在维护的包;如果你的依赖已弃用,工具也会明确提示。

这也不是第一次有人尝试。MCPTrust 会为服务器对外暴露的工具清单(tool surface)的快照签名;mcpguards 的 mcp-lock 会锁定 npm 版本和完整性,并提供 verify 命令和 GitHub Action。动手前这两个我都看过。mcp-audit 这一版加的是:带 CI 退出码的离线校验;绝不记录密钥值的硬性规则;由两个生成器共同写入的文件;还有一点,它和扫描配置文件里的凭据、被投毒的工具描述、跨服务器数据流的功能,本来就在同一个工具里,没有拆出去。

lock 在这个版本里仍标着实验性。文件结构在冻结前可能还会变一次。

还有一件事你需要知道。上一个版本里,--verify 命令遇到从未解析过条目的锁文件,会返回退出码 0。它先打印一条诚实的警告,然后告诉 CI 一切正常。这是 Prachet 在做 tree 生成器时发现的。0.18.0 已修复;更新日志没有把它当成普通 bug 修复一笔带过,而是解释了旧行为为什么是错的。

我不是工程师,这些内容都是借助编码代理(coding agent)完成的,并且每处改动我都亲自审阅。这个 bug 最能说明问题:当代理来写代码时,多一双眼睛只会更重要,而不是更不重要。

这周可以做的事,用不用工具都行

按投入精力从少到多,分三件事。

  1. 先打开 MCP 配置文件,把能锁定的版本都锁上。比如写 @modelcontextprotocol/server-filesystem@2026.8.31,而不是只写包名。每个服务器一行配置,就能彻底去掉“哪个最新用哪个”的行为。顺手对每个包跑一下 npm view <package> deprecated,结果可能会让你意外。
  2. 把团队批准过的服务器清单提交到仓库里。哪怕只是仓库里的一个 Markdown 列表,也比现在通常的“什么都没有”强。
  3. 让这份清单的任何改动都能在代码审查中看到。

如果想用自动化版本:

pip install mcp-audit-scanner
mcp-audit lock .
mcp-audit lock --verify .

仓库的 demo/lock/ 目录下有一个示例,能复现 README 里展示的那套“漂移然后被捕获”的完整过程。你可以先看看失败是什么样子,再决定要不要接进流程。

mcp-audit 基于 Apache 2.0 许可证,免费开源。如果你在真实仓库上跑 lock 时出了问题,欢迎提 issue。这是我最想要的那种反馈。

查看原文