背景
随着 MCP
协议越来越火热,使用大模型 + MCP
服务完成更定制化的能力变成现在Agent开发的重要一环。
虽然目前很多 MCP Client
工具都能很方便的帮我们实现 MCP
服务的调用,如 Cursor
, Claude
, Trae
等等,但是当我们需要在自己的Agent服务中进行 MCP
的调用,即使使用 MCP SDK
也会写比较多的代码逻辑。
本文我们将 openai sdk
与 MCP
进行整合,封装一个极简的调用模块,提升整合 LLM
和 MCP
的效率;
如何实现?
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