MCP Configuration Scope (Four Levels) and the Secrets-in-Config Anti-Pattern
CoreUnderstand MCP servers, their primitives, and transports · Difficulty 2/5
Explanation
Four Scope Levels, Four Config Locations
An MCP Server's configuration lives at one of four scope levels, and each level answers a different question about who sees it and where it's stored:
| Scope | Config location | Shared? | Fits |
|---|---|---|---|
| Local | ~/.claude.json, per-project entry | Not shared, not committed | A server tied to one project's context you're not ready to commit to the repo |
| User | Personal settings, applied across all your projects | Personal -- not shared with teammates | A personal utility you use regardless of which codebase you're in |
| Project | .mcp.json at the repository root | Committed to VCS, shared with every clone | A server the whole team needs, traveling with the code |
| Enterprise | Admin-managed, org-wide | Pushed to everyone by an administrator | Shared internal services or security tooling that must be present org-wide, not left to individual configuration |
A subtlety on Project scope: committing .mcp.json shares the *configuration*, not a running server. For a stdio server configured this way, each teammate's clone still spawns its own local subprocess on their own machine -- so every teammate needs the same local runtime available (e.g., Node.js, if the server launches via npx). Committing the config doesn't eliminate the need for each machine to be able to run the server.
The Anti-Pattern: Secrets Placed Inline in Config
Here's a worked scenario that shows exactly how this goes wrong. A developer connects to a data-warehouse MCP Server and, to get it working quickly, places the server's auth token inline in .mcp.json:
{
"type": "http",
"url": "https://warehouse.internal/mcp",
"headers": {
"Authorization": "Bearer sk-abc123..."
}
}Swapping it for an environment variable was the intended next step -- except the file gets pushed to the repo before that happens, "just so the team can keep moving." Two days later, that same token is sitting in four or more separate places: the original machine, the repo's commit history (a later commit that removes the value doesn't erase it from history), every teammate's local clone the moment they pull, and whatever CI system checked the repo out. Rotating the now-compromised key is the correct remediation, but it's expensive: every other service still configured with the old key breaks the moment the key rotates, and each of those has to be found and updated separately.
The Fix: Reference an Environment Variable
{
"type": "http",
"url": "https://warehouse.internal/mcp",
"headers": {
"Authorization": "Bearer ${WAREHOUSE_MCP_TOKEN}"
}
}The committed configuration file now holds only the *reference* to a variable, never the secret value itself. Each teammate (and each CI runner) supplies WAREHOUSE_MCP_TOKEN through their own environment, and the actual credential never enters version control at all -- so there's nothing in history to rotate away from in the first place.
Common exam traps
- Assuming that removing a secret from
.mcp.jsonin a later commit removes it from exposure. It does not -- prior commits still carry the plaintext value in history, and the credential must be treated as compromised and rotated regardless of the follow-up commit. - Assuming Project scope means the server itself runs centrally. A committed
.mcp.jsonfor a stdio server still launches a fresh local subprocess on every teammate's own machine -- each of them needs the runtime installed. - Picking Local or User scope for a capability the whole team needs, or Project scope for a purely personal utility -- scope should match who needs to see and use the configuration, not habit.
Key Takeaways
- MCP configuration has four scope levels: Local (~/.claude.json, per-project, not shared), User (personal, across all projects, not shared), Project (.mcp.json at repo root, committed, shared with clones), and Enterprise (admin-managed, org-wide)
- A Project-scoped stdio server still runs per-teammate-machine after cloning -- each teammate needs the local runtime (e.g. Node for an npx-launched server) installed
- Placing a secret inline in .mcp.json and committing it puts that credential in 4+ places within days: local machine, full repo history, every clone, and any CI runner -- and a later commit removing it does not remove it from history
- Rotating a compromised key found this way is expensive: every other service still configured with the old value breaks and each has to be found and fixed separately
- The fix is to reference an environment variable in the config (e.g. Bearer ${WAREHOUSE_MCP_TOKEN}) instead of an inline value, so the committed file never carries the secret at all
Glossary Terms
The project-scoped MCP configuration file placed in the repository root. Defines which MCP servers are available for the project, their commands, arguments, and environment variable bindings. Checked into version control to share server configuration with the team. Supports ${ENV_VAR} expansion for credentials.
A process that implements the MCP protocol and exposes tools, resources, and prompts to MCP clients. Built with official SDKs (Python, TypeScript). Deployed locally via stdio or remotely via StreamableHTTP. Claude Code auto-discovers servers configured in .mcp.json.
A security principle applied to agent tool design: give each agent and subagent only the minimum tools required to complete its specific task. Reduces blast radius if an agent is compromised or makes an error. Implemented via AgentDefinition tool lists and skill allowed-tools.
Related Concepts
MCP Transports: stdio vs. Streamable HTTP/Sockets
stdio: local subprocess transport, ideal for local, single-user integrations
The API MCP Connector: mcp_toolset, defer_loading, and enabled
The API MCP Connector attaches a remote MCP server directly via the Messages API using an mcp_toolset object in the tools array, with a default_config block plus optional per-tool configs keyed by tool name