MCP Config Scopes in Claude Code: User vs Project vs Local
How .mcp.json scope resolution works in Claude Code, where to put env vars, and a verification flow that catches broken servers before they break your session.
MCP Config Scopes in Claude Code: User vs Project vs Local
The Model Context Protocol turns Claude Code from a chat box into a tool-calling agent that can read your filesystem, query Postgres, hit a private API, or drive a browser. The catch: where you declare an MCP server determines who can run it, what gets committed to git, and which environment variables it sees at boot. Get the scope wrong and you either leak credentials into a public repo or spend twenty minutes wondering why a tool isn't showing up in /mcp.
Three scopes exist. They resolve in a fixed order, they overlap on purpose, and the rules for env var injection differ at each layer.
The three scopes, ranked by precedence
Claude Code loads MCP servers from three locations and merges them with local > project > user precedence. A server defined at local scope wins over the same name at project, which wins over user.
| Scope | Location | Committed to git? | Typical use |
|---|---|---|---|
user | ~/.claude.json (global) | No (lives in your home dir) | Personal tools you want everywhere: a notes server, a personal GitHub PAT |
project | .mcp.json at the repo root | Yes \u2014 checked in | Team-shared servers: project-specific database, internal API client |
local | .claude/settings.local.json in the repo | No (gitignored by default) | Per-developer overrides: your local Postgres password, a feature-flagged dev server |
Run claude mcp list to see which scope each server resolved from. The CLI prints the source path next to the server name, which is the fastest way to debug "why is the wrong version of this server loading."
Adding servers without hand-editing JSON
The CLI handles the scope flag for you. These three commands write to the three locations above:
# user scope \u2014 available in every project
claude mcp add --scope user notes \
-- npx -y @modelcontextprotocol/server-filesystem ~/notes
# project scope \u2014 committed for the team
claude mcp add --scope project postgres \
-- npx -y @modelcontextprotocol/server-postgres \
postgresql://localhost/myapp
# local scope \u2014 your machine only
claude mcp add --scope local postgres-dev \
-- npx -y @modelcontextprotocol/server-postgres \
postgresql://localhost/myapp_dev
The -- separator is mandatory. Everything after it is the launch command Claude Code runs to spawn the server's stdio process. Forget the -- and the CLI will try to interpret your server's flags as its own.
If you prefer hand-editing, the schema is the same across all three files:
{
"mcpServers": {
"postgres": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-postgres", "postgresql://localhost/myapp"],
"env": {
"PGSSLMODE": "require"
}
}
}
}
Where to put credentials
This is the part that bites people. The .mcp.json file at project scope gets committed, so anything in its env block ends up in git history. Two patterns avoid leaking secrets.
Pattern 1: shell expansion in args. Claude Code expands ${VAR} references inside args and env values at server launch. Set the variable in your shell before running claude:
{
"mcpServers": {
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": {
"GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_TOKEN}"
}
}
}
}
The committed file references ${GITHUB_TOKEN}. The actual token lives in your ~/.zshrc or a .envrc loaded by direnv. Teammates set their own values; nothing sensitive ships to git.
Pattern 2: local scope override. Define the server at project scope with a placeholder, then override at local scope with the real credential. The local file is gitignored, so the override never leaves your machine.
A weaker third option is to define the whole server at user scope, but that breaks reproducibility \u2014 a new contributor cloning the repo won't know the server exists until someone tells them, and your README has to document something that should have been declarative.
Scope resolution gotchas
A few behaviors that aren't obvious from the docs:
- No partial merging within a server. If
postgresexists at both project and local scope, local scope replaces the entire definition. You can't override just theenvblock while keeping project-scopeargs. Copy the whole entry when you override. - User scope loads even when you
cdinto a project. This is a feature for tools like a notes server that should follow you everywhere. It's a footgun if you defined a server at user scope months ago and forgot \u2014claude mcp listis the cure. - Project scope requires
claudeto be launched from the repo root or a subdirectory. Runclaudefrom/tmpand your project-scope servers won't load even if.mcp.jsonexists somewhere on disk. - Local scope lives in
.claude/settings.local.json, not.mcp.local.json. Easy to miss because the project-scope filename suggests a parallel naming convention that doesn't exist.
Verification flow
A working install verification catches three failure modes: the server didn't start, the server started but tools didn't register, or tools registered but they error on first call. Run this sequence after adding a new server:
# 1. Did the server appear in the list with the right scope?
claude mcp list
# 2. Did the launch command actually work?
claude mcp test postgres
# Prints stdout/stderr from the spawned process. Look for
# "Server started" or framework-specific ready messages.
# 3. Inside a Claude Code session, did tools register?
# Type /mcp and inspect the tool list for that server.
# 4. Smoke-test one tool with a low-impact call.
# In Claude Code: "Use postgres.query to run SELECT 1"
Step 2 is where most install failures surface. A common one: the package isn't published yet, so npx -y @some-org/some-server hangs forever waiting for a registry entry that doesn't exist. Set a 30-second timeout on claude mcp test and treat anything longer as a failed install.
When to choose which scope
A practical decision rule:
- User scope when the server is personal and stateless across projects (notes, your own GitHub account, a sandbox filesystem)
- Project scope when the server is part of the project's contract \u2014 a teammate cloning the repo and running
claudeshould get it without setup beyond their own credentials - Local scope when you need to override a project-scope server temporarily, run a development variant, or test a server before committing its config to the team
Mix them deliberately. A typical project ships postgres at project scope pointing at production read-replica config, and individual developers override it at local scope to point at their dev database. The team config stays declarative; nobody's local credentials leak.
References: