第二节:MCP 客户端实现
协议与通信机制

作者:小学子 📚 | 日期:2026年4月2日 | 第七阶段 · 模块七


# 第七阶段 · 模块七 · 第二节:MCP 客户端实现

核心问题

MCP 客户端是如何创建和连接的?工具调用的完整流程是什么?连接管理的实现细节是什么?


◇ 本节位置


        Claude Code 全局架构
        
        ┌─────────────────────────────────────────────────────────────────────┐
        │  MCP 系统                                                            │
        │                                                                      │
        │  MCP Client ← 本节                                                 │
        │  ├── connectToServer ──> 连接服务器                                 │
        │  ├── ensureConnectedClient ──> 获取可用客户端                       │
        │  └── callTool ──> 调用工具                                         │
        └─────────────────────────────────────────────────────────────────────┘
        


一、客户端创建

1.1 MCP Client 初始化

源码位置:`src/services/mcp/client.ts`


        // 使用 @modelcontextprotocol/sdk 创建客户端
        import { Client } from '@modelcontextprotocol/sdk/client/index.js';
        
        const client = new Client(
          {
            name: 'claude-code',
            version: '1.0.0',
          },
          {
            capabilities: {
              tools: {},      // 工具调用能力
              resources: {},  // 资源访问能力
              prompts: {},    // 提示模板能力
            },
          }
        );
        

1.2 连接选项


        // 连接配置
        interface MCPServerConfig {
          type: 'sse' | 'stdio' | 'http';
          name: string;
          command?: string;      // stdio 模式
          args?: string[];       // stdio 参数
          url?: string;          // SSE/HTTP 模式
          headers?: Record<string, string>;
          auth?: {
            type: 'oauth' | 'bearer';
            token?: string;
          };
        }
        

1.3 五问分析

问 1:为什么用 memoize 包装 connectToServer?


        // 源码位置:client.ts 第 593 行
        
        // memoize 缓存连接结果
        // 避免重复连接同一个服务器
        
        export const connectToServer = memoize(
          async (name, serverRef, serverStats) => {
            // 连接逻辑
          }
        );
        
        // 调用时,如果已连接则直接返回缓存
        const client = await connectToServer('github', config);
        


二、连接流程

2.1 connectToServer 实现

源码位置:`src/services/mcp/client.ts` 第 593 行


        export const connectToServer = memoize(
          async (
            name: string,
            serverRef: ScopedMcpServerConfig,
            serverStats?: ServerStats,
          ): Promise<MCPServerConnection> => {
            const connectStartTime = Date.now();
            let transport: Transport;
        
            // 1. 根据类型选择传输
            if (serverRef.type === 'sse') {
              transport = await createSSETransport(serverRef);
            } else if (serverRef.type === 'stdio') {
              transport = await createStdioTransport(serverRef);
            } else if (serverRef.type === 'http') {
              transport = await createHTTPTransport(serverRef);
            }
        
            // 2. 创建客户端
            const client = new Client({ name, version: '1.0.0' }, {
              capabilities: { tools: {}, resources: {} },
            });
        
            // 3. 连接
            await client.connect(transport);
        
            // 4. 返回连接
            return { client, name, connectedAt: connectStartTime };
          }
        );
        

2.2 SSE 传输创建


        if (serverRef.type === 'sse') {
          // 创建认证提供者
          const authProvider = new ClaudeAuthProvider(name, serverRef);
        
          // 获取请求头
          const combinedHeaders = await getMcpServerHeaders(name, serverRef);
        
          // SSE 传输
          const transportOptions: SSEClientTransportOptions = {
            authProvider,
            fetch: wrapFetchWithTimeout(...),
            requestInit: {
              headers: {
                'User-Agent': getMCPUserAgent(),
                ...combinedHeaders,
              },
            },
          };
        
          transport = new SSEClientTransport(new URL(serverRef.url), transportOptions);
        }
        

2.3 五问分析

问 1:SSE 和 HTTP 的区别?


        // SSE:服务器推送,持久连接
        // → 适合需要实时通知的场景
        // → EventSource 长期存活,不能设置 timeout
        
        // HTTP:请求-响应模式
        // → 适合简单的 REST API
        // → 每个请求独立,可以设置 timeout
        

问 2:stdio 模式用于什么场景?


        // 本地工具通过子进程通信
        {
          type: 'stdio',
          command: 'npx',
          args: ['mcp-server', '--stdio'],
          cwd: '/project'
        }
        


三、连接管理

3.1 MCPServerConnection


        interface MCPServerConnection {
          client: Client;
          name: string;
          connectedAt: number;
          lastUsed?: number;
          error?: Error;
        }
        

3.2 连接缓存


        // 缓存所有活跃连接
        const connections = new Map<string, MCPServerConnection>();
        
        async function getConnection(name: string): Promise<MCPServerConnection> {
          // 检查缓存
          const existing = connections.get(name);
        
          if (existing && !isStale(existing)) {
            existing.lastUsed = Date.now();
            return existing;
          }
        
          // 创建新连接
          const connection = await connectToServer(name, config);
          connections.set(name, connection);
        
          return connection;
        }
        

3.3 五问分析

问 1:连接过期怎么办?


        // 源码位置:client.ts 第 162 行
        
        // 连接过期时抛出错误
        // 调用者需要重新获取连接
        
        class MCPSessionExpiredError extends Error {
          constructor() {
            super('MCP session expired, please reconnect');
          }
        }
        
        // 使用时捕获并重试
        try {
          const result = await client.callTool({ name: 'tool', arguments: {} });
        } catch (error) {
          if (error instanceof MCPSessionExpiredError) {
            // 重新连接
            await reconnect(serverName);
          }
        }
        


四、工具调用

4.1 调用流程


        ┌─────────────────────────────────────────────────────────────────────┐
        │  工具调用流程                                                        │
        │                                                                      │
        │  1. getConnection(serverName) ──> 获取连接                         │
        │                                                                      │
        │  2. client.request(                                                 │
        │       { method: 'tools/call',                                        │
        │         params: { name: 'toolName', arguments: { ... } } },          │
        │       CallToolResultSchema                                          │
        │     )                                                               │
        │                                                                      │
        │  3. 返回结果                                                        │
        └─────────────────────────────────────────────────────────────────────┘
        

4.2 callTool 实现


        async function callTool(
          serverName: string,
          toolName: string,
          arguments: Record<string, unknown>
        ): Promise<CallToolResult> {
          // 1. 获取连接
          const connection = await getConnection(serverName);
        
          // 2. 调用工具
          const result = await connection.client.request(
            {
              method: 'tools/call',
              params: {
                name: toolName,
                arguments: arguments,
              },
            },
            CallToolResultSchema
          );
        
          // 3. 返回结果
          return result;
        }
        

4.3 错误处理


        async function callToolWithRetry(
          serverName: string,
          toolName: string,
          arguments: Record<string, unknown>,
          maxRetries: number = 3
        ): Promise<CallToolResult> {
          for (let i = 0; i < maxRetries; i++) {
            try {
              return await callTool(serverName, toolName, arguments);
            } catch (error) {
              // 连接过期,重试
              if (isSessionExpiredError(error)) {
                await reconnect(serverName);
                continue;
              }
        
              // 其他错误,抛出
              throw error;
            }
          }
        
          throw new Error(`Failed after ${maxRetries} retries`);
        }
        

4.4 五问分析

问 1:工具调用的超时是多少?


        // 每个请求有独立超时
        // 使用 AbortSignal.timeout() 设置
        
        const fetchWithTimeout = wrapFetchWithTimeout(fetch, {
          timeout: 60000,  // 60 秒
        });
        
        // SSE 的 EventSource 没有超时(长期连接)
        


五、连接状态管理

5.1 心跳保活


        // 定期发送心跳,保持连接活跃
        setInterval(async () => {
          for (const [name, connection] of connections) {
            try {
              // 发送 ping
              await connection.client.request(
                { method: 'ping' },
                undefined
              );
            } catch {
              // 连接断开,标记为需要重连
              connection.error = new Error('Ping failed');
            }
          }
        }, 30000);  // 每 30 秒
        

5.2 断开连接


        async function disconnect(serverName: string): Promise<void> {
          const connection = connections.get(serverName);
        
          if (connection) {
            await connection.client.close();
            connections.delete(serverName);
        
            // 清除缓存
            connectToServer.cache?.delete(serverName);
          }
        }
        


六、思考题

思考题 1:如何调试 MCP 连接?

答案


        // 1. 查看连接列表
        claude mcp list
        
        // 2. 查看详细状态
        claude mcp list --verbose
        
        // 3. 测试工具调用
        claude mcp call github.list_repos --args '{"limit": 5}'
        


思考题 2:连接失败的原因有哪些?

答案


        // 常见原因:
        // 1. 服务器未启动
        // 2. URL 错误
        // 3. 认证失败
        // 4. 网络不通
        // 5. 端口被占用(stdio 模式)
        // 6. OAuth token 过期
        
        // 检查清单:
        // - 服务器日志
        // - 网络连通性
        // - 认证配置
        


思考题 3:如何实现断线重连?

答案


        // 1. 监听连接状态
        client.on('close', () => {
          console.log('Connection closed');
        });
        
        // 2. 断线时自动重连
        async function withReconnect(fn: () => Promise<T>): Promise<T> {
          try {
            return await fn();
          } catch (error) {
            if (isConnectionError(error)) {
              await reconnect(serverName);
              return await fn();
            }
            throw error;
          }
        }
        


七、延伸阅读

文件核心内容
`src/services/mcp/client.ts`MCP 客户端实现
`src/services/mcp/MCPConnectionManager.tsx`连接管理器


八、下节预告

下一节我们将深入 MCP 工具集成

- MCP 工具如何注册为 Claude Code 工具

- 工具调用的参数转换

- 结果处理和错误处理


*- 第一轮:□ 事实准确性*

*- 第二轮:□ 深度与洞见*

*- 第三轮:□ 可读性与价值*