在构建 AI Agent 时,我们经常需要给大模型绑定各种工具(Tool)。比如一个查询用户信息的工具:

const queryUserTool = {name: 'query_user',description: '查询用户信息',// ...}
但这样写存在两个明显的问题:
核心诉求就是:让 Tool 独立于 LLM,实现本地/远程、跨进程、跨语言的调用。
MCP(Model Context Protocol) 是一种标准化的协议,用于规范 LLM 与 Tool、Resource 之间的通信,核心目标是 解耦 LLM 和 Tool。
MCP 支持两种通信模式:
| 模式 | 传输方式 | 适用场景 |
|---|---|---|
| stdio | 标准输入输出流 | 本地跨进程调用 |
| HTTP | 远程 HTTP 通信 | 远程跨进程调用 |
本质上,MCP 就是让 Agent 能够跨进程调用工具——不管是本地进程还是远程进程,通过 MCP 协议就能搞定。
MCP 和 fetch 调用接口不一样——它不是去拿接口数据,而是要扩展 Context(工具能力 + 资源知识),让 LLM 在推理时拥有更丰富的上下文。
本质就是工具,和普通的 Function Calling / Tool Use 没有本质差别,区别在于它是跨进程提供的。Agent(MCP Client / Host)通过协议去发现和调用远程进程中的工具——就像"抛饵"出去,让别的进程来执行。
Resource 是 MCP 另一大亮点。它允许 MCP Server 提供静态资源(文档、指南等),这些资源可以作为 System Prompt 的一部分注入到 Context 中。
看一个实际的 Resource 注册例子(来自 my-mcp-server.mjs):
server.registerResource('使用指南','docs://guide', // URI 格式的访问路径{description: '使用指南',mimeType: 'text/plain'},async () => {return {contents: [{uri: 'docs://guide',mimeType: 'text/plain',text: `MCP Server 使用指南功能:提供用户查询等工具。使用:在 Cursor 等 MCP Client 中通过自然语言对话,Cursor 会自动调用相应工具。`}]}})
在 Client 端读取 Resource 的方式:
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;}}// 将 resource 内容作为 SystemMessage 注入const messages = [new SystemMessage(resourceContent),// 资源内容成为上下文的一部分new HumanMessage(query),];
下面通过实际代码展示一个完整的 MCP 工作流程。架构如下:
┌──────────────┐stdio ┌─────────────────┐│MCP Client│ ◄──────────► │MCP Server ││(LangChain) │跨进程通信 │(Node.js 进程)││││ ││- 获取工具 ││- query_user ││- 获取资源 ││- docs://guide ││- Agent 循环 ││ │└──────────────┘└─────────────────┘
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: '祖豪', email: '[email protected]', role: 'admin' },'002': { id: '002', name: '光光', email: '[email protected]', role: 'user' },'003': { id: '003', name: '小红', email: '[email protected]', role: 'user' },}}const server = new McpServer({name: 'my-mcp-server',version: '1.0.0'});// 注册工具server.registerTool('query_user', {description: '查询数据库中的用户信息',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} 不存在` }]}}return {content: [{type: 'text',text: `用户 ${user.id} 的信息是:姓名:${user.name}, 邮箱:${user.email}, 角色:${user.role}`}]}});// 注册资源server.registerResource('使用指南', 'docs://guide', { /* ... */ }, async () => { /* ... */ });// 通过 stdio 启动通信const transport = new StdioServerTransport();await server.connect(transport);
关键点:
McpServer创建服务实例registerTool注册工具,使用 Zod 定义参数 SchemaregisterResource注册静态资源StdioServerTransport建立 stdio 通信通道import { MultiServerMCPClient } from '@langchain/mcp-adapters';import { ChatOpenAI } from '@langchain/openai';const model = new ChatOpenAI({modelName: 'deepseek-v4-flash',// ...配置});// 配置 MCP Client,可以同时连接多个 MCP Serverconst mcpClient = new MultiServerMCPClient({mcpServers: {'my-mcp-server': {command: 'node',// 启动命令args: ['src/my-mcp-server.mjs'],// 脚本路径cwd: '/path/to/mcp-demo'// 工作目录}}});// 获取工具和资源const tools = await mcpClient.getTools();// 获取资源并拼成上下文字符串const res = await mcpClient.listResources();// ...拼接 resourceContent ...// 绑定工具到模型const modelWithTools = model.bindTools(tools);
关键点:
MultiServerMCPClient可以同时配置多个MCP Serverchild_process启动子进程,通过 stdio 通信async function runAgentWithTools(query, maxIterations = 30) {const messages = [new SystemMessage(resourceContent), // 资源内容作为系统提示new HumanMessage(query),];for (let i = 0; i < maxIterations; i++) {const response = await modelWithTools.invoke(messages);messages.push(response);// 没有工具调用 → 直接返回最终回复if (!response.tool_calls || response.tool_calls.length === 0) {return response.content;}// 执行每个工具调用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);messages.push(new ToolMessage({content: toolResult,tool_call_id: toolCall.id, // ⚠️ 必须带上 tool_call_id}));}}}return messages[messages.length - 1].content;}
这个循环有几个值得注意的细节:
tool_call_id必须回传:ToolMessage 必须带上对应的tool_call_id,这是模型关联工具调用和结果的唯一标识tools.find()匹配:find方法找到第一个匹配项就停止,适合工具名唯一的场景// 关闭所有 MCP 子进程与通信通道,释放进程资源await mcpClient.close();
这一步很重要! close() 会:
child_process启动的子进程主进程 (Agent/LangChain)││child_process.spawn('node', ['my-mcp-server.mjs'])│├── stdin──►MCP Server 子进程│ (接收工具调用请求)│◄── stdout ──MCP Server 子进程(返回工具执行结果)
child_process启动 Server 作为子进程整个通信链路中,JavaScript 的单线程异步无阻塞模型保证了:
在处理 MCP 返回的资源列表时,Object.entries() 非常实用:
// MCP 返回的是按 Server 分组的资源对象const res = {'my-mcp-server': [{ uri: 'docs://guide', name: '使用指南' },{ uri: 'docs://api', name: 'API文档' },]};// Object.entries 拆解为 [key, value] 遍历for (const [serverName, resources] of Object.entries(res)) {for (const resource of resources) {// 逐个读取资源内容}}
| 维度 | 传统 Tool | MCP Tool |
|---|---|---|
| 复用性 | 绑定在项目中 | 独立进程,任意项目可用 |
| 跨语言 | 仅限同语言 | 通过 stdio/http,任意语言 |
| 通信方式 | 同进程函数调用 | 跨进程(本地 stdio / 远程 HTTP) |
| 扩展性 | 手动添加 | 配置式添加 MCP Server |
| 上下文丰富 | 仅 Tool | Tool + Resource + Prompt |
MCP 协议的核心价值在于: