Are duplicate MCP server registrations running twice?
Duplicate MCP server registrations can launch parallel processes and muddy audits. Detect overlaps across global and project configs before calls repeat.

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:
{
"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:
{
"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:
- Compare the normalized remote endpoint, or the final executable that the command starts.
- Compare arguments that select an account, tenant, repository, workspace, or write target.
- Compare working directory and non-secret environment variable names that change behavior.
- 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:
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:
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:
ps -ax -o pid,ppid,lstart,command | grep '[b]illing_mcp.py'
A duplicate stdio launch has an output shape like this:
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.
#!/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:
python3 mcp_duplicates.py .mcp.json ~/tmp/user-mcp.json
The output should look like this:
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:
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:
- Save the removed entry somewhere outside active configuration for the duration of the test.
- Start a fresh agent process. Existing processes can retain old connections.
- Run
claude mcp listand inspect the remaining entry withclaude mcp get <name>. - Make one safe read-only call and record one connection and one target request.
- 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:
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.
FAQ
Can the same MCP server be started twice?
It can, if the registrations resolve to separate server entries. This is most common when the entries use different names but start the same command or point at the same remote endpoint. A same-name conflict may resolve by scope precedence instead, which is a different problem.
Do different MCP server names prevent duplicate processes?
No. A unique name only prevents a visible naming collision in one client view. Two names can still launch the same stdio command, reuse the same working directory, or call the same HTTP MCP endpoint.
Should an MCP server be global or project scoped?
Keep it in the project configuration only when every contributor needs the same integration and the definition contains no personal credential. Put personal utilities in user scope. Do not keep an identical shared integration in both places just because it feels convenient.
How do I find duplicate MCP configurations in Claude Code?
Run claude mcp list from the repository, then inspect suspicious entries with claude mcp get <name>. Compare the command, arguments, endpoint, working directory, and environment-variable names, not only the server labels.
Can duplicate MCP processes corrupt local state?
They can. A stateful server may create two independent caches, queue consumers, background pollers, or local databases. Even a read-only looking tool can produce confusing records if its startup routine writes a session marker or refreshes a token.
Is it safe to remove a duplicate MCP registration?
Usually, yes. Remove the extra registration first, restart the agent, and confirm that one process remains. Keep the project entry if the team needs it, or keep the user entry if it is genuinely personal, but do not leave both as a fallback.
Are stdio MCP servers shared across clients?
A server can usually work with several clients only when its transport and state model support that arrangement. A local stdio server is simpler: each client launches its own child process, so duplicate registrations create separate process instances.
Why does a config diff miss duplicate MCP servers?
A JSON diff catches text changes, not identity. One entry might invoke a wrapper while another invokes the underlying executable, and both can reach the same service. Normalize command paths, arguments, URLs, and working directories before comparing them.
Will an MCP gateway fix duplicate registrations?
A broker is useful when it gives you a clear action boundary, per-run attribution, and a way to revoke access. It will not repair duplicate registration by itself. You still need one intentional route from each agent run to each external capability.
What should I do if duplicate MCP servers are already making calls?
Do not delete configuration during an incident unless you know which process owns active work. First stop new agent runs, record the process tree and recent calls, remove the duplicate entry, then restart a single controlled session and compare the resulting records.