# 仓库变更的 MCP 配置安全

仓库中的 MCP 配置，本质上是披着小型 JSON 文件外衣的可执行部署材料。它可以选择二进制文件、获取软件包、决定进程从哪里启动、向进程传入数据，还能让进程看到检出目录。这意味着，在它成为代理安全问题之前，MCP 配置安全首先就是一个拉取请求审查问题。

我见过团队花一个小时讨论是否应该允许代理调用某个工具，随后却合并了一个启动器，让它在当天下午根据公共软件包名解析并安装任意内容。工具列表只是看得见的部分。创建工具列表的进程早已跨过了一道边界。

## 配置就是一条启动进程的指令

`.mcp.json` 的变更值得像构建脚本变更一样受到审查，因为客户端必须把其中的字段转换成本地进程。具体架构因主机而异，但常见结构很熟悉：一个命令、一个参数数组、可选的环境变量，有时还包括工作目录或传输设置。

进程启动时，会继承启动它的开发者或代理主机的权限。它可以读取该用户有权读取的文件，在该用户有权写入的位置创建文件，继承选定的环境变量，并发起网络请求，除非操作系统或其他控制措施阻止它。进程最终扮演的角色是「MCP 服务器」，并不会削弱这些普通的进程权限。

审查者常常根据软件包名称或工具列表的范围来判断安全性。这两种推断都不可靠。一个名为 `issue-reader` 的服务器，启动后可能只提供读取操作，但它的启动器却调用了一个会从主目录读取令牌的 Shell 包装脚本。一个名字听起来权限很广的服务器，也可能是已经提交到仓库的二进制文件，实际行为完全符合仓库文档。先看证据，不要先看名字。

危险的一行通常看起来很无聊：

```json
{
  "mcpServers": {
    "docs": {
      "command": "npx",
      "args": ["-y", "@example/docs-mcp"]
    }
  }
}
```

这并不意味着 `npx` 或仓库软件包自动就不可接受。它意味着，拉取请求把可执行软件供应链的一部分交给了之后的某个时刻，并且每台启动服务器的机器都要承担这个不确定性。审查者必须知道将运行哪个版本的软件包、它来自哪里、会安装什么，以及首次启动时会做什么。

提交到仓库的配置还会带来一种群体压力。它一旦出现在仓库里，新贡献者就会把服务器视为项目设置的一部分，并假设它已经经过审查。正因为这种假设，这类文件才需要明确的负责人和固定的审查习惯。

## MCP 消息与仓库启动器属于不同层次

Model Context Protocol 规范描述 MCP 客户端与服务器之间的对话，包括初始化、工具发现、工具调用和传输行为。它并没有规定一种统一的 `.mcp.json` 格式，也不会让客户端配置变得无害。每个客户端决定从哪里读取配置，以及如何启动本地 stdio 服务器。

这一区别听起来很学术，直到一次审查出错。协议可以在连接建立后把服务器限制在一组声明过的方法上。主机配置决定了究竟哪个程序有机会首先声明这些方法。如果你只检查面向协议的行为，审查就已经晚了一步，因为启动器已经行动过了。

请分开考虑两个问题：

- 已连接的服务器可以通过工具请求或执行哪些操作？
- 在客户端与服务器建立连接之前，本地启动器会做什么？

第一个问题属于代理权限、审批流程和工具设计。第二个问题属于仓库信任、进程执行、依赖来源和文件系统范围。好的审查需要回答两个问题，但绝不能用其中一个问题的答案代替另一个问题的答案。

协议文档鼓励客户端和服务器协商能力。这对兼容性很有帮助，但不能用来建立对仓库命令的信任。能力协商可以告诉客户端服务器支持哪些工具，却无法证明命令路径、软件包内容或启动钩子值得获得开发者的访问权限。

这也解释了为什么熟悉的服务器放进新配置后仍然可能有风险。服务器代码可能完全没变，但修改过的参数可能把它指向另一个端点、另一种凭据来源或另一个项目目录。启动上下文本身就是行为。

## 先读取实际启动的命令，再看工具列表

请把命令解析看成一条链，而不是只看 JSON 中的第一个字符串。要问的是，主机根据当前环境解析命令后，实际会执行哪个程序。`python`、`node`、`uvx`、`npx`、`bunx`、`sh` 和相对路径，都意味着最终可执行文件由另一个解析器决定。

直接使用绝对路径最容易检查，但仍然需要说明来源。仓库相对路径在文件位于仓库内、且普通代码审查能够覆盖它时，可能是可以接受的。裸命令名要求你检查开发者的 PATH 行为。Shell 命令则需要最高级别的警惕，因为引号处理、展开、管道、重定向和命令替换，会隐藏比普通参数数组更多的执行行为。

参数需要单独审查。审查者常常读前两个参数，认出一个软件包名，然后继续往下看。应逐个读取所有元素。某个参数可能选择检出目录外的配置文件、指定输出目录、启用插件加载器、指向备用仓库，或把原本无害的命令变成脚本解释器。

请比较下面两项配置的区别：

```json
{
  "command": "node",
  "args": ["tools/mcp-server.js", "--root", "."]
}
```

```json
{
  "command": "node",
  "args": ["tools/mcp-server.js", "--config", "../../shared/runtime.json"]
}
```

第二项可能完全合理。但它也引入了一个位于检出目录边界之外的文件，而拉取请求可能并未展示该文件。如果服务器从那里读取指令或凭据，审查者就无法仅凭差异内容评估这项变更。请作者把引用的配置纳入审查，改成明确的仓库路径，或解释为什么必须使用外部文件。

不要接受「软件包会处理这些事情」之类含糊的回答。软件包今天可能确实能正确处理，但这项变更仍然把权限交给了它。合格的回答应该说明程序、版本、输入和预期文件。如果作者没有在全新检出目录中运行过准确的命令，他们描述的是意图，而不是行为。

对 Shell 包装脚本可以采用一个直接的默认规则：除非仓库有明确需求，而参数数组无法表达，否则拒绝使用。包装脚本看起来很方便，因为它可以设置环境变量并启动两个辅助程序。但它也会让审查依赖 Shell 解析规则，以及包装脚本能够触及的所有命令。如果确实需要，把设置过程放进一个经过审查、文件名明确的脚本中，然后像审查应用代码一样审查它。

## 软件包运行器会把启动变成供应链事件

软件包运行器可以在服务器首次启动时下载并执行代码，因此仅凭软件包名无法完成依赖审查。这也是为什么 `npx -y package`、`uvx package` 及类似形式，需要比调用仓库内文件的命令接受更多检查。

支持不固定版本运行器的人通常强调便利性。贡献者不必手动安装任何东西，项目看起来也能保持最新。代价是，仓库不再说明它要求每位贡献者运行的确切代码。新发布的软件包版本、变化的依赖或仓库路由变化，都可能在没有新拉取请求的情况下改变行为。

至少要明确版本，并让来源清晰可查：

```json
{
  "mcpServers": {
    "schema-checker": {
      "command": "npx",
      "args": ["-y", "@acme/schema-mcp@1.4.2"],
      "cwd": "${workspaceFolder}"
    }
  }
}
```

固定版本并不能证明软件包安全。它能让审查对象保持稳定。审查者可以检查该版本，与上一版本进行比较，并要求通过有意发起的拉取请求更新。只要周边工具链支持，就应使用软件包管理器的锁文件、完整性数据或提交到仓库的构建产物。只有版本字符串，却没有办法验证下载内容，仍然会留下缺口，但它依然比不受限制的软件包名更好。

不要把软件包管理器缓存当成审查。缓存只会改变某台机器上字节的来源。它无法告诉审查者其他贡献者会收到哪些字节，也无法说明安装钩子是否会运行，或干净机器上的行为是否一致。

请提出一个有些尴尬、却能发现许多薄弱变更的问题：如果软件包不存在，会发生什么？如果答案是「它会自行安装」，那么配置就包含网络访问和代码执行路径。如果答案是「启动失败，并提示安装文档中记录的依赖」，团队选择的是更慢但更透明的设置流程。两种答案都不一定错误。假装两者没有区别才是错误。

## 工作目录和环境变量决定实际范围

`cwd` 字段告诉进程从哪里开始寻找相对路径文件，而这个选择常常比人们预想的影响更大。许多工具会从工作目录开始向上查找项目配置、语言运行时、忽略文件、凭据和插件。服务器从仓库根目录启动时看到的环境，可能与从专用测试目录启动时完全不同。

请有意识地设置工作目录。如果服务器只需要读取 `tools/specs` 下生成的 API 规范，就不要让它从包含部署材料和私密开发笔记的父目录启动。如果它必须检查整个检出目录，请在拉取请求中说明。目标不是让每条路径都尽可能小，而是让审查者批准的访问范围与服务器实际获得的访问范围一致。

环境变量同样重要。有些只是语言环境或端口之类的普通运行设置。另一些会选择软件包仓库、扩展模块搜索路径、启用会写入请求正文的调试模式，或携带凭据。配置使用 `${TOKEN}` 会让令牌值不出现在文件中，但它仍然会把这个秘密传给子进程。

只要服务器可以通过独立的操作边界运行，就不要在仓库 MCP 配置中放置凭据。如果服务器确实需要秘密信息，请记录它的来源、用途，以及进程是否能把它传给子进程。不要在注释或复制的 Shell 记录中放入生产环境令牌示例。开发者会复制那个最容易运行成功的示例。

也要关注继承的环境变量。进程继承的内容可能远多于 JSON 中列出的内容。主机决定传递什么，但仓库作者可以选择一个会读取标准运行时位置的命令。这也是应该优先使用直接、简单的启动器，而不是通用 Shell 和软件包运行器的原因。

## 拉取请求差异可以揭示执行图

你通常无需运行不受信任的代码，就能通过把配置变更展开成它暗示的进程和输入来完成大部分审查。先查看聚焦的差异，不要只看渲染后的文件，这样才能保留被删除的参数和被修改的路径：

```sh
git diff --check
git diff -- .mcp.json
```

第一条命令会报告发现的空白错误。第二条应该显示配置路径中每一行新增、删除和修改的内容。如果仓库把文件存放在其他位置，或通过生成器创建它，请调整路径，并同时要求查看生成器的变更。没有源文件的生成配置，审查是不完整的。

看下面这个小改动：

```diff
 "mcpServers": {
   "release-notes": {
-    "command": "node",
-    "args": ["tools/release-notes-server.js"],
-    "cwd": "${workspaceFolder}"
+    "command": "npx",
+    "args": ["-y", "release-notes-mcp"],
+    "cwd": ".."
   }
 }
```

表面上看，团队只是用发布的软件包服务器替换了本地辅助程序。但执行图揭示了更多信息。客户端通过开发者的 PATH 解析 `npx`。`npx` 可能会获取 `release-notes-mcp` 及其依赖。由于 `cwd` 现在指向父目录，软件包会从仓库根目录之外启动。服务器可能在那里发现配置、写入缓存文件，或读取相邻项目。每一条箭头都必须在合并前得到解释。

有纪律的审查可以按下面的顺序索取证据：

1. 确定最终可执行文件，以及提供它的确切软件包或仓库文件。
2. 列出所有可能改变启动行为的文件路径、URL、仓库地址和环境变量。
3. 说明进程的工作目录，以及服务器在正常使用期间会读取或写入哪些目录。
4. 确认在没有任何软件包缓存的干净机器上会发生什么。
5. 将声明的工具用途与启动器实际需要的权限进行比较。

这套流程是具体的审查产物，不是形式主义。它能避免常见的失败：审查者批准了工具描述，却让安装器、运行时选择器或父目录通过了一行变更。

如果必须执行配置来验证它，请使用一次性账户或隔离环境，环境中不得有生产凭据，并且软件包缓存应为空。记录解析出的可执行文件、软件包版本、出站主机和写入的文件。仅凭工具发现成功的截图，证据力度很弱，因为它省略了风险最大的部分。

## 工具描述无法弥补宽泛的启动器

狭窄的工具架构并不能抹去 MCP 握手前已经发生的事情。团队经常把服务器的 `tools/list` 输出当成权限清单，然后因为它只提供 `search_docs` 和 `read_status` 就判定它安全。这个清单描述的是初始化后的接口。它几乎无法说明软件包安装、配置发现、遥测、子进程或启动时读取文件等行为。

这一区别是双向的。当启动器固定了版本、使用本地文件、有文档说明，并且限制在预期项目内时，一个具备写入能力的工具可能完全可以接受。如果仓库配置悄悄运行来自网络的可变软件包，并且能够访问开发者更广泛的工作区，那么一个只读服务器也可能不可接受。

请作者用通俗语言说明完整生命周期：启动什么程序，连接前读取什么内容，如果有下载会下载什么，以及连接后服务器能触碰哪些内容。这些内容应该能放进拉取请求描述中。如果回答需要长时间调查，说明配置对日常审查来说过于间接。

不要让「我们信任这个供应商」结束讨论。信任很重要，但它不会固定版本、缩小工作目录，也不会说明命令是否使用了备用仓库。这些是彼此独立的控制措施，也有各自不同的故障模式。

## 仓库所有权需要运行时后盾

仓库审查可以阻止不安全的启动指令变成常规项目设置。运行时控制仍然重要，因为经过批准的配置可能被受感染的依赖、恶意提示，或要求代理采取过大行动的用户滥用。

请保持边界清晰。仓库控制它建议启动什么。操作机器的人控制某次代理会话是否可以执行特定操作。敏感操作需要留下记录，让操作者能够还原代理做过什么，并在运行出错时撤销会话。

Sallyport 让 API 和 SSH 凭据留在代理进程之外，同时通过会话授权和活动记录，为操作者提供独立的位置来审批和检查操作。这并不能让危险的仓库命令变得无害，所以配置审查仍然要放在前面。

如果团队使用其他边界，也应采用同样的检验方式。开发者能否看到是哪一个代理进程请求访问？能否在不终止无关工作的情况下停止该进程？能否区分尝试执行的操作和实际成功的操作？如果答案是否定的，错误配置合并后就有更多时间造成问题。

## 把这类文件当作有负责人管理的执行代码

仓库应该为 MCP 配置、包装脚本，以及决定启动器安装什么的软件包清单指定代码负责人或采用等效的审查规则。负责人不必是安全专家，但需要有足够背景去追问：为什么这项变更要让这个程序以这些权限启动？

让配置保持简洁。按用途使用一个服务器，比使用一个能够触及开发者所用每个系统的通用辅助程序更容易审查。把特殊要求用普通文字写在配置旁边：预期命令、软件包版本策略、工作目录、所需网络访问和预期输入。这样可以把口耳相传的知识变成下一位审查者能够质疑的内容。

实际测试很简单。把差异交给一位不了解该功能、但经验丰富的开发者，请他说明在干净机器上会执行什么、在哪里执行，以及能看到什么。如果他无法从拉取请求及其引用的代码中回答这些问题，这项变更在进入仓库前还需要继续完善。
