让大模型调用MCP服务变得超级简单

背景

随着 MCP 协议越来越火热,使用大模型 + MCP 服务完成更定制化的能力变成现在Agent开发的重要一环。

虽然目前很多 MCP Client 工具都能很方便的帮我们实现 MCP 服务的调用,如 Cursor, Claude, Trae 等等,但是当我们需要在自己的Agent服务中进行 MCP 的调用,即使使用 MCP SDK 也会写比较多的代码逻辑。

本文我们将 openai sdkMCP 进行整合,封装一个极简的调用模块,提升整合 LLMMCP 的效率;

如何实现?

MCP 服务的本质是给大模型提供一批解决问题的工具,目前我们通过 openai 的接口可以很容易的将一些工具函数传递给大模型,由大模型在需要时进行调用。

那么这里问题就变成了,我们把 MCP 服务提供的工具列举出来,在调用 openai sdk 进行大模型访问时,将MCP 工具传递给大模型,并在大模型返回 tool_calls 时,处理 MCP 工具的调用。

在使用时,我们只需要按照现在常见的MCP客户端配置MCP服务的形式,将要用到的 MCP 服务传入即可;

模块实现

首先我们先定义好 MCP 配置的类型声明,用户如何指定要调用哪些 MCP 服务?

目前 MCP 服务主要有两种调用形式,我把它分别叫做 Command 命令模式SSE 远程服务模式,那么我们实现也主要针对这两种模式的 MCP 服务进行封装.

Command 命令模式

这个模式本质就是在本地通过命令的模式启动 MCP 服务然后调用,那么对应的就是需要命令 以及 命令执行的参数

ts 复制代码
interface CommandMCPConfig {
  type: 'command',
  command: string;
  args: string[];
}

SSE 服务模式

这个模式实际是通过 HTTP 服务调用的形式来实现 MCP 服务提供的,对应的也就主要需要服务的URL链接即可:

ts 复制代码
interface SSEMCPConfig {
  type: 'sse',
  url: string;
}

我们以配置 12306-mcp 为例,编写一个大模型调用MCP的配置:

ts 复制代码
const MCPConfig = {
  '12306-mcp': {
    type: 'command',
    command: 'npx',
    args: ["-y", "12306-mcp"]
  }
}

初始化连接 MCP 服务

连接 MCP 服务我们只需要使用 MCP SDK 创建一个个 Client并根据对应的服务类型创建对应的 Transport 并连接即可;

ts 复制代码
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';

class LLMWithMCP {
  private mcpConfig: MCPConfig;
  private sessions: Map<string, Client> = new Map();
  private transports: Map<string, StdioClientTransport | SSEClientTransport> = new Map();
  
  // 初始化MCP连接
  async initMCPConnect() {
    if (!this.mcpConfig) {
      logger.warn('MCP config is not provided');
      return;
    }
    const promises = [];
    for (const serverName in this.mcpConfig) {
      promises.push(this.connectToMcp(serverName, this.mcpConfig[serverName]));
    }
    return Promise.all(promises);
  }
  
  private async connectToMcp(serverName: string, server: MCPServer) {
    let transport: StdioClientTransport | SSEClientTransport;
    switch (server.type) {
      case 'command':
        transport = this.createCommandTransport(server);
        break;
      case 'sse':
        transport = this.createSSETransport(server);
        break;
      default:
        logger.warn(`Unknown server type: ${(server as any).type}`);
    }
    const client = new Client(
      LLMWithMCPLib,
      {
        capabilities: {
          prompts: {},
          resources: {},
          tools: {}
        }
      }
    );
    
    await client.connect(transport);
    this.sessions.set(serverName, client);
    this.transports.set(serverName, transport);
    logger.debug(`Connected to MCP server ${serverName}`);
  }

  private createCommandTransport(server: Extract<MCPServer, { type: 'command' }>) {
    const { command, args, opts = {} } = server;
    
    if (!command) {
      throw new Error('Invalid command');
    }

    return new StdioClientTransport({
      command,
      args,
      env: Object.fromEntries(
        Object.entries(process.env).filter(([, v]) => !!v)
      ),
      ...opts
    });
  }

  private createSSETransport(server: Extract<MCPServer, { type: 'sse' }>) {
    const { url, opts } = server;
    return new SSEClientTransport(new URL(url), opts)
  }
}

整理 MCP 服务的工具

MCP Client 提供了 listTools 方法可以帮助我们列举出每个 MCP 服务提供的 tools; 我们只需要将这些 tools 处理成 openai 调用的工具形式

ts 复制代码
class LLMWithMCP {
  private async listMCPTools() {
    const availableTools: any[] = [];
    for (const [serverName, session] of this.sessions) {
      const response = await session.listTools();
      if (this.opts.debug) {
        logger.debug(`List tools from MCP server ${serverName}: ${response.tools.map((tool: Tool) => tool.name).join(', ')}`);
      }
      // 构造成 openai 工具
      const tools = response.tools.map((tool: Tool) => ({
        type: 'function' as const,
        function: {
          name: `${LLMWithMCPLib.symbol}__${serverName}__${tool.name}`,
          description: `[${serverName}] ${tool.description}`,
          parameters: tool.inputSchema
        }
      }));
      availableTools.push(...tools);
    }
    return availableTools;
  }
}

封装 openai 方法触发大模型调用

通过 openai sdk 我们进一步封装大模型调用能力,自动集成 MCP 和管理 MCP Tool 调用;

ts 复制代码
type QueryOptions = {
  callTools?: (name: string, args: Record<string, any>) => Promise<CallToolResult>;
} & ChatCompletionCreateParamsNonStreaming;

class LLMWithMCP {
  private openai: OpenAI;
  
  async query(opts: QueryOptions) {    
    const availableTools = await this.listMCPTools();

    // 注入 MCP tools
    if (availableTools.length) {
      if (!opts.tools) {
        opts.tools = [];
      }
      opts.tools.push(...availableTools);
      opts.tool_choice = 'auto';
    }

    let finalText: string[] = [];
    await this.queryWithAI(opts, (message) => finalText.push(message));
    return finalText.join('\n');
  }
  
  private async queryWithAI(opts: QueryOptions, callback: (message: string) => void) {
    const completion = await this.openai.chat.completions.create(opts);
    
    for (const choice of completion.choices) {
      const message = choice.message;
      if (message.content) {
        callback(message.content);
      }
      if (message.tool_calls) {
        for (const toolCall of message.tool_calls) {
          const { name: toolName, arguments: args } = toolCall.function;
          logger.debug(`Calling tool ${toolName} with args ${args}`);
          const result = await this.callTool(toolCall, opts);

          if (!result) {
            continue;
          }

          logger.debug(`Tool ${toolName} response ${result}`);
          callback(this.formatCallToolContent(toolCall as any, result));
          
          // 将工具调用的结果填充到 messages 列表中,再次发送给大模型进行处理
          opts.messages.push({
            role: 'assistant',
            content: '',
            tool_calls: [toolCall]
          });
          opts.messages.push({
            role: 'tool',
            tool_call_id: toolCall.id,
            content: result
          });
          await this.queryWithAI(opts, callback);
        }
      }
    }
  }
  
  private async callTool(tool: ChatCompletionMessageToolCall, opts: QueryOptions) {
    let toolName = tool.function.name;
    const toolArgs = JSON.parse(tool.function.arguments);

    let result: CallToolResult;
    // 解析工具调用,按照 `MCP` Serverv 名称拼接规则解析出对应的 ServervName 和 toolName
    if (!toolName.startsWith(LLMWithMCPLib.symbol)) {
      // 不满足 MCP 工具形式的话,直接抛出去给程序自己处理
      result = await opts.callTools?.(toolName, toolArgs) as CallToolResult;
    } else {
      const [, serverName, tool] = toolName.split('__');
      toolName = tool;
      const session = this.sessions.get(serverName);
      if (!session) {
        logger.warn(`MCP session ${serverName} is not connected`);
        return;
      }
      
      // 使用 MCP Client 调用服务工具
      result = await session.callTool({
        name: tool,
        arguments: toolArgs
      }) as CallToolResult;
    }

    if (!result) return;

    const content = this.formatToolsContent(result.content);
    if (result.isError) {
      logger.error(`Call tool ${toolName} failed: ${content}`);
    }

    return content;
  }
  
  // 处理MCP工具调用返回的结果
  private formatToolsContent(content: CallToolResult['content']) {
    return content.reduce((text, item) => {
      switch (item.type) {
        case 'text':
          text += item.text;
          break;
        case 'image':
          text += item.data;
          break;
        case 'audio':
          text += item.data;
          break;
      }
      return text;
    }, '');
  }
  
  // 将MCP工具的调用过程和处理结果包装一下返回给,方便展示
  private formatCallToolContent(tool: ToolCall, result: any) {
    const { name: toolName, arguments: toolArgs } = tool.function;
    return `<tool>
      <header>Calling ${toolName} Tool.</header>
      <code class="tool-args">${toolArgs}</code>
      <code class="tool-resp">${JSON.stringify(result, null, 2)}</code>
    </tool>`
  }
}

这样我们就完成了 openai + MCP 工具调用的封装,现在我们就可以在业务中非常方便的调用了:

ts 复制代码
const openai = new OpenAI({
  apiKey: process.env.API_KEY,
  baseURL: process.env.BASE_URL,
});
const llmWithMCP = new LLMWithMCP({
  openai,
  mcpConfig,
});

(async () => {
  try {
    // 初始化MCP服务链接
    await llmWithMCP.initMCPConnect();
    const result = await llmWithMCP.query({
      model: process.env.MODEL as string,
      messages: [
        { role: 'user', content: '广州到上海明天的高铁班次?' },
      ],
    });
    console.log(result);
  } finally {
    // 执行清理
    await llmWithMCP.cleanup();
  }
})();

文章中只实现了同步调用结果返回的方式,stream 流式返回的形式流程是类似的,只是需要对中间流式返回的分片的 tool_calls 进行组装成一个完整的工具调用;

完整的代码已经发布到 Github,大家可以前往查看详情,项目也已发布为了 NPM 包,可以直接安装尝试: LLM-With-MCP

相关推荐
千宇宙航3 分钟前
闲庭信步使用SV搭建图像测试平台:第三十一课——基于神经网络的手写数字识别
图像处理·人工智能·深度学习·神经网络·计算机视觉·fpga开发
onceco31 分钟前
领域LLM九讲——第5讲 为什么选择OpenManus而不是QwenAgent(附LLM免费api邀请码)
人工智能·python·深度学习·语言模型·自然语言处理·自动化
小小小小宇2 小时前
虚拟列表兼容老DOM操作
前端
悦悦子a啊2 小时前
Python之--基本知识
开发语言·前端·python
安全系统学习3 小时前
系统安全之大模型案例分析
前端·安全·web安全·网络安全·xss
jndingxin3 小时前
OpenCV CUDA模块设备层-----高效地计算两个 uint 类型值的带权重平均值
人工智能·opencv·计算机视觉
涛哥码咖3 小时前
chrome安装AXURE插件后无效
前端·chrome·axure
Sweet锦3 小时前
零基础保姆级本地化部署文心大模型4.5开源系列
人工智能·语言模型·文心一言
OEC小胖胖4 小时前
告别 undefined is not a function:TypeScript 前端开发优势与实践指南
前端·javascript·typescript·web
行云&流水4 小时前
Vue3 Lifecycle Hooks
前端·javascript·vue.js