# 重复的 MCP 服务器注册会运行两次吗？

重复的 MCP 注册很少只是无害的配置杂乱。当两个条目用不同名称描述同一个服务器时，代理可能会获得通往同一能力的两条路径。对于 stdio，这通常意味着两个子进程。对于远程端点，则可能意味着两个经过身份验证的连接、两份工具清单，以及两个独立的位置让代理做出错误调用。

麻烦之处在于，配置优先级无法解决这类故障。优先级只决定名称冲突时使用哪个条目。它无法判断 `repo-api`、`internal-api` 和 `my-api` 是三个可执行文件和三个账户，还是同一个可执行文件和同一个账户的三个标签。应把注册身份视为运营问题，而不是命名问题。

## 重复注册会创建独立的执行路径

两个不同的 MCP 服务器条目可能会启动同一个对象两次，因为客户端把注册视为连接定义，而不是应该自动去重的别名。MCP 传输规范规定，客户端会将 stdio 服务器作为子进程启动，并通过该进程的标准输入和标准输出交换 JSON-RPC。如果两个配置条目都调用同一个命令，通常就会得到两个子进程，每个进程都有自己的初始化过程和生命周期。

这不意味着代理一定会机械地把每个工具调用两次。模型会决定调用哪个公开工具。实际风险更大：代理可能会看到两个注册中描述相近的工具，第一轮调用其中一个，重试时调用另一个，或者因为两者名称暗示不同职责而同时使用它们。与此同时，服务器在第一次工具调用之前就可能已经产生启动副作用。

我见过一些服务器，在检查初始化路径之前看起来很被动。它们会刷新访问令牌、创建缓存目录、打开本地 SQLite 数据库、启动轮询循环来保持索引热更新，或者注册 webhook 消费者。这些选择本身都不违反 MCP。问题在于，团队常常以为「MCP 服务器」就是一个共享且不会产生副作用的对象。

远程服务器会改变故障的表现形式，但不会改变避免故障的必要性。Streamable HTTP 的设计目标是让一个独立的服务器进程处理多个客户端连接。多个客户端确实有意共享时，这很有用。但这也意味着，除非服务有清晰的方法来区分并约束连接，否则它可能会看到两个都声称属于同一位开发者代理的连接。

因此，第一个诊断问题很简单：这两个条目是否为同一个外部权威创建了两条执行路径？如果是，即使 JSON 不同、名称看起来合理，它们仍然是重复注册。

## 服务器名称不等于服务器身份

一个 MCP 注册至少有两种身份，而团队经常把它们混为一谈。

**显示身份**是配置中的名称，例如 `repo-api` 或 `staging-db`。它很重要，因为客户端用它来展示工具并解决配置冲突。它服务于人员和客户端管理。

**执行身份**是注册实际访问的对象：可执行文件及其参数和相关环境，或者带有身份验证上下文的远程 URL。它服务于进程和外部系统。

在整洁的配置中，这两种身份会一致。但它们不必一致。假定两者总是一致，正是重复注册能够通过审查的原因。

看看下面这些条目：

```json
{
  "mcpServers": {
    "billing": {
      "command": "python3",
      "args": ["tools/billing_mcp.py", "--account", "prod"]
    },
    "finance-tools": {
      "command": "python3",
      "args": ["tools/billing_mcp.py", "--account", "prod"]
    }
  }
}
```

名称不同，但这是同一个程序，参数也相同。除非程序本身强制单实例运行，否则它们会启动两次。

再看一个不那么明显的版本：

```json
{
  "mcpServers": {
    "deploy": {
      "command": "./bin/deploy-mcp",
      "args": ["--workspace", "/Users/dev/work/acme"]
    },
    "release-helper": {
      "command": "node",
      "args": ["scripts/mcp-launch.js", "deploy", "--workspace", "/Users/dev/work/acme"]
    }
  }
}
```

文本比较会认为这两个条目不同。进程级比较却可能发现，包装器启动的是同一个 `deploy-mcp` 可执行文件，并且使用同一个工作区。这就是配置审查需要身份规则，而不是简单检查重复行的原因。

比较条目时，请按以下顺序进行：

1. 比较规范化后的远程端点，或者比较命令最终启动的可执行文件。
2. 比较用于选择账户、租户、仓库、工作区或写入目标的参数。
3. 比较会改变行为的工作目录和非机密环境变量名称。
4. 单独比较凭据归属。两个条目访问同一个端点，却使用不同权限主体时，并不是无害的重复，而是一项需要说明理由的权限设计决策。

不要读取秘密值来执行这项审计。你不需要它们，把秘密复制到审计输出中还会制造第二个安全问题。记录一个条目使用 `BILLING_TOKEN`，另一个使用 `PERSONAL_BILLING_TOKEN`，然后确认这些变量是否授权访问同一账户。

## 作用域优先级无法清理不同名称

Claude Code 文档说明了三种 MCP 作用域：本地、项目和用户。项目作用域的条目位于仓库的 `.mcp.json` 文件中，用户作用域的条目则可跨项目使用。文档还说明，同名服务器会按本地作用域、项目作用域、用户作用域的顺序解析。更早的文档把用户作用域称为「全局」。

这种行为只能保护你免受一种较窄情况的影响：完全相同的名称出现在多个作用域中。它无法防止最常见的重复注册：

```text
User scope:    personal-git      -> /Users/dev/bin/git-mcp
Project scope: repository-git    -> /Users/dev/bin/git-mcp
```

两个名称都不会发生冲突，因此都可能保持可见。两个条目都可能启动，并公开几乎相同的工具。

还有一个陷阱。开发者看到项目文件中有 `repository-git`，于是因为想在当前仓库之外使用该工具，又在用户作用域添加了 `personal-git`，却忘了用户条目在当前仓库中也会加载。刚开始使用体验很好，直到某次工具调用写入两条审计记录，或者后台工作线程锁住同一个状态目录，清理工作才被迫开始。

应将作用域用于明确归属，而不是图方便：

- 当仓库需要某个注册，且配置可以安全共享时，将它放在项目作用域。
- 当它是应该跨仓库工作的个人工具时，将它放在用户作用域。
- 对于不应提交到代码库的私有仓库实验，使用本地作用域。
- 不要在用户作用域复制项目条目。如果其他地方也需要它，要么只在项目配置生效的地方调用，要么定义一个目标不同、用途有文档说明的独立条目。

同名覆盖也值得关注。它不会像两个不同名称那样创建两个活动条目，但可能会用个人配置遮蔽团队配置。于是代理会使用私有可执行文件或个人端点执行操作，而审查者却以为仓库定义正在生效。这是来源问题，不是进程数量问题，但同样需要修复。

## 从配置、进程到调用证明重复注册

不要看到一个条目似乎多余就立即删除。应建立从配置到进程，再到外部操作的完整链路。这样可以避免看似干净的修复，却悄悄删除了唯一使用正确账户或工作区的条目。

先在受影响的仓库中运行：

```sh
claude mcp list
claude mcp get repository-git
claude mcp get personal-git
```

Anthropic 将 `claude mcp list`、`claude mcp get` 和 `claude mcp remove` 记录为常规管理命令。利用输出找出所有可见名称，然后逐一检查可疑条目。

在临时文件中为每个条目记录五项事实：配置名称、作用域、命令或 URL、参数，以及它访问的外部账户或工作区。不要粘贴环境变量值。对于远程服务器，记录主机和路径，不要记录授权请求头。

接着启动一个短暂的代理会话，并在连接期间检查进程。在 macOS 或 Linux 上，将 `billing_mcp.py` 替换为预期命令中的唯一片段：

```sh
ps -ax -o pid,ppid,lstart,command | grep '[b]illing_mcp.py'
```

重复启动 stdio 进程可能会显示为：

```text
91204 91188 Tue Jul 21 10:14:07 2026 python3 tools/billing_mcp.py --account prod
91219 91188 Tue Jul 21 10:14:09 2026 python3 tools/billing_mcp.py --account prod
```

进程 ID 不同。父进程可能是同一个代理进程，也可能是两个有关联的代理进程。关键证据是：两个命令具有相同的执行身份，并且生命周期存在重叠。

然后发起一次明确安全的只读工具调用。选择预期结果范围很小的调用，例如获取当前账户标识，或列出一个已知对象。检查目标系统自身的日志、服务器日志或操作日志。如果看到两个独立连接但只有一次调用，就证明服务器启动重复。如果看到两次调用，需要判断是代理选择了两个工具、错误后重试，还是服务器自身重复执行。这些是不同原因，需要分别修复。

MCP 生命周期规范要求在正常运行前完成初始化。看到两次初始化事件，足以证明存在两个连接。但这不能证明发生了任何业务操作，因此不要仅仅因为看到了两次握手，就告诉事件响应人员「部署运行了两次」。

## 不读取凭据也能为配置生成指纹

有用的重复检查会为每个配置条目生成稳定指纹，并排除秘密值。下面的脚本读取一个或多个 JSON 配置文件，提取 `mcpServers`，然后比较传输方式、命令、参数、URL、工作目录和环境变量名称。只向它提供你获准检查的文件。

```python
#!/usr/bin/env python3
# save as mcp_duplicates.py
import hashlib
import json
import pathlib
import sys
from collections import defaultdict

if len(sys.argv) < 2:
    raise SystemExit("usage: mcp_duplicates.py CONFIG [CONFIG ...]")

entries = defaultdict(list)

for raw_path in sys.argv[1:]:
    path = pathlib.Path(raw_path).expanduser()
    with path.open() as handle:
        document = json.load(handle)

    for name, server in document.get("mcpServers", {}).items():
        identity = {
            "type": server.get("type", "stdio"),
            "command": server.get("command"),
            "args": server.get("args", []),
            "url": server.get("url"),
            "cwd": server.get("cwd"),
            "env_names": sorted(server.get("env", {}).keys()),
            "header_names": sorted(server.get("headers", {}).keys()),
        }
        encoded = json.dumps(identity, sort_keys=True, separators=(",", ":"))
        fingerprint = hashlib.sha256(encoded.encode()).hexdigest()[:12]
        entries[fingerprint].append((str(path), name, identity))

for fingerprint, matches in sorted(entries.items()):
    if len(matches) < 2:
        continue
    print(f"DUPLICATE EXECUTION IDENTITY {fingerprint}")
    for path, name, identity in matches:
        print(f"  {path}: {name}")
        print(f"    {json.dumps(identity, sort_keys=True)}")
```

针对项目的 `.mcp.json` 和客户端使用的用户级配置脱敏导出文件或副本运行它：

```sh
python3 mcp_duplicates.py .mcp.json ~/tmp/user-mcp.json
```

输出应类似这样：

```text
DUPLICATE EXECUTION IDENTITY 64e0e2509d8a
  .mcp.json: repository-git
    {"args":["tools/git_mcp.py"],"command":"python3","cwd":null,"env_names":["GIT_ACCOUNT"],"header_names":[],"type":"stdio","url":null}
  /Users/dev/tmp/user-mcp.json: personal-git
    {"args":["tools/git_mcp.py"],"command":"python3","cwd":null,"env_names":["GIT_ACCOUNT"],"header_names":[],"type":"stdio","url":null}
```

这项检查有意保持保守。它会标记声明的执行形态相同的条目，但无法证明两个不同的包装命令不会最终汇聚到同一个进程，也无法证明两个不同的 URL 不会路由到同一个服务。把输出视为待审查队列，而不是自动删除清单。

当一个配置使用相对路径，另一个使用绝对路径时，也要预期会出现漏报。如果团队同时使用这两种形式，请在比较前规范化路径。应在了解仓库根目录的受控脚本中完成这项工作，不要跨配置文件进行宽泛的搜索替换。

## 两个独立实例可能对状态产生不同判断

代价最高的故障不一定是重复 API 调用。两个实例可能在本地状态上产生分歧，同时各自都完全按照作者预期运行。

假设某个服务器维护仓库元数据的本地缓存。实例 A 使用较旧的检出版本启动，并把缓存记录写入共享默认目录。实例 B 在切换分支后启动，读取同一个目录，并因为文件存在而判断缓存有效。此时工具调用返回的数据不属于任何一个进程当前的视图。代理随后可能针对一个过时对象发起完全有效的调用。

另一个常见故障涉及队列消费者。两个实例使用同一身份主体进行身份验证，并轮询同一个任务流。如果队列提供至少一次投递，重复处理可能原本就在预期范围内。如果工具作者添加了本地去重映射，每个进程都会拥有自己的映射。它能阻止单个进程内的重复，却无法阻止两个进程之间的重复。

这里不应给出的糟糕建议是「把服务器做成无状态」。这个建议很流行，因为听起来安全，而且无状态 HTTP 服务确实能很好地处理多个连接。但对于有意维护缓存、OAuth 刷新状态、文件观察器或操作句柄的本地工具来说，它是错误的。正确要求更具体：说明是否支持并发实例、实例共享哪些资源，以及两个实例使用同一身份时会发生什么。

请服务器负责人在 README 或启动输出中回答这些问题：

- 启动时是否会写入本地文件、刷新凭据或启动后台任务？
- 两个进程能否使用同一个工作区、账户和缓存目录？
- 改变外部系统的每次工具调用是否包含幂等键？
- 操作者能否识别产生某条记录的客户端进程或会话？

如果第二个问题的答案是否定的，就要明确处理冲突。可以使用操作系统锁、为每个进程设置独立运行时目录，或使用服务器端租约。不要依赖人们记住只能配置一次。

## 工具重复和操作重复是两种不同的事件

客户端可以公开两个相似工具，却不执行其中任何一个两次。它也可能只通过一个工具重复执行外部操作。当有人把这两种结果都叫作「MCP 重复」时，调查就会偏离方向。

**工具重复公开**表示两个注册公布了重叠能力。代理可能会看到来自两个服务器的 `billing_get_invoice`。这是配置和提示方面的风险，应修复注册及其描述。

**重复执行**表示存在两个本地进程或两个远程会话。这是连接和生命周期方面的风险，应修复注册路径、服务器并发行为，或两者一起修复。

**重复外部操作**表示目标系统收到了不止一个有实际意义的请求。这可能来自重复公开、重试逻辑、超时、用户干预、服务器行为或客户端缺陷。应通过目标系统中的操作标识来证明，而不是根据 MCP 进程数量推断。

事件处理中应将以下记录放在一起：

```text
Agent run ID:          run-7f3a
Configured name:       repository-git
Server process ID:     91204
MCP connection start:  2026-07-21T10:14:07Z
Tool request ID:       58
Target operation ID:   commit-3a8b
```

标识符不必使用这些确切名称，但必须能在代理、服务器和目标服务之间建立关联。如果某一层无法生成关联值，就在事件记录中说明，不要用时间猜测填补空白。

Sallyport 将代理运行记录放入 Sessions 日志，将单独调用放入 Activity 日志，这种区分在这里很有用，因为它保留了两者之间的差异。第二次运行或连接并不自动证明发生了第二次外部操作，仍然需要调用记录来证明。

## 删除一个注册，同时避免造成盲区

确认是真正的重复后，先选择一个规范注册，再删除其他条目。规范条目应有明确负责人、可预测的作用域、经过审查的命令或端点，以及明确记录的凭据来源。「它在我的机器上碰巧能用」不是选择标准。

对于团队拥有的集成，项目条目通常更合适，因为它可以和代码库一起接受审查。不要把凭据放进共享文件。Claude Code 支持在 `.mcp.json` 中展开环境变量，包括命令、参数、环境字段、URL 和请求头中的值。这样可以在不提交令牌的情况下共享定义。

对于个人跨项目工具，用户作用域可能是合适位置。只有在仓库不要求其他贡献者使用共同工具定义时，才删除项目条目。不要把团队依赖变成没有文档说明的个人前置条件。

修改后使用安静的验证流程：

1. 在测试期间，将删除的条目保存在活动配置之外。
2. 启动一个全新的代理进程。已有进程可能仍保留旧连接。
3. 运行 `claude mcp list`，并使用 `claude mcp get <name>` 检查剩余条目。
4. 发起一次安全的只读调用，并记录一次连接和一次目标请求。
5. 再次重启，确认被删除的注册不会重新出现。

如果删除操作破坏了工作流，只恢复规范定义，并修正缺少的路径、环境变量或权限。不要为了快速修复而恢复两个条目，那会重新制造你刚刚花时间诊断的歧义。

## 将重复检测纳入配置审查

最好的控制措施是一条简单的审查规则：每个 MCP 注册都必须有负责人、作用域，以及为其预期用途保持唯一的执行身份。这样足以在代理运行前发现大多数意外。

如果团队将 `.mcp.json` 纳入版本控制，就把指纹检查放入仓库脚本。在本地审查和持续集成中针对共享配置运行它。它看不到开发者的用户作用域条目，因此对于报告奇怪工具行为的贡献者，还应将 `claude mcp list` 加入设置检查清单。

对于用户作用域配置，在配置之外保留一份简短清单。每个服务器一行即可：

```text
personal-git | user | git tooling across repositories | owner: developer
repository-git | project | repository release workflow | owner: platform team
```

如果两行指向同一个可执行文件和账户，其中一行就需要删除，或者必须让它们的目标有意区别开来。不要因为某个标签在提示中听起来更友好，就接受两条路径。

这套规范并不复杂：一个用途对应一条路径，所有权清晰可见，并且有证据表明一次请求的操作产生了一条外部记录。做到这些后，重复进程就会成为可观察的缺陷，而不再是隐藏在两个几乎相同工具名称背后的深夜谜团。
