# 第七阶段 · 模块七 · 第二节:MCP 客户端实现
MCP 客户端是如何创建和连接的?工具调用的完整流程是什么?连接管理的实现细节是什么?
Claude Code 全局架构
┌─────────────────────────────────────────────────────────────────────┐
│ MCP 系统 │
│ │
│ MCP Client ← 本节 │
│ ├── connectToServer ──> 连接服务器 │
│ ├── ensureConnectedClient ──> 获取可用客户端 │
│ └── callTool ──> 调用工具 │
└─────────────────────────────────────────────────────────────────────┘
源码位置:`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: {}, // 提示模板能力
},
}
);
// 连接配置
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:为什么用 memoize 包装 connectToServer?
// 源码位置:client.ts 第 593 行
// memoize 缓存连接结果
// 避免重复连接同一个服务器
export const connectToServer = memoize(
async (name, serverRef, serverStats) => {
// 连接逻辑
}
);
// 调用时,如果已连接则直接返回缓存
const client = await connectToServer('github', config);
源码位置:`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 };
}
);
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);
}
问 1:SSE 和 HTTP 的区别?
// SSE:服务器推送,持久连接
// → 适合需要实时通知的场景
// → EventSource 长期存活,不能设置 timeout
// HTTP:请求-响应模式
// → 适合简单的 REST API
// → 每个请求独立,可以设置 timeout
问 2:stdio 模式用于什么场景?
// 本地工具通过子进程通信
{
type: 'stdio',
command: 'npx',
args: ['mcp-server', '--stdio'],
cwd: '/project'
}
interface MCPServerConnection {
client: Client;
name: string;
connectedAt: number;
lastUsed?: number;
error?: Error;
}
// 缓存所有活跃连接
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;
}
问 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);
}
}
┌─────────────────────────────────────────────────────────────────────┐
│ 工具调用流程 │
│ │
│ 1. getConnection(serverName) ──> 获取连接 │
│ │
│ 2. client.request( │
│ { method: 'tools/call', │
│ params: { name: 'toolName', arguments: { ... } } }, │
│ CallToolResultSchema │
│ ) │
│ │
│ 3. 返回结果 │
└─────────────────────────────────────────────────────────────────────┘
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;
}
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`);
}
问 1:工具调用的超时是多少?
// 每个请求有独立超时
// 使用 AbortSignal.timeout() 设置
const fetchWithTimeout = wrapFetchWithTimeout(fetch, {
timeout: 60000, // 60 秒
});
// SSE 的 EventSource 没有超时(长期连接)
// 定期发送心跳,保持连接活跃
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 秒
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. 查看连接列表
claude mcp list
// 2. 查看详细状态
claude mcp list --verbose
// 3. 测试工具调用
claude mcp call github.list_repos --args '{"limit": 5}'
答案:
// 常见原因:
// 1. 服务器未启动
// 2. URL 错误
// 3. 认证失败
// 4. 网络不通
// 5. 端口被占用(stdio 模式)
// 6. OAuth token 过期
// 检查清单:
// - 服务器日志
// - 网络连通性
// - 认证配置
答案:
// 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 工具
- 工具调用的参数转换
- 结果处理和错误处理
*- 第一轮:□ 事实准确性*
*- 第二轮:□ 深度与洞见*
*- 第三轮:□ 可读性与价值*