掘金 人工智能 07月06日 18:08
让大模型调用MCP服务变得超级简单
index_new5.html
../../../zaker_core/zaker_tpl_static/wap/tpl_guoji1.html

 

本文介绍了一种使用OpenAI SDK整合MCP(Model Context Protocol)的方法,旨在简化Agent开发中调用MCP服务的流程。通过封装,开发者可以更高效地将大型语言模型(LLM)与MCP工具集成,实现定制化功能。文章详细阐述了如何配置MCP服务、初始化连接、整理工具、封装OpenAI调用以及处理工具调用结果,并提供了完整的代码示例和NPM包,方便开发者实践。

💡文章首先阐述了使用大模型和MCP服务进行Agent开发的需求,以及现有MCP Client工具的局限性,引出了通过整合OpenAI SDK来简化MCP调用的方案。

⚙️ 核心在于封装了MCP服务的两种主要调用形式:Command命令模式和SSE远程服务模式。通过定义相应的配置类型,方便用户指定要调用的MCP服务,并提供了初始化连接MCP服务的功能。

🛠️ 接下来,文章介绍了如何整理MCP服务提供的工具,将其转换为OpenAI可调用的工具形式,并封装了OpenAI的调用方法,自动集成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 服务然后调用,那么对应的就是需要命令 以及 命令执行的参数

interface CommandMCPConfig {  type: 'command',  command: string;  args: string[];}

SSE 服务模式

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

interface SSEMCPConfig {  type: 'sse',  url: string;}

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

const MCPConfig = {  '12306-mcp': {    type: 'command',    command: 'npx',    args: ["-y", "12306-mcp"]  }}

初始化连接 MCP 服务

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

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 调用的工具形式

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 调用;

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 工具调用的封装,现在我们就可以在业务中非常方便的调用了:

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

Fish AI Reader

Fish AI Reader

AI辅助创作,多种专业模板,深度分析,高质量内容生成。从观点提取到深度思考,FishAI为您提供全方位的创作支持。新版本引入自定义参数,让您的创作更加个性化和精准。

FishAI

FishAI

鱼阅,AI 时代的下一个智能信息助手,助你摆脱信息焦虑

联系邮箱 441953276@qq.com

相关标签

OpenAI MCP Agent开发 SDK整合
相关文章