背景
随着 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
服务然后调用,那么对应的就是需要命令
以及 命令执行的参数
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