引言
MCP客户端是AI模型与外部工具之间的关键中介层。一个设计良好的客户端不仅需要正确的API调用,更需要考虑安全、性能、可维护性等生产级要求。
客户端架构
推荐的分层架构
┌─────────────────────────────────────────────────────────┐
│ AI Model (LLM) │
├─────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ Integration Layer │ │
│ │ LangChain / LlamaIndex / 自定义Agent框架 │ │
│ └────────────────────┬────────────────────────────┘ │
│ │ │
│ ┌────────────────────▼────────────────────────────┐ │
│ │ MCP Client Core │ │
│ ├─────────────────────────────────────────────────┤ │
│ │ Connection Mgr │ Tool Router │ Cache Layer │ │
│ ├─────────────────────────────────────────────────┤ │
│ │ Error Handler │ Retry Logic │ Degradation │ │
│ └────────────────────┬────────────────────────────┘ │
│ │ │
│ ┌────────────────────▼────────────────────────────┐ │
│ │ MCP Server Transport │ │
│ │ (stdio / SSE / WebSocket) │ │
│ └─────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘
核心模块职责
| 模块 | 职责 | 关键实现 |
|---|---|---|
| Connection Manager | 连接生命周期、心跳、重连 | 状态机 + 指数退避 |
| Tool Router | 工具发现、缓存、路由 | 多Server工具注册表 |
| Cache Layer | 工具列表、资源缓存 | TTL + LRU |
| Error Handler | 错误分类、重试、降级 | 策略模式 |
| Security Layer | 输入验证、权限检查 | 白名单 + Schema校验 |
安全最佳实践
输入验证
// 所有用户输入必须经过验证
import { z } from "zod";
// 工具参数白名单
const allowedTools = new Set([
"search_docs",
"read_file",
"calculate",
]);
// 参数大小限制
const maxArgSize = 1024 * 10; // 10KB
function validateToolCall(toolName: string, args: any): void {
// 1. 工具名白名单
if (!allowedTools.has(toolName)) {
throw new Error(`禁止调用的工具: ${toolName}`);
}
// 2. 参数大小限制
const argSize = JSON.stringify(args).length;
if (argSize > maxArgSize) {
throw new Error(`参数过大: ${argSize} bytes (限制: ${maxArgSize})`);
}
// 3. 参数类型校验
if (typeof args !== "object" || args === null) {
throw new Error("参数必须是对象");
}
}
输出净化
// 敏感信息过滤
const sensitivePatterns = [
/api[_\-]?key[=\s:]+["']?[^"'&\s]+/gi,
/token[=\s:]+["']?[^"'&\s]+/gi,
/password[=\s:]+["']?[^"'&\s]+/gi,
/secret[=\s:]+["']?[^"'&\s]+/gi,
/Bearer\s+[a-zA-Z0-9\-_.]+/g,
/\b[A-Za-z0-9]{32,}\b/g, // 疑似API Key的32+字符字符串
];
function sanitizeOutput(text: string): string {
let safe = text;
for (const pattern of sensitivePatterns) {
safe = safe.replace(pattern, "[REDACTED]");
}
return safe;
}
// 在工具结果返回前过滤
function processToolResult(result: ToolResult): ToolResult {
return {
...result,
content: result.content.map(content => ({
...content,
text: content.type === "text"
? sanitizeOutput(content.text)
: content.text,
})),
};
}
性能优化
工具列表缓存
class ToolListCache {
private cache: {
tools: Tool[];
timestamp: number;
} | null = null;
private ttl: number;
constructor(ttlMs: number = 30000) {
this.ttl = ttlMs;
}
async getTools(fetcher: () => Promise<Tool[]>): Promise<Tool[]> {
if (this.cache && Date.now() - this.cache.timestamp < this.ttl) {
return this.cache.tools;
}
const tools = await fetcher();
this.cache = { tools, timestamp: Date.now() };
return tools;
}
invalidate(): void {
this.cache = null;
}
}
批量调用优化
// 批量获取资源的工具
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
if (name === "batch_read_resources") {
const uris: string[] = args.uris;
const results = await Promise.all(
uris.map(uri => readResource(uri))
);
return {
content: [{ type: "text", text: JSON.stringify(results) }],
isError: false,
};
}
});
测试策略
模拟Server测试
// 测试用的Mock Server
class MockMCPServer {
private tools: Tool[];
private handlers: Map<string, Function>;
constructor() {
this.tools = [
{
name: "greet",
description: "打招呼",
inputSchema: {
type: "object",
properties: {
name: { type: "string" },
},
required: ["name"],
},
},
];
this.handlers = new Map();
this.handlers.set("greet", (args: any) => ({
content: [{ type: "text", text: `你好,${args.name}!` }],
isError: false,
}));
}
async handleRequest(request: any): Promise<any> {
if (request.method === "tools/list") {
return { tools: this.tools };
}
if (request.method === "tools/call") {
const handler = this.handlers.get(request.params.name);
if (handler) {
return handler(request.params.arguments);
}
return {
error: { code: -32601, message: "工具未找到" },
};
}
if (request.method === "initialize") {
return {
protocolVersion: "2024-11-05",
capabilities: { tools: {} },
serverInfo: { name: "mock", version: "1.0.0" },
};
}
return {};
}
}
// 测试
import { describe, it, expect } from "vitest";
describe("MCP Client", () => {
it("should list tools from server", async () => {
const mockServer = new MockMCPServer();
const response = await mockServer.handleRequest({
method: "tools/list",
});
expect(response.tools).toHaveLength(1);
expect(response.tools[0].name).toBe("greet");
});
it("should call tool and get result", async () => {
const mockServer = new MockMCPServer();
const response = await mockServer.handleRequest({
method: "tools/call",
params: {
name: "greet",
arguments: { name: "小明" },
},
});
expect(response.content[0].text).toContain("小明");
});
it("should handle unknown tools", async () => {
const mockServer = new MockMCPServer();
const response = await mockServer.handleRequest({
method: "tools/call",
params: { name: "unknown_tool", arguments: {} },
});
expect(response.error).toBeDefined();
});
});
端到端测试
import { describe, it, expect, beforeAll, afterAll } from "vitest";
describe("MCP Client Integration", () => {
let client: Client;
beforeAll(async () => {
const transport = new StdioClientTransport({
command: "node",
args: ["test/mock-server.js"],
});
client = new Client(
{ name: "test", version: "1.0.0" },
{ capabilities: {} }
);
await client.connect(transport);
});
afterAll(async () => {
await client.close();
});
it("should succeed on normal tool call", async () => {
const result = await client.callTool({
name: "greet",
arguments: { name: "测试" },
});
expect(result.content[0].text).toBe("你好,测试!");
expect(result.isError).toBe(false);
});
it("should handle server errors gracefully", async () => {
const result = await client.callTool({
name: "error_tool",
arguments: {},
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toBeDefined();
});
it("should respect timeout", async () => {
const startTime = Date.now();
try {
await client.callTool({
name: "slow_tool",
arguments: {},
}, { timeout: 1000 });
expect.fail("应该超时");
} catch (error) {
const elapsed = Date.now() - startTime;
expect(elapsed).toBeLessThan(5000);
}
});
});
常见陷阱
1. 忽略初始化
// ❌ 错误:跳过initialize直接调用
const result = await client.callTool({...});
// ✅ 正确:必须先initialize
await client.connect(transport);
// 或
await session.initialize();
2. 不处理错误
// ❌ 错误:未捕获异常
const result = await client.callTool({ name: "search", arguments: {} });
console.log(result);
// ✅ 正确:完整错误处理
try {
const result = await client.callTool({ name: "search", arguments: {} });
if (result.isError) {
console.error("工具返回错误:", result.content[0].text);
return null;
}
return result;
} catch (error) {
if (error instanceof TransportError) {
await handleReconnect();
} else {
console.error("工具调用异常:", error);
}
}
3. 无限重试
// ❌ 错误:无限重试
while (true) {
try {
result = await callTool();
break;
} catch {
// 无限循环!
}
}
// ✅ 正确:有限次重试
for (let i = 0; i < 3; i++) {
try {
result = await callTool();
break;
} catch (error) {
if (i === 2 || !isRetryable(error)) throw error;
await sleep(1000 * Math.pow(2, i));
}
}
4. 阻塞主线程
// ❌ 错误:同步阻塞
function callToolSync() {
const result = client.callTool({...}); // 阻塞!
return result;
}
// ✅ 正确:异步处理
async function callToolAsync() {
const result = await client.callTool({...});
return result;
}
配置管理
推荐配置模板
{
"mcp_clients": {
"default": {
"connection": {
"reconnect": {
"enabled": true,
"maxRetries": 5,
"baseDelayMs": 1000,
"maxDelayMs": 30000
},
"heartbeat": {
"enabled": true,
"intervalMs": 30000,
"maxFailures": 3
}
},
"performance": {
"toolCacheTTLMs": 30000,
"timeoutMs": 30000,
"maxConcurrentCalls": 10
},
"security": {
"maxArgSize": 10240,
"sanitizeOutput": true,
"allowedTools": ["search", "read", "calculate"]
},
"logging": {
"level": "info",
"traceRequests": true
}
}
}
}
监控指标体系
| 指标 | 说明 | 告警阈值 |
|---|---|---|
mcp.tool.calls.total | 工具调用总数 | - |
mcp.tool.calls.errors | 调用错误数 | > 5% |
mcp.tool.calls.latency | 调用延迟P99 | > 10s |
mcp.connection.state | 连接状态 | disconnected |
mcp.heartbeat.failures | 心跳失败次数 | > 3 |
mcp.cache.hit_rate | 工具列表缓存命中率 | < 50% |
总结
构建生产级MCP客户端的关键原则:
| 原则 | 说明 | 优先级 |
|---|---|---|
| 安全第一 | 输入验证、输出净化 | P0 |
| 健壮性 | 重试、降级、容错 | P0 |
| 可观测性 | 日志、指标、追踪 | P1 |
| 性能 | 缓存、并发控制 | P1 |
| 可测试性 | Mock Server、集成测试 | P1 |
下一步学习建议:
本文最后更新于 2024-07-18。