🔌 把 MCP 装进大脑!手把手带你构建能“热插拔”工具的 AI Agent

各位 AI 探索者们,大家好!👋

在上一篇文章中,我们拒绝了"手搓"工具的原始方式,成功编写了一个符合 MCP (Model Context Protocol) 标准的 Server,并在其中模拟了一个数据库查询工具。

但是!写好了 Server 只是第一步。这就好比你造出了一把削铁如泥的宝剑(Server),但如果没人(Host)能挥舞它,它也就是块废铁。🧱

今天,我们要进行一次维度升级 。我们将跳出 Server 的视角,站在上帝视角(Host)来审视整个 MCP 生态,并且我们要干一件大事:不依赖 Cursor 或 Trae,我们要自己写一个 Agent,让它通过代码动态加载并使用我们昨天写的 MCP 工具!

准备好了吗?硬核干货,发车!🚀


🧩 第一部分:MCP 的"三国演义" ------ Host, Client, Server

很多刚接触 MCP 的朋友,容易被文档里的一堆名词绕晕:MCP Host, MCP Client, MCP Server......它们到底是谁?谁又是谁的爸爸?🤔

让我们来理清这个关系。

1.1 角色大揭秘

MCP 的架构其实非常清晰:

  • 🏰 MCP Host (东道主)

    • 谁是 Host? 你的编辑器(Cursor, Trae),或者我们今天要写的 LangChain 应用程序,它们都是 Host。
    • 作用:它是发起者,是"甲方爸爸"。它负责发现工具、管理连接、向 LLM 发送 Prompt。
    • 比喻 :Host 就像是一个大工头,手里拿着图纸(用户需求),指挥干活。
  • 🔌 MCP Client (客户端)

    • 谁是 Client? 这是一个存在于 Host 内部的组件。
    • 作用:你可能会问,"Host 不就是客户端吗?" 严格来说,Host 是应用程序,而 Client 是 Host 内部负责和 Server 说"MCP 语言"的翻译官。Host 想要调用工具,必须通过 Client 去建立连接、发送请求。
    • 比喻 :Client 就像是接口转换器翻译官。大工头(Host)说"我要那个锤子",翻译官(Client)把它翻译成 MCP 协议的标准指令发给工具箱。
  • 🧰 MCP Server (服务器)

    • 谁是 Server? 就是我们昨天写的 my-mcp-server.mjs
    • 作用:它是工具和资源的容器。它不知道谁在调用它,它只管提供能力。
    • 比喻 :Server 就是超级工具箱。里面装着锤子(Tools)、说明书(Resources)和模版(Prompts)。

1.2 工作流程:一次握手的旅程 🤝

当我们在 Cursor 里输入"查询用户 002"时,后台发生了什么?

  1. Initialize (初始化) : Host 启动,通过 Client 向 Server 发送握手请求:"兄弟,你那都有啥宝贝?" Server 回复:"我有 query-user 这个工具,还有个使用指南。"

  2. Discovery (发现): Host 拿到了工具列表(Tools List),把它转换成 LLM 能看懂的 JSON Schema,喂给大模型。

  3. Execution (执行) : LLM 思考后说:"我要调用 query-user,参数是 userId: '002'"。 Host 收到指令,通过 Client 将这个请求通过 Stdio(标准输入输出)管道发给 Server。

  4. Response (响应): Server 干完活,把结果扔回给 Host。Host 再把结果喂给 LLM,最终生成人类可读的回复。


📚 第二部分:不仅仅是 Tool,还有 Resource(资源)

细心的同学可能发现了,MCP 的全称是 Model Context Protocol ,而不是 Model Tool Protocol。为什么叫 Context(上下文)

因为 AI 不仅需要做事(Tools) ,还需要看书(Resources)

2.1 什么是 Resource?

  • Tool 是动态的,是函数调用(查天气、写库、发邮件)。
  • Resource 是静态的(或准静态的),是数据内容(API 文档、日志文件、系统配置、代码片段)。

Resource 的意义在于:它允许 Server 主动把数据"喂"给 LLM,作为上下文的一部分,而不需要 LLM 去猜或者通过 Tool 去抓取。

2.2 实战:给我们的 Server 添加"使用指南"

让我们回到 my-mcp-server.mjs,给它增加一个 Resource 能力。

javascript 复制代码
// 注册资源:使用指南
// URI: 统一资源定位符,这是资源的唯一身份证
server.registerResource(
    '使用指南',          // 资源名称
    'docs://guide',      // 资源 URI,类似于网页的 URL
    {
        description: 'MCP Server 使用文档', // 描述,LLM 会看这个
        mimeType: 'text/plain',            // 告诉 LLM 这是纯文本
    }, 
    async () => {
        // 当 Client 请求读取这个资源时,返回的内容
        return {
            contents: [
                {
                    uri: 'docs://guide',
                    mimeType: 'text/plain',
                    text: `MCP Server 使用指南
                    功能:提供用户查询等工具。
                    使用:在 Cursor 等 MCP Client 中通过自然语言对话,Cursor 会自动调用相应工具。`,
                }
            ]
        }
    }
)

🔍 代码深度解析

  1. server.registerResource: 这是注册资源的入口。
  2. docs://guide : 自定义协议头。你可以随便定,比如 mysql://table/users 或者 log://app/error。这让 LLM 可以通过 URI 引用特定的上下文。
  3. mimeType : 既然是传输内容,就得告诉对方格式。是文本(text/plain)?是图片(image/png)?还是 JSON?
  4. Prompt Templates : 虽然代码里没写,但 MCP 还支持 Prompt。你可以把写好的优质 Prompt 模板预置在 Server 里,让小白用户也能直接调出高质量的回答。

总结: Context (上下文) = Tool (工具) + Resource (资源) + Prompt (提示词模板)。 这就是 MCP 强大的原因,它打包了一切 LLM 需要的东西。


🤖 第三部分:自制 Agent ------ 将 MCP 接入 LangChain

好了,理论知识储备完毕,Server 也升级了。现在我们来做一个真正的 Host!

我们将使用 Node.js + LangChain 来构建一个 Agent,让它连接我们本地运行的 my-mcp-server.mjs

3.1 引入适配器:LangChain 的 MCP 魔法 🪄

首先,你需要安装一个关键的包:@langchain/mcp-adapters

javascript 复制代码
// adapters mcp 适配器
import {
    MultiServerMCPClient
} from '@langchain/mcp-adapters';

这个 MultiServerMCPClient 非常强大,它帮我们屏蔽了底层协议的复杂性,支持同时连接多个 MCP Server(就像 Cursor 的配置列表一样)。

3.2 配置 Client:复刻 Cursor 的配置

记得我们在 Cursor 的 config.json 里是怎么配的吗?我们需要指定 command (node) 和 args (脚本路径)。在代码里,也是一样的:

javascript 复制代码
// client 初始化
const mcpClient = new MultiServerMCPClient({
    mcpServers: {
        'my-mcp-server': { // 给这个 Server 起个 ID
            command: 'node', // 运行命令
            // ⚠️ 注意:这里必须是绝对路径!
            args: ['C:\\Users\\MR\\Desktop\\workspace\\lesson_jp\\ai\\agent\\mini-cursor\\mcp\\mcp-tool\\my-mcp-server.mjs'],
        },
    },
});

💡 核心点 : 这里实例化了一个 mcpClient。此时,连接还没有建立,它只是记住了配置。这和你在 IDE 里填好配置但还没点击"Retry"是一样的状态。

3.3 动态获取工具:从 Server 到 Model

接下来是见证奇迹的时刻。我们要让 Client 去连接 Server,把工具"吸"过来,然后绑在 LLM 身上。

javascript 复制代码
// 1. 获取工具列表
// 这一步会触发 initialize 流程,自动启动子进程并握手
const tools = await mcpClient.getTools(); 

console.log(tools, '////'); // 打印看看,你应该能看到 query-user 的详细 Schema

// 2. 绑定工具到模型
// 这里假设 model 是一个 ChatOpenAI 实例
const modelWithTools = model.bindTools(tools);

逻辑解析

  • mcpClient.getTools():这行代码背后发生了巨大的工作量。它启动了 Node 子进程,建立了 Stdio 管道,发送了 tools/list 请求,接收了响应,并把 MCP 格式的 Tool 定义转换成了 LangChain/OpenAI 兼容的 Tool 定义。
  • model.bindTools(tools):这是 LangChain 的标准操作,把工具描述告诉 LLM,让 LLM 知道它有了外挂。

3.4 编写 Agent 循环:让 AI 动起来 🔄

如果只是绑定了工具,LLM 只会返回一个"我想要调用工具"的信号(Tool Call)。真正执行工具并把结果喂回去,需要我们写一个循环。这其实就是 ReAct Agent 的雏形。

javascript 复制代码
async function runAgentWithTools(query, maxIterations=30) {
    const messages = [
        new HumanMessage(query) // 初始问题
    ];

    // 开启思考-执行循环
    for (let i = 0; i < maxIterations; i++) {
        console.log(chalk.bgGreen('⏳正在等待AI思考...'));
        
        // 1. 调用模型
        const response = await modelWithTools.invoke(messages);
        
        // 把 AI 的回复(可能包含工具调用请求)加入历史记录
        messages.push(response); 

        // 2. 判断是否结束
        // 如果 AI 没有想调用的工具,说明它已经生成了最终答案
        if (!response.tool_calls || response.tool_calls.length === 0) {
            console.log(`\n AI 最终回复:\n ${response.content}\n`);
            return response.content;
        }

        // 3. 处理工具调用
        console.log(chalk.bgBlue(`🔍 检测到 ${response.tool_calls.length} 个工具调用`));

        // 遍历所有工具调用请求(AI 可能一次想调多个工具)
        for (const toolCall of response.tool_calls) {
            // 在我们从 MCP 拉取回来的 tools 数组里找到对应的工具
            const foundTool = tools.find(t => t.name === toolCall.name);
            
            if (foundTool) {
                // 4. 执行工具!
                // 这里 foundTool.invoke 实际上会通过 MCP 协议
                // 把参数发给 my-mcp-server.mjs,拿到结果
                const toolResult = await foundTool.invoke(toolCall.args);
                
                // 5. 将工具结果封装为 ToolMessage
                messages.push(new ToolMessage({
                    content: toolResult,
                    tool_call_id: toolCall.id // 必须带上 ID,让 AI 知道这是哪个调用的结果
                }));
            }
        }
        // 循环继续,下一轮 invoke 会带上最新的 ToolMessage
    }
    return messages[messages.length - 1].content;
}

🧪 实验结果

当我们运行 await runAgentWithTools("查一下用户 002 的信息") 时:

  1. 第一轮
    • LLM 收到问题。
    • LLM 思考:"我要用 query-user,参数 userId='002'"。
    • 代码捕获到 tool_calls
    • 代码调用 mcpClient -> 发送请求给 my-mcp-server.mjs
    • Server 返回:"姓名:李四,邮箱..."。
    • 代码把这个结果存入 ToolMessage

注意第一轮有tool_calls,这个时候context为空,llm不会返回context只会返回需要调用哪些工具,与从用户提问中解析出的参数。

  1. 第二轮
    • LLM 收到历史记录(问题 + 工具调用请求 + 工具返回结果)。
    • LLM 思考:"我有信息了,组织语言回答"。
    • LLM 输出:"用户 002 是李四,邮箱是..."。
    • 代码检测到没有 tool_calls,循环结束,输出最终答案。

🎉 总结

今天我们不仅搞懂了 Host-Client-Server 的三角关系,还解锁了 Resource 这个新技能,最重要的是,我们亲手实现了一个 MCP Host

现在,你不再只是 MCP 的使用者,你是 MCP 的掌控者

想象一下未来的可能性:

  • 你可以写一个 Python 的 MCP Server 处理数据分析。
  • 写一个 Go 的 MCP Server 处理高并发任务。
  • 然后用今天的这段 Node.js 代码(或者直接用 Cursor),把它们全部串联起来。

这就是 Agent 的未来,这就是 MCP 的魅力。

觉得文章有用?点个赞再走吧!👍 咱们下期见!👋


附:运行环境检查

  • 确保安装了 @langchain/mcp-adapters, @langchain/openai, @modelcontextprotocol/sdk
  • 确保 my-mcp-server.mjs 路径正确。
  • 配置好 .env 里的 OPENAI_API_KEY
相关推荐
小兵张健3 小时前
AI 页面与交互迁移流程参考
前端·ai编程·mcp
小兵张健3 小时前
掘金发布 SOP(Codex + Playwright MCP + Edge)
前端·mcp
智泊AI6 小时前
一文讲清:Agent、Workflow、MCP的区别是啥?
llm
Qinana6 小时前
从代码到智能体:MCP 协议如何重塑 AI Agent 的边界
前端·javascript·mcp
神秘的猪头6 小时前
🚀 拒绝“手搓”工具!带你硬核手写 MCP Server,解锁 Agent 的无限潜能
agent·mcp·trae
warm3snow1 天前
Claude Code 黑客马拉松:5 个获奖项目,没有一个是"纯码农"做的
ai·大模型·llm·agent·skill·mcp
是一碗螺丝粉1 天前
5分钟上手LangChain.js:用DeepSeek给你的App加上AI能力
前端·人工智能·langchain
是一碗螺丝粉1 天前
LangChain 核心组件深度解析:模型与提示词模板
前端·langchain·aigc
马腾化云东1 天前
Agent开发应知应会(langfuse):Langfuse Score概念详解和实战应用
人工智能·llm·ai编程