详情

首页手游攻略 从零搭建MCP Server:让LLM 拥有工具调用能力

从零搭建MCP Server:让LLM 拥有工具调用能力

佚名 2026-07-14 08:26:51

一、前言

我们在与LLM对话时,问出一个实时性比较强的问题,如今天天气怎么样?这时候,LLM其实是不知道今天天气怎么样的,因为LLM只能根据它被训练过的数据进行回答,此时,LLM就会出现幻觉,开始胡言乱语。
那要怎样让LLM给出正确回答呢?这时候就需要我们人为准备好Tools工具函数来让LLM唤起,也就是FunctionCalling Tools,但是让LLM唤起工具这一行为缺乏一套让LLM在工具池中高效、准确的找到正确的工具并使用的交互规则,这会导致:

  1. 决策瘫痪:有些工具函数功能相似,容易选错。
  2. 感知过载:工具太多,选不过来,因为要传递工具函数的详情,所以这些描述都会被塞进上下文,占用过多上下文空间,挤占了模型用来推理和思考的空间。

二、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 的工具调用项目。

相关资讯
点击查看更多
游戏推荐
推荐专题
热门阅读
推荐下载