跳到主要内容

构建 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");
    }
  }
}