# 创建项目目录 mkdir mcp-client cd mcp-client # 初始化 npm 项目 npm init -y # 安装依赖项 npm install @modelcontextprotocol/sdk @anthropic-ai/sdk dotenv npm install -D typescript @types/node # 创建 TypeScript 配置 npx tsc --init # 创建必要的文件 mkdir src touch src/client.ts touch .env
package.json
{ "type": "module", "scripts": { "build": "tsc", "start": "node build/client.js" } }
tsconfig.json
{ "compilerOptions": { "target": "ES2022", "module": "Node16", "moduleResolution": "Node16", "outDir": "./build", "rootDir": "./src", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true }, "include": ["src/**/*"] }
.env
ANTHROPIC_API_KEY=your_key_here
.gitignore
echo ".env" >> .gitignore
src/client.ts
import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; import Anthropic from "@anthropic-ai/sdk"; import dotenv from "dotenv"; import { Tool } from "@anthropic-ai/sdk/resources/messages.js"; import { CallToolResultSchema, ListToolsResultSchema, } from "@modelcontextprotocol/sdk/types.js"; import * as readline from "node:readline"; dotenv.config(); interface MCPClientConfig { name?: string; version?: string; } class MCPClient { private client: Client | null = null; private anthropic: Anthropic; private transport: StdioClientTransport | null = null; constructor(config: MCPClientConfig = {}) { this.anthropic = new Anthropic(); } // 方法将在这里添加 }
async connectToServer(serverScriptPath: string): Promise<void> { const isPython = serverScriptPath.endsWith(".py"); const isJs = serverScriptPath.endsWith(".js"); if (!isPython && !isJs) { throw new Error("服务器脚本必须是 .py 或 .js 文件"); } const command = isPython ? "python" : "node"; this.transport = new StdioClientTransport({ command, args: [serverScriptPath], }); this.client = new Client( { name: "mcp-client", version: "1.0.0", }, { capabilities: {}, } ); await this.client.connect(this.transport); // 列出可用工具 const response = await this.client.request( { method: "tools/list" }, ListToolsResultSchema ); console.log( "\n已连接到具有工具的服务器:", response.tools.map((tool: any) => tool.name) ); }
async processQuery(query: string): Promise<string> { if (!this.client) { throw new Error("客户端未连接"); } // 使用用户查询初始化消息数组 let messages: Anthropic.MessageParam[] = [ { role: "user", content: query, }, ]; // 获取可用工具 const toolsResponse = await this.client.request( { method: "tools/list" }, ListToolsResultSchema ); const availableTools: Tool[] = toolsResponse.tools.map((tool: any) => ({ name: tool.name, description: tool.description, input_schema: tool.inputSchema, })); const finalText: string[] = []; let currentResponse = await this.anthropic.messages.create({ model: "claude-3-5-sonnet-20241022", max_tokens: 1000, messages, tools: availableTools, }); // 处理响应和任何工具调用 while (true) { // 将 Claude 的响应添加到最终文本和消息中 for (const content of currentResponse.content) { if (content.type === "text") { finalText.push(content.text); } else if (content.type === "tool_use") { const toolName = content.name; const toolArgs = content.input; // 执行工具调用 const result = await this.client.request( { method: "tools/call", params: { name: toolName, args: toolArgs, }, }, CallToolResultSchema ); finalText.push( `[调用工具 ${toolName},参数 ${JSON.stringify(toolArgs)}]` ); // 将 Claude 的响应(包括工具使用)添加到消息中 messages.push({ role: "assistant", content: currentResponse.content, }); // 将工具结果添加到消息中 messages.push({ role: "user", content: [ { type: "tool_result", tool_use_id: content.id, content: [ { type: "text", text: JSON.stringify(result.content) }, ], }, ], }); // 获取 Claude 的下一个响应,包括工具结果 currentResponse = await this.anthropic.messages.create({ model: "claude-3-5-sonnet-20241022", max_tokens: 1000, messages, tools: availableTools, }); // 将 Claude 对工具结果的解释添加到最终文本中 if (currentResponse.content[0]?.type === "text") { finalText.push(currentResponse.content[0].text); } // 继续循环以处理任何其他工具调用 continue; } } // 如果我们到达这里,响应中没有工具调用 break; } return finalText.join("\n"); }
async chatLoop(): Promise<void> { console.log("\nMCP 客户端已启动!"); console.log("输入您的查询或 'quit' 退出。"); // 使用 Node 的 readline 进行控制台输入 const rl = readline.createInterface({ input: process.stdin, output: process.stdout, }); const askQuestion = () => { rl.question("\n查询: ", async (query: string) => { try { if (query.toLowerCase() === "quit") { await this.cleanup(); rl.close(); return; } const response = await this.processQuery(query); console.log("\n" + response); askQuestion(); } catch (error) { console.error("\n错误:", error); askQuestion(); } }); }; askQuestion(); } async cleanup(): Promise<void> { if (this.transport) { await this.transport.close(); } }
// 主执行 async function main() { if (process.argv.length < 3) { console.log("用法: ts-node client.ts <path_to_server_script>"); process.exit(1); } const client = new MCPClient(); try { await client.connectToServer(process.argv[2]); await client.chatLoop(); } catch (error) { console.error("错误:", error); await client.cleanup(); process.exit(1); } } // 如果这是主模块,则运行 main if (import.meta.url === new URL(process.argv[1], "file:").href) { main(); } export default MCPClient;
# 构建 TypeScript 代码。每次更新 `client.ts` 时都要重新运行此命令! npm run build # 运行客户端 node build/client.js path/to/server.py # 对于 Python 服务器 node build/client.js path/to/server.js # 对于 Node.js 服务器
MCPClient
processQuery()
# 相对路径 node build/client.js ./server/weather.js # 绝对路径 node build/client.js /Users/username/projects/mcp-server/weather.js # Windows 路径(任一格式均可) node build/client.js C:/projects/mcp-server/weather.js node build/client.js C:\\projects\\mcp-server\\weather.js