跨进程调用工具:MCP + LangChain 让 Agent 不再"锁死"在单一语言
摘要:本地编写的 Tool 只能在当前项目中使用,换一个项目就要重写。MCP 协议让 Tool 可以独立于 LLM,实现跨进程、跨语言调用。本文用 LangChain 的 MCP 适配器连接 MCP Server,获取 Tools 和 Resources,构建一个能跨进程调用工具的 Agent。
📑 目录
- 本地 Tool 的复用困境
- MCP 协议:让工具解耦的"桥梁"
- 环境准备与依赖
- MCP Server 端:注册 Tool 和 Resource
- MCP Client 端:MultiServerMCPClient 连接 Server
- 获取 Resources:将资源注入 SystemMessage
- Agent 循环调用跨进程 Tool
- 进程管理:为什么需要 close?
- 一点总结
- 互动讨论
本地 Tool 的复用困境

在之前开发 Agent 时,我们直接在项目里用 LangChain 的 tool() 函数定义 Tool:
javascript
php
const readFileTool = tool(
async ({ FilePath }) => { ... },
{ name: 'read_file', description: '...', schema: ... }
);
这种做法有两个明显的问题:
- Tool 只能在当前项目中使用,换一个项目就要复制粘贴,几乎没有复用性。
- 语言绑定:用 Node.js 写的 Tool,如果其他团队用 Python/Java/Rust,就需要重写。
理想的方案是:Tool 应该独立于 LLM,可以本地/远程、跨进程、跨语言调用。
这就是 MCP(Model Context Protocol)要解决的问题。
MCP 协议:让工具解耦的"桥梁"
MCP 是 LLM 与工具、资源之间的通信协议。它的核心价值在于标准化 : 
- 标准化 LLM 与 Tool 和 Resource 之间的通信 → 解耦
- 基于 stdio(标准输入输出流)进行本地进程通信,当一个进程(Agent)调用子进程时,通过 stdio 实现通信
- 基于 HTTP 进行远程进程通信

不管是本地工具还是远程工具,Agent 想跨进程调用某个工具,通过 MCP 协议就可以完成。MCP 出现的背景是:Tool 已经编写了很多,但只能在不同项目中使用不同语言不断复写。
MCP 是给 Model 扩展 Context 上下文,让它做得更多(Tool),知道得更多(Resource)的 Protocol。
环境准备与依赖
项目使用 LangChain 的 MCP 适配器:
bash
bash
pnpm i @langchain/mcp-adapters @modelcontextprotocol/sdk zod dotenv
@langchain/mcp-adapters:LangChain 官方提供的 MCP 适配器,让 LangChain Agent 可以连接 MCP Server@modelcontextprotocol/sdk:MCP 协议的标准 SDK,用于编写 Server 端zod:参数校验
MCP Server 端:注册 Tool 和 Resource
在 my-mcp-server.mjs 中,我创建了一个 MCP Server,注册了一个 Tool 和一个 Resource。
创建 Server 实例
javascript
javascript
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';
const server = new McpServer({
name: 'my-mcp-server',
version: '1.0.0'
});
注册 Tool:query-user
javascript
css
// 模拟数据库,实际项目可以换成真实的数据库连接
const database = {
users:{
'001':{ id:'001', name:'许', email:'x@qq.com', role:'admin' },
'002':{ id:'002', name:'锋', email:'f@qq.com', role:'user' },
'003':{ id:'003', name:'聂', email:'n@qq.com', role:'user' }
},
};
server.registerTool(
'query-user',
{
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.role}`
}
]
};
}
);
registerTool 接收三个参数:
- 工具名称(LLM 通过名称识别和调用)
- 配置对象:
description(告诉 LLM 这个工具能做什么)和inputSchema(用 Zod 定义参数结构) - 处理函数:异步执行查询逻辑,返回结果
在实际业务中,database 可以换成真实的数据库、API 调用或其他数据源。这里的处理逻辑是:如果查询到用户,返回用户信息;如果没查到,返回友好的错误提示,告诉调用方可用的 ID 有哪些。
注册 Resource:使用指南
javascript
php
server.registerResource(
'使用指南', // 资源名称
'docs://guide', // URI,MCP 协议中资源的唯一标识
{
description: 'MCP Server 使用指南',
mimeType: 'text/plain'
},
async () => {
return {
contents: [
{
uri: 'docs//guide',
mimeType: 'text/plain',
text: `
MCP Server 使用指南
功能:提供用户查询等工具。
使用:在 Cursor 等 MCP Client 中通过自然语言对话,Cursor 会自动调用相应工具。
`
}
]
};
}
);
Resource 和 Tool 的关键区别:
- Tool:可执行的函数,有副作用(查询、写入、操作外部世界)
- Resource:只读的数据/文档内容,作为上下文提供给 LLM,让 LLM "知道"更多信息
注册 Resource 需要指定一个 URI(docs://guide),Client 通过这个 URI 来读取资源内容。mimeType 告诉 Client 返回内容的格式,text/plain 表示纯文本。
启动 Server(stdio 通信)
javascript
ini
const transport = new StdioServerTransport();
await server.connect(transport);
StdioServerTransport 使用标准输入输出进行通信,适合本地进程间通信。启动后 Server 会进入"伺服状态",等待 Client 通过 stdin/stdout 发送请求。
MCP Client 端:MultiServerMCPClient 连接 Server
在 langchain-mcp-test.mjs 中,我用 LangChain 的 MultiServerMCPClient 连接 MCP Server。

创建 MCP Client
javascript
javascript
import { MultiServerMCPClient } from '@langchain/mcp-adapters';
const mcpClient = new MultiServerMCPClient({
mcpServers: {
'my-mcp-server': {
command: 'node',
args: ['D:/workspace/yjs_ai/ai/agent_in_action/mcp-demo/src/my-mcp-server.mjs']
}
}
});
MultiServerMCPClient 是 LangChain 提供的适配器,可以同时连接多个 MCP Server。每个 Server 通过 command + args 启动一个子进程------相当于 Client 进程会 fork 一个子进程来运行 Server。
获取 Tools 和 Resources
javascript
ini
const tools = await mcpClient.getTools();
const res = await mcpClient.listResources();
getTools() 返回一个 Tool 数组,可以直接用于 model.bindTools(tools)。这些 Tool 背后是通过 MCP 协议跨进程调用的------调用时 Client 通过 stdio 向 Server 发送请求,Server 执行后返回结果。

listResources() 返回一个对象,键是 Server 名称,值是 Resource 列表,结构如下:
javascript
css
{
'my-mcp-server': [
{ uri: 'docs://guide', name: '使用指南', description: '...' }
]
}
获取 Resources:将资源注入 SystemMessage
Resource 作为只读数据,最好的用途是作为 SystemMessage 的一部分,成为 LLM 的上下文。

javascript
ini
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;
}
}
// 将资源内容作为 SystemMessage
const messages = [
new SystemMessage(resourceContent),
new HumanMessage(query)
];
Object.entries() 把对象的 key 和 value 组成一个二维数组,key 在前面,value 在后面:
javascript
css
const obj = { 'bytedance': ['AI全栈开发','Agent工程师'], 'tecent': ['后端开发'] };
for (let [key, value] of Object.entries(obj)) {
console.log(key, value);
}
// 输出:bytedance ['AI全栈开发','Agent工程师']
// tecent ['后端开发']
这种遍历方式比 for...in 更清晰,因为 for...in 会遍历原型链上的属性,而 Object.entries() 只处理对象自身的可枚举属性。
Resource 和 RAG 的定位不同。RAG 适合大规模知识检索(向量数据库),Resource 适合小规模静态文档,因为 LLM 的上下文窗口有限,Resource 需要控制大小。
补充内容
for...in vs for...of 的区别
这里有两种方式遍历对象:
javascript
javascript
const obj = {
'bytedance': ['AI全栈开发','Agent工程师'],
'tecent': ['后端开发','Agent 工程师'],
'163': ['前端开发']
};
// 方式一:for...in
for (let key in obj) {
console.log(key, obj[key]);
}
// 方式二:for...of + Object.entries()
for (let [key, value] of Object.entries(obj)) {
console.log(key, value);
}
两者输出结果相同,但底层机制完全不同:
| 特性 | for...in |
for...of |
|---|---|---|
| 遍历内容 | 对象的可枚举属性名(包括原型链上的) | 可迭代对象的值 |
| 适用对象 | 普通对象 | 数组、字符串、Map、Set、类数组等可迭代对象 |
配合 Object.entries() |
不需要 | 需要,因为普通对象本身不可迭代 |
| 返回值 | 属性名(key) | 元素值(value) |
| 是否遍历原型链 | ✅ 会(需要用 hasOwnProperty 过滤) |
❌ 不会,只遍历自身元素 |
简单理解:
for...in是给对象用的,遍历的是 keyfor...of是给数组/可迭代对象用的,遍历的是 valueObject.entries(obj)将对象转换成[[key, value], ...]的二维数组,让它变成可迭代的
这就是为什么在遍历对象时,for...of + Object.entries() 更推荐------它只处理对象自身的属性,不会意外遍历到原型链上的属性。for...in 虽然写法简单,但如果不做过滤,可能拿到意想不到的属性。
Agent 循环调用跨进程 Tool
获取 Tools 后,绑定到模型,进入 Agent 循环:
javascript
ini
const modelWithTools = model.bindTools(tools);
async function runAgentWithTools(query, maxIterations = 30) {
const messages = [
new SystemMessage(resourceContent),
new HumanMessage(query)
];
for (let i = 0; i < maxIterations; i++) {
console.log(chalk.bgGreen(`正在等待AI思考,第${i}轮...`));
const response = await modelWithTools.invoke(messages);
messages.push(response);
if (!response.tool_calls || !response.tool_calls.length) {
console.log(`\nAI 最终回复: \n${response.content}`);
return response.content;
}
console.log(chalk.bgBlue(`检测到${response.tool_calls.length}个工具调用`));
console.log(chalk.bgBlue(`工具调用: ${response.tool_calls.map(t => t.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);
messages.push(new ToolMessage({
content: toolResult,
tool_call_id: toolCall.id
}));
}
}
}
return messages[messages.length - 1].content;
}
tools.find() 匹配成功时不会再往后遍历,返回第一个匹配项。这是 find 方法的特点,适合查找唯一工具。如果找不到对应的 Tool,foundTool 为 undefined,这里会跳过不处理。
这里的 tools 是通过 MCP 协议跨进程获取的,调用 foundTool.invoke() 时,LangChain 的适配器会通过 MCP 协议把请求发送到 Server 进程,Server 执行后返回结果。整个过程对 Agent 来说是透明的------它只知道调用了一个 Tool,不知道这个 Tool 在另一个进程里运行。
进程管理:为什么需要 close?
运行 node langchain-mcp-test.mjs 后,程序执行完任务后不会自动退出。
原因在于进程关系:
- 主进程(Client) :运行
langchain-mcp-test.mjs - 子进程(Server) :由 Client 通过
command: 'node'启动,运行my-mcp-server.mjs
MCP Server 是一个"伺服"进程------启动后会一直等待请求,不会主动退出。主进程完成任务后,由于子进程还在运行,主进程不会自动退出(需要等待子进程结束)。
解决方案是调用 mcpClient.close():
javascript
csharp
await mcpClient.close();
close() 会关闭所有子进程与通信通道,释放进程资源。如果忘记调用,脚本会一直挂着不退出,占用内存和进程资源。
一点总结
通过这个项目,我理解了:
- 本地 Tool 的局限性:只能在当前项目使用,不能跨语言、跨进程复用。
- MCP 协议:标准化了 LLM 与 Tool/Resource 的通信,实现解耦。
- MCP Server:可以注册 Tool 和 Resource,通过 stdio(本地)或 HTTP(远程)通信。
- MultiServerMCPClient:LangChain 提供的适配器,让 Agent 可以连接多个 MCP Server。
- Resource 作为上下文 :通过
listResources+readResource获取文档内容,注入 SystemMessage。 - 进程管理 :MCP Server 是常驻进程,需要主动
close()释放资源。 Object.entries()遍历对象 :比for...in更清晰,只处理自身属性。
互动讨论
- 本地 Tool 和 MCP Tool 的核心区别是什么? 为什么 MCP 能实现跨进程调用?
- Tool 和 Resource 在 MCP 中的定位有什么不同? 分别适合什么场景?
Object.entries()和for...in遍历对象的区别是什么? 为什么Object.entries()更推荐?- 为什么 MCP Server 需要
close()才能让进程退出? 如果不关闭会有什么后果? - RAG 和 MCP Resource 都可以给 LLM 提供知识,它们各自适合什么场景?
📌 一点心得:MCP 让 Tool 从"项目内部函数"变成了"可独立部署的服务单元"。这种解耦不仅提升了复用性,还让 Agent 可以跨语言、跨进程调用工具,真正实现了工具的"资产化"。