从零搭建MCP Server:让LLM 拥有工具调用能力
一、前言
我们在与LLM对话时,问出一个实时性比较强的问题,如今天天气怎么样?这时候,LLM其实是不知道今天天气怎么样的,因为LLM只能根据它被训练过的数据进行回答,此时,LLM就会出现幻觉,开始胡言乱语。
那要怎样让LLM给出正确回答呢?这时候就需要我们人为准备好Tools工具函数来让LLM唤起,也就是FunctionCalling Tools,但是让LLM唤起工具这一行为缺乏一套让LLM在工具池中高效、准确的找到正确的工具并使用的交互规则,这会导致:
- 决策瘫痪:有些工具函数功能相似,容易选错。
- 感知过载:工具太多,选不过来,因为要传递工具函数的详情,所以这些描述都会被塞进上下文,占用过多上下文空间,挤占了模型用来推理和思考的空间。
二、MCP核心概念
MCP全称 Model Context Protocol,模型上下文协议。
- MCP主要是用来制定一套标准,统一LLM与外部工具、系统和数据源的交互方式。
- 工具被定义为MCPserver上可执行的功能模块,通过MCP协议暴露给客户端,供LLM调用
架构角色:Client(客户端)<-> MCPServer(服务端)<-> LLM
交互流程图为:
三、环境准备
本文使用 Node.js + ESM模块(“type”:“module”)
安装依赖:pnpm i @modelcontextprotocol/sdk zod axios express
本文使用ollama部署的本地模型
四、手把手实现
4.1 搭建MCP Server (注册工具)
首先先引入需要用到的函数:
复制代码import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
import { z } from 'zod'
import { exec } from 'child_process'
接着创建一个MCPServer实例:
复制代码const server = new McpServer({
name: 'Demo',
version: '1.0.0'
})
名字和版本可以自己随意填写。
然后在Server上注册工具,本文注册一个listFiles工具用来读取指定目录文件列表:
复制代码server.tool('listFiles', '列出执行目录下的文件', {path: z.string()}, ( {path} ) => {
return new Promise((resolve, reject) => {
exec(`dir ${path}`, (error, stdout, stderr) => {
if(error) {
console.error(`执行命令出错:${error}`)
resolve({
content: [{type: 'text', text: `执行命令出错:${error}`}]
})
return
}
if(stderr) {
console.error(`命令stderr:${stderr}`)
}
resolve({
content: [{type: 'text', text: `已获取到目录${path}下的文件列表:n```n${stdout}```n`}]
})
})
})
})
最后用StdioServerTransport连接进程通信:
复制代码const transport = new StdioServerTransport() // transport 用来跟进程交互
await server.connect(transport)
4.2 搭建MCP Client (连接Server)
同样先引入:
复制代码import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
接着封装一个函数并抛出:
复制代码export async function create(){
}
然后在函数中创建Client实例:
复制代码export async function create(){
const client = new Client({
name: 'Demo',
version: '1.0.0'
})
}
name 和 version 要和 创建的Server实例一致。
最后用StdioClientTransport指定启动的Server命令:
复制代码const transport = new StdioClientTransport({ // 定义可以启用MCPServer的指令
command: 'node',
args: ['mcp/server.js']
}) try {
await client.connect(transport)
console.log('mcp客户端连接成功');
} catch (error) {
console.error(`mcp客户端连接失败${error}`);
throw error
}
最终抛出的函数代码为:
复制代码export async function createClient() {
const client = new Client({
name: 'Demo',
version: '1.0.0'
})
const transport = new StdioClientTransport({ // 定义可以启用MCPServer的指令
command: 'node',
args: ['mcp/server.js']
}) try {
await client.connect(transport)
console.log('mcp客户端连接成功');
} catch (error) {
console.error(`mcp客户端连接失败${error}`);
throw error
}
return client
}
4.3 封装一个类来桥接MCP和LLM
先引入:
复制代码import { createClient } from '../mcp/client.js'
import axios from 'axios'
接着抛出一个类:
复制代码export class LLM {
mcpClient = new createClient()
constructor(model, base_url, api_key = null) {
this.model = model
this.base_url = base_url
this.api_key = api_key
}
get headers() {
if(this.api_key) {
return {
'Authorization': `Bearer ${this.api_key}`
}
} else {
return {
'Content-Type': 'application/json'
}
}
}
}
构造器接收三个参数:模型名、通信地址以及api秘钥,api秘钥默认值为null,如果接入本地部署的LLM就不需要传入api。
然后在Class中封装一个listTools方法,用来从MCP Server获取工具列表:
复制代码async listTools() {
return (await (await this.mcpClient).listTools()).tools.map(tool => (
{
type: 'function',
function: {
name: tool.name,
description: tool.description,
parameters: tool.inputSchema
}
}
))
}
本文是转为Ollama的tools格式。
接着封装一个callTool方法,用来调用MCP Server上的工具:
复制代码async callTool(tool_name, tool_args) {
const result = await (await this.mcpClient).callTool({
name: tool_name,
arguments: tool_args
})
return {
role: 'tool',
name: tool_name,
content: result.content[0].text
}
}
最后是核心递归逻辑:
复制代码async chat(messages) {
const tools = await this.listTools() // 工具函数的描述
const response = await axios.post(`${this.base_url}/api/chat`, {
model: this.model,
messages,
tools,
stream: false
}, {
headers: this.headers
}) const data = response.data
const reply = data.message.content // LLM 本身能解决问题,得到的返回的结果
const tool_calls = data.message.tool_calls // LLM 不能解决问题,说需要工具函数
if(tool_calls && tool_calls.length > 0) {
const toolResArr = tool_calls.map((tool_call) => (
// 触发LLM需要的工具
this.callTool(tool_call.function.name, tool_call.function.arguments)
))
const results = await Promise.all(toolResArr)
// 再次跟LLM通信,将工具的结果传给LLM
return await this.chat([...messages, ...results])
}
return reply
}
发送messages+ tools给LLM,如果返回tool_calls -> 执行工具 -> 把结果拼入 messages -> 递归再问,如果没有tool_calls就直接返回LLM回复。
4.4 搭建 Express 服务向LLM通信
首先引入:
复制代码import express from 'express'
import { LLM } from '../lib/llm.js'
接着使用express提供的方法向LLM发送接口请求,向LLM要发送post请求,并监听运行端口:
复制代码const app = express()const llm = new LLM('qwen3.5:9b', 'http://localhost:11434')app.use(express.json())app.post('/chat', async (req, res) => {
const { message } = req.body const messages = [
{
role: 'system',
content: '你是一个香香软软白毛红瞳一米五萝莉,如果有需要你可以借助tools函数来完成任务'
},
{
role: 'user',
content: message
}
] const reply = await llm.chat(messages)
res.json({reply})
})app.listen(3000, () => {
console.log('Server is running on port 3000')
})
此时我们就可以跟LLM通信,我们让LLM调用我们在Server注册的工具来帮我们列出目录下的文件列表。
五、总结
本文从 Function Calling 的痛点出发,介绍了 MCP(Model Context Protocol)协议的核心概念,并完整实现了一个基于本地 Ollama + MCP 的工具调用项目。
-
07.14
火影忍者忍者新世代 魔像降临玩法攻略
-
07.14
无期迷途怦怦电波放送中活动详解 无期迷途心动互动玩法与奖励指南
-
07.14
永远的蔚蓝星球全新英雄皮肤效果介绍-火焰法师与闪电之子介绍
-
07.14
永远的蔚蓝星球新坐骑瞌睡小飞象介绍-技能效果详解
-
07.14
永远的蔚蓝星球戒指图腾选择推荐指南
-
07.14
永远的蔚蓝星球111-115关如何通关
-
-
下载
- |
-
-
下载
- 《行尸走肉第一章》免安装中文汉化硬盘版下载
- 单机|436 MB
- 一款以动作冒险为主题的游戏
-
-
下载
- 《街头霸王X铁拳》免安装中文汉化硬盘版下载
- 单机|111MB
- 一款非常好玩的格斗游戏
-
-
下载
- |
-
-
下载
- 《暗黑破坏神3》免安装繁体中文正式版下载
- 单机|7630 MB
- 一款以角色扮演为主题的游戏
-
-
下载
- 《马克思佩恩3》免安装硬盘版下载
- 单机|27033 MB
- 一款以第三人称射击为主题的游戏