D2.1 through D2.3 designed tools, their failures, and their distribution; this chapter is where an external MCP server actually gets connected. Almost every trap here is about location — which file holds the config, which scope it lives in, which directory it resolves against — plus one notorious naming collision and one silent-failure mode: a server that never connected. Get the location right and check the connection, and a server resolves predictably across personal, team, and machine contexts; get it wrong and the agent silently never sees the tools.
Two ways to configure a server
An MCP server reaches an agent through one of two configuration paths. You can register it programmatically — mcp_servers in Python, mcpServers in TypeScript — or declare it in a .mcp.json file at the project root.
[Official]
Connect to external tools with MCP · AnthropicT1-official original The file-based path is not automatic, though: .mcp.json loads only when the SDK’s settingSources includes "project".
[Official]
Use Claude Code features in the SDK · AnthropicT1-official original Connect to external tools with MCP · AnthropicT1-official original
For a reproducible, clean-room config there is a third lever: strictMcpConfig: true uses only the servers you pass in mcpServers, ignoring .mcp.json, user settings, and plugins.
[Official]
Connect to external tools with MCP · AnthropicT1-official original It is how you guarantee an SDK run sees exactly the servers you declared and nothing the machine happens to carry.
Three scopes — and claude mcp add --scope
Claude Code stores MCP servers in three scopes, each a different file with a different audience: Local (~/.claude.json, under a per-project key — current project only, not shared), Project (.mcp.json at the repo root — shared via version control, with a one-time approval prompt on first use), and User (~/.claude.json — available across all your projects).
[Official]
Connect Claude Code to tools via MCP · AnthropicT1-official original
The CLI is how you actually install one and pick its scope: claude mcp add <name> --scope <local|project|user> --transport <http|stdio|sse> … — for example, claude mcp add --transport http --scope project notion https://mcp.notion.com/mcp registers a project-scoped HTTP server.
[Official]
Connect Claude Code to tools via MCP · AnthropicT1-official original --scope is the flag that decides who sees the server; omit it and you get Local (the default).
When the same server name appears in more than one scope, Claude Code connects once, using the highest-precedence source: Local → Project → User → plugin-provided → claude.ai connectors (the first three match duplicates by name). [Official] Connect Claude Code to tools via MCP · AnthropicT1-official original
"Local scope” is not “local settings”
The single most confusing collision in MCP configuration is the word local. “MCP local-scoped servers are stored in ~/.claude.json (your home directory), while general local settings use .claude/settings.local.json (in the project directory).”
[Official]
Connect Claude Code to tools via MCP · AnthropicT1-official original They are different files in different directories — one in your home, one in the project — and they hold different things.
Env-var expansion keeps secrets out of the file
Because a Project-scoped .mcp.json is committed to version control, secrets must never be written into it literally — they are referenced through env-var expansion instead. .mcp.json supports ${VAR} (expands, or fails the parse if unset) and ${VAR:-default} (expands, or uses the default), and the expansion works inside command, args, env, url, and headers.
[Official]
Connect Claude Code to tools via MCP · AnthropicT1-official original So a committed config carries "Authorization": "Bearer ${API_KEY}", and the key itself lives only in the environment.
Verify the server connected
A wired server that never connected is the silent failure of this chapter — the agent simply runs without the tools and you find out from a confusing answer. Don’t assume; check. Detect connection failures via the system:init message: each server’s status is one of connected | failed | needs-auth | pending | disabled — read it before letting the agent run.
[Official]
Connect to external tools with MCP · AnthropicT1-official original The default connection timeout is 60 seconds for server initialization, so a slow-starting server may need pre-warming or a lighter-weight package.
[Official]
Connect to external tools with MCP · AnthropicT1-official original
Transports and the snapshot-dated wire protocol
A server’s type selects its transport: stdio for local processes, sse (Server-Sent Events, now deprecated — use HTTP), and http (Streamable HTTP; JSON configs accept streamable-http as an alias for http). A fourth type, ws (WebSocket), is configurable only through .mcp.json or claude mcp add-json, not the --transport flag (whose values are http/stdio/sse).
[Official]
Connect Claude Code to tools via MCP · AnthropicT1-official original Separately — and this is not a .mcp.json type — the SDK lets you run an MCP server in-process inside your application (an SDK deployment mode, e.g. a built-in tool server), rather than as an external process or endpoint.
[Official]
Connect to external tools with MCP · AnthropicT1-official original
Beneath the config sits the MCP wire protocol — and it is mid-revision, so cite it with a date. Under the 2025-11-25 specification, an initialize handshake “MUST be the first interaction between client and server,” negotiating protocol version and capabilities before any tool call.
[Official]
Lifecycle — Model Context Protocol Specification 2025-11-25 · AnthropicT1-official original The 2026-07-28 release candidate (locked May 2026; the final spec ships 2026-07-28) removes that handshake for a stateless model, so the wire details here are a dated snapshot, not a permanent contract.
[Official]
The 2026-07-28 MCP Specification Release Candidate · Model Context ProtocolT2-release-notes original
Practice
Exercise solutions
(a) Project scope — a .mcp.json at the repo root (committed so every clone gets the server), installed with claude mcp add … --scope project. (b) Reference the secret through env-var expansion rather than inlining it, e.g. "env": { "DATABASE_URL": "${DATABASE_URL}" } (or "${DATABASE_URL:-…}" if a safe default exists) — expansion works in env, url, and headers, so no literal credential is committed. (c) The teammate’s definition wins on their machine. Their ~/.claude.json entry is Local scope, and precedence runs Local → Project → User, matched by name — so the local definition overrides the shared project one for them. That is the intended override path for personal credentials, not a conflict.
The mistake is conflating “MCP local scope” with “general local settings.” A local-scoped MCP server is stored in ~/.claude.json (the home directory, under a per-project key), not in .claude/settings.local.json (the project’s machine-local settings). Editing the latter to change an MCP server is a silent no-op — point them at ~/.claude.json.
Inspect the system:init message and its mcp_servers field — each server reports a status. A status other than connected explains the missing tools: failed (missing env var, uninstalled package, bad connection string, unreachable host), needs-auth (OAuth not completed), pending, or disabled. The 60-second default initialization timeout is a common cause of failed/pending for slow-starting servers — pre-warm or use a lighter package. Always read status before letting the agent run rather than discovering the gap from a wrong answer.
Exam essentials
- Two config paths — programmatic (
mcp_servers/mcpServers) or a.mcp.jsonat the project root (loads only whensettingSourcesincludes"project").strictMcpConfig: trueuses onlymcpServers, ignoring.mcp.json/user/plugins. claude mcp add … --scope <local|project|user> --transport <http|stdio|sse>installs a server and picks its scope;--scopedefaults to Local.- Three scopes, three audiences — Local (
~/.claude.json, per-project, private), Project (.mcp.json, committed/shared, approval-prompted), User (~/.claude.json, all projects). Precedence: Local → Project → User → plugin → claude.ai, matched by name. - “Local scope” ≠ “local settings” — a local-scoped MCP server lives in
~/.claude.json(home), not.claude/settings.local.json(project). - Env-var expansion —
${VAR}/${VAR:-default}incommand/args/env/url/headers;${CLAUDE_PROJECT_DIR}needs the:-form in hand-written configs. - Verify the connection — read the
system:initstatus(connected/failed/needs-auth/pending/disabled) before running; the default init timeout is 60s. - Config transports —
stdio/sse(deprecated) /http(streamable-httpalias), plusws(WebSocket,.mcp.json-only); the SDK in-process server is a separate deployment mode, not atype. The 2025-11-25initializehandshake is a dated snapshot the 2026-07-28 RC removes.