mcp-protocol.mdx
April 1, 2026 · View on GitHub
{/* The goal of this chapter: reveal the connection management, tool discovery protocol and execution link of the MCP client from the source code perspective */}
Architecture overview: from configuration to available tools
settings.json: { mcpServers: { "my-db": { command: "npx", args: [...] } } }
↓
getAllMcpConfigs() ← Merge user/project/local third-level configuration
↓
useManageMCPConnections() ← React Hook manages connection life cycle
↓
connectToServer(name, config) ← memoize cache (lodash memoize)
├── Create Transport (stdio/sse/http/...)
├── new Client() ← @modelcontextprotocol/sdk
├── client.connect(transport) ← Timeout control (MCP_TIMEOUT, default 30s)
└── Return MCPServerConnection ← { connected | failed | needs-auth | pending }
↓
fetchToolsForClient(client) ← LRU(20) cache
├── client.request({ method: 'tools/list' })
└── Each tool is packaged as MCPTool ← Unified Tool interface
↓
assembleToolPool() ← merge built-in tools + MCP tools
↓
Tool name format: mcp__<serverName>__<toolName> ← buildMcpToolName()
7 transport layer implementations
connectToServer() (client.ts:596-1643) is distributed to different Transport implementations based on config.type:
| Transport type | Transport class | Applicable scenarios | Authentication method |
|---|---|---|---|
stdio (default) | StdioClientTransport | local child process | none |
sse | SSEClientTransport | Remote SSE service | ClaudeAuthProvider + OAuth |
http | StreamableHTTPClientTransport | HTTP streaming | ClaudeAuthProvider + OAuth |
sse-ide | SSEClientTransport | IDE integration | lockfile token |
ws-ide | WebSocketTransport | IDE WebSocket | X-Claude-Code-Ide-Authorization |
ws | WebSocketTransport | WebSocket service | session ingress token |
claudeai-proxy | StreamableHTTPClientTransport | claude.ai proxy | OAuth bearer + 401 retry |
Process management of stdio transmission
The stdio type MCP server runs as a child process, and uses the signal upgrade strategy (client.ts:1431-1564) during cleanup:
SIGINT (100ms) → SIGTERM (400ms) → SIGKILL
The total cleanup time is capped at 600ms to prevent the MCP server from shutting down and blocking the CLI exit.
Authentication state machine for remote transmission
The SSE/HTTP type uses ClaudeAuthProvider to implement the OAuth authentication process. When authentication fails, it enters the needs-auth state and writes a 15-minute TTL cache file (mcp-needs-auth-cache.json) to avoid repeated authentication prompts.
Connection attempt → 401 Unauthorized
↓
handleRemoteAuthFailure()
├── logEvent('tengu_mcp_server_needs_auth')
├── setMcpAuthCacheEntry(name) ← Write 15min TTL cache
└── return { type: 'needs-auth' } ← UI displays authentication prompts
Connection caching and reconnection mechanism
connectToServer uses lodash memoize to cache the connection object, and the cache key is ${name}-${JSON.stringify(config)}.
Cache invalidation trigger
When the connection is closed (client.onclose), clear all related caches (client.ts:1376-1404):
client.onclose = () => {
const key = getServerCacheKey(name, serverRef)
fetchToolsForClient.cache.delete(name) // Tool cache
fetchResourcesForClient.cache.delete(name) // Resource cache
fetchCommandsForClient.cache.delete(name) // Command cache
connectToServer.cache.delete(key) // Connection cache
}
Connection degradation detection
The remote transport has a continuous error counter (client.ts:1229):
let consecutiveConnectionErrors = 0
const MAX_ERRORS_BEFORE_RECONNECT = 3
After encountering terminal errors (ECONNRESET, ETIMEDOUT, EPIPE, etc.) three times in a row, the transport will be actively closed to trigger reconnection. For HTTP transports, session expiration (404 + JSON-RPC code -32001) is also detected.
Request-level timeout protection
Use an independent setTimeout timeout (wrapFetchWithTimeout, client.ts:493) for each HTTP request instead of sharing AbortSignal.timeout(). The reason is that Bun's GC for AbortSignal.timeout is lazy - each request consumes about 2.4KB of native memory, and even if the request is completed in milliseconds, it will take 60s to be recycled.
const controller = new AbortController()
const timer = setTimeout(c => c.abort(...), MCP_REQUEST_TIMEOUT_MS, controller)
timer.unref?.() // Do not prevent the process from exiting
Tool discovery: from MCP to Tool interface
fetchToolsForClient() (client.ts:1745-2000) uses memoizeWithLRU cache (limited to 20) to convert MCP tools into Claude Code’s unified Tool interface:
const fullyQualifiedName = buildMcpToolName(client.name, tool.name)
// Result: "mcp__my-db__query"
Tool description truncation
MCP tool description limit is 2048 characters (MAX_MCP_DESCRIPTION_LENGTH). Description documents of 15-60KB have been observed for MCP servers generated by OpenAPI.
Tool capability annotation
Each MCP tool is automatically annotated according to tool.annotations:
| annotation | maps to | meaning |
|---|---|---|
readOnlyHint | isReadOnly() + isConcurrencySafe() | Read-only, parallelizable |
destructiveHint | isDestructive() | Destructive operations |
openWorldHint | isOpenWorld() | Open world (not enumerable) |
title | userFacingName() | display name |
Permission check for MCP tools
MCP tools return { behavior: 'passthrough' } (client.ts:1816-1834) by default, meaning they always enter the permission confirmation process. Tool names use the mcp__ prefix to match permission rules exactly.
Execution link of MCP tool
AI generation tool_use: { name: "mcp__my-db__query", input: { sql: "..." } }
↓
MCPTool.call() ← client.ts:1835
├── ensureConnectedClient() ← Ensure the connection is valid (reconnect)
├── callMCPToolWithUrlElicitationRetry() ← Retry with Elicitation
│ ├── client.request({ method: 'tools/call' })
│ ├── Process image results (resize + persist)
│ └── Content truncation (mcpContentNeedsTruncation)
├── McpSessionExpiredError → Try again
└── Return { data: content, mcpMeta }
Automatically retry when session expires
The MCP session for HTTP transport may expire. Automatically retry once after detecting McpSessionExpiredError (client.ts:1862) because ensureConnectedClient() has cleared the cache and established a new connection.
Content truncation and persistence
Large MCP tool output is truncated via truncateMcpContentIfNeeded and binary content (images) is written to a file via persistBinaryContent and the file path is returned. The image is automatically resized (maybeResizeAndDownsampleImageBuffer).
Concurrency control of MCP connections
// Number of concurrent connections to the local server
getMcpServerConnectionBatchSize() //Default 3
// Number of concurrent connections to the remote server
getRemoteMcpServerConnectionBatchSize() //Default 20
The local MCP server (stdio) is a heavyweight subprocess with a default limit of 3 concurrent connections. The remote server is a lightweight HTTP request, allowing 20 concurrency.
Actual configuration example
// MCP configuration in settings.json
{
"mcpServers": {
"my-database": {
"command": "npx",
"args": ["@my-org/db-mcp-server"],
"env": { "DB_URL": "postgres://..." }
},
"remote-api": {
"type": "http",
"url": "https://api.example.com/mcp"
}
}
}
Once configured, the mcp__my-database__query and mcp__remote-api__* tools will appear in AI's tool list - using the same permissions as the built-in tools for link checking and UI rendering.