跑通第一个 MCP Server 后,我终于搞懂它到底解决了什么问题

前阵子写 Agent 工具调用的时候,我一直觉得别扭。写个查询用户的小工具,只能绑在当前 Node 项目里用,换个项目就得拷过去改改;想接个 Python 写的数据分析工具,还得额外包一层 HTTP 接口,处理参数、鉴权、错误返回,麻烦得要死。

那时候我就琢磨,有没有什么标准,能把工具和 Agent 彻底拆开?不管工具用啥语言写、跑在哪,Agent 都能直接调用。直到我照着文档搭完第一个 MCP Server,盯着控制台那两行绿底的日志发呆的时候,才反应过来:哦,原来这玩意儿就是干这个的。

先看跑通的效果:两行日志背后的逻辑

这是我第一次跑通测试时截的图,现在看很简单,但当时看到「正在等待第 2 轮 AI 思考」跳出来的时候,确实有点小激动。

我就输入了一句「查一下用户 002 的信息」,剩下的事全是自动完成的:AI 自己判断要调用查询用户的工具,自动把参数传过去,拿到返回结果之后,还自己整理成了表格再回复给我。

整个过程我没写任何手动调用工具的判断逻辑,也没处理参数解析,全是 MCP 协议和 SDK 在背后干活。

MCP 到底是个啥?我用人话捋一遍

MCP 全称是 Model Context Protocol,直译过来叫模型上下文协议。听着挺高大上,其实说白了就是一套统一的通信规矩。

以前我们给大模型加工具,都是和业务代码写在一起的,工具怎么定义、参数怎么传、结果怎么返,全是自己定的。换个项目、换门语言,就得重写一遍,相当于每个电器都自己做个专属插头,互相之间完全不通用。

MCP 就相当于大模型世界的通用插座标准。它规定了客户端(也就是我们写的 Agent)和服务端(提供工具、资源的一方)之间,该怎么打招呼、怎么传参数、怎么返回结果、怎么报错。只要两边都遵守这个标准,不管你服务端是用 Node、Java 还是 Python 写的,不管跑在本地还是远程服务器,客户端都能直接用。

它和普通的 HTTP 接口调用不是一回事。接口调用是我们写代码去拿数据,而 MCP 是给大模型扩展上下文用的 ------ 要么给它增加能调用的工具(让它能做更多事),要么给它提供额外的资料(让它知道更多事)。本质上都是在给大模型的上下文添东西,这也是「Context Protocol」名字的由来。

MCP 最大的特点就是天生支持跨进程调用。本地跑就用 stdio 标准输入输出流,父子进程之间传数据;远程部署就换成 HTTP 协议,走网络通信。两种方式底层协议是一致的,只是传输通道不一样。

动手写第一个 MCP Server

说再多不如写两行代码。我写了个最小可运行的 demo,就做两件事:提供一个查询用户的工具,再提供一份使用指南文档。整个 demo 分成 Server 端和 Client 端两部分。

先搭 Server 端

Server 端就是提供工具和资源的一方,用官方的 SDK 写起来很简单。

javascript 复制代码
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';

// 整个假数据库,省得连库麻烦,实际用可以换成真实数据库
const database = {
    users: {
        '001': {id: '001', name: 'Alice', email: '111@qq.com', age: 30},
        '002': {id: '002', name: 'Bob', email: '222@qq.com', age: 25},
        '003': {id: '003', name: 'Charlie', email: '333@qq.com', age: 28},
    }
}

// 初始化服务,起个名字,标个版本
const server = new McpServer({
    name: 'my-mcp-server',
    version: '1.0.0',
});

// 注册查询用户的工具
server.registerTool('query_user', 
{
    // 描述一定要写清楚,不然AI真的会瞎调用
    description: '查询数据库中的用户信息,输入用户 ID,返回该用户的详细信息 (姓名、邮箱、年龄)',
    inputSchema: {
        userId: z.string().describe('用户 ID,例如 001、002、003 等'),
    }         
}, 
async ({ userId }) => {
    const user = database.users[userId];
    if(!user) {
        return {
            content: [
                { type: 'text', text: `用户 ID ${userId} 不存在,可用的ID: 001、002、003`}
            ]
        }
    }
    return {
        content: [
            { type: 'text', text: `用户 ${user.id} 的姓名是 ${user.name},邮箱是 ${user.email},年龄是 ${user.age}`}
        ]
    }
});

// 注册一份使用指南资源
server.registerResource(
    '使用指南',
    'docs://guide',
    {
        description: 'MCP Server 使用指南',
        mimeType: 'text/plain'
    },
    async () => {
        return {
            contents: [
                {
                    uri: 'docs://guide',
                    mimeType: 'text/plain',
                    text: `
                        MCP Server 使用指南
                        功能:提供用户查询等工具。
                        使用:在 Cursor 等 MCP Client 中通过自然语言对话,会自动调用相应工具。
                    `
                }
            ]
        }
    }
);

// 用 stdio 方式启动服务,也就是通过标准输入输出和客户端通信
const transport = new StdioServerTransport();
await server.connect(transport);

这里我一开始没搞懂 registerResource 是干嘛的,以为和工具差不多。后来跑通了才明白:

  • Tool 是给 AI 「用」的,是能力,AI 可以调用它来做事、拿数据
  • Resource 是给 AI 「读」的,是资料,客户端可以提前把资源内容读出来,当成系统提示词喂给 AI

就像这个使用指南,客户端启动的时候先读一遍,然后塞给 AI 当背景知识,AI 就知道这个 Server 有啥功能、该怎么用了。

Resource 是一种很轻量的上下文补充方式,适合放使用说明、字段定义、固定规则这类内容。和 RAG 不一样,它不需要检索,是 Server 自带的固定内容,直接读取就行。

再写 Client 端

客户端就是我们的 Agent 程序,负责连接 MCP Server,拿到工具和资源,然后交给大模型去调度。我这里用了 LangChain 的适配包,用起来更顺手。

javascript 复制代码
    import 'dotenv/config';
    import { MultiServerMCPClient } from '@langchain/mcp-adapters';
    import { ChatOpenAI } from '@langchain/openai';
    import chalk from 'chalk';
    import {
        HumanMessage,
        SystemMessage,
        ToolMessage
    } from '@langchain/core/messages';

    // 初始化大模型,这里用的DeepSeek,换成别的也一样
    const model = new ChatOpenAI({
        modelName: process.env.DEEPSEEK_API_MODEL,
        apiKey: process.env.DEEPSEEK_API_KEY,
        temperature: 0,
        configuration: {
            baseURL: process.env.DEEPSEEK_API_BASE_URL,
        },
    })

    // 初始化 MCP 客户端,配置要连接的 Server
    // 可以同时配置好多个,统一管理
    const mcpClient  = new MultiServerMCPClient({
        mcpServers: {
            'my-mcp-server': {
                command: 'node',
                args: ['你的server文件路径/my-mcp-server.mjs'],
            }
        }
    });

    // 获取所有工具,还有所有资源内容
    const tools = await mcpClient.getTools();
    const res = await mcpClient.listResources();
    let resourceContent = '';
    for(const [serverName, resources] of Object.entries(res)){
        for(const resource of resources){
            const content = await mcpClient.readResource(serverName, resource.uri);
            resourceContent += content[0].text;    
        }
    }

    // 把工具绑定给模型
    const modelWithTools = model.bindTools(tools);

    // 核心的 Agent 多轮调用逻辑
    async function runAgentWithTools(query, maxIterations = 30) {
        // 把资源内容当成系统消息,先给AI看一遍说明书
        const messages = [
            new SystemMessage(resourceContent),
            new HumanMessage(query)
        ];

        for(let i = 0; i < maxIterations; i++){
            console.log(chalk.bgGreen(`正在等待第${i+1}轮 AI 思考...`));
            const response = await modelWithTools.invoke(messages);
            messages.push(response);

            // 没有工具调用了,说明AI思考完了,直接返回结果
            if(!response.tool_calls || response.tool_calls.length === 0){
                console.log(`\n AI 最终回复: \n ${response.content}`);
                return response.content;
            }

            console.log(chalk.bgBlue(`检测到 ${response.tool_calls.length} 个工具调用`));
            console.log(chalk.bgBlue(`工具调用: ${response.tool_calls.map(tc => tc.name).join(', ')}`));

            // 逐个执行工具调用,把结果塞回消息列表
            for(const toolcall of response.tool_calls){
                const foundTool = tools.find(t => t.name === toolcall.name);
                if(foundTool){
                    const toolResult = await foundTool.invoke(toolcall.args);
                    // 注意这里必须传 tool_call_id,不然AI对应不上结果
                    // 别问我为什么知道,漏了之后卡了半小时
                    messages.push(new ToolMessage({
                        tool_call_id: toolcall.id,
                        content: toolResult
                    }))
                }
            }
        }
        return messages[messages.length - 1].content;
    }

    // 测试一下
    await runAgentWithTools('查一下用户 002 的信息');

    // 这行一定要加!不然脚本跑完不会退出
    // 别问我怎么知道的,对着卡着的终端发呆了二十分钟
    await mcpClient.close();

运行之后,就能看到最开始截图里的效果了。

我踩过的几个坑,个个都卡半小时

写这个 demo 看着简单,其实踩了好几个坑,都是不看文档想当然踩的。说出来给大家避避坑。

第一个坑就是上面说的,忘了调用 close ()。MCP Server 是作为子进程被客户端启动的,你不主动关闭,它就会一直挂在后台等着接收请求,主进程自然就不会退出。一开始我以为是异步没处理好,翻了半天 Promise 写法,最后才发现是没关子进程。

使用完 MCP Client 后,务必调用 close () 方法释放资源,终止子进程,否则脚本会一直挂起无法正常退出。

第二个坑,ToolMessage 漏传 tool_call_id。这个坑更隐蔽。我最开始写 ToolMessage 的时候,只传了 content 内容,没带 tool_call_id。结果就是 AI 永远收不到工具的返回结果,控制台一直在刷「第 N 轮 AI 思考」,无限循环调用工具,就是不输出最终答案。

我当时还吐槽模型笨,调用完工具都不会看结果。后来翻了半天才发现,大模型是靠 tool_call_id 来匹配「哪次调用对应哪次结果」的,你不传 id,它根本不知道这个结果是给谁的,自然就一直重复调用了。

第三个坑,工具描述写得太敷衍。最开始 query_user 的描述我就写了「查询用户信息」六个字,结果 AI 经常传错参数,一会儿传姓名,一会儿传邮箱,死活不知道传 userId。后来我把描述补得巨详细,连参数示例都加上了,调用准确率一下就到了 100%。

给 AI 写描述真的比写产品需求文档还要严谨,半点歧义都不能有。

说点实在的:这玩意儿什么时候该用?

聊到这肯定有人会说,这不就是套了层协议的工具调用吗,我直接写接口不行?

确实,简单场景下完全没必要用 MCP。如果你的项目就是个单语言应用,工具也没几个,直接用框架原生的工具调用就行,上 MCP 反而平白增加复杂度。

但如果碰到下面这几种情况,MCP 就能体现出价值了:

  • 工具需要跨语言开发。比如部分算法工具用 Python 写,业务工具用 Java 写,不用再各自包接口,统一走 MCP 标准就行。
  • 工具需要给多个项目复用。比如公司内部有很多 Agent 项目,都要用到用户查询、权限校验这类通用工具,做成 MCP Server 一次开发,所有项目都能接。
  • 需要远程调用工具能力。把工具部署在服务器上,本地或者其他端的 Agent 通过 HTTP 方式远程调用,不用把代码拷来拷去。
  • 想统一管理工具和文档资源。所有能力都按 MCP 标准封装,有统一的描述、参数规范,维护起来也方便。

最后随便聊两句

说实话,最开始听到 MCP 这个新名词的时候,我是有点抵触的,觉得又是 AI 圈在炒概念。真的沉下心写了个 demo,踩了几个坑,才明白它解决的是实实在在的痛点 ------ 把大模型的能力扩展,从单进程、单语言的限制里解放出来了。

技术这东西就是这样,听一百遍概念讲解,不如自己动手跑一遍。当你亲手把 Server 启起来,看着 AI 自动调用工具、整理结果返回的那一刻,很多原理性的东西自然就通了。

要是你也跑了 MCP 的 demo,或者有不一样的理解,欢迎在评论区聊聊,我也想看看大家都用它做了啥有意思的东西。

相关推荐
楷哥爱开发1 小时前
如何使用 Claude Fable 5 进行网页抓取?2026最新实战教程
大数据·网络·人工智能
YMWM_1 小时前
lerobot中use_relative_actions=True需要重新计算meta/stats.json等信息
人工智能·深度学习·lerobot
触底反弹1 小时前
🔥 DeepSeek 560 万美金干翻 OpenAI?一文讲透「蒸馏」的来龙去脉
人工智能
私人珍藏库1 小时前
[Android] 会计快题库 -财会职称考试刷题学习
android·人工智能·学习·app·软件·多功能
Sirius Wu2 小时前
OpenClaw Skill:Matplotlib 可视化技能 + 沙箱双层隔离完整详解
服务器·网络·人工智能·安全·ai·架构·aigc
闻道且行之2 小时前
TurboOCR:基于PP-OCRv6的极速Windows离线OCR工具,深度解析3.4GB依赖背后的技术架构
c++·人工智能·python·qt·机器学习·ocr
We0 AI2 小时前
2026 年的 B2B 官网竞争,已经不只是页面竞争了
人工智能·aigc·#ai建站
冬奇Lab2 小时前
每日一个开源项目(第160篇):Destructive Command Guard - 在 AI Agent 运行 rm -rf 之前拦截它
人工智能·安全·开源
IvorySQL2 小时前
PG 日报|SQL/PGQ 图查询基于联接重写机制实现
数据库·人工智能·sql·postgresql·区块链·ivorysql