Add an MCP Server to Claude Code: Local, Project or User Scope, and the File Each One Writes
📅 October 12, 2026 · ⏱️ 16 min read
To add an MCP server to Claude Code, run claude mcp add --transport stdio <name> -- <command> for a server that runs as a local process, or claude mcp add --transport http <name> <url> for a remote one. Add --scope to choose where it is saved: local (the default) and user both go into ~/.claude.json, and project writes a .mcp.json file in the project root that you can commit for your team. Adding a server does not connect to it. The Added ... line the command prints means the configuration was written, and that is all it means.
Both commands are the “Basic syntax” lines on the MCP page of Claude Code’s docs, read on 2026-10-12, and every quote below about scopes, precedence and approval comes from that page unless another is linked. The rest of this page is what the commands did on this machine, where claude --version printed 2.1.296 (Claude Code). Each command ran in an empty git repository, with HOME and CLAUDE_CONFIG_DIR pointed at scratch directories and CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC and DISABLE_TELEMETRY set to 1. The stdio server is node -e "", a Node process that does nothing and exits. The HTTP server is a localhost address with nothing listening on it. No Claude Code session was started and no model was called.
claude --version
2.1.296 (Claude Code)
The two commands and the double dash
For a stdio server the docs give claude mcp add [options] <name> -- <command> [args...], and explain the --: “For stdio servers, the -- (double dash) separates Claude’s own options, such as --transport, --env, and --scope, from the command and arguments that run the server. Everything after -- is passed to the server untouched.” Without it, an argument meant for your server, such as --port 8080, would be read as an option of claude mcp add.
On 2.1.296, claude mcp add --help says --transport “Defaults to stdio if not specified” and that --scope is one of “local, user, or project” with (default: "local"). So claude mcp add demo -- node -e "" alone gives a local stdio server. Every command below spells both options out, so you can see which one did what.
Local scope: written to .claude.json
claude mcp add --transport stdio --scope local demo -- node -e ""
Added stdio MCP server demo with command: node -e to local config
File modified: /home/loopdev/dankdev/node_modules/.scratch/mcp/cfg/.claude.json [project: /home/loopdev/dankdev/node_modules/.scratch/mcp/repo]
The File modified line names .claude.json, and the path in square brackets is the repository the command ran in. That is what the docs describe: “Local scope is the default. A local-scoped server loads only in the project where you added it and stays private to you. Claude Code stores it in ~/.claude.json under that project’s path, so the same server won’t appear in your other projects.”
The file landed in the scratch config directory rather than in the scratch home because CLAUDE_CONFIG_DIR was set. The environment variables page describes that variable as “Override the configuration directory (default: ~/.claude). All settings, session history, and plugins are stored under this path.” With it set, .claude.json moved inside that directory. With it unset, the docs put the file at ~/.claude.json, beside the ~/.claude directory rather than in it. None of the three add commands on this page wrote anything under HOME.
Project scope: written to .mcp.json
claude mcp add --transport stdio --scope project demo -- node -e ""
cat .mcp.json; echo
Added stdio MCP server demo with command: node -e to project config
File modified: /home/loopdev/dankdev/node_modules/.scratch/mcp/repo/.mcp.json
{
"mcpServers": {
"demo": {
"type": "stdio",
"command": "node",
"args": [
"-e",
""
],
"env": {}
}
}
}
This is the file you commit. The entry has a type, the command, its args (the empty string is the "" after -e) and an empty env. In the Added line, that empty argument shows up only as a double space after -e. The docs: “Project-scoped servers enable team collaboration by storing configurations in a .mcp.json file at your project’s root directory.”
User scope: top level of .claude.json
claude mcp add --transport stdio --scope user demo -- node -e ""
Added stdio MCP server demo with command: node -e to user config
File modified: /home/loopdev/dankdev/node_modules/.scratch/mcp/cfg/.claude.json
This time File modified has no project in brackets. To see both entries, this prints only the MCP parts of .claude.json. The rest of that file holds a first-start time, a machine ID and migration flags, none of which matter here:
node -e '
const c = require(process.env.CLAUDE_CONFIG_DIR + "/.claude.json");
const projects = {};
for (const [dir, p] of Object.entries(c.projects)) projects[dir] = { mcpServers: p.mcpServers };
console.log(JSON.stringify({ mcpServers: c.mcpServers, projects }, null, 2));
'
{
"mcpServers": {
"demo": {
"type": "stdio",
"command": "node",
"args": [
"-e",
""
],
"env": {}
}
},
"projects": {
"/home/loopdev/dankdev/node_modules/.scratch/mcp/repo": {
"mcpServers": {
"demo": {
"type": "stdio",
"command": "node",
"args": [
"-e",
""
],
"env": {}
}
}
}
}
}
So one name, demo, now exists three times. Local scope and user scope both wrote to .claude.json; local scope put the server under the project’s path, user scope put it at the top level. Project scope wrote .mcp.json in the repository root.
| Scope | File written | Where in the file |
|---|---|---|
local (default) | .claude.json, config directory | projects, then the repository path, then mcpServers |
project | .mcp.json, repository root | mcpServers |
user | .claude.json, config directory | top-level mcpServers |
Which scope wins when one name is in two
The docs: “When the same server is defined in more than one place, Claude Code connects to it once, using the definition from the highest-precedence source. The entire server entry from that source is used; fields are not merged across scopes.” The order is “1. Local scope 2. Project scope 3. User scope”, followed by plugin servers and claude.ai connectors, and “Claude Code matches duplicates across the three scopes by name.” Local scope wins over project scope, and project scope wins over user scope, and the whole entry comes from the winner. You cannot keep a URL in user scope and override only a header in local scope.
claude mcp get with all three in place:
claude mcp get demo
demo:
Scope: Local config (private to you in this project)
Status: ✘ Failed to connect
Issue: CONNECTION_CLOSED: Connection closed
Type: stdio
Command: node
Args: -e
Environment:
To remove this server, run: claude mcp remove demo -s local
The local entry won. The status is ✘ Failed to connect because get checks the health of a server it is allowed to start. On 2.1.296 its help says “approved servers are health-checked unless disabled for this project”, and for a stdio server that means running the command. node -e "" exits at once, so the connection closed. That check also left the first file under the scratch HOME: a log at .cache/claude-cli-nodejs/<project>/mcp-logs-demo/<timestamp>.jsonl.
Removing without --scope does not pick one for you when the name is in more than one place:
claude mcp remove demo
Exit code 1, printed on stderr:
MCP server "demo" exists in multiple scopes:
- Local config (private to you in this project) (/home/loopdev/dankdev/node_modules/.scratch/mcp/cfg/.claude.json [project: /home/loopdev/dankdev/node_modules/.scratch/mcp/repo])
- Project config (shared via .mcp.json) (/home/loopdev/dankdev/node_modules/.scratch/mcp/repo/.mcp.json)
- User config (available in all your projects) (/home/loopdev/dankdev/node_modules/.scratch/mcp/cfg/.claude.json)
To remove from a specific scope, use:
claude mcp remove demo -s local
claude mcp remove demo -s project
claude mcp remove demo -s user
The help for remove says that without a scope it “removes from whichever scope it exists in”. With three, it refused, changed nothing, and listed each file. Removing the local one and asking again:
claude mcp remove demo --scope local
claude mcp get demo
Removed MCP server demo from local config
File modified: /home/loopdev/dankdev/node_modules/.scratch/mcp/cfg/.claude.json [project: /home/loopdev/dankdev/node_modules/.scratch/mcp/repo]
demo:
Scope: User config (available in all your projects)
Status: ✘ Failed to connect
Issue: CONNECTION_CLOSED: Connection closed
Type: stdio
Command: node
Args: -e
Environment:
To remove this server, run: claude mcp remove demo -s user
With the local entry gone, get showed the user entry, not the project one, even though the docs rank project above user. The project entry here had never been approved: no session was ever run in this scratch repository, so nobody answered the approval prompt for .mcp.json (more on that below). This page did not start a session to approve it, so it cannot show what get prints once the project entry is approved.
Removing a server
claude mcp remove demo --scope project
claude mcp remove demo --scope user
cat .mcp.json; echo
Removed MCP server demo from project config
File modified: /home/loopdev/dankdev/node_modules/.scratch/mcp/repo/.mcp.json
Removed MCP server demo from user config
File modified: /home/loopdev/dankdev/node_modules/.scratch/mcp/cfg/.claude.json
{
"mcpServers": {}
}
Removing the last project server does not delete .mcp.json. It leaves an empty mcpServers object, so if the file is committed, the removal is a change you commit too. With all three gone:
claude mcp get demo
Exit code 1, printed on stderr:
No MCP server named "demo". Run `claude mcp add` to add one.
An HTTP server with a header
Written the way the stdio commands above were, with every option before the name:
claude mcp add --transport http --scope project --header "X-Demo-Team: docs" team-http http://127.0.0.1:59999/mcp
Exit code 1, printed on stderr:
error: missing required argument 'name'
On 2.1.296 the help lists the option as -H, --header <header...>. The --header option takes several values, so it took the server name and the URL as two more header values, which left the command with no name. The docs warn about the same thing for --env: “If the server name comes directly after --env, the CLI reads the name as another pair and rejects it”. Put the header after the URL instead, as the docs’ own header example does:
claude mcp add --transport http --scope project team-http http://127.0.0.1:59999/mcp --header "X-Demo-Team: docs"
Added HTTP MCP server team-http with URL: http://127.0.0.1:59999/mcp to project config
Headers: {
"X-Demo-Team": "docs"
}
File modified: /home/loopdev/dankdev/node_modules/.scratch/mcp/repo/.mcp.json
Then count the processes listening on that port, and ask for the server:
ss -Hltn "sport = :59999" | wc -l
claude mcp get team-http
0
team-http:
Scope: Project config (shared via .mcp.json)
Status: ⏸ Pending approval (run `claude` to approve)
Type: http
URL: http://127.0.0.1:59999/mcp
Headers:
X-Demo-Team: docs
To remove this server, run: claude mcp remove team-http -s project
Nothing was listening on port 59999, and the add command still succeeded, because it only writes configuration. get printed the URL and the header from .mcp.json and the status ⏸ Pending approval, which, in the docs’ words, is one of the statuses that “report a configuration decision rather than a connection attempt, so Claude Code prints them without connecting to the server”. When the outputs on this page were recorded, every block ran under strace, which counted zero connect() calls to a network address across all of them. The only connect() calls it saw were failed lookups of the local name-service socket.
A header in .mcp.json is committed with the file, so keep tokens out of it. The docs list headers, url, command, args and env as places where .mcp.json expands environment variables, written as ${VAR}, or ${VAR:-default}, which “expands to VAR if set, otherwise uses default”.
A stdio server with env vars
claude mcp add --env DEMO_MODE=1 --transport stdio --scope project demo-env -- node -e ""
cat .mcp.json; echo
claude mcp get demo-env
Added stdio MCP server demo-env with command: node -e to project config
File modified: /home/loopdev/dankdev/node_modules/.scratch/mcp/repo/.mcp.json
{
"mcpServers": {
"team-http": {
"type": "http",
"url": "http://127.0.0.1:59999/mcp",
"headers": {
"X-Demo-Team": "docs"
}
},
"demo-env": {
"type": "stdio",
"command": "node",
"args": [
"-e",
""
],
"env": {
"DEMO_MODE": "1"
}
}
}
}
demo-env:
Scope: Project config (shared via .mcp.json)
Status: ⏸ Pending approval (run `claude` to approve)
Type: stdio
Command: node
Args: -e
Environment:
DEMO_MODE=1
To remove this server, run: claude mcp remove demo-env -s project
--env became an env object in the entry, and get printed it. The --transport stdio between --env DEMO_MODE=1 and the name is there on purpose: “--env accepts multiple KEY=value pairs. If the server name comes directly after --env, the CLI reads the name as another pair and rejects it, so place at least one other option, such as --transport stdio, between --env and the server name.”
A checked-in .mcp.json: what teammates see
The docs say to “Check .mcp.json into version control so everyone on your team gets the same MCP tools and services”, and then: “For security reasons, Claude Code prompts for approval in interactive sessions before using project-scoped servers from .mcp.json files. To reset those approval choices, run claude mcp reset-project-choices.” So a teammate who pulls your .mcp.json sees the server as pending until they approve it, which is what get printed for team-http and demo-env above.
Where the answer is stored, from the settings reference: approving one server writes enabledMcpjsonServers, approving all of them writes enableAllProjectMcpServers, and both go to .claude/settings.local.json. A rejection is disabledMcpjsonServers, and “A rejection in any settings file applies, including a project .claude/settings.json checked into the repository.” The reverse does not work: “A cloned repository can’t approve its own servers”. An approval committed to the project’s .claude/settings.json is ignored until the teammate trusts the folder.
One case gets no prompt at all: “In claude -p runs, Agent SDK sessions, and cloud sessions, Claude Code can’t show that prompt: it loads project-scoped servers without asking.” In those runs, a committed .mcp.json is loaded without anyone approving it, unless disabledMcpjsonServers names the server.
To approve, start a session in the repository; /mcp inside it shows each server’s status. claude mcp list prints every server with a health status.
Not run here: The first line starts an interactive Claude Code session, which talks to a model; the second health-checks every approved server, which means connecting to them. This page ran neither.
claude
claude mcp list
Allowing an MCP server’s tools
Tool names from a server start with mcp__<server>__, and permission rules match on that. From the permissions page: “Allow rules accept tool-name globs only after a literal mcp__<server>__ prefix. The server segment must be glob-free so the rule names a specific server you configured.” So mcp__demo__* as an allow rule would cover every tool from the demo server above, and mcp__demo alone matches the same set. A bare "mcp__*" works as a deny rule, where it “matches every MCP tool across all servers”, but not as an allow rule: “An unanchored allow glob such as "*", "B*", or "mcp__*" is skipped with a warning and doesn’t auto-approve anything.” Which settings file to put an allow rule in, and how to test one, is in Stop Claude Code Asking Before Safe Commands.
What was run here, and what was not
The version came from claude --version, which the CLI reference lists as “Output the version number”. Every command block above that has output under it ran on 2.1.296 in that order, in one scratch git repository, with the output printed as it came out. The output is saved alongside the files each command left behind, and a test in this site’s repository runs the blocks again in a fresh scratch directory and fails if any output, exit code or written file differs. The md5 of this machine’s real Claude Code config file, the .claude.json that a normal session uses, was the same before and after.
Not run: any Claude Code session, the approval prompt itself, claude mcp list, and any real MCP server. The get health check started node -e "", and that was the only server process this page ever started. What a session does with an approved server, or with a project entry and a user entry of the same name once both are approved, is quoted from the docs above, not shown.
Follow new posts by RSS: paste dankdev.com/feed.xml into any feed reader. It is a plain XML file — no signup, no email address, no account. Posts land there the same day they go up, and there is nothing to unsubscribe from later.