# How MCP config precedence changes across terminals and IDEs

MCP configuration conflicts are not an MCP problem. They are a client problem, and that distinction saves a lot of bad debugging.

The protocol tells a client how to talk to a server after it starts one. It does not tell Claude Code, VS Code, Cursor, GitHub Copilot CLI, or an extension which JSON file to read first, whether two same-named entries merge, or whether a plugin can register a server after file-based configuration loads. If you assume one universal hierarchy, you can run the wrong executable with the right-looking tool name.

That failure is easy to miss. A tool list says `github`, the agent calls `github.search_code`, and the call succeeds. Meanwhile, your terminal agent may have launched a project wrapper against a test account, while the IDE launched a global command against your personal account. The name matched. The command did not.

This is the working rule I use: resolve MCP configuration per client, per process, and per server name. Treat the server command, arguments, environment, working directory, transport URL, and credential source as one launch definition. Do not infer any of them from the label shown in an agent panel.

## MCP has no shared precedence ladder

MCP standardizes messages and capabilities between clients and servers. It does not prescribe a portable file layout or a winner when configuration sources disagree. A client can use JSON files, a settings database, a plugin API, managed enterprise policy, a command-line flag, or all of them.

That means four phrases that people often treat as interchangeable are not interchangeable:

- A **user config** is a client-specific global definition for one account or profile.
- A **project config** is a definition associated with a repository or workspace.
- A **plugin config** is a server registered or supplied by an installed extension or plugin.
- An **editor setting** is a setting belonging to the editor, which may or may not control MCP server configuration at all.

The last distinction causes more trouble than it should. VS Code has a mature general settings hierarchy: workspace settings override user settings for ordinary settings, and object values can merge while primitive and array values override. That is real behavior for `settings.json`. It does not prove that `.vscode/mcp.json` follows the same merge and collision rules. VS Code documents MCP configuration as its own `mcp.json` file in either a workspace or user profile. Do not import assumptions from the settings engine into a separate configuration format.

The practical consequence is blunt: “workspace wins” is not a useful answer until you add the client name, client version, config format, and the exact collision. A workspace file can add a server, shadow a same-named global server, coexist with it, or fail to load because the workspace is untrusted. Those are different outcomes, and a generic precedence diagram hides that.

## The server name is the unit of conflict

Most clients organize MCP configuration as a map: a server name on the left, one launch definition on the right. The map key is often what the client uses to decide that two declarations conflict.

Consider these two files:

```json
// user configuration
{
  "mcpServers": {
    "catalog": {
      "command": "node",
      "args": ["/Users/dev/bin/catalog-live.js"],
      "env": { "CATALOG_TARGET": "production" }
    }
  }
}
```

```json
// project configuration
{
  "mcpServers": {
    "catalog": {
      "command": "node",
      "args": ["./tools/catalog-fixture.js"],
      "env": { "CATALOG_TARGET": "fixture" }
    }
  }
}
```

A person sees two catalog services. A client sees two values at the `catalog` key. If it selects one definition, it generally selects the whole definition. Do not expect the client to combine the global command with project arguments, or the project command with the global environment. Some setting systems merge objects; an MCP client is under no obligation to do so.

This is why partial overrides are a poor design. A project that wants a different endpoint should declare its intended command completely. A user who wants a personal tool should use a different name. A half-override makes it difficult to tell whether the client replaced the entire server object or merged fields in a way you did not test.

Use names that expose ownership and purpose while you are still diagnosing the setup:

```json
{
  "mcpServers": {
    "catalog-user-live": { "command": "node", "args": ["/Users/dev/bin/catalog-live.js"] },
    "catalog-repo-fixture": { "command": "node", "args": ["./tools/catalog-fixture.js"] }
  }
}
```

Those names are not elegant. They are honest. Once the team has a single canonical definition, you can rename it to `catalog`. Before that, a concise duplicate name turns every tool call into a guessing exercise.

Also separate a duplicate name from a duplicate capability. Two servers may both expose a tool called `search`, yet remain distinct because their server names differ. The agent can be confused by similar descriptions, but the client configuration is not necessarily in a name collision. Solve client resolution first, then improve tool descriptions and naming.

## Claude Code has an explicit MCP scope order

Claude Code is the easiest case because its MCP documentation states the order for same-named server entries. Local scope wins over project scope, and project scope wins over user scope. Its current terminology matters: `local` is the private, project-specific default; `project` writes a shared `.mcp.json`; `user` applies across projects. Anthropic previously used different names for some of these scopes, which is why old notes and shell history can be misleading.

In concrete terms, if all three scopes contain `catalog`, Claude Code starts the local definition. The shared `.mcp.json` definition is next, and the user definition is the fallback.

```sh
# private to this checkout and this user
claude mcp add catalog --scope local -- node ./tools/catalog-fixture.js

# shared with the repository
claude mcp add catalog --scope project -- node ./tools/catalog-service.js

# available in all repositories for this user
claude mcp add catalog --scope user -- node ~/bin/catalog-personal.js
```

The expected inspection result is one effective server named `catalog`, sourced from the local scope when all three exist. Run the client command that lists or gets the server after each change rather than trusting the file you just edited:

```sh
claude mcp get catalog
```

The output shape should identify the named server and show its configured transport or command details. Compare the actual command, arguments, and environment with the definition you expected. If the command is not the one you edited, stop changing files and identify which scope still owns the name.

Do not confuse this MCP scope rule with Claude Code's broader settings precedence. Anthropic documents managed enterprise policy, command-line arguments, local project settings, shared project settings, and user settings for general Claude Code settings. A managed setting may therefore constrain behavior that surrounds MCP use without acting as a second MCP server definition. The two hierarchies answer different questions.

There is another trap in project scope. Claude Code asks for approval before using a server supplied by `.mcp.json`. That approval is about whether the client may use the project-provided server. It does not change which same-named server configuration has priority. Do not read an approval prompt as evidence that the shared command won.

## VS Code keeps MCP files apart from ordinary settings

VS Code offers two documented locations for MCP server configuration: `.vscode/mcp.json` in the workspace and a user-profile `mcp.json`, opened through the `MCP: Open User Configuration` command. The workspace file is meant to be shareable through source control, while the profile file follows the user and can differ by VS Code profile.

That layout encourages a reasonable expectation: a repository's configuration should define repository tools, and a profile should define personal tools. It does not, by itself, document a complete duplicate-name resolution rule. In particular, the public MCP configuration reference describes the schema and locations, but does not say that ordinary VS Code setting precedence applies field by field to `servers` entries.

This is where experienced users make a bad shortcut. They know that VS Code workspace settings override user settings. They put the same MCP server name in profile and workspace configs, change the project command, and conclude that the workspace command must launch. It might. But a conclusion based on an adjacent settings subsystem is still a conclusion, not a documented contract.

Treat a same-name collision in VS Code as a test requirement. Make the two candidates visibly different and use a harmless command that proves which one started:

```json
{
  "servers": {
    "precedence-probe": {
      "type": "stdio",
      "command": "sh",
      "args": ["-lc", "printf 'workspace probe started\\n' >&2; exec node ./tools/probe-server.js"]
    }
  }
}
```

Put a different marker in the user-profile configuration:

```json
{
  "servers": {
    "precedence-probe": {
      "type": "stdio",
      "command": "sh",
      "args": ["-lc", "printf 'profile probe started\\n' >&2; exec node $HOME/bin/probe-server.js"]
    }
  }
}
```

Then fully restart the server through VS Code's MCP management UI, or restart the editor if the UI does not make the process state clear. Inspect the MCP server output or logs for the marker. Do not test against a real database or a real deployment target. A precedence check should prove a launch path, not alter data.

The same caution applies to enablement. VS Code documents that enable and disable state is stored separately from server configuration, so a shared config file can be present while the server does not start in one workspace. “I can see it in `mcp.json`” and “this client launched it” are separate facts.

Multi-root workspaces add another place for mistaken certainty. VS Code has workspace and workspace-folder scopes for general settings, yet an MCP file tied to a workspace is not automatically a per-folder server declaration. If a tool needs a repository-relative command, make the workspace root and the expected working directory explicit in your test. A command that works in one root can fail or silently reach a different file when the editor opens several folders.

## Cursor adds project, global, and extension registration paths

Cursor documents project-specific MCP configuration at `.cursor/mcp.json` and global configuration at `~/.cursor/mcp.json`. It also exposes an extension API that can register MCP servers programmatically. Those are three distinct sources: repository file, user file, and code running inside an extension.

The first two are easy to reason about when names are unique. Put a repository's shared development service in `.cursor/mcp.json`. Put a personal utility, such as a local notes search service, in the global file. Cursor's CLI says it detects and respects `mcp.json`, which makes shared configuration useful when the IDE and its terminal agent actually run in the same environment.

The hard case is a duplicate name across all three sources. Cursor's public MCP documentation tells you where to put project and global files, but it does not publish a complete contract for every collision involving global configuration, project configuration, and a dynamically registered extension server. Do not manufacture one from a forum post, a stale release note, or a behavior observed in one machine.

The safe design is to avoid the collision. If an extension supplies `issue-tracker`, do not put a second `issue-tracker` entry in `.cursor/mcp.json` and hope the repository command replaces it. Give the repository-owned server a different name, such as `issue-tracker-fixture`, and make the agent instructions or tool descriptions clear about when it should use it.

This is especially important in teams that use a plugin for convenience and a file for reproducibility. The plugin may handle installation, OAuth, updates, or registration on its own schedule. A repository file is visible in code review. Those are different ownership models. Decide which one owns the server before trying to make them agree by coincidence.

Environment boundaries make Cursor conflicts look stranger than they are. A desktop editor may run on one operating system while its terminal, remote workspace, or development container runs elsewhere. A global file in a home directory may exist in one environment but not the other. The command in a project file may resolve `node`, `python`, or a relative executable through different `PATH` values. Before discussing precedence, write down where the client process runs and where the server process runs.

For each candidate, record this small launch record:

```text
client: Cursor desktop / Cursor CLI
client environment: local macOS / WSL / remote host / container
config source: ~/.cursor/mcp.json / .cursor/mcp.json / extension registration
server name: issue-tracker
command: node ./tools/issue-mcp.js
working directory: repository root
credential source: OAuth / local environment / external gateway
```

A five-minute record like this beats an afternoon of toggling settings panels.

## GitHub Copilot CLI documents project-over-user MCP resolution

GitHub Copilot CLI is more explicit than many clients. Its documentation says project-level MCP configuration in `.mcp.json` or `.github/mcp.json` takes precedence over a same-named definition in `~/.copilot/mcp-config.json`. That is a clear project-over-user rule for MCP server names in the CLI.

Use it as a client-specific rule, not as a general truth about GitHub Copilot in every editor. The Copilot CLI has its own configuration directory, repository settings, local settings, plugin support, permissions store, and command-line process. A VS Code Copilot session is another client surface with its own MCP configuration documentation and lifecycle.

A clean Copilot CLI setup can look like this:

```json
// ~/.copilot/mcp-config.json
{
  "mcpServers": {
    "docs": {
      "command": "node",
      "args": ["/Users/dev/bin/company-docs-mcp.js"]
    },
    "catalog": {
      "command": "node",
      "args": ["/Users/dev/bin/catalog-live.js"]
    }
  }
}
```

```json
// .mcp.json in the repository
{
  "mcpServers": {
    "catalog": {
      "command": "node",
      "args": ["./tools/catalog-fixture.js"]
    }
  }
}
```

In that repository, Copilot CLI should use the project `catalog` definition and retain the user-level `docs` definition because no project entry conflicts with `docs`. That is the useful model: override only the name the project owns; leave unrelated personal utilities alone.

Plugins complicate the picture because a plugin can bring its own agent behavior and MCP server lifecycle. GitHub's configuration documentation describes repository-enabled plugins as repository-scoped and says the client tears down a plugin's MCP server when the repository no longer enables the plugin. That tells you the plugin's server is tied to plugin activation, not merely copied into your user MCP file.

If a plugin-provided server and a file-provided server use the same name, do not guess which command wins. Inspect the CLI's visible server state, plugin state, and startup output. If the documentation for the installed release does not state the collision behavior, rename one definition or remove one producer. The popular advice to “just override the plugin server in the repo” is attractive because it is short. It is wrong when the plugin owns registration after file loading or supplies extra lifecycle state.

## An editor and its terminal are separate MCP clients

A terminal inside an IDE feels like part of the editor. It is still a shell process. When you run `claude`, `copilot`, or `cursor-agent`, that command can read its own files, current directory, environment, home directory, and configuration override variables. The editor's chat extension is a different process with a different client implementation.

This is the source of the familiar report: “The MCP server works in the IDE but not in the terminal.” There are several ordinary explanations:

- The editor opened a workspace file, while the terminal started in a subdirectory or a sibling checkout.
- The editor uses a remote host or container, while the terminal command runs locally.
- The terminal inherited a shell-specific `PATH`, `HOME`, proxy setting, or credential variable.
- The editor activated a plugin server that the CLI never loads.
- The CLI found a project server with the same name and replaced its user-level entry.

Do not begin by copying every config file into every location. That produces a larger collision surface and hides the first cause.

Instead, run one client at a time and collect evidence. For terminal clients, use the built-in command that lists or gets MCP servers. For editor clients, use the MCP server management view and its output or logs. Record the server name, command, arguments, process location, and start timestamp. If the terminal and editor show the same name but different commands, you have found a configuration resolution issue. If they show the same command but different behavior, investigate working directory, credentials, network access, or the server itself.

One useful test is to replace the intended server command temporarily with a wrapper that writes an unambiguous marker to standard error before it executes the real server. Keep it read-only and remove it after the test.

```sh
#!/bin/sh
printf '%s client=%s cwd=%s\n' \
  "MCP probe started" \
  "${MCP_CLIENT_LABEL:-unknown}" \
  "$PWD" >&2
exec node "$(dirname "$0")/real-server.js"
```

Do not print tokens, headers, credential values, full environment dumps, or request payloads. Logs often outlive the terminal session, and a precedence probe should not create a secret leak while trying to explain one.

## Plugin registration is not a config-file override

A plugin is a producer of server configuration, not merely another folder where JSON happens to sit. It may register a server dynamically, manage authentication, choose a version, react to workspace changes, or stop the server when the plugin deactivates.

That makes two popular recommendations unsafe.

The first is “put the same name in the project file to override the plugin.” This works only when the client documents that files load after plugins and that same-name collision behavior replaces the plugin registration. Without that contract, the duplicate can yield an error, a hidden replacement, two similarly described tools, or behavior that changes after an update.

The second is “disable the server in the UI and the project is clean.” Enablement state may live outside the shared file. VS Code explicitly separates its enable or disable state from MCP configuration. A teammate can clone the repository, receive the same file, and still have a different active-server set.

Use one of these ownership patterns instead:

1. **File-owned server:** The repository commits the server definition. Everyone uses the file, and no plugin registers the same service.
2. **Plugin-owned server:** The plugin manages registration and authentication. The repository does not declare a duplicate.
3. **Separated server roles:** A plugin owns `tracker-live`; a project file owns `tracker-fixture`. The names, descriptions, and permissions make the targets distinct.

The third option is often the least glamorous and the safest. Teams frequently need a personal live service and a repository fixture at the same time. Pretending those are one server because both talk to an issue tracker only makes an accidental live call more likely.

## A repeatable way to prove the winning command

You can prove configuration resolution without relying on agent behavior. The method below uses a harmless stdio server or wrapper and works whether the client starts a local command or points to a remote service.

### Build a two-source probe

Choose one server name, such as `precedence-probe`. Define it in exactly two sources you want to compare. Make each candidate produce a different marker before it starts the same harmless test server.

For a command-based server, distinguish every part that matters:

```json
{
  "mcpServers": {
    "precedence-probe": {
      "command": "sh",
      "args": ["-lc", "printf 'SOURCE=PROJECT CWD=%s\\n' \"$PWD\" >&2; exec node ./tools/probe.js"],
      "env": { "MCP_PROBE_SOURCE": "project" }
    }
  }
}
```

The user candidate should print `SOURCE=USER` and point to a different, known file. Do not rely only on an environment variable if the client redacts it, filters it, or starts a stale process. Put a visible marker in the command path and output too.

### Restart the server, not only the chat

MCP servers are often long-lived child processes. Editing JSON while a server remains running proves nothing about the next request. Use the client control that stops and restarts the server. If that control is unclear, close the relevant client completely and reopen the workspace.

Then inspect the client-side logs first. The expected evidence has this shape:

```text
MCP probe started
SOURCE=PROJECT
CWD=/path/to/repository
```

If you get no marker, the client may have rejected the configuration before process launch, used a remote transport, or kept an old server alive. That result is useful. It narrows the question from “which config wins?” to “did this config load and did this client start a process?”

### Change one boundary at a time

Run the probe in the terminal client, then in the IDE client. Change the current working directory only after you have a baseline. Disable the plugin only after you have measured file-only behavior. Test the user and project sources before adding local overrides or managed settings.

Keep a small result table in the repository issue or team notes:

| Client | Environment | Candidate sources | Observed marker | Effective command |
| --- | --- | --- | --- | --- |
| Claude Code | local shell | local, project, user | LOCAL | `node ./tools/probe-local.js` |
| VS Code | dev container | workspace, profile | WORKSPACE | `node ./tools/probe-workspace.js` |
| Cursor | desktop | project, global, extension | extension marker | plugin-managed |

The table should report observed behavior, not expected behavior. A team can act on observed behavior. A diagram copied from another client is only a hypothesis.

## Put credentials outside the precedence contest

A command conflict is an execution problem. A credential conflict is a security problem. Do not solve the first by spreading secrets across project, plugin, user, and editor configuration until something works.

Committed project configuration should normally carry a server command, non-sensitive endpoint information, and instructions for obtaining credentials. It should not carry a long-lived bearer token in `env`, an SSH private key path that every teammate lacks, or a custom header value that grants production access. A project file becomes an execution invitation for every clone and every agent that the client allows to use it.

If a server needs a secret, pick a credential boundary that matches the work:

- OAuth is appropriate when the server and client support it and the user should grant access interactively.
- A local secret manager or environment injection is appropriate for a personal server definition.
- A repository fixture should use non-production credentials with narrow permissions.
- An action gateway is appropriate when an agent should request an action without ever receiving the underlying API or SSH credential.

This is where a server command can look harmless and still be dangerous. `npx some-mcp-server` may resolve to a different installed version between machines. `node ./tools/server.js` may inherit `AWS_PROFILE`, `GH_TOKEN`, or a corporate proxy from the parent process. A project declaration can be reviewed, while its actual credential source remains invisible.

When agents need HTTP or SSH access, Sallyport can keep the API or SSH credential in its encrypted vault while the agent connects through the `sp mcp` shim and receives only action results. That does not decide which client config wins, but it prevents a winning command from also handing raw credentials to the agent.

Do not treat configuration precedence as a permission system. A project definition can select a command; it does not prove that the command should be allowed to act without review. Keep approval, credential storage, and audit evidence as separate controls.

## Make the canonical command obvious

Teams need one answer to a plain question: which command should this repository's agent start for this service? If the answer is hidden among user files, plugin behavior, README snippets, and editor settings, the setup is already too loose.

Write the canonical shared definition in one place. Give it a stable server name. State which clients support it and which source owns it. Put personal variants under different names. If a plugin must own the service, document that the plugin is canonical and do not ship a competing file definition.

Then keep a small verification command or probe in the repository. Configuration changes deserve tests just as deployment scripts do. A server that starts the wrong command can read the wrong files, call the wrong endpoint, or inherit the wrong credential before an agent says a word.

The useful outcome is not a universal precedence chart. It is a setup where each client has one observable launch path, each server name has one owner, and nobody needs to guess which command their agent will execute.
