Agent 智能体开发实战 · 第六课:MCP 协议 —— 让 Agent 跨进程调用工具

Agent 智能体开发实战 · 第六课:MCP 协议 —— 让 Agent 跨进程调用工具
上节回顾第五课我们用四工具模块 ReAct 循环手写了 Mini-Cursor一个最普通、最基本的 AI 编程 Agent——所有工具都在同一进程内定义和运行。本课聚焦基础 Agent 有两个天生缺陷——工具不能跨项目复用不能跨语言调用。MCPModel Context Protocol协议是解决这个问题的优化方案让工具脱离 Agent 进程独立运行实现跨进程、跨语言、跨网络调用还给 LLM 增加了 Resource静态知识注入能力。 本课目录一、前五课 Tool 的局限性二、MCP 协议是什么三、MCP Server注册 Tool Resource四、MCP ClientAgent 如何连接 MCP Server五、完整流程从 Client 启动到 Server 响应六、实战Agent 通过 MCP 查询用户信息七、本课学习总结一、基础 Agent 的痛点 前五课搭建的基础 Agent┌─────────────────────────────────────────┐ │ Agent 进程 │ │ │ │ LLM → tool_calls → tools.find() │ │ ↓ │ │ tool.invoke() │ │ ↓ │ │ 同一个进程内执行 │ │ fs.readFile / spawn │ └─────────────────────────────────────────┘这是一个最普通、最基本的 Agent它完全可用。但在实际工程中会遇到两个问题问题说明项目绑定工具只能在本项目用不能跨项目复用语言锁定Node.js 写的工具Java/Python/Rust 写的工具没法直接用 真实的开发场景后端团队用 Python 写了一个数据库查询工具前端团队用 Node.js 写 Agent——怎么让 Agent 调用 Python 写的工具这就是 MCP 要解决的问题。二、MCP 协议是什么 核心定义**MCPModel Context Protocol**是给 LLM **扩展 Context上下文**的标准化协议——让 LLM 能做的更多Tool知道的更多Resource。┌──────────────────────────────────────────────────┐ │ │ │ ┌──────────┐ MCP 协议 ┌────────────┐ │ │ │ MCP Client │◄──── stdio / HTTP ────►│ MCP Server │ │ │ │ (Agent) │ │ (Tool 提供方)│ │ │ └──────────┘ └────────────┘ │ │ │ │ 可以是 Node.js 进程 可以是 Node/Python/Java/ │ │ LangChain Agent Rust 写的任何进程 │ │ │ └──────────────────────────────────────────────────┘ MCP 的两大通信方式通信方式场景原理️stdio标准输入输出流本地跨进程调用键盘输入 → 控制台输出进程间通过 stdin/stdout 通信HTTP远程跨网络调用通过网络请求调用远程 MCP Server它不是 fetch 接口调用——MCP 不只是拿接口数据它的目的是扩展 LLM 的 Context提供 Tool Resource Prompt让 LLM 拥有更多能力。️ MCP 的五大核心能力能力说明类比前五课Tool让 LLM 调用的工具函数本质就是 Tool只是跨进程了Resource静态资源文档、指南等给 LLM 补充知识 前五课没有的概念Prompt预定义的提示词模板 前五课没有的概念跨进程stdio / HTTP 通信第三课的 child_process 是单向的MCP 是双向协商的跨语言不限制实现语言Node / Python / Java / Rust 都可以写 MCP Server三、MCP Server注册 Tool Resource 架构定位MCP Server 是工具的提供方。它通过StdioServerTransport暴露自己等待 MCP Client 连接和调用。┌──────────────────────────────────┐ │ MCP Server │ │ │ │ server.registerTool(...) │ │ server.registerResource(...) │ │ ↓ │ │ StdioServerTransport │ │ ↓ │ │ 通过 stdin/stdout 与 Client 通信 │ └──────────────────────────────────┘第1步导入依赖 准备数据import{McpServer}frommodelcontextprotocol/sdk/server/mcp.js;import{StdioServerTransport}frommodelcontextprotocol/sdk/server/stdio.js;import{z}fromzod;// 假数据未来可以走数据库constdatabase{users:{001:{id:001,name:zuhao,email:zuhhaoexample.com,role:admin},002:{id:002,name:xiaoming,email:xiaomingexample.com,role:user},003:{id:003,name:zohong,email:zohongexample.com,role:user},}}database是一个内存假数据对象。注释未来可以走数据库说明实际项目中这里会替换成 MySQL/MongoDB 等真实数据源。MCP Server 的业务逻辑和 Agent 完全无关——它只管提供数据。第2步创建 McpServer 实例constservernewMcpServer({name:my-mcp-server,version:1.0.0,});McpServer是 MCP SDK 的核心类。name用来标识这个服务Client 配置中会用到version用于版本管理。这类似于 npm 包名和版本号。第3步注册 Tool ——registerToolserver.registerTool(query-user,{description:查询数据库中的用户信息。输入用户ID返回该用户的详细信息姓名、邮箱、角色,inputSchema:{userId:z.string().describe(用户ID例如001002003)},},async({userId}){constuserdatabase.users[userId];if(!user){return{content:[{type:text,text:用户ID${userId}不存在。可用的ID有001002003}]}}return{content:[{type:text,text:用户ID${userId}的信息如下 姓名${user.name}邮箱${user.email}角色${user.role}}]}})参数说明query-userTool 名称LLM 会在 tool_calls 中使用description告诉 LLM 这个工具做什么、什么时候调用inputSchema用 Zod 定义参数跟前五课schema: z.object(...)一样async ({ userId })处理函数从inputSchema中解构参数⚠️关键差异返回格式是{ content: [{ type: text, text: ... }] }不是纯字符串。前五课的tool()直接返回字符串即可MCP 需要包装成content数组。第4步注册 Resource ——registerResource// http:// stdio - 定义的访问路径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 中通过自然语言对话Cursor 会自动调用相应工具。}]}})参数说明使用指南Resource 名称人类可读docs://guideResource URI定义的访问路径——Client 通过这个 URI 读取内容description/mimeType元信息描述内容和类型async ()返回{ contents: [...] }格式和 Tool 返回类似Resource 是前五课完全没有的概念。Tool 是做事情执行动作Resource 是给知识提供文档/指南/配置。Client 读取后在 System Prompt 中注入LLM 不用调 Tool 就能知道这些信息。第5步启动 stdio 传输层// 跨进程通信方式stdioconsttransportnewStdioServerTransport();awaitserver.connect(transport);StdioServerTransport是 MCP 的本地通信实现。它把 MCP Server 挂在 stdin/stdout 上等待 Client 进程的输入。server.connect(transport)启动监听——从这一刻起其他进程可以通过 stdio 与这个 Server 通信。 MCP Server vs 前五课 Tool 的对比维度前五课tool()MCPregisterTool()定义方式tool(func, { name, schema })server.registerTool(name, { description, inputSchema }, func)返回格式纯字符串{ content: [{ type: text, text: ... }] }运行位置Agent 同一进程独立子进程通过 stdio 通信语言限制只能用 Node.js任何语言只要实现了 MCP 协议可复用性只能在定义它的项目用所有项目都可以连上这个 Server 使用 Resource无有提供静态文档/指南给 LLM四、MCP ClientAgent 如何连接 MCP Server 架构定位MCP Client 是Agent 这一侧。它启动 MCP Server 子进程通过MultiServerMCPClient连接获取 Tool 和 Resource然后驱动 Agent Loop。┌─────────────────────────────────────────────────────┐ │ MCP Client (Agent) │ │ │ │ MultiServerMCPClient │ │ ├─ 启动 my-mcp-server.mjs 子进程 │ │ ├─ 通过 stdio 连接 │ │ ├─ mcpClient.getTools() → 动态获取 Tool │ │ └─ mcpClient.listResources() → 获取 Resource │ │ │ │ model.bindTools(tools) → Agent Loop → close() │ └─────────────────────────────────────────────────────┘第1步导入依赖 LLM 初始化importdotenv/config;// agent 配置 mcp client —— 可以配置多个 mcp server 的 clientimport{MultiServerMCPClient}fromlangchain/mcp-adapters;import{ChatOpenAI}fromlangchain/openai;importchalkfromchalk;import{HumanMessage,SystemMessage,ToolMessage}fromlangchain/core/messages;constmodelnewChatOpenAI({modelName:deepseek-v4-pro,apiKey:process.env.DEEPSEEK_API_KEY,temperature:0,configuration:{baseURL:https://api.deepseek.com/v1,},}); 核心新面孔是MultiServerMCPClient——它能同时管理多个 MCP Server 的连接。注释可以配置多个 mcp server 的 client说明 Agent 可以接 N 个 MCP Server各自提供不同领域的工具。第2步配置 MCP Client —— 连接 ServerconstmcpClientnewMultiServerMCPClient({mcpServers:{my-mcp-server:{command:node,args:[C:/Users/yihao/Desktop/workspace/yh_ai/ai/ai/agent_in_action/mcp-demo/src/my-mcp-server.mjs]}}})字段说明my-mcp-serverServer 名称对应new McpServer({ name: my-mcp-server })command: node用什么启动 MCP Server 进程args: [...]脚本路径传给 node 的参数 这里的command args跟第三课spawn(node, [script.mjs])是一个原理。MultiServerMCPClient内部用child_process.spawn启动了 MCP Server 子进程。第3步动态获取 Tool// 获取工具 —— 动态从 MCP Server 获取不是本地定义的consttoolsawaitmcpClient.getTools();前五课的工具都是本地import或直接定义的。这里getTools()一行代码从 MCP Server 子进程动态拉取所有注册的 Tool。加新工具在 Server 里registerTool就行Agent 端零改动。第4步获取 Resource 并注入 System PromptconstresawaitmcpClient.listResources();letresourceContent;for(const[serverName,resources]ofObject.entries(res)){for(constresourceofresources){constcontentawaitmcpClient.readResource(serverName,resource.uri)resourceContentcontent[0].text;}}console.log(resourceContent,---------------);方法作用listResources()列出所有 MCP Server 提供的 Resource名称 URIreadResource(serverName, uri)通过 URI 读取 Resource 的完整内容JS 基础Object.entries()for...of解构上面遍历 Resource 的代码用到了Object.entries()这是理解 MCP Client 代码的前提。来看1.jsconstobj{bytedance:[AI全栈开发,Agent 工程师],tecent:[后端开发,Agent 工程师],163:[前端开发]}// 二维数组for(let[key,value]ofObject.entries(obj)){console.log(key,value);}Object.entries(obj)把对象转成二维数组[[bytedance, [...]], [tecent, [...]], ...]然后for...of 解构语法[key, value]一次性拿到键和值。回到 MCP 代码Object.entries(res)把{ my-mcp-server: [resource1, resource2] }转成可遍历的[serverName, resources]外层拿服务名内层拿每个资源。遍历所有 Server 的所有 Resource拼接成文本后面直接作为 System Prompt 注入——让 LLM 提前知道这些知识不用再调 Tool 去查。第5步Agent LoopconstmodelWithToolsmodel.bindTools(tools);asyncfunctionrunAgentWithTools(query,maxIterations30){constmessages[newSystemMessage(resourceContent),// Resource 直接注入 System PromptnewHumanMessage(query)];for(leti0;imaxIterations;i){console.log(chalk.bgGreen(正在等待AI思考第${i}轮....));constresponseawaitmodelWithTools.invoke(messages);messages.push(response);if(!response.tool_calls||response.tool_calls.length0){console.log(\n AI 最终回复 \n${response.content});returnresponse.content;}console.log(chalk.bgBlue(检测到${response.tool_calls.length}个工具调用));console.log(chalk.bgBlue(工具调用:${response.tool_calls.map(tt.name).join(, )}))for(consttoolCallofresponse.tool_calls){// find 方法 匹配的哪一项如果找到了后面不会执行// Promise.all 只要一个失败了不会等待剩下结果// 已经发起的异步任务会继续执行constfoundTooltools.find(tt.nametoolCall.name);if(foundTool){consttoolResultawaitfoundTool.invoke(toolCall.args);// 返回的是纯文本tool 的返回是由上下文的相关性// 一定得带上 tool_call_idmessages.push(newToolMessage({content:toolResult,tool_call_id:toolCall.id}))}}}// 循环次数轮数达到30次仍无法回复问题返回最后一轮returnmessages[messages.length-1].content;}Agent Loop 的逻辑和前五课完全一样。注释中几个要点注释知识点Resource 直接注入 System PromptResource 不进 Tool 调用流程直接喂给 LLMfind 方法匹配的哪一项如果找到了后面不会执行Array.find()返回第一个匹配项就停止Promise.all 只要一个失败了不会等待剩下结果这也是第五课用for...of串行执行的原因之一返回的是纯文本tool 的返回是由上下文的相关性Tool 返回的内容会被 LLM 当作上下文理解一定得带上 tool_call_idToolMessage必须绑定tool_call_id否则 LLM 不知道哪个结果对应哪个调用循环次数达到30次仍无法回复问题返回最后一轮同第五课的maxIterations安全护栏第6步清理 ——close()// 启动// await runAgentWithTools(查一下用户002的信息);awaitrunAgentWithTools(MCP Server的使用指南是什么);// 关闭所有 MCP 子进程与通信通道释放进程资源// 关闭和 MCP Server 的通信通道// my-mcp-server.mjs 被启动了手动关闭进程// 释放相关资源避免脚本一直挂着不退出// node langchain-mcp-test.mjs 启动进程// 启动一个子进程 child-process client// 子进程连接 my-mcp-Server.mjs// 主进程通过 stdio 和他们通话// close() 把这个链接和子进程一起关掉awaitmcpClient.close();⚠️close()是 MCP 特有的清理步骤前五课没有。因为 MCP Server 作为子进程一直运行不关闭的话脚本会一直挂着。close()做了三件事关闭和 MCP Server 的通信通道、终止子进程、释放相关资源。 关键设计点设计点说明MultiServerMCPClient一个 Client 可以连接多个 MCP Server每个 Server 提供不同工具command args通过node 脚本路径启动 MCP Server 子进程同第三课的 spawn 原理getTools()工具不是本地定义而是从 MCP Server 动态获取listResources()获取 Server 提供的静态资源内容注入 System PromptAgent Loop和第五课的 for ToolMessage 完全一样只是工具来源变了close()关闭所有 MCP 子进程与通信通道释放进程资源五、完整流程从 Client 启动到 Server 响应┌─────────────────────────────────────────────────────────────────┐ │ ️ 终端: node langchain-mcp-test.mjs │ └─────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ Step 1: MultiServerMCPClient 启动 │ │ ↓ │ │ spawn node my-mcp-server.mjs → 子进程启动 │ │ ↓ │ │ StdioServerTransport 连接建立 │ │ 主进程和子进程通过 stdin/stdout 双向通信 │ └─────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ Step 2: mcpClient.getTools() │ │ ↓ │ │ 通过 stdio 向 MCP Server 请求工具列表 │ │ ↓ │ │ Server 返回 [query-user] │ │ ↓ │ │ tools [{ name: query-user, ... }] │ └─────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ Step 3: mcpClient.listResources() │ │ ↓ │ │ 获取 MCP Server 使用指南 文本 │ │ ↓ │ │ 注入 SystemMessage │ └─────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ Step 4: model.bindTools(tools) → Agent Loop 启动 │ │ ↓ │ │ LLM 推理 → tool_call { name: query-user, ... } │ │ ↓ │ │ tools.find() → tool.invoke() │ │ ↓ │ │ 通过 stdio 发请求到 MCP Server 子进程 │ │ ↓ │ │ Server 执行业务逻辑返回结果 │ │ ↓ │ │ ToolMessage 喂回 LLM → 继续推理 │ └─────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ Step 5: LLM 生成最终回复 │ │ ↓ │ │ mcpClient.close() → 关闭所有子进程 │ │ 释放资源避免脚本一直挂着不退出 │ └─────────────────────────────────────────────────────────────────┘ 整个流程的核心差异只有一处工具不在 Agent 本地定义而是通过 MCP 协议从外部进程动态获取。Agent Loop 的逻辑完全不变。六、实战Agent 通过 MCP 查询用户信息 执行过程 Round 1 — REASON 用户要查用户 002 的信息 → 调用 query_user 工具 │ ▼ Round 1 — ACT Tool: query-user Args: { userId: 002 } │ ▼ MCP Client ──stdio──► MCP Server 子进程 database.users[002] 返回 { name: xiaoming, ... } MCP Server ──stdio──► MCP Client │ ▼ Round 1 — OBSERVE 用户ID 002 的信息如下 姓名xiaoming 邮箱xiaomingexample.com 角色user │ ▼ Round 2 — REASON (FINAL) 已获取用户信息格式化输出给用户️ Resource 的实际效果当问MCP Server的使用指南是什么时Agent 因为 System Prompt 中已经注入了 Resource 的完整内容可以直接回答——不需要调用任何 Tool。Resource 的价值Tool 是做事情Resource 是给知识。把文档、配置、规则注入 LLM 的上下文比让它自己去读文件高效得多。七、本课学习总结 思维导图 MCP 协议 · 知识点 │ ├── 基础 Agent 的痛点 │ ├── 项目绑定工具只能在本项目用 │ └── 语言锁定只能用 Node.js 写工具 │ ├── MCP 优化方案 │ ├── Model Context Protocol │ ├── 为 LLM 扩展 ContextTool Resource Prompt │ ├── 不是 fetch 接口调用是跨进程协议 │ └── LLM 和 Tool 解耦 │ ├── 两大通信方式 │ ├── stdio本地跨进程 │ │ └── 键盘输入 → 控制台输出 → 进程间 stdin/stdout │ └── HTTP远程跨网络 │ └── 通过网络调用远程 MCP Server │ ├── ️ MCP Servermy-mcp-server.mjs │ ├── McpServer({ name, version }) │ ├── registerTool(name, { description, inputSchema }, handler) │ │ ├── 返回 { content: [{ type, text }] } 格式 │ │ └── 不同于 tool() 的纯字符串返回 │ ├── registerResource(name, uri, metadata, handler) │ │ └── 前五课没有提供静态文档/指南 │ └── StdioServerTransport server.connect() │ ├── MCP Clientlangchain-mcp-test.mjs │ ├── MultiServerMCPClient({ mcpServers }) │ │ └── command args 启动子进程同 spawn 原理 │ ├── mcpClient.getTools() │ │ └── 动态获取工具非本地定义 │ ├── mcpClient.listResources() readResource() │ │ └── 获取资源内容注入 System Prompt │ ├── Agent Loop │ │ └── for ToolMessage和第五课完全一样 │ └── mcpClient.close() │ └── 关闭子进程 通信通道 释放资源 │ ├── 完整调用流程5 步 │ ├── ① Client 启动 → spawn Server 子进程 │ ├── ② getTools() → stdio 请求 → Server 返回工具列表 │ ├── ③ listResources() → 获取文档 → 注入 System Prompt │ ├── ④ Agent Loop → tool_calls → stdio → Server 执行 → ToolMessage │ └── ⑤ LLM 回复 → close() 清理所有子进程 │ └── 核心价值 ├── 跨进程工具和 Agent 解耦独立进程运行 ├── 跨语言Node/Python/Java/Rust 都可以写 MCP Server └── 跨项目一套工具所有 Agent 项目都能用✅ 知识清单编号掌握项核心要点1为什么需要 MCP前五课工具是项目内、同语言的MCP 实现跨进程、跨语言2MCP 协议本质标准化 LLM 与 Tool/Resource 的通信不是 fetch 接口调用3McpServer创建new McpServer({ name, version })4registerTool不同于tool()返回{ content: [{ type, text }] }5registerResource 前五课没有提供静态文档给 LLM6StdioServerTransport基于 stdin/stdout 的跨进程通信7MultiServerMCPClient一个 Client 可连接多个 Server每个配置command args8mcpClient.getTools()工具从外部进程动态获取非本地定义9mcpClient.listResources()获取资源内容注入 System Prompt10mcpClient.close()关闭所有子进程 通信通道 释放资源 基础 Agent vs MCP 优化维度前五课 · 基础 Agent第六课 · MCP 优化核心能力Tool 定义 Agent Loop CLI 完整工具集跨进程工具 Resource工具来源本地定义 / 模块导入MCP 动态获取工具数量1 → 4N不限跨语言❌ 只能用 Node.js✅ Node/Python/Java/Rust 皆可跨项目复用❌ 绑定在项目内✅ 一套 Server所有项目共用Resource❌✅ 静态知识注入本课定位前五课搭建的是最普通、最基本的 AgentLLM Tool Loop它已经能独立完成编程任务。MCP 是在这个基础上的优化升级——把工具从Agent 的附属品变成独立的、可共享的、可跨语言的服务。基础 Agent 够用的时候不必上 MCP但当你需要跨团队共享工具、调用 Python/Java 写的服务时MCP 就是解题钥匙。 2026-07-11 | ️ Agent · MCP · Model Context Protocol · 跨进程 · Resource · 第六课