mcp-client-builder
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseMCP Client Development Guide
MCP客户端开发指南
Core Mental Model
核心思维模型
Host Application (user-facing app like Claude Desktop)
└─> Creates 1+ MCP Clients (protocol components)
└─> Each Client connects to exactly 1 Server (1:1 mapping)
└─> Server exposes Tools/Resources/Prompts
└─> LLM decides which to use
└─> Client executes, returns resultsKey Principle: Client = stateful messenger, NOT decision maker. LLM chooses tools, client facilitates execution.
Host Application (user-facing app like Claude Desktop)
└─> Creates 1+ MCP Clients (protocol components)
└─> Each Client connects to exactly 1 Server (1:1 mapping)
└─> Server exposes Tools/Resources/Prompts
└─> LLM decides which to use
└─> Client executes, returns results核心原则:客户端是有状态的信使,而非决策者。由LLM选择工具,客户端负责执行。
Development Workflow
开发流程
Phase 1: Architecture Design
阶段1:架构设计
1.1 Determine Requirements
1.1 确定需求
Client Capabilities (what client provides TO servers):
- - Allow server to request LLM completions
sampling - - Declare filesystem boundaries
roots - - Allow server to request user input
elicitation
Expected Server Capabilities (what servers provide TO client):
- - Execute operations
tools - - Access data
resources - - Use templates
prompts
客户端能力(客户端向服务器提供的功能):
- - 允许服务器请求LLM补全
sampling - - 声明文件系统边界
roots - - 允许服务器请求用户输入
elicitation
预期服务器能力(服务器向客户端提供的功能):
- - 执行操作
tools - - 访问数据
resources - - 使用模板
prompts
1.2 Select Transport Strategy
1.2 选择传输策略
| Transport | Use When | Pros | Cons |
|---|---|---|---|
| stdio | Server on same machine | Fast, simple | Local only |
| HTTP Stream | Remote server, modern | Bidirectional, sessions | More complex |
| SSE | Legacy compatibility | Simple | Unidirectional |
Decision Rule: stdio for local development/testing, HTTP Stream for production remote servers.
| 传输方式 | 使用场景 | 优势 | 劣势 |
|---|---|---|---|
| stdio | 服务器在本地机器 | 快速、简单 | 仅支持本地 |
| HTTP Stream | 远程服务器、现代场景 | 双向通信、支持会话 | 复杂度更高 |
| SSE | 遗留系统兼容 | 简单 | 单向通信 |
决策规则:本地开发/测试使用stdio,生产环境远程服务器使用HTTP Stream。
1.3 Plan Connection Management
1.3 规划连接管理
1:1 Mapping Pattern:
typescript
// CORRECT: One client per server
const weatherClient = new Client(/* weather server config */);
const calendarClient = new Client(/* calendar server config */);
// INCORRECT: One client trying to talk to multiple servers
const multiClient = new Client(/* won't work */);Host Manages Multiple Clients:
typescript
class HostApplication {
private clients: Map<string, Client> = new Map();
connectToServer(serverConfig) {
const client = new Client(config);
this.clients.set(serverConfig.id, client);
}
}1:1映射模式:
typescript
// CORRECT: One client per server
const weatherClient = new Client(/* weather server config */);
const calendarClient = new Client(/* calendar server config */);
// INCORRECT: One client trying to talk to multiple servers
const multiClient = new Client(/* won't work */);宿主应用管理多客户端:
typescript
class HostApplication {
private clients: Map<string, Client> = new Map();
connectToServer(serverConfig) {
const client = new Client(config);
this.clients.set(serverConfig.id, client);
}
}Phase 2: Implementation
阶段2:实现
2.1 Project Structure
2.1 项目结构
TypeScript:
src/
client.ts # Main Client class
transports/ # stdio, http, sse implementations
types.ts # Zod schemas
errors.ts # Error handling
session.ts # Session managementPython:
client.py # Main Client class
transports/ # stdio, http, sse
schemas.py # Pydantic models
errors.py # Error handling
session.py # Session managementTypeScript:
src/
client.ts # Main Client class
transports/ # stdio, http, sse implementations
types.ts # Zod schemas
errors.ts # Error handling
session.ts # Session managementPython:
client.py # Main Client class
transports/ # stdio, http, sse
schemas.py # Pydantic models
errors.py # Error handling
session.py # Session management2.2 Connection Lifecycle Implementation
2.2 连接生命周期实现
Three-Phase Pattern:
typescript
// Phase 1: Initialize
async connect(transport: Transport) {
await transport.connect();
const initResponse = await this.sendRequest({
method: "initialize",
params: {
protocolVersion: "2025-06-18",
capabilities: {
sampling: {}, // If client supports sampling
roots: { listChanged: true }, // If client supports roots
elicitation: {} // If client supports elicitation
},
clientInfo: { name: "my-client", version: "1.0.0" }
}
});
this.serverCapabilities = initResponse.capabilities;
// Phase 2: Confirm
await this.sendNotification({ method: "initialized" });
// Phase 3: Ready for operations
}Server Capabilities Extraction:
typescript
interface ServerCapabilities {
tools?: { listChanged?: boolean };
resources?: { subscribe?: boolean, listChanged?: boolean };
prompts?: { listChanged?: boolean };
logging?: {};
}三阶段模式:
typescript
// Phase 1: Initialize
async connect(transport: Transport) {
await transport.connect();
const initResponse = await this.sendRequest({
method: "initialize",
params: {
protocolVersion: "2025-06-18",
capabilities: {
sampling: {}, // If client supports sampling
roots: { listChanged: true }, // If client supports roots
elicitation: {} // If client supports elicitation
},
clientInfo: { name: "my-client", version: "1.0.0" }
}
});
this.serverCapabilities = initResponse.capabilities;
// Phase 2: Confirm
await this.sendNotification({ method: "initialized" });
// Phase 3: Ready for operations
}服务器能力提取:
typescript
interface ServerCapabilities {
tools?: { listChanged?: boolean };
resources?: { subscribe?: boolean, listChanged?: boolean };
prompts?: { listChanged?: boolean };
logging?: {};
}2.3 Transport Abstraction
2.3 传输抽象
Interface Pattern:
typescript
interface Transport {
connect(): Promise<void>;
send(message: JSONRPCMessage): Promise<void>;
receive(): AsyncIterator<JSONRPCMessage>;
close(): Promise<void>;
}
class StdioTransport implements Transport { /* ... */ }
class HTTPStreamTransport implements Transport { /* ... */ }
class SSETransport implements Transport { /* ... */ }Usage:
typescript
const transport = config.remote
? new HTTPStreamTransport(config.url)
: new StdioTransport(config.command, config.args);
await client.connect(transport);接口模式:
typescript
interface Transport {
connect(): Promise<void>;
send(message: JSONRPCMessage): Promise<void>;
receive(): AsyncIterator<JSONRPCMessage>;
close(): Promise<void>;
}
class StdioTransport implements Transport { /* ... */ }
class HTTPStreamTransport implements Transport { /* ... */ }
class SSETransport implements Transport { /* ... */ }使用方式:
typescript
const transport = config.remote
? new HTTPStreamTransport(config.url)
: new StdioTransport(config.command, config.args);
await client.connect(transport);2.4 Tool Orchestration Pattern
2.4 工具编排模式
Critical: LLM Decides, Client Executes:
typescript
async processUserQuery(query: string): Promise<string> {
// 1. Get available tools from server
const toolsResponse = await this.request({ method: "tools/list" });
const tools = toolsResponse.tools;
// 2. Present tools to LLM in structured format
const llmTools = tools.map(tool => ({
name: tool.name,
description: tool.description,
input_schema: tool.inputSchema
}));
// 3. LLM DECIDES which tools to use
const llmResponse = await anthropic.messages.create({
model: "claude-3-5-sonnet-20241022",
messages: [{ role: "user", content: query }],
tools: llmTools // LLM sees available tools
});
// 4. Execute LLM-requested tool calls
for (const toolUse of llmResponse.content) {
if (toolUse.type === 'tool_use') {
const result = await this.request({
method: "tools/call",
params: {
name: toolUse.name,
arguments: toolUse.input
}
});
// 5. Return results to LLM for final response
// ... continuation logic
}
}
}Key Point: Client never chooses tools. Client only:
- Discovers available tools
- Presents them to LLM
- Executes LLM's choices
- Returns results to LLM
关键规则:LLM决策,客户端执行:
typescript
async processUserQuery(query: string): Promise<string> {
// 1. Get available tools from server
const toolsResponse = await this.request({ method: "tools/list" });
const tools = toolsResponse.tools;
// 2. Present tools to LLM in structured format
const llmTools = tools.map(tool => ({
name: tool.name,
description: tool.description,
input_schema: tool.inputSchema
}));
// 3. LLM DECIDES which tools to use
const llmResponse = await anthropic.messages.create({
model: "claude-3-5-sonnet-20241022",
messages: [{ role: "user", content: query }],
tools: llmTools // LLM sees available tools
});
// 4. Execute LLM-requested tool calls
for (const toolUse of llmResponse.content) {
if (toolUse.type === 'tool_use') {
const result = await this.request({
method: "tools/call",
params: {
name: toolUse.name,
arguments: toolUse.input
}
});
// 5. Return results to LLM for final response
// ... continuation logic
}
}
}核心要点:客户端从不选择工具。客户端仅需:
- 发现可用工具
- 以结构化格式呈现给LLM
- 执行LLM选择的工具
- 将结果返回给LLM
2.5 Error Handling Strategy
2.5 错误处理策略
JSON-RPC Error Codes:
typescript
enum ErrorCode {
ParseError = -32700, // Invalid JSON
InvalidRequest = -32600, // Malformed request
MethodNotFound = -32601, // Tool doesn't exist
InvalidParams = -32602, // Wrong arguments
InternalError = -32603, // Server failure
// Custom range: -32000 to -32099
Timeout = -32001,
ResourceNotFound = -32002,
Unauthorized = -32003
}Error Classification & Retry Logic:
typescript
class ErrorHandler {
async handleError(error: JSONRPCError): Promise<'retry' | 'fail' | 'escalate'> {
// Transient errors: retry with exponential backoff
if ([ErrorCode.InternalError, ErrorCode.Timeout].includes(error.code)) {
return 'retry';
}
// Permanent errors: fail immediately
if ([ErrorCode.MethodNotFound, ErrorCode.InvalidParams].includes(error.code)) {
return 'fail';
}
// Security errors: escalate to user
if (error.code === ErrorCode.Unauthorized) {
return 'escalate';
}
}
}Retry Pattern:
typescript
async executeWithRetry<T>(
fn: () => Promise<T>,
maxRetries = 3,
baseDelay = 1000
): Promise<T> {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
const action = await this.errorHandler.handleError(error);
if (action === 'retry' && attempt < maxRetries - 1) {
await sleep(baseDelay * Math.pow(2, attempt));
continue;
}
throw error;
}
}
}JSON-RPC错误码:
typescript
enum ErrorCode {
ParseError = -32700, // Invalid JSON
InvalidRequest = -32600, // Malformed request
MethodNotFound = -32601, // Tool doesn't exist
InvalidParams = -32602, // Wrong arguments
InternalError = -32603, // Server failure
// Custom range: -32000 to -32099
Timeout = -32001,
ResourceNotFound = -32002,
Unauthorized = -32003
}错误分类与重试逻辑:
typescript
class ErrorHandler {
async handleError(error: JSONRPCError): Promise<'retry' | 'fail' | 'escalate'> {
// Transient errors: retry with exponential backoff
if ([ErrorCode.InternalError, ErrorCode.Timeout].includes(error.code)) {
return 'retry';
}
// Permanent errors: fail immediately
if ([ErrorCode.MethodNotFound, ErrorCode.InvalidParams].includes(error.code)) {
return 'fail';
}
// Security errors: escalate to user
if (error.code === ErrorCode.Unauthorized) {
return 'escalate';
}
}
}重试模式:
typescript
async executeWithRetry<T>(
fn: () => Promise<T>,
maxRetries = 3,
baseDelay = 1000
): Promise<T> {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
const action = await this.errorHandler.handleError(error);
if (action === 'retry' && attempt < maxRetries - 1) {
await sleep(baseDelay * Math.pow(2, attempt));
continue;
}
throw error;
}
}
}2.6 Session Management (HTTP Transport)
2.6 会话管理(HTTP传输)
Session ID Propagation:
typescript
class HTTPStreamTransport {
private sessionId?: string;
async send(message: JSONRPCMessage) {
const headers: Record<string, string> = {
'Content-Type': 'application/json'
};
// Propagate session ID bidirectionally
if (this.sessionId) {
headers['Mcp-Session-Id'] = this.sessionId;
}
const response = await fetch(this.url, {
method: 'POST',
headers,
body: JSON.stringify(message)
});
// Extract session ID from response
const receivedSessionId = response.headers.get('Mcp-Session-Id');
if (receivedSessionId) {
this.sessionId = receivedSessionId;
}
}
}Session State Management:
typescript
interface SessionState {
id: string;
lastActivity: Date;
conversationHistory: Message[];
resources: Map<string, ResourceState>;
}会话ID传播:
typescript
class HTTPStreamTransport {
private sessionId?: string;
async send(message: JSONRPCMessage) {
const headers: Record<string, string> = {
'Content-Type': 'application/json'
};
// Propagate session ID bidirectionally
if (this.sessionId) {
headers['Mcp-Session-Id'] = this.sessionId;
}
const response = await fetch(this.url, {
method: 'POST',
headers,
body: JSON.stringify(message)
});
// Extract session ID from response
const receivedSessionId = response.headers.get('Mcp-Session-Id');
if (receivedSessionId) {
this.sessionId = receivedSessionId;
}
}
}会话状态管理:
typescript
interface SessionState {
id: string;
lastActivity: Date;
conversationHistory: Message[];
resources: Map<string, ResourceState>;
}Phase 3: Security Implementation
阶段3:安全实现
3.1 Multi-Layer Defense
3.1 多层防御
Layer 1: Network Security:
typescript
class SecureTransport {
validateTLS(url: string) {
if (!url.startsWith('https://') && !this.isLocalhost(url)) {
throw new Error('Remote servers must use HTTPS');
}
}
validateOrigin(origin: string) {
// DNS rebinding protection
if (!this.allowedOrigins.includes(origin)) {
throw new Error(`Untrusted origin: ${origin}`);
}
}
}Layer 2: Authentication:
typescript
interface AuthProvider {
authenticate(): Promise<Credentials>;
refresh(credentials: Credentials): Promise<Credentials>;
}
class OAuth2PKCEProvider implements AuthProvider {
async authenticate(): Promise<Credentials> {
const codeVerifier = generateCodeVerifier();
const codeChallenge = await generateCodeChallenge(codeVerifier);
// OAuth 2.1 with PKCE flow
const authUrl = buildAuthUrl({ challenge: codeChallenge });
const code = await getUserConsent(authUrl);
return await exchangeCodeForToken(code, codeVerifier);
}
}Layer 3: Authorization:
typescript
class AuthorizationManager {
async checkPermissions(toolName: string, params: any): Promise<boolean> {
const tool = await this.getToolMetadata(toolName);
// Destructive operations require explicit user consent
if (tool.destructiveHint === true) {
return await this.requestUserApproval(
`Allow ${toolName}? This will modify data.`
);
}
// Check scopes
const requiredScopes = tool.requiredScopes || [];
return this.hasScopes(requiredScopes);
}
}Layer 4: Validation:
typescript
async callTool(name: string, args: unknown) {
// 1. Schema validation
const tool = await this.getTool(name);
const validatedArgs = tool.inputSchema.parse(args); // Zod/Pydantic
// 2. Sanitization
const sanitized = sanitizeInputs(validatedArgs);
// 3. Authorization check
const authorized = await this.authz.checkPermissions(name, sanitized);
if (!authorized) throw new UnauthorizedError();
// 4. Execute
return await this.executeToolCall(name, sanitized);
}第一层:网络安全:
typescript
class SecureTransport {
validateTLS(url: string) {
if (!url.startsWith('https://') && !this.isLocalhost(url)) {
throw new Error('Remote servers must use HTTPS');
}
}
validateOrigin(origin: string) {
// DNS rebinding protection
if (!this.allowedOrigins.includes(origin)) {
throw new Error(`Untrusted origin: ${origin}`);
}
}
}第二层:身份认证:
typescript
interface AuthProvider {
authenticate(): Promise<Credentials>;
refresh(credentials: Credentials): Promise<Credentials>;
}
class OAuth2PKCEProvider implements AuthProvider {
async authenticate(): Promise<Credentials> {
const codeVerifier = generateCodeVerifier();
const codeChallenge = await generateCodeChallenge(codeVerifier);
// OAuth 2.1 with PKCE flow
const authUrl = buildAuthUrl({ challenge: codeChallenge });
const code = await getUserConsent(authUrl);
return await exchangeCodeForToken(code, codeVerifier);
}
}第三层:授权:
typescript
class AuthorizationManager {
async checkPermissions(toolName: string, params: any): Promise<boolean> {
const tool = await this.getToolMetadata(toolName);
// Destructive operations require explicit user consent
if (tool.destructiveHint === true) {
return await this.requestUserApproval(
`Allow ${toolName}? This will modify data.`
);
}
// Check scopes
const requiredScopes = tool.requiredScopes || [];
return this.hasScopes(requiredScopes);
}
}第四层:验证:
typescript
async callTool(name: string, args: unknown) {
// 1. Schema validation
const tool = await this.getTool(name);
const validatedArgs = tool.inputSchema.parse(args); // Zod/Pydantic
// 2. Sanitization
const sanitized = sanitizeInputs(validatedArgs);
// 3. Authorization check
const authorized = await this.authz.checkPermissions(name, sanitized);
if (!authorized) throw new UnauthorizedError();
// 4. Execute
return await this.executeToolCall(name, sanitized);
}3.2 Credential Management
3.2 凭证管理
NEVER:
typescript
// ❌ WRONG: Hardcoded credentials
const client = new Client({ apiKey: "sk-1234..." });
// ❌ WRONG: Environment variables (visible to process)
const client = new Client({ apiKey: process.env.API_KEY });ALWAYS:
typescript
// ✅ CORRECT: OS keychain
import { getSecret } from '@keychain/secure-store';
const apiKey = await getSecret('mcp-server-credentials');
// ✅ CORRECT: Vault service
const credentials = await vault.getCredentials('mcp-server');严禁:
typescript
// ❌ WRONG: Hardcoded credentials
const client = new Client({ apiKey: "sk-1234..." });
// ❌ WRONG: Environment variables (visible to process)
const client = new Client({ apiKey: process.env.API_KEY });正确做法:
typescript
// ✅ CORRECT: OS keychain
import { getSecret } from '@keychain/secure-store';
const apiKey = await getSecret('mcp-server-credentials');
// ✅ CORRECT: Vault service
const credentials = await vault.getCredentials('mcp-server');Phase 4: Performance & Optimization
阶段4:性能与优化
4.1 Token Efficiency (Primary Goal)
4.1 令牌效率(首要目标)
Problem: Every token in tool I/O consumes LLM context window.
Solution Pattern:
typescript
interface ToolResponse {
format: 'concise' | 'detailed'; // Let LLM choose
}
async executeTool(name: string, args: { format?: string }) {
const result = await this.server.callTool(name, args);
// Default to concise
if (args.format !== 'detailed') {
return this.truncateResponse(result, MAX_TOKENS);
}
return result;
}
private truncateResponse(data: any, maxTokens: number): any {
// Remove low-signal fields
const { id, timestamp, metadata, ...essential } = data;
// Truncate arrays
if (Array.isArray(essential.items)) {
essential.items = essential.items.slice(0, 10);
essential.truncated = true;
}
return essential;
}Server Response Design:
typescript
// ❌ BAD: Verbose response
{
"temperature": 72.5,
"temperature_unit": "fahrenheit",
"humidity": 65,
"humidity_unit": "percentage",
"wind_speed": 5,
"wind_speed_unit": "mph",
"wind_direction": "N",
"pressure": 1013,
"pressure_unit": "mb",
// ... 20 more fields
}
// ✅ GOOD: Concise response
{
"temp": "72°F",
"conditions": "cloudy",
"wind": "5mph N"
}问题:工具输入输出中的每个令牌都会占用LLM的上下文窗口。
解决方案模式:
typescript
interface ToolResponse {
format: 'concise' | 'detailed'; // Let LLM choose
}
async executeTool(name: string, args: { format?: string }) {
const result = await this.server.callTool(name, args);
// Default to concise
if (args.format !== 'detailed') {
return this.truncateResponse(result, MAX_TOKENS);
}
return result;
}
private truncateResponse(data: any, maxTokens: number): any {
// Remove low-signal fields
const { id, timestamp, metadata, ...essential } = data;
// Truncate arrays
if (Array.isArray(essential.items)) {
essential.items = essential.items.slice(0, 10);
essential.truncated = true;
}
return essential;
}服务器响应设计:
typescript
// ❌ BAD: Verbose response
{
"temperature": 72.5,
"temperature_unit": "fahrenheit",
"humidity": 65,
"humidity_unit": "percentage",
"wind_speed": 5,
"wind_speed_unit": "mph",
"wind_direction": "N",
"pressure": 1013,
"pressure_unit": "mb",
// ... 20 more fields
}
// ✅ GOOD: Concise response
{
"temp": "72°F",
"conditions": "cloudy",
"wind": "5mph N"
}4.2 Connection Pooling
4.2 连接池
For Database-Backed Servers:
typescript
class ConnectionPool {
private pool: Connection[] = [];
private maxSize = 10;
async acquire(): Promise<Connection> {
if (this.pool.length > 0) {
return this.pool.pop()!;
}
if (this.activeConnections < this.maxSize) {
return await this.createConnection();
}
// Wait for available connection
return await this.waitForConnection();
}
release(conn: Connection) {
this.pool.push(conn);
}
}针对数据库后端服务器:
typescript
class ConnectionPool {
private pool: Connection[] = [];
private maxSize = 10;
async acquire(): Promise<Connection> {
if (this.pool.length > 0) {
return this.pool.pop()!;
}
if (this.activeConnections < this.maxSize) {
return await this.createConnection();
}
// Wait for available connection
return await this.waitForConnection();
}
release(conn: Connection) {
this.pool.push(conn);
}
}4.3 Request Queuing
4.3 请求队列
Handle Burst Traffic:
typescript
class RequestQueue {
private queue: PendingRequest[] = [];
private processing = 0;
private maxConcurrent = 5;
async enqueue(request: Request): Promise<Response> {
return new Promise((resolve, reject) => {
this.queue.push({ request, resolve, reject });
this.processQueue();
});
}
private async processQueue() {
while (this.queue.length > 0 && this.processing < this.maxConcurrent) {
const { request, resolve, reject } = this.queue.shift()!;
this.processing++;
try {
const result = await this.executeRequest(request);
resolve(result);
} catch (error) {
reject(error);
} finally {
this.processing--;
this.processQueue();
}
}
}
}处理突发流量:
typescript
class RequestQueue {
private queue: PendingRequest[] = [];
private processing = 0;
private maxConcurrent = 5;
async enqueue(request: Request): Promise<Response> {
return new Promise((resolve, reject) => {
this.queue.push({ request, resolve, reject });
this.processQueue();
});
}
private async processQueue() {
while (this.queue.length > 0 && this.processing < this.maxConcurrent) {
const { request, resolve, reject } = this.queue.shift()!;
this.processing++;
try {
const result = await this.executeRequest(request);
resolve(result);
} catch (error) {
reject(error);
} finally {
this.processing--;
this.processQueue();
}
}
}
}Phase 5: Testing & Validation
阶段5:测试与验证
5.1 Use MCP Inspector
5.1 使用MCP Inspector
bash
npx @modelcontextprotocol/inspector <server-command>bash
npx @modelcontextprotocol/inspector <server-command>Proxy: http://localhost:3000
Proxy: http://localhost:3000
**Validation Checklist**:
- [ ] Connection establishes successfully
- [ ] Capabilities negotiated correctly
- [ ] Tools discovered and listed
- [ ] Tool calls execute and return results
- [ ] Errors display meaningful messages
- [ ] Session IDs propagate (HTTP transport)
- [ ] OAuth flow completes (if applicable)
**验证清单**:
- [ ] 连接成功建立
- [ ] 能力协商正确
- [ ] 工具被发现并列出
- [ ] 工具调用执行并返回结果
- [ ] 错误显示有意义的信息
- [ ] 会话ID正确传播(HTTP传输)
- [ ] OAuth流程完成(如适用)5.2 Automated Testing
5.2 自动化测试
Technical Tests (fast, comprehensive):
typescript
describe('Client', () => {
it('negotiates capabilities', async () => {
const client = new Client({ capabilities: { sampling: {} } });
await client.connect(mockTransport);
expect(client.serverCapabilities).toBeDefined();
});
it('handles tool calls', async () => {
const result = await client.callTool('test_tool', { arg: 'value' });
expect(result.content).toBeDefined();
});
it('retries on transient errors', async () => {
mockTransport.failTimes(2); // Fail twice, then succeed
const result = await client.callTool('flaky_tool', {});
expect(result).toBeDefined();
});
});Behavioral Tests (with real LLM):
typescript
describe('Client with LLM', () => {
it('LLM can discover and use tools', async () => {
const query = "What's the weather in Tokyo?";
const response = await client.processQuery(query);
expect(response).toContain('Tokyo');
expect(response).toMatch(/\d+°[FC]/); // Contains temperature
});
});技术测试(快速、全面):
typescript
describe('Client', () => {
it('negotiates capabilities', async () => {
const client = new Client({ capabilities: { sampling: {} } });
await client.connect(mockTransport);
expect(client.serverCapabilities).toBeDefined();
});
it('handles tool calls', async () => {
const result = await client.callTool('test_tool', { arg: 'value' });
expect(result.content).toBeDefined();
});
it('retries on transient errors', async () => {
mockTransport.failTimes(2); // Fail twice, then succeed
const result = await client.callTool('flaky_tool', {});
expect(result).toBeDefined();
});
});行为测试(使用真实LLM):
typescript
describe('Client with LLM', () => {
it('LLM can discover and use tools', async () => {
const query = "What's the weather in Tokyo?";
const response = await client.processQuery(query);
expect(response).toContain('Tokyo');
expect(response).toMatch(/\d+°[FC]/); // Contains temperature
});
});Reference Architecture
参考架构
Recommended Client Structure
推荐客户端结构
typescript
class MCPClient {
private transport: Transport;
private session: SessionManager;
private auth: AuthProvider;
private authz: AuthorizationManager;
private errorHandler: ErrorHandler;
private requestQueue: RequestQueue;
// Core protocol methods
async connect(transport: Transport): Promise<void> { /* ... */ }
async listTools(): Promise<Tool[]> { /* ... */ }
async callTool(name: string, args: any): Promise<ToolResult> { /* ... */ }
async listResources(): Promise<Resource[]> { /* ... */ }
async readResource(uri: string): Promise<ResourceContent> { /* ... */ }
async listPrompts(): Promise<Prompt[]> { /* ... */ }
async getPrompt(name: string, args: any): Promise<PromptContent> { /* ... */ }
// Client capability implementations
async handleSamplingRequest(request: SamplingRequest): Promise<SamplingResult> { /* ... */ }
async handleElicitationRequest(request: ElicitationRequest): Promise<ElicitationResult> { /* ... */ }
// Lifecycle
async close(): Promise<void> { /* ... */ }
}typescript
class MCPClient {
private transport: Transport;
private session: SessionManager;
private auth: AuthProvider;
private authz: AuthorizationManager;
private errorHandler: ErrorHandler;
private requestQueue: RequestQueue;
// Core protocol methods
async connect(transport: Transport): Promise<void> { /* ... */ }
async listTools(): Promise<Tool[]> { /* ... */ }
async callTool(name: string, args: any): Promise<ToolResult> { /* ... */ }
async listResources(): Promise<Resource[]> { /* ... */ }
async readResource(uri: string): Promise<ResourceContent> { /* ... */ }
async listPrompts(): Promise<Prompt[]> { /* ... */ }
async getPrompt(name: string, args: any): Promise<PromptContent> { /* ... */ }
// Client capability implementations
async handleSamplingRequest(request: SamplingRequest): Promise<SamplingResult> { /* ... */ }
async handleElicitationRequest(request: ElicitationRequest): Promise<ElicitationResult> { /* ... */ }
// Lifecycle
async close(): Promise<void> { /* ... */ }
}Common Patterns
常见模式
Pattern: Bidirectional Communication
模式:双向通信
typescript
class Client {
// Client → Server requests
async request(method: string, params: any): Promise<any> {
return this.sendRequest({ method, params });
}
// Server → Client requests (handlers)
private handlers = new Map<string, RequestHandler>();
registerHandler(method: string, handler: RequestHandler) {
this.handlers.set(method, handler);
}
// Message router
private async handleIncomingMessage(message: JSONRPCMessage) {
if ('method' in message && 'id' in message) {
// This is a request FROM server TO client
const handler = this.handlers.get(message.method);
if (handler) {
const result = await handler(message.params);
await this.sendResponse(message.id, result);
}
}
}
}
// Usage:
client.registerHandler('sampling/createMessage', async (params) => {
// Server is asking client to get LLM completion
return await this.llm.complete(params.messages);
});typescript
class Client {
// Client → Server requests
async request(method: string, params: any): Promise<any> {
return this.sendRequest({ method, params });
}
// Server → Client requests (handlers)
private handlers = new Map<string, RequestHandler>();
registerHandler(method: string, handler: RequestHandler) {
this.handlers.set(method, handler);
}
// Message router
private async handleIncomingMessage(message: JSONRPCMessage) {
if ('method' in message && 'id' in message) {
// This is a request FROM server TO client
const handler = this.handlers.get(message.method);
if (handler) {
const result = await handler(message.params);
await this.sendResponse(message.id, result);
}
}
}
}
// Usage:
client.registerHandler('sampling/createMessage', async (params) => {
// Server is asking client to get LLM completion
return await this.llm.complete(params.messages);
});Pattern: Resource vs Tool Usage
模式:资源与工具的使用差异
typescript
// Resources: Data client can READ
const calendarData = await client.readResource('calendar://events/today');
// Returns: { text: "Meeting at 3pm, Lunch at 12pm" }
// Tools: Actions client can EXECUTE
const result = await client.callTool('create_event', {
title: "Team Meeting",
time: "2024-01-15T15:00:00Z"
});
// Returns: { content: [{ type: "text", text: "Event created" }] }typescript
// Resources: Data client can READ
const calendarData = await client.readResource('calendar://events/today');
// Returns: { text: "Meeting at 3pm, Lunch at 12pm" }
// Tools: Actions client can EXECUTE
const result = await client.callTool('create_event', {
title: "Team Meeting",
time: "2024-01-15T15:00:00Z"
});
// Returns: { content: [{ type: "text", text: "Event created" }] }Pattern: Prompts as Templates
模式:模板化提示词
typescript
// Get prompt template from server
const prompt = await client.getPrompt('write_email', {
recipient: "boss@company.com",
topic: "project update"
});
// prompt.messages contains pre-filled conversation
// Send to LLM with additional context
const completion = await llm.complete([
...prompt.messages,
{ role: "user", content: "Include metrics from Q4" }
]);typescript
// Get prompt template from server
const prompt = await client.getPrompt('write_email', {
recipient: "boss@company.com",
topic: "project update"
});
// prompt.messages contains pre-filled conversation
// Send to LLM with additional context
const completion = await llm.complete([
...prompt.messages,
{ role: "user", content: "Include metrics from Q4" }
]);Anti-Patterns to Avoid
需避免的反模式
❌ Client Choosing Tools
❌ 客户端选择工具
typescript
// WRONG: Client decides what to do
if (query.includes('weather')) {
return await client.callTool('check_weather', { city: extractCity(query) });
}typescript
// WRONG: Client decides what to do
if (query.includes('weather')) {
return await client.callTool('check_weather', { city: extractCity(query) });
}❌ Forgetting Session IDs
❌ 忘记会话ID
typescript
// WRONG: Not propagating session
fetch(url, {
headers: { 'Content-Type': 'application/json' } // Missing Mcp-Session-Id
});typescript
// WRONG: Not propagating session
fetch(url, {
headers: { 'Content-Type': 'application/json' } // Missing Mcp-Session-Id
});❌ No Retry Logic
❌ 无重试逻辑
typescript
// WRONG: Fail on first error
const result = await client.callTool('flaky_api', {}); // Will fail randomlytypescript
// WRONG: Fail on first error
const result = await client.callTool('flaky_api', {}); // Will fail randomly❌ Verbose Responses
❌ 冗长响应
typescript
// WRONG: Returning all data
return await database.query('SELECT * FROM users'); // 10,000 rows
// CORRECT: Return summary
return { count: 10000, sample: users.slice(0, 10) };typescript
// WRONG: Returning all data
return await database.query('SELECT * FROM users'); // 10,000 rows
// CORRECT: Return summary
return { count: 10000, sample: users.slice(0, 10) };Quick Start Templates
快速开始模板
See reference files:
- TypeScript: typescript_mcp_client.md
- Python: python_mcp_client.md
- Architecture: client_architecture.md
- Best Practices: mcp_client_best_practices.md
参考以下文件:
- TypeScript: typescript_mcp_client.md
- Python: python_mcp_client.md
- Architecture: client_architecture.md
- Best Practices: mcp_client_best_practices.md
Success Criteria
成功标准
Client is production-ready when:
- Connects to servers via all required transports
- Negotiates capabilities correctly
- Discovers and executes tools, resources, prompts
- Implements retry logic with exponential backoff
- Handles errors gracefully with actionable messages
- Enforces security at network, auth, authz, validation layers
- Optimizes for token efficiency
- Passes both technical and behavioral tests
- Includes comprehensive logging to stderr (never stdout in stdio mode)
- Manages sessions correctly (HTTP transport)
客户端达到生产就绪状态的标志:
- 可通过所有所需传输方式连接到服务器
- 正确协商能力
- 发现并执行工具、资源、提示词
- 实现带指数退避的重试逻辑
- 优雅处理错误并提供可操作的信息
- 在网络、认证、授权、验证层面实施安全措施
- 优化令牌效率
- 通过技术测试和行为测试
- 包含全面的日志输出到stderr(stdio模式下绝不能输出到stdout)
- 正确管理会话(HTTP传输)