MCP Client 实现详解:构建智能 Agent 的工具管理中枢
深入解析 MCP Client 的实现原理,包括 Server 连接管理、工具发现、调用路由和错误处理的完整实现方案。
MCP Client 是 AI Agent 与 MCP Server 之间的桥梁——它负责发现工具、路由调用、处理错误。一个健壮的 Client 实现是构建可靠 Agent 系统的基础。
Client 的职责
MCP Client 承担以下核心职责:
- 连接管理——建立和维护与多个 Server 的连接
- 能力发现——查询每个 Server 提供的 Tools、Resources 和 Prompts
- 调用路由——将 LLM 的工具调用请求路由到正确的 Server
- 错误处理——处理连接断开、调用失败等异常情况
- 生命周期管理——Server 的启动、监控和关闭
基础 Client 实现
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
interface ServerConfig {
name: string;
command: string;
args: string[];
env?: Record<string, string>;
}
class MCPClientManager {
private clients: Map<string, Client> = new Map();
private tools: Map<string, string> = new Map(); // tool name -> server name
async addServer(config: ServerConfig): Promise<void> {
const client = new Client({
name: config.name,
version: '1.0.0',
});
const transport = new StdioClientTransport({
command: config.command,
args: config.args,
env: config.env,
});
await client.connect(transport);
this.clients.set(config.name, client);
// 发现并注册工具
const { tools } = await client.listTools();
for (const tool of tools) {
this.tools.set(tool.name, config.name);
}
console.error(`[MCP] 已连接 ${config.name},注册 ${tools.length} 个工具`);
}
async callTool(name: string, args: Record<string, any>): Promise<any> {
const serverName = this.tools.get(name);
if (!serverName) {
throw new Error(`工具 ${name} 未找到`);
}
const client = this.clients.get(serverName);
if (!client) {
throw new Error(`Server ${serverName} 未连接`);
}
return await client.callTool({ name, arguments: args });
}
getAllTools(): Array<{ name: string; description: string; inputSchema: any }> {
const tools: any[] = [];
// 需要缓存工具定义
return tools;
}
async cleanup(): Promise<void> {
for (const [name, client] of this.clients) {
try {
await client.close();
console.error(`[MCP] 已断开 ${name}`);
} catch (error) {
console.error(`[MCP] 断开 ${name} 失败:`, error);
}
}
this.clients.clear();
this.tools.clear();
}
}
完整的工具缓存实现
interface CachedTool {
name: string;
description: string;
inputSchema: any;
serverName: string;
}
class MCPClientManager {
private clients: Map<string, Client> = new Map();
private toolCache: Map<string, CachedTool> = new Map();
async addServer(config: ServerConfig): Promise<void> {
const client = new Client({ name: config.name, version: '1.0.0' });
await client.connect(new StdioClientTransport({
command: config.command,
args: config.args,
env: config.env,
}));
this.clients.set(config.name, client);
const { tools } = await client.listTools();
for (const tool of tools) {
this.toolCache.set(tool.name, {
name: tool.name,
description: tool.description || '',
inputSchema: tool.inputSchema,
serverName: config.name,
});
}
}
getToolsForLLM(): Array<{
name: string;
description: string;
input_schema: any;
}> {
return Array.from(this.toolCache.values()).map(tool => ({
name: tool.name,
description: tool.description,
input_schema: tool.inputSchema,
}));
}
async callTool(name: string, args: Record<string, any>): Promise<any> {
const cached = this.toolCache.get(name);
if (!cached) {
throw new Error(`Tool ${name} not found`);
}
const client = this.clients.get(cached.serverName);
if (!client) {
throw new Error(`Server ${cached.serverName} not connected`);
}
return await client.callTool({ name, arguments: args });
}
}
与 LLM 集成
Claude API 集成
import Anthropic from '@anthropic-ai/sdk';
class ClaudeAgent {
private claude = new Anthropic();
private mcpManager: MCPClientManager;
constructor() {
this.mcpManager = new MCPClientManager();
}
async chat(message: string): Promise<string> {
const tools = this.mcpManager.getToolsForLLM();
const messages: any[] = [{ role: 'user', content: message }];
while (true) {
const response = await this.claude.messages.create({
model: 'claude-sonnet-4-20250514',
max_tokens: 4096,
tools,
messages,
});
if (response.stop_reason === 'tool_use') {
const toolResults = [];
for (const block of response.content) {
if (block.type === 'tool_use') {
try {
const result = await this.mcpManager.callTool(
block.name,
block.input
);
toolResults.push({
type: 'tool_result',
tool_use_id: block.id,
content: result.content.map((c: any) => c.text || '').join('\n'),
});
} catch (error) {
toolResults.push({
type: 'tool_result',
tool_use_id: block.id,
content: `错误:${error.message}`,
is_error: true,
});
}
}
}
messages.push({ role: 'assistant', content: response.content });
messages.push({ role: 'user', content: toolResults });
continue;
}
return response.content
.filter((b): b is any => b.type === 'text')
.map((b: any) => b.text)
.join('');
}
}
}
连接健康监控
class HealthMonitor {
private clients: Map<string, Client>;
private healthStatus: Map<string, boolean> = new Map();
constructor(clients: Map<string, Client>) {
this.clients = clients;
}
startMonitoring(intervalMs: number = 30000) {
setInterval(() => this.checkAll(), intervalMs);
}
private async checkAll() {
for (const [name, client] of this.clients) {
try {
// 尝试列出工具来验证连接
await client.listTools();
this.healthStatus.set(name, true);
} catch {
this.healthStatus.set(name, false);
console.error(`[Health] Server ${name} 不可用`);
}
}
}
isHealthy(serverName: string): boolean {
return this.healthStatus.get(serverName) ?? false;
}
}
常见问题(FAQ)
Client 应该在什么时候初始化 Server 连接?
在应用启动时初始化所有连接。如果某个 Server 连接失败,记录错误但不阻塞其他 Server 的初始化。
如何处理 Server 崩溃?
实现重连机制——检测到连接断开后,等待一段时间后尝试重新连接。对于关键 Server,可以配置多个实例进行故障转移。
一个 Client 可以连接多少个 Server?
没有硬性限制,但建议不超过 20 个。过多的连接会增加管理复杂度和资源消耗。
总结
MCP Client 是 Agent 系统的中枢——它管理着与所有 Server 的连接,将 LLM 的意图转化为具体的工具调用。一个健壮的 Client 实现需要关注连接管理、工具缓存、错误处理和健康监控四个方面。