MCP (Model Context Protocol)
MCP connects Claude Code to external tools and services. The implementation spans 23 files in src/services/mcp/ and supports 4 transport types, 2 auth flows, 6 configuration scopes, and a full enterprise policy system.
mcp__<server>__<tool>.
Characters outside [a-zA-Z0-9_-] are replaced with _,
capped at 64 characters. This prefix is what you use in allow/deny permission rules.
Architecture
| Component | File | Purpose |
|---|---|---|
| Client | client.ts (~3,400 lines) | Connection, transport creation, tool wrapping, tool calls |
| Config | config.ts (~800 lines) | Config loading from all scopes |
| Auth | auth.ts (~1,200 lines) | OAuth + XAA authentication flows |
| XAA | xaa.ts (512 lines) | Cross-App Access protocol (RFC 8693 + RFC 7523) |
| Normalization | normalization.ts | Name normalization for mcp__ prefix |
| Env Expansion | envExpansion.ts | ${VAR} and ${VAR:-default} expansion in config |
| In-Process | InProcessTransport.ts | Linked transport pair for in-process servers (Chrome, Computer Use) |
| Elicitation | elicitationHandler.ts | MCP elicitation request handler (URL-based auth prompts) |
| MCPTool | src/tools/MCPTool/ | Base template cloned per MCP tool exposed to the model |
| McpAuthTool | src/tools/McpAuthTool/ | Pseudo-tool for servers needing authentication |
Configuration scopes (priority order)
Servers are loaded from 6 sources. Higher priority overrides lower when names conflict.
The enterprise scope is exclusive: if managed-mcp.json exists,
all other scopes are ignored.
| # | Scope | Source | Notes |
|---|---|---|---|
| 1 | claudeai | API fetch | Claude.ai organization connectors (lowest priority) |
| 2 | plugin | dynamic | Servers from installed plugins |
| 3 | user | ~/.claude/settings.json | Global user config |
| 4 | project | .mcp.json | Walks up directory tree to home |
| 5 | local | .claude/settings.local.json | Private project config (not committed) |
| 6 | enterprise | managed-mcp.json | EXCLUSIVE: all other scopes ignored when this file exists |
Project config example (.mcp.json)
{
"mcpServers": {
"github": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": { "GITHUB_TOKEN": "${GITHUB_TOKEN}" }
},
"my-api": {
"type": "sse",
"url": "https://example.com/mcp/sse",
"headers": { "Authorization": "Bearer ${API_TOKEN}" }
}
}
}
All config fields support ${VAR} and
${VAR:-default} expansion.
Servers in .mcp.json require explicit user approval before connecting
(stored in enabledMcpjsonServers in settings).
Transport types
Launches a subprocess communicating via stdin/stdout. Most common transport. type field is optional for backwards compatibility.
Stderr is captured for debugging (capped at 64MB). Chrome and Computer Use servers use in-process optimization to avoid ~325MB subprocess overhead.
Long-lived EventSource connection. No timeout on the stream; individual HTTP requests have 60s timeout.
Supports OAuth with browser-based flow. Supports headersHelper script for dynamic authentication headers.
Implements the MCP 2025-03-26 Streamable HTTP protocol. Adds Accept: application/json, text/event-stream automatically.
Supports session management via session ID. Supports OAuth authentication.
WebSocket transport. Supports both Bun and Node.js runtimes. Protocol: ["mcp"].
Supports proxy and TLS options. Headers are configurable.
Connection lifecycle
Startup
Load configs from all scopes (enterprise takes exclusive control if present)
Deduplicate servers by signature (URL for remote, command for stdio)
Filter by enterprise allow/deny policies
Check project server approval status
Connect local servers (concurrency: 3) and remote servers (concurrency: 20) in parallel
On auth failure: cache 15 min, create McpAuthTool pseudo-tool
On success: fetch tools, commands, and resources in parallel
Tool call flow
Model outputs tool_use block with name "mcp__server__tool"
ensureConnectedClient() verifies or reconnects if cache was cleared
Permission check via standard permission system (passthrough by default)
callMCPTool() with timeout, checks isError flag in response
processMCPResult(): if output > 100,000 chars, saves to temp file and returns Read instructions
On URL elicitation error (-32042): retries up to 3 times
Shutdown
Stdio servers
SIGINT → 100ms → SIGTERM → 400ms → SIGKILL
Total failsafe: 600ms maximum
Remote servers
client.close() → closes transport, rejects pending requests
Authentication
Browser-based OAuth with automatic token storage in the OS keychain, keyed by server name and config hash. Auto-refreshes on 401. Supports step-up (403 with scope) and revocation (RFC 7009).
"oauth": {
"clientId": "my-app",
"callbackPort": 8080,
"authServerMetadataUrl": "https://auth.example.com/.well-known/..."
}
Enterprise SSO flow using RFC 8693 token exchange and RFC 7523 JWT Bearer grant.
Enabled with CLAUDE_CODE_ENABLE_XAA=1.
Key benefit: a single browser popup authenticates N MCP servers (cached id_token is reused).
Flow: PRM Discovery → AS Metadata → id_token → ID-JAG → access_token
When a server fails to connect due to auth, a pseudo-tool named
mcp__<server>__authenticate is created.
When the model calls it, the OAuth flow starts, and upon completion the real tools replace the pseudo-tool.
Always auto-approved (no user permission prompt). Auth failure is cached for 15 minutes.
Tool exposure
Naming examples
| Server name | Tool name | Exposed as |
|---|---|---|
| github | create_issue | mcp__github__create_issue |
| slack | search_public | mcp__slack__search_public |
| My Server! | do-thing | mcp__My_Server___do_thing |
Tool annotations (from MCP spec)
| Annotation | Property | Effect |
|---|---|---|
| readOnlyHint | isConcurrencySafe, isReadOnly | Tool can run in parallel, treated as read-only |
| destructiveHint | isDestructive | Higher risk signal in permission prompts |
| openWorldHint | isOpenWorld | Tool can reach external systems |
| _meta.anthropic/searchHint | searchHint | Tool is deferred (not loaded into context until searched) |
| _meta.anthropic/alwaysLoad | alwaysLoad | Tool bypasses deferral, always in context |
Tool descriptions are capped at 2048 characters.
When output exceeds 100,000 characters, it is saved to a temp file
and the model receives instructions to use the Read tool.
Permission system
All MCP tools return checkPermissions() → passthrough.
This means in default mode the user is always prompted,
in bypassPermissions mode they are auto-approved,
and in auto mode the YOLO classifier decides.
User permission rules
{
"permissions": {
"allow": [
"mcp__github__search_code",
"mcp__slack__*"
],
"deny": [
"mcp__untrusted_server__*"
]
}
} Enterprise server policies (managed-mcp.json)
{
"allowedMcpServers": [
{ "serverName": "github" },
{ "serverUrl": "https://*.company.com/*" },
{ "serverCommand": ["npx", "-y", "@company/mcp-*"] }
],
"deniedMcpServers": [
{ "serverName": "untrusted" }
]
} Denylist always takes precedence over allowlist.
CLI commands
# Add a stdio server (default scope: local)
claude mcp add my-server npx -y @org/mcp-server
-s, --scope <scope> # local, user, or project
-e, --env KEY=VALUE # environment variables
# Add a remote server (transport auto-detected from URL)
claude mcp add my-api https://api.example.com/mcp
-t, --transport sse|http
-H, --header "Key: Value"
--client-id <id> # OAuth client ID
--xaa # Enable Cross-App Access
# Add with full JSON config
claude mcp add-json my-server '{"type":"http","url":"https://example.com/mcp"}'
# Other operations
claude mcp remove my-server [-s scope]
claude mcp list
claude mcp get my-server /mcp slash command
| Subcommand | Description |
|---|---|
| /mcp | Open MCP settings UI |
| /mcp enable [name|all] | Enable server(s) |
| /mcp disable [name|all] | Disable server(s) |
| /mcp reconnect <name> | Reconnect a specific server (clears cache, fresh connection) |
Advanced features
Headers helper script
Set headersHelper to a script path. The script receives the server name and URL as env vars and must output JSON headers. Runs with 10s timeout. Dynamic headers override static ones.
MCP resources
Servers can expose resources (files, data) via ListMcpResourcesTool and ReadMcpResourceTool. Both are deferred tools. Blob resources are base64-decoded and written to disk.
MCP prompts as commands
Server-exposed prompts become slash commands (mcp__server__prompt-name). Arguments are parsed from the prompt's argument schema and passed to client.getPrompt().
Agent-specific servers
Agents can reference existing servers by name (shared) or define inline servers (cleaned up when the agent finishes). Required servers are declared and must be configured.
Server instructions
Servers provide usage instructions during handshake (truncated at 2048 chars). Injected as a system prompt section each turn. Delta mode (feature-gated) avoids cache invalidation.
In-process transport
Chrome and Computer Use servers run in-process via InProcessTransport, avoiding ~325MB subprocess overhead. Uses linked transport pairs with queueMicrotask() delivery.
Key constants and timeouts
| Variable / Constant | Value | Purpose |
|---|---|---|
| MCP_TIMEOUT | 30,000ms | Connection timeout per server |
| MCP_TOOL_TIMEOUT | 100,000,000ms (~27.8h) | Tool call timeout (effectively infinite) |
| MCP_REQUEST_TIMEOUT_MS | 60,000ms | Per HTTP request timeout (SSE/HTTP) |
| MAX_MCP_OUTPUT_TOKENS | 25,000 | Max tokens in tool output |
| maxResultSizeChars | 100,000 chars | Output size cap before saving to file |
| MAX_MCP_DESCRIPTION_LENGTH | 2048 chars | Tool description cap |
| MCP_AUTH_CACHE_TTL_MS | 15 min | needs-auth cache duration |
| MCP_FETCH_CACHE_SIZE | 20 | LRU cache size for tools/resources/commands |
| local concurrency | 3 | Simultaneous local (stdio) server connections |
| remote concurrency | 20 | Simultaneous remote server connections |
| MAX_ERRORS_BEFORE_RECONNECT | 3 | Terminal errors before reconnect |
| MAX_URL_ELICITATION_RETRIES | 3 | URL elicitation (-32042) retries |
MCP_TIMEOUT, MCP_TOOL_TIMEOUT, and MAX_MCP_OUTPUT_TOKENS are in the SAFE_ENV_VARS set: they can be set via managed settings without triggering a security dialog.
Error handling
| Error type | Handling |
|---|---|
| Connection timeout (30s) | Server marked as failed |
| Auth error (401) | Cached as needs-auth for 15 min, McpAuthTool created |
| ECONNRESET / ETIMEDOUT / EPIPE | Counted: 3 consecutive errors close the transport |
| ECONNREFUSED / EHOSTUNREACH | Terminal error, transport closed immediately |
| Session expired (404 + -32001) | Cache cleared, auto-retry once |
| URL elicitation (-32042) | Show URL dialog to user, retry up to 3 times |
.mcp.json need explicit approval before connecting — use
enableAllProjectMcpServers: true in settings to bulk-approve.
Second: you can wildcard permission rules like mcp__slack__* to allow an
entire server without approving each tool. Third: the MCP tool timeout is effectively infinite (~27.8 hours) —
if a tool seems stuck, use /mcp reconnect server-name to force a fresh connection.