构建 Node.js MCP 客户端
系统要求
开始之前,确保你的系统满足以下要求:
- Mac 或 Windows 计算机
- Node.js 16 或更高版本
- npm(Node.js 自带)
环境配置
首先,创建一个新的 Node.js 项目:
# 创建 项目目录
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/**/*"]
}
API 密钥配置
从 Anthropic 控制台 获取 Anthropic API 密钥。
创建 .env
文件:
ANTHROPIC_API_KEY=your_key_here
将 .env
添加到 .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 {
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();
}
// 后续方法将在此处添加
}
服务器连接管理
接下来,实现连接 MCP 服务器的方法:
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 = 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,
arguments: 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,
});
}
}
// 如果没有更多工具调用,返回最终文本
if (!currentResponse.content.some(content => content.type === "tool_use")) {
return finalText.join("\n");
}
}
}