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 typeTransport classApplicable scenariosAuthentication method
stdio (default)StdioClientTransportlocal child processnone
sseSSEClientTransportRemote SSE serviceClaudeAuthProvider + OAuth
httpStreamableHTTPClientTransportHTTP streamingClaudeAuthProvider + OAuth
sse-ideSSEClientTransportIDE integrationlockfile token
ws-ideWebSocketTransportIDE WebSocketX-Claude-Code-Ide-Authorization
wsWebSocketTransportWebSocket servicesession ingress token
claudeai-proxyStreamableHTTPClientTransportclaude.ai proxyOAuth 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:

annotationmaps tomeaning
readOnlyHintisReadOnly() + isConcurrencySafe()Read-only, parallelizable
destructiveHintisDestructive()Destructive operations
openWorldHintisOpenWorld()Open world (not enumerable)
titleuserFacingName()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.

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.