開發一個 MCP 協議客戶端

MCP 並不侷限於 Claude 桌面,它可以用任何支持它的其他 LLM 客戶端使用。考慮到這一點,我們決定構建一個 MCP CLI 客戶端來展示這一點。這個 MCP 客戶端可以更快速地測試 MCP 服務器。

模型上下文協議(MCP)在 AI 領域持續獲得關注,自從 Neon MCP 服務器發佈以來(大約兩週前),社區已經在廣泛的領域內構建了數十個這些服務器 [鏈接]。然而,Claude 桌面應用程序已經成爲默認的 MCP 客戶端,大多數服務器都有專門的說明來指導如何與該客戶端集成。

但 MCP 並不侷限於 Claude 桌面,它可以用任何支持它的其他 LLM 客戶端使用。考慮到這一點,我們決定構建一個 MCP CLI 客戶端來展示這一點。這個 MCP 客戶端可以更快速地測試 MCP 服務器。

1、如何構建 MCP 客戶端

所有 MCP 客戶端都基於相同的原理並遵循相同的協議。對於工具使用(我們的用例),需要實現的主要概念如下:

1.1 MCP 服務器連接

第一步是連接到 MCP 服務器,以便它可以發現和使用服務器上的工具。

const mcpClient = new Client(
  { name: 'cli-client', version: '1.0.0' },
  { capabilities: {} },
);

// 此函數將MCP客戶端連接到MCP服務器
await mcpClient.connect(new StdioClientTransport(serverConfig));

1.2 工具列表

我們需要從 MCP 服務器獲取可用工具。這允許 LLM 知道在交互過程中可以使用哪些工具

// 此函數將返回MCP服務器上可用工具的列表
const toolsAvailable = await this.mcpClient.request(
  { method: 'tools/list' },
  ListToolsResultSchema,
);

1.3 工具使用

一旦 LLM 決定使用哪個工具,我們需要調用 MCP 服務器上的工具處理程序。

// 此函數將在MCP服務器上調用工具處理程序
const toolResult = await this.mcpClient.request(
  {
    method: 'tools/call',
    params: {
      name: toolName,
      arguments: toolArgs,
    },
  },
  CallToolResultSchema,
);

1.4 LLM 集成

這是一個多步驟的過程,將 LLM 連接到可用工具:

  1. 向 LLM 發送初始提示

  2. 等待 LLM 響應工具使用

  3. 在 MCP 服務器上調用工具處理程序

  4. 將工具結果注入 LLM 的上下文中

  5. 向 LLM 發送下一個提示

由於我們正在使用來自 Anthropic API 的工具 API,如果僅僅依賴他們的官方 SDK,則會簡單得多。

// 1- 發送初始提示
const response = await this.anthropicClient.messages.create({
  messages: [
    {
      role: 'user',
      content: 'Can you list my Neon projects?',
    },
  ],
  model: 'claude-3-5-sonnet-20241022',
  max_tokens: 8192,
  tools: this.tools,
});

for (const content of response.content) {
 // 2- 等待LLM響應工具使用
if (content.type === 'tool_use') {
    const toolName = content.name;
    const toolArgs = content.input;
    // 3- 在MCP服務器上調用工具處理程序
    const toolResult = await this.mcpClient.request(
      {
        method: 'tools/call',
        params: {
          name: toolName,
          arguments: toolArgs,
        },
      },
      CallToolResultSchema,
    );
    
    // 4- 將工具結果注入LLM的上下文中
    const contextWithToolResult = [
        ...previousMessages,
        { role: 'user', content: toolResult.content },
     ];

  // 5- 向LLM發送下一個提示
    const nextResponse = await this.anthropicClient.messages.create({
      messages: contextWithToolResult,
      model: 'claude-3-5-sonnet-20241022',
      max_tokens: 8192,
    });
  }
}

2、構建 CLI 客戶端

一旦我們有了所有核心組件,我們只需要構建一個酷炫的 CLI 客戶端,可以用來與 MCP 服務器交互。

2.1 LLM 處理

處理 LLM 消息和工具使用。重要的是我們要在每次交互之間保留消息,以便可以將工具結果注入 LLM 的上下文中。

private async processQuery(query: string) {
  try {
    // 1 - 向LLM發送用戶的查詢
    this.messages.push({ role: 'user', content: query });
    const response = await this.anthropicClient.messages.create({
      messages: this.messages,
      model: 'claude-3-5-sonnet-20241022',
      tools: this.tools,
    });

    // 2 - 處理LLM響應
    for (const content of response.content) {
      if (content.type === 'text') {
        process.stdout.write(content.text);
      }

      // 3 - 處理工具使用
      if (content.type === 'tool_use') {
        const toolResult = await this.mcpClient.request({
          method: 'tools/call',
          params: {
            name: content.name,
            arguments: content.input,
          }
        });

        // 4 - 將工具結果添加到對話中
        this.messages.push({
          role: 'user',
          content: JSON.stringify(toolResult)
        });

        // 5 - 獲取LLM對工具結果的響應
        const nextResponse = await this.anthropicClient.messages.create({
          messages: this.messages,
          model: 'claude-3-5-sonnet-20241022'
        });

        // 6 - 顯示LLM的響應
        if (nextResponse.content[0].type === 'text') {
          process.stdout.write(nextResponse.content[0].text);
        }
      }
    }
  } catch (error) {
    console.error('Error during query processing:', error);
  }
}

2.2 聊天循環

創建一個聊天循環,用於向 LLM 發送消息並處理響應。

private async chat_loop() {
    while (true) {
      try {
        const query = (await this.rl.question(styles.prompt)).trim();
        // 處理查詢
        await this.processQuery(query);
      } catch (error) {
        console.error(styles.error('\\\\nError:'), error);
      }
    }
}

2.3 入口點

設置客戶端的主入口點,初始化 MCP 客戶端,獲取工具並啓動聊天循環

  // 這是客戶端的主入口點
  async start() {
    try {
      console.log(styles.info('🤖 Interactive Claude CLI'));
      console.log(
        styles.info(`Type your queries or "${EXIT_COMMAND}" to exit`),
      );

      // 1 - 將MCP客戶端連接到MCP服務器
      await this.mcpClient.connect(this.transport);

      // 2 - 獲取MCP服務器上可用的工具
      await this.initMCPTools();

      // 3 - 啓動聊天循環
      await this.chat_loop();
    } catch (error) {
      console.error(styles.error('Failed to initialize tools:'), error);
      process.exit(1);
    } finally {
      this.rl.close();
      process.exit(0);
    }
  }

2.4 運行

現在我們已經構建了一個通用的 MCP 客戶端,可以通過傳遞 MCP 服務器 URL 和其他所需參數來運行它。

const cli = new InteractiveCLI({
  command: '../dist/index.js',
  args: ['start', process.env.NEON_API_KEY!],
});
cli.start();

2.5 改進

這個簡單實現有兩個主要缺點:

幸運的是,這兩個問題已經在我們在 Neon 構建的 MCP 客戶端 CLI 中得到了解決。

參考:https://neon.tech/blog/building-a-cli-client-for-model-context-protocol-servers

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/MyJx-CaGRz9H6N-Eb4oSEQ