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 programmaticallymcp_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

Solution ↑ Exercise

(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.

Solution ↑ Exercise

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.

Solution ↑ Exercise

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