# MCP 服务器更新：将发布版本视为供应链事件

MCP 服务器更新属于供应链事件，因为它会改变代理可以请求执行的可执行代码。服务器在本地运行、使用 stdio，或者保留旧的工具名称，都不会改变这一点。更新可能改变工具读取哪些数据、将请求发送到哪里、选择哪些默认值，以及如何解释由提示生成的参数。

我见过一些团队把代理集成当成无害的连接工作，直到一次更新把一个范围狭窄的助手变成了广泛的访问通道。问题通常从一个看似合理的捷径开始：浮动版本、会议间隙匆匆扫过的发布说明，以及为了快速测试而复用的生产凭据。解决办法不是成立一个庞大的审批委员会，而是建立一套可重复的采用门槛，锁定构件、比较实际行为、重新测试访问权限，并保留完整的回滚路径。

## MCP 服务器就是可执行的依赖代码

MCP 服务器不会因为代理通过协议发现它，就变成配置文件。它是一个包含依赖、启动代码、解析器、网络客户端，并且通常可以访问文件或外部账户的程序。更新它，就等于改变了代理通过源自自然语言的输入控制的一条操作路径中的程序。

人们经常把两个不同的风险混在一起。第一个是**构件完整性**：安装的是否正是你打算安装的代码？第二个是**操作语义**：这段确切的代码是否仍然只执行你认为它会执行的有限操作？签名软件包或匹配的校验和有助于回答第一个问题，但对第二个问题几乎没有说明。

当服务器加入一个看似方便的功能时，这种区别很重要。一个原本只接受单个工作区根目录的文件系统工具，可能开始以不同方式解析符号链接。源代码管理工具可能加入自动拉取远程内容的功能。API 工具可能决定跟随重定向。每项变化都可以保留命令名称，通过只检查成功与否的测试，却仍然改变数据边界。

本地 stdio 改变的是传输暴露面，不是信任关系。stdio 服务器不需要监听入站端口，这很有用。但它仍然继承启动它的进程所拥有的权限。如果该进程可以读取主目录、检查环境变量或访问互联网，那么除非操作系统或启动器阻止，服务器通常也可能这样做。

请像对待新的构建代理插件或新的命令行二进制文件那样对待更新请求。弄清楚哪些代码会进入机器、它获得什么权限、可以向外发送什么，以及你将如何证明正在审查的版本就是实际运行的版本。

## 浮动版本会让审查沦为表演

如果部署命令明天可能安装不同的构件，那么审查就没有意义。版本范围、可变标签和未提交的锁定文件都会带来这种结果。

对于基于 Node 的服务器，应精确锁定直接依赖并提交锁定文件。下面的示例会阻止通常的插入符版本范围，避免静默接受之后的小版本更新。

```json
{
  "dependencies": {
    "example-mcp-server": "1.4.2"
  }
}
```

然后在自动化流程中强制根据锁定文件安装：

```sh
npm ci
npm ls example-mcp-server
```

第二条命令应输出包含预期版本的依赖树，大致如下：

```text
project@0.1.0 /work/project
└── example-mcp-server@1.4.2
```

不要只检查直接依赖。检查锁定文件的差异，关注发生变化的传递依赖，尤其是会运行安装脚本、解析不可信输入、建立网络连接或提供身份验证代码的软件包。直接依赖可能保持不变，但宽松的传递依赖范围却在其下方移动。

对于容器分发，应锁定摘要而不是标签：

```text
registry.example/team/mcp-server@sha256:0123456789abcdef...
```

像 `1.4.2` 这样的标签之后可能指向不同的镜像。摘要则标识一个通过内容寻址的镜像。锁定并不会让镜像自动合格，它只会让测试结果对应一个稳定对象，而这是进行有效审查的最低要求。

对于从源代码安装的情况，应锁定完整的提交标识，并记录获取方式。不要把分支名称写进引导脚本后就称之为锁定。分支本来就会移动。如果构建过程会在安装时拉取依赖，也要锁定这些依赖，否则你的源代码锁定只覆盖了实际运行程序的一部分。

在同一份配置中保留之前的构件引用。依赖某人记得上个月的版本，不能算回滚计划。那只是生产访问仍然暴露时，人们用来解释情况的故事。

## 协议兼容性不会保留工具行为

Model Context Protocol 规范定义了客户端和服务器如何初始化、公布能力、列出工具以及调用工具。它不会认证名为 `search_files`、`deploy` 或 `send_message` 的工具具有什么含义或安全性。协议兼容是客户端和服务器通信的必要条件，但不能证明服务器仍然拥有相同的权限。

规范中的工具流程清楚地说明了这一点。客户端通过 `tools/list` 获取工具清单，并通过 `tools/call` 调用工具。服务器还可以通知客户端工具列表发生了变化。应将这些协议事实作为审查输入，而不是将它们当成自动信任某个发布版本的理由。

在相同的测试配置下，分别记录旧版本和新版本的工具清单。保存原始 JSON，然后进行比较。只比较名称会遗漏很多内容。应查看描述、输入架构、必填字段、枚举值、文本中描述的默认值、存在时的注释，以及输出结构。

一个最小的记录流程如下：

```text
1. 使用一次性测试账户启动旧服务器。
2. 调用 tools/list，将完整响应保存为 tools-old.json。
3. 使用完全相同的配置启动已锁定的候选版本。
4. 调用 tools/list，将响应保存为 tools-new.json。
5. 比较文件，然后使用固定测试夹具调用发生变化的工具。
```

假设服务器保留了一个名为 `read_project_file` 的工具。旧架构要求使用相对 `path`。新版本接受 `path` 以及可选的 `root`，描述中还说省略 `root` 时可以使用环境默认值。即使所有现有客户端调用仍然有效，这也是访问变化。如果服务器没有限制默认值，代理现在就可能生成一个访问工作区之外位置的参数。

描述值得比许多工程师通常做的更仔细的审查。代理会根据描述决定何时以及如何调用工具。新的描述如果写着「使用此工具检查调试所需的任何本地文件」，就可能在源代码漏洞出现之前扩大代理的实际行为。工具实现也许仍然会拒绝不安全路径，但你应该测试这一点，而不是从一句友好的描述中推断结论。

还要比较失败行为。一个以前会拒绝模糊参数的工具，现在可能会自行猜测。在交互式命令行中，猜测可能显得很方便。但在代理执行中，它会把不确定的意图变成实际操作。

## 发布说明是证据，不是审查

发布说明只会告诉你维护者选择提及的内容。它不会列出每个发生变化的依赖、默认值或错误路径。应该阅读发布说明，但仍要验证与你的安装相关的重要部分。

从发布构件及其来源信息开始。确认软件包版本、镜像摘要、源代码提交和安装命令。项目提供时，再检查源代码差异。重点关注进程启动、路径处理、HTTP 客户端、凭据加载、遥测、更新检查和启动脚本附近的代码。这些位置往往比工具处理器本身更能决定权限范围。

软件包管理器文档在这里提供了一个有用的提醒。npm 说明生命周期脚本可能在安装过程中运行。这意味着更新审查在服务器进程启动前就已经开始。如果你的流程在开发者机器上安装软件包，而机器上有个人凭据和广泛的文件系统访问权限，那么安装脚本已经有了造成实际损害的机会。

在干净环境中安装候选版本。一次性虚拟机或专用测试账户都比开发者的日常工作站更合适。为它提供空的主目录，在可行时使用独立的软件包缓存，并且只提供测试所需的凭据。记录运行过的命令，让其他人可以重复结果。

不要接受「开源项目免除了这项工作」这种懒惰的说法。公开源代码让你可以检查更多内容，但它不会自行完成检查，也不会冻结传递依赖，更不能证明软件包仓库交付的就是你审查过的源代码。

另一种错误是要求对每个补丁版本进行逐行审计。大多数团队会跳过这种负担，之后又回到盲目更新。审查深度应与权限相匹配。没有网络访问权限的格式化辅助工具，值得投入的精力应少于一个可以读取代码库、执行 shell 命令或调用云 API 的服务器。流程必须在后果严重的地方足够严格，同时又足够快速，让人们愿意真正使用它。

## 先测试被拒绝的路径，再测试成功路径

成功的 `tools/call` 只能证明服务器可以完成某项操作，不能说明它会在哪里停止。访问测试应从你期望服务器强制执行的边界开始。

建立一组小型测试夹具，包含允许的输入和有意禁止的输入，并将它与服务器配置一起进行版本控制。对于项目文件工具，应测试普通文件、父目录遍历尝试、绝对路径、指向测试夹具根目录之外的符号链接、不存在的文件以及不可读文件。对于 HTTP 工具，应测试批准的主机、未批准的主机、重定向到未批准主机的情况、环境中需要关注的私有地址，以及带有格式错误的方法或请求头的请求。

预期结果必须具体。「失败了」还不够。工具可能只是因为网络路由暂时中断而失败。应写出这样的结果：

- 它读取 `fixtures/app/config.json` 并返回预期内容。
- 它在打开文件前拒绝 `../outside.txt`。
- 它拒绝解析后目标离开测试夹具根目录的符号链接。
- 它在向新主机发送凭据前拒绝重定向。
- 它返回结构化错误，且诊断输出中不打印秘密信息。

这正是更新让有经验的团队感到意外的地方。重构替换了路径库、默认值发生变化，或者错误处理器现在记录请求对象。成功路径仍然通过，边界测试却能捕获回归。

将权限测试与语法测试分开。工具可能正确解析了受限路径，但使用的凭据自上次审查后已经获得了更广的权限。使用无法访问某个已知受保护对象的测试身份，然后确认服务器无法读取或修改它。如果服务器支持不同的账户配置，应分别测试每种配置，不要假定权限最受限的配置能代表全部配置。

测试时观察出站行为。网络监控、受控 DNS 记录、代理日志或隔离网络，都可以显示工具输出没有揭示的目标。你不必对每台服务器都进行复杂监控，但必须知道更新是否联系了新的主机、跟随了重定向，或发送了包含请求上下文的错误报告。

## 代理会放大细小的架构变化

人看到一个新的可选字段时会停下来思考。代理看到工具描述中出现新的可选字段，可能会在一项长任务中反复尝试。正因如此，行为审查必须覆盖代理的决策面，而不只是服务器的 API 面。

在直接测试夹具之后，使用有代表性的提示进行测试。使用反映真实工作的提示，同时限制测试环境，例如检查代码库、获取已知问题、更新虚拟记录，或连接到非生产主机。收集实际的工具调用。比较新旧运行的调用次数、参数、错误恢复方式，以及代理在失败后尝试执行的任何操作。

不要把抵抗提示注入与更新安全混为一谈。服务器可以拥有完美的输入验证，却仍然改变原本的权限范围。反过来，服务器也可以保持完全相同的行为，但新的工具描述会让代理更频繁地请求某些操作。必须同时关注这两个层面。

一个有用的测试提示应要求保持某个边界关闭。例如：「查找这个示例项目的构建配置。不要检查项目目录之外的文件。」如果候选服务器尝试访问父目录、跟随符号链接离开目录树，或要求更宽的根目录，就说明更新改变了决策面。

在比较过程中固定客户端版本。将 MCP 客户端和服务器一起更新，会让意外结果难以归因。先使用当前客户端测试候选服务器。如果还计划更新客户端，应将它作为独立变更测试，并保持服务器不变。一起升级能节省一点日历时间，却会增加大量调试时间。

## 尽可能让凭据留在服务器进程之外

从自身环境中读取长期 API 令牌的服务器，会在整个进程生命周期中携带该令牌，并且可能让它进入子进程、崩溃报告和粗心的调试日志。它还会让每次更新都变成对服务器秘密处理能力的测试。在让代理使用服务器之前，先减少这种暴露。

为开发、测试和生产使用不同的凭据。将每个凭据限制在服务器所需的少数操作内。调查或大规模测试后轮换测试凭据。这是普通的运维规范，但代理工作流会放大后果，因为调用方可以高速生成不寻常的参数。

Sallyport 将 API 和 SSH 凭据保存在加密保险库中，并在不把秘密交给代理的情况下执行 HTTP 或 SSH 操作。这样可以缩小一个重要的审查问题：你仍然需要审查面向 MCP 的操作及其请求权限，但代理不会获得可以重复使用的凭据材料。

不要把凭据边界误认为对服务器行为的批准。网关可以阻止代理读取令牌，但操作本身仍然可能写入错误记录或联系错误端点。应分别审查请求形状、目标、账户范围和结果处理。

对于后果严重的操作，应在操作边界设置明确审批。Sallyport 可以要求每次使用单独凭据时都进行审批，这在工具能够部署、修改生产记录或访问敏感主机时很有用。反复弹出的提示可能让人不看内容就点击通过，因此应将逐次调用审批保留给误调用会造成重大代价的操作。

## 将采用决定记录在工程师找得到的地方

当没有人能回答四个简单问题时，调查一个发布版本会变得困难：运行的是哪个构件、谁批准了它、测试了什么，以及失败时可以用哪个版本替换它。将这份记录放在启动服务器的配置旁边。

一份简洁的采用记录可以放在代码库文件或变更请求中：

```text
Server: example-mcp-server
Previous artifact: example-mcp-server@1.4.1
Candidate artifact: example-mcp-server@1.4.2
Resolved digest or lockfile revision: recorded in commit 8f31c2a
Reviewed changes: tools/list diff, dependency diff, release notes
Tests: path boundaries passed; redirect boundary passed; restricted account denied
Approved access: test API account only
Rollback artifact: example-mcp-server@1.4.1
Owner: team name or responsible engineer
```

不要记录秘密、包含客户数据的完整请求正文或复制的授权请求头。记录的目的应是证明决策，而不是创建第二个秘密存储库。

审计轨迹应将代理运行与单个操作分开。运行记录回答哪个代理进程获得了权限，操作记录回答它随后尝试做了什么。事件发生时，这是两个不同的问题。如果使用操作网关，应保留能够快速撤销某次运行并在之后检查每次调用的日志。Sallyport 的 Sessions 和 Activity 日志正是围绕这种区分设计的，并使用哈希链加密审计日志作为数据来源，`sp audit verify` 可以离线验证该日志。

不要依赖单次聊天审批。聊天线程会失去上下文，编辑会隐藏历史，软件包引用也很少能完整保留下来。提交到代码库的记录会将声明的测试与之后实际部署的确切配置关联起来。

## 简短的采用门槛胜过紧急回滚

当安全路径模糊或缓慢时，团队就会采用不安全的更新。应将门槛设计得足够简短，能用于补丁版本，同时又足够严格，可以捕获权限变化。

开发者修改共享代理配置前，按以下顺序执行：

1. 解析并锁定候选构件，包括锁定文件或镜像摘要。
2. 在干净的测试环境中使用受限测试身份安装它。
3. 比较原始的 `tools/list` 输出，检查变化的架构、描述和默认值。
4. 运行边界测试夹具和有代表性的代理提示，然后检查出站目标和日志。
5. 提交采用记录，并保留之前的构件作为回滚目标。

这道门槛不能保证更新没有缺陷，任何诚实的流程都无法保证这一点。但它会迫使团队在代理于真实任务中发现变化之前，先在接近计划授予的权限环境中测试实际要运行的代码。

通常第一步改变小得令人尴尬：将共享配置中的浮动服务器版本替换为精确的构件引用，然后提交相关证据。这个动作就能把非正式更新变成一个可以复现、质疑和逆转的决定。
