# Are duplicate MCP server registrations running twice?

A duplicate MCP registration is rarely a harmless bit of config clutter. When two entries describe the same server under different names, an agent can receive two routes to the same capability. With stdio, that often means two child processes. With a remote endpoint, it can mean two authenticated connections, two tool inventories, and two independent places for the agent to make the wrong call.

The annoying part is that config precedence does not solve this class of failure. Precedence only decides what happens when entries collide by name. It does not determine whether `repo-api`, `internal-api`, and `my-api` are three labels for one executable and one account. Treat registration identity as an operational concern, not a naming concern.

## Duplicate registrations create separate execution paths

Two distinct MCP server entries can start the same thing twice because the client treats registrations as connection definitions, not as aliases that it should deduplicate. The MCP transport specification says that a client launches a stdio server as a subprocess and exchanges JSON-RPC over that process's standard input and output. If two configured entries each invoke the same command, the ordinary outcome is two subprocesses, each with its own initialization and lifetime.

That does not mean an agent will mechanically invoke every tool twice. Models decide which exposed tool to call. The risk is worse in practice: the agent may see similarly described tools from both registrations, call one on the first turn and the other on a retry, or use both because their names imply different responsibilities. Meanwhile, server startup can already have side effects before the first tool call.

I have seen servers that look passive until you inspect their initialization path. They refresh an access token. They create a cache directory. They open a local SQLite database. They start a polling loop to keep an index warm. They register a webhook consumer. None of those choices violates MCP. They become a problem when a team assumes "MCP server" means one shared, inert object.

A remote server changes the shape of the failure, not the need to prevent it. Streamable HTTP is designed for an independent server process that can handle multiple client connections. That is useful when multiple clients are intentional. It also means the service may see two connections that both claim to be the same developer's agent, unless the service has a clean way to distinguish and constrain them.

The first diagnostic question is therefore plain: do these two entries create two execution paths to the same external authority? If yes, they are duplicates even when their JSON differs and their names look sensible.

## A server name is not a server identity

An MCP registration has at least two identities, and teams routinely blend them together.

The **display identity** is the configured name, such as `repo-api` or `staging-db`. It matters because the client uses it to present tools and resolve configuration conflicts. It is for people and client bookkeeping.

The **execution identity** is what the registration actually reaches: an executable plus its arguments and relevant environment, or a remote URL plus its authentication context. It is for processes and external systems.

Those identities match in a tidy setup. They do not have to match, and assuming they do is how duplicates survive review.

Consider these entries:

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

The names differ, but this is one program with the same arguments. Unless the program itself enforces a single instance, those are two launches.

Now consider a less obvious version:

```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"]
    }
  }
}
```

A text comparison says these entries are different. A process-level comparison may show that the wrapper starts the same `deploy-mcp` executable with the same workspace. This is why configuration review needs an identity rule, not a simple duplicate-line check.

Use this order when comparing entries:

1. Compare the normalized remote endpoint, or the final executable that the command starts.
2. Compare arguments that select an account, tenant, repository, workspace, or write target.
3. Compare working directory and non-secret environment variable names that change behavior.
4. Compare credential ownership separately. Two entries that reach the same endpoint with different authorities are not harmless duplicates. They are a permission design decision that needs a reason.

Do not compare secret values to perform this audit. You do not need them, and copying them into audit output creates a second security problem. Record that one entry uses `BILLING_TOKEN` and another uses `PERSONAL_BILLING_TOKEN`; then establish whether those variables authorize the same account.

## Scope precedence cannot clean up distinct names

Claude Code documents three MCP scopes: local, project, and user. Project-scoped entries live in a repository `.mcp.json` file, while user-scoped entries are available across projects. Its documentation also says that an identical server name resolves with local scope first, then project, then user scope. Earlier documentation called user scope "global."

That behavior protects you from one narrow case: the exact same name appears in more than one scope. It does not protect you from the usual duplicate registration:

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

Both names can remain visible because nothing conflicts by name. Both may start, and both may expose nearly identical tools.

There is another trap. A developer sees that the project file contains `repository-git`, adds `personal-git` at user scope because they want the tool outside this repository, then forgets that the user entry also loads inside the repository. The immediate experience is pleasant. The cleanup is deferred until a tool call writes two audit records or a background worker locks the same state directory.

Use scopes for ownership, not convenience:

- Put a registration in project scope when the repository requires it and the configuration is safe to share.
- Put it in user scope when it is a personal utility that should work across repositories.
- Use local scope for a private, repository-specific experiment that should not be committed.
- Do not duplicate a project entry in user scope. If you need it elsewhere, either invoke it only where the project config applies or define a deliberately separate entry with a different target and a documented purpose.

A same-name override deserves attention too. It does not create two active entries in the way two names can, but it can hide a team configuration behind a personal one. The agent then performs actions with a private executable or personal endpoint while reviewers assume the repository definition is in effect. That is a provenance failure, not a process-count failure, and it still needs fixing.

## Prove the duplicate from config to process to call

Do not delete the first entry that looks redundant. Establish the chain from configuration to process to external action. That prevents a clean-looking fix that quietly removes the only entry using the right account or workspace.

Start inside the affected repository:

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

Anthropic documents `claude mcp list`, `claude mcp get`, and `claude mcp remove` as the normal management commands. Use the output to identify every visible name, then inspect the suspicious entries one at a time.

Record five facts for each entry in a scratch file: configured name, scope, command or URL, arguments, and the external account or workspace it reaches. Avoid pasting environment values. For a remote server, capture the host and path, not an authorization header.

Next, start one short agent session and inspect processes while it is connected. On macOS or Linux, replace `billing_mcp.py` with a unique portion of the command you expect:

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

A duplicate stdio launch has an output shape like this:

```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
```

The process IDs differ. The parent may be the same agent process or two related agent processes. The important evidence is that both commands have the same execution identity and overlapping lifetimes.

Then make one deliberately safe, read-only tool call. Pick a call with a narrow expected record, such as retrieving the current account identifier or listing one known object. Check the target system's own logs, the server's logs, or your action journal. If you see two independent connections but one call, you have found duplicate server startup. If you see two calls, determine whether the agent selected two tools, retried after an error, or the server itself repeated work. Those are separate causes that need separate fixes.

The MCP lifecycle specification requires initialization before normal operation. Seeing two initialization events is enough to prove two connections. It does not prove that any business action occurred, so do not tell incident responders "the deployment ran twice" merely because you saw two handshakes.

## Fingerprint configurations without reading credentials

A useful duplicate check produces a stable fingerprint for each configured entry and excludes secret values. The following script reads one or more JSON configuration files, extracts `mcpServers`, and compares transport, command, arguments, URL, working directory, and environment variable names. Give it only files you are authorized to inspect.

```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)}")
```

Run it against a project's `.mcp.json` and a sanitized export or copy of the user-level configuration that your client uses:

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

The output should look like this:

```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}
```

This check is intentionally conservative. It flags entries with the same declared execution shape. It cannot prove that two different wrapper commands do not converge on one process, and it cannot prove that two different URLs do not route to the same service. Treat its output as a review queue, not an automatic deletion list.

Also expect false negatives when one config uses a relative path and another uses an absolute path. Normalize paths before comparison if your team uses both forms. Do that in a controlled script that knows the repository root. Do not make a broad search-and-replace across configuration files.

## Two independent instances can disagree about state

The most expensive failures are not always duplicate API calls. Two instances can disagree about local state while each behaves exactly as its author expected.

Take a server that maintains a local cache of repository metadata. Instance A starts with an older checkout and writes cache records under a shared default directory. Instance B starts after a branch switch, reads the same directory, and decides the cache is valid because the file exists. A tool call now returns data that belongs to neither process's current view. The agent may then make a perfectly valid call against a stale object.

Another familiar failure involves a queue consumer. Both instances authenticate as the same principal and poll the same job stream. If the queue offers at-least-once delivery, duplicate handling may already be expected. If the tool author adds a local deduplication map, each process gets its own map. The map prevents duplicates within one process and does nothing across the pair.

The bad recommendation here is "just make the server stateless." It is popular because it sounds safe and because stateless HTTP services handle many connections well. It is wrong for local tools that intentionally keep caches, OAuth refresh state, file watchers, or operation handles. The correct requirement is narrower: document whether concurrent instances are supported, which resources they share, and what behavior occurs when two instances use the same identity.

Ask server owners to answer these questions in the README or startup output:

- Does startup write local files, refresh credentials, or start a background task?
- Can two processes use the same workspace, account, and cache directory?
- Does each tool call include an idempotency key when it changes an external system?
- Can an operator identify the client process or session that produced a record?

If the answer to the second question is no, make the conflict explicit. Use an operating-system lock, a unique runtime directory per process, or a server-side lease. Do not rely on people remembering that they should configure it once.

## Tool duplication and action duplication are different incidents

A client can expose two similar tools without executing either one twice. It can also make a repeated external action through one tool only. Investigations go off track when someone calls both outcomes "duplicate MCP."

**Duplicate tool exposure** means two registrations advertise overlapping capabilities. The agent may see `billing_get_invoice` from two servers. This is a configuration and prompting hazard. Fix the registrations and the descriptions.

**Duplicate execution** means two local processes or two remote sessions exist. This is a connection and lifecycle hazard. Fix the registration path, server concurrency behavior, or both.

**Repeated external action** means the target system received more than one meaningful request. This may result from duplicate exposure, retry logic, timeouts, user intervention, server behavior, or a client bug. Prove it with an operation identifier at the target, not an inference from the number of MCP processes.

Keep these records together during an incident:

```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
```

The identifiers need not use those exact names. They need to join across the agent, the server, and the target service. If one layer cannot produce a correlation value, say so in the incident record instead of filling the gap with timing guesses.

Sallyport's split between a Sessions journal for agent runs and an Activity journal for individual calls is useful here because it preserves that distinction. A second run or connection is not automatically proof of a second external action; the call records still need to show it.

## Remove one registration without creating a blind spot

Once you have identified a true duplicate, choose a canonical registration before you remove anything. The canonical entry should have a clear owner, predictable scope, a reviewed command or endpoint, and a stated credential source. "This one happened to work on my machine" is not a selection rule.

For a team-owned integration, the project entry usually wins because it is reviewable alongside the codebase. Keep credentials out of that shared file. Claude Code supports environment-variable expansion in `.mcp.json`, including values in commands, arguments, environment fields, URLs, and headers. That makes shared definitions possible without committing a token.

For a personal cross-project utility, user scope can be the right home. Then remove the project entry only if the repository does not require a common tool definition for other contributors. Do not turn a team dependency into an undocumented personal prerequisite.

Use a quiet validation sequence after the change:

1. Save the removed entry somewhere outside active configuration for the duration of the test.
2. Start a fresh agent process. Existing processes can retain old connections.
3. Run `claude mcp list` and inspect the remaining entry with `claude mcp get <name>`.
4. Make one safe read-only call and record one connection and one target request.
5. Restart once more and confirm that the removed registration does not return.

If removal breaks a workflow, restore only the canonical definition and correct its missing path, environment variable, or permissions. Do not restore both entries as a fast fix. That recreates the ambiguity you just spent time diagnosing.

## Make duplicate detection part of configuration review

The best control is a small review rule: every MCP registration needs an owner, a scope, and an execution identity that is unique for its intended purpose. That is enough to catch most accidents before agents run.

Put the fingerprint check in a repository script if the team keeps `.mcp.json` under version control. Run it in local review and in continuous integration against the shared configuration. It will not see a developer's user-scope entries, so also make `claude mcp list` part of the setup checklist for contributors who report strange tool behavior.

For user-scope configuration, keep a short inventory outside the config itself. One line per server is enough:

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

If both lines point to the same executable and account, one of them needs to go or their targets need to become intentionally distinct. Do not accept two labels because one sounds friendlier in a prompt.

The discipline is modest: one route for one purpose, visible ownership, and evidence that a single requested action produced a single external record. Once you have that, a duplicate process is an observable defect rather than a late-night mystery hidden behind two nearly identical tool names.
