構建 MCP 客戶端-Node.js

構建 MCP 客戶端-Node.js

系統要求

在開始之前,請確保你的系統滿足以下要求:

  • 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 Console 獲取 Anthropic API 密鑰。

創建 .env 文件:

ANTHROPIC_API_KEY=你的密鑰

.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 { 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();
  }

  // 方法將在這裡實現
}

服務器連接管理

接下來,實現連接到 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: 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 <服務器腳本路徑>");
    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;

運行客戶端

要使用任何 MCP 服務器運行你的客戶端:

# 構建 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 服務器

客戶端將:

  1. 連接到指定的服務器
  2. 列出可用工具
  3. 啟動交互式聊天會話,你可以:
    • 輸入查詢
    • 查看工具執行情況
    • 獲取來自 Claude 的響應

關鍵組件說明

1. 客戶端初始化

  • MCPClient 類初始化會話管理和 API 客戶端
  • 設置具有基本功能的 MCP 客戶端
  • 配置用於 Claude 交互的 Anthropic 客戶端

2. 服務器連接

  • 支持 Python 和 Node.js 服務器
  • 驗證服務器腳本類型
  • 設置適當的通信通道
  • 連接時列出可用工具

3. 查詢處理

  • 維護對話上下文
  • 處理 Claude 的響應和工具調用
  • 管理 Claude 和工具之間的消息流
  • 將結果組合成連貫的響應

4. 交互界面

  • 提供簡單的命令行界面
  • 處理用戶輸入並顯示響應
  • 包含基本錯誤處理
  • 允許優雅退出

5. 資源管理

  • 正確清理資源
  • 連接問題的錯誤處理
  • 優雅的關閉程序

常見自定義點

  1. 工具處理

    • 修改 processQuery() 以處理特定工具類型
    • 為工具調用添加自定義錯誤處理
    • 實現工具特定的響應格式化
  2. 響應處理

    • 自定義工具結果的格式化方式
    • 添加響應過濾或轉換
    • 實現自定義日誌記錄
  3. 用戶界面

    • 添加 GUI 或 Web 界面
    • 實現豐富的控制檯輸出
    • 添加命令歷史或自動完成

最佳實踐

  1. 錯誤處理

    • 始終將工具調用包裝在 try-catch 塊中
    • 提供有意義的錯誤消息
    • 優雅地處理連接問題
  2. 資源管理

    • 使用適當的清理方法
    • 完成後關閉連接
    • 處理服務器斷開連接
  3. 安全性

    • .env 中安全存儲 API 密鑰
    • 驗證服務器響應
    • 謹慎處理工具權限

故障排除

服務器路徑問題

  • 仔細檢查服務器腳本的路徑
  • 如果相對路徑不起作用,請使用絕對路徑
  • 對於 Windows 用戶,使用正斜槓(/)或轉義的反斜槓(\)
  • 驗證服務器文件具有正確的擴展名(.py 或 .js)

正確路徑使用示例:

# 相對路徑
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

連接問題

  • 驗證服務器腳本存在並具有正確的權限
  • 檢查服務器腳本是否可執行
  • 確保已安裝服務器腳本的依賴項
  • 嘗試直接運行服務器腳本以檢查錯誤

工具執行問題

  • 檢查服務器日誌中的錯誤消息
  • 驗證工具輸入參數是否匹配模式
  • 確保工具依賴項可用
  • 添加調試日誌以跟蹤執行流程