Inside the MCP Client: A Deep Technical Walkthrough of Its Architecture
The article dissects the MCP Client used by Claude Code, detailing its layered architecture, type system, connection protocols, caching strategies, tool discovery and conversion, authentication flow, lifecycle management, reconnection logic, and design decisions, illustrating how external tool servers are seamlessly integrated as internal Claude tools.
Architecture Overview
The MCP Client sits between Claude Code and external tool servers. It translates any server’s capabilities—regardless of language, protocol, or deployment—into a form that Claude Code can invoke. The implementation is split into three layers:
Presentation – CLI and REPL UI.
Business – QueryEngine, Tools, Commands.
Core – the MCP client ( client.ts) with a transport layer, authentication provider, and tool‑conversion logic.
Communication with external services uses JSON‑RPC over one of five transport protocols (stdio, SSE, HTTP, WebSocket, SDK).
┌─────────────────────────────────────────────┐
│ Presentation (CLI / REPL) │
│ ┌───────┐ ┌───────┐ │
│ │ CLI │ │ REPL │ │
│ └───────┘ └───────┘ │
└──────────────────────┬──────────────────────┘
│ User runs /mcp
┌──────────────────────▼──────────────────────┐
│ Business (QueryEngine, Tools, Commands) │
│ ┌─────────────┐ ┌─────────────┐ ┌───────┐ │
│ │ QueryEngine │ │ Tools │ │Cmds │ │
│ └─────────────┘ └─────────────┘ └───────┘ │
└──────────────────────┬──────────────────────┘
│ Calls MCP client
┌──────────────────────▼──────────────────────┐
│ Core – MCP client (client.ts) │
│ Transport │ Auth Provider │ Tool Converter │
│ types.ts │ config.ts │ ConnectionMgr │
└──────────────────────┬──────────────────────┘
│ JSON‑RPC communication
┌──────────────────────▼──────────────────────┐
│ External services (stdio, sse, http, ws) │
└─────────────────────────────────────────────┘Core Type System
The connection state type is defined in src/services/mcp/types.ts:
export type MCPServerConnection =
| ConnectedMCPServer // normal operation
| FailedMCPServer // initial connection failure
| NeedsAuthMCPServer // OAuth token expired
| PendingMCPServer // reconnecting after network jitter
| DisabledMCPServer; // user manually disabledThese five mutually exclusive states allow the UI to display precise status and drive reconnection logic.
Connection Mechanism
The entry point is connectToServer in src/services/mcp/client.ts. It is memoized so that identical (name, serverRef) configurations reuse an existing connection.
export const connectToServer = memoize(async (name: string, serverRef: ScopedMcpServerConfig, serverStats?) => {
let transport: Transport;
// Step 1 – create transport based on protocol
if (serverRef.type === 'sse') {
const authProvider = new ClaudeAuthProvider(name, serverRef);
const combinedHeaders = await getMcpServerHeaders(name, serverRef);
transport = new SSEClientTransport(new URL(serverRef.url), { authProvider, requestInit: { headers: combinedHeaders } });
} else if (serverRef.type === 'stdio') {
transport = new StdioClientTransport({ command: serverRef.command, args: serverRef.args, env: serverRef.env });
} else if (serverRef.type === 'http') {
const authProvider = new ClaudeAuthProvider(name, serverRef);
transport = new StreamableHTTPClientTransport(new URL(serverRef.url), { authProvider, requestInit: { headers: combinedHeaders } });
}
// Step 2 – create MCP client instance
const client = new Client({ name: 'claude-code', version: '1.0.0' });
// Step 3 – connect transport (handshake, capability exchange)
await client.connect(transport);
// Step 4 – return connection result
return { client, name, type: 'connected', capabilities, config: serverRef };
});Memoization prevents repeated handshakes; when .mcp.json changes the cache entry is explicitly deleted.
Caching Strategy – Why Memoize?
Repeated calls with the same (name, serverRef) return the cached connection, avoiding the overhead of establishing multiple identical connections.
Cache key is the pair (name, serverRef). Changing the configuration invalidates the entry via connectToServer.cache.delete(key).
Tool Discovery & Conversion
After a successful connection, fetchToolsForClient queries the server for its tool list via the JSON‑RPC method tools/list. The result is cached with an LRU cache limited to 20 servers.
export const fetchToolsForClient = memoizeWithLRU(async (client: MCPServerConnection): Promise<Tool[]> => {
if (client.type !== 'connected') return [];
if (!client.capabilities?.tools) return [];
const result = await client.client.request({ method: 'tools/list' }, ListToolsResultSchema) as ListToolsResult;
const toolsToProcess = recursivelySanitizeUnicode(result.tools);
return toolsToProcess.map(tool => ({
...MCPTool,
name: buildMcpToolName(client.name, tool.name),
mcpInfo: { serverName: client.name, toolName: tool.name },
isMcp: true,
async description() {
const desc = tool.description ?? '';
return desc.length > MAX_MCP_DESCRIPTION_LENGTH
? desc.slice(0, MAX_MCP_DESCRIPTION_LENGTH) + '…'
: desc;
},
isConcurrencySafe() { return tool.annotations?.readOnlyHint ?? false; },
isDestructive() { return tool.annotations?.destructiveHint ?? false; },
inputJSONSchema: tool.inputSchema as Tool['inputJSONSchema'],
}));
}, { maxSize: MCP_FETCH_CACHE_SIZE }); // LRU limit: 20 serversEach external tool name is prefixed with mcp__ and the server name (e.g., mcp__github__search) to avoid name collisions when multiple servers expose tools with the same base name.
Why the mcp__ Prefix?
Server A provides search → mcp__github__search.
Server B provides search → mcp__slack__search.
This guarantees unique identifiers across servers.
Authentication System
Authentication logic lives in src/services/mcp/auth.ts. If a server configuration contains an oauth field, a ClaudeAuthProvider is created. Tokens are cached for 15 minutes to suppress repeated auth prompts.
const MCP_AUTH_CACHE_TTL_MS = 15 * 60 * 1000; // 15 min cache
let authCachePromise: Promise<McpAuthCacheData> | null = null;
function getMcpAuthCache(): Promise<McpAuthCacheData> {
if (!authCachePromise) {
authCachePromise = readFile(getMcpAuthCachePath(), 'utf-8')
.then(data => jsonParse(data))
.catch(() => ({})); // missing file → empty cache
}
return authCachePromise;
}
export class McpAuthError extends Error {
serverName: string;
constructor(serverName: string, message: string) {
super(message);
this.name = 'McpAuthError';
this.serverName = serverName;
}
}
export function isMcpSessionExpiredError(error: Error): boolean {
const httpStatus = 'code' in error ? (error as any).code : undefined;
if (httpStatus !== 404) return false;
return (
error.message.includes('"code":-32001') ||
error.message.includes('"code": -32001')
);
}Three error classes are defined: McpAuthError – 401 or invalid token. isMcpSessionExpiredError – 404 with JSON‑RPC code -32001 (session not found).
Generic connection errors are handled by the underlying SDK.
Token Cache Design
The 15‑minute TTL reduces the frequency of user prompts. A shared authCachePromise ensures that simultaneous 401 responses trigger only a single file read.
Lifecycle Management
Connection state is managed by MCPConnectionManager.tsx and the useManageMCPConnections hook. A React Context provides two main actions:
reconnectMcpServer(name) – cleans up the old connection, clears caches, re‑invokes connectToServer, and updates global app state.
toggleMcpServer(name) – updates the disabled flag in the configuration, clears the connection cache, and updates app state.
Automatic Reconnection – Exponential Backoff
When a connection drops, the client attempts up to five reconnections with exponential backoff (1 s, 2 s, 4 s, 8 s, 16 s, capped at 30 s). This mitigates thundering‑herd effects and gives the remote server time to recover.
const MAX_RECONNECT_ATTEMPTS = 5;
const INITIAL_BACKOFF_MS = 1000; // 1 s
const MAX_BACKOFF_MS = 30000; // 30 s cap
// backoff = Math.min(INITIAL_BACKOFF_MS * 2 ** attempt, MAX_BACKOFF_MS)Reasons for exponential backoff:
Provides recovery time for the server.
Prevents a retry storm when many clients reconnect simultaneously.
Balances user experience by limiting the total wait time.
Key Design Decisions
Memoize connection – avoids repeated expensive handshakes; improves performance at the cost of a small memory footprint.
LRU cache for tool lists – limits memory usage; caches up to 20 servers.
Five connection states – covers all error scenarios for UI clarity; adds modest code complexity.
Exponential backoff – prevents retry storms; results in longer failure‑recovery time.
mcp__ prefix – prevents name collisions across servers; makes tool identifiers longer.
OAuth token cache (15 min) – suppresses repeated auth dialogs; may delay detection of token revocation.
JSON‑RPC protocol – language‑agnostic standard; relies on SDK implementation.
Performance Optimizations
Parallel connection initialization using Promise.all for N servers.
Lazy loading of heavy modules (e.g., insights.ts) only when needed, reducing startup time by ~135 ms.
Connection reuse via memoization eliminates repeated handshakes.
Concurrent execution of read‑only tools ( isConcurrencySafe() true) speeds up multi‑tool scenarios.
Overall Flow
When Claude Code starts, it reads .mcp.json, parses N server configurations, and concurrently calls connectToServer for each. Successful connections are stored in the memoization cache. For each connected server, fetchToolsForClient retrieves the tool list, converts each tool into Claude Code’s internal Tool format (adding the mcp__ prefix, mapping safety annotations, and truncating long descriptions), and merges them into the global tool pool. During operation, the client uses the JSON‑RPC tools/call method to invoke remote tools, handling authentication, session expiration, and automatic reconnection as described above. Lifecycle hooks keep the connection state up‑to‑date, react to configuration changes, and expose UI actions for manual reconnection or disabling of servers.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
ZhiKe AI
We dissect AI-era technologies, tools, and trends with a hardcore perspective. Focused on large models, agents, MCP, function calling, and hands‑on AI development. No fluff, no hype—only actionable insights, source code, and practical ideas. Get a daily dose of intelligence to simplify tech and make efficiency tangible.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
