我们已经写了一些 Tool 了:读写文件和目录、执行命令。
只要声明 Tool 的名字、描述、参数格式,模型会在发现需要用到 Tool 的时候自动解析出参数传进来调用,然后把执行结果封装成 ToolMessage 传入 chat。
比如上节我们实现了简易的 Cursor:声明了读写文件和目录、执行命令的 Tool,这样你让大模型创建 react + vite 项目,它就会自动判断什么时候调用哪个 Tool,自动实现目录、文件的创建,以及 pnpm install 和 pnpm run dev 的执行。
我们只是告诉它要创建的项目,然后安装依赖跑起来。这些 Tool 怎么调用、参数是什么,都是大模型自己决定的。
Tool 给大模型扩展了做事情的能力——本来它只能思考,不能做事情,现在可以自己调用 Tool 来帮你做事情了。
但你会发现 Tool 有个问题:Node 写的 AI Agent 代码,你的 Tool 也得是 Node 写。如果你之前有一些工具是 Java、Python、Rust 写的呢?你想封装成 Tool 怎么办?
从跨语言到统一协议
有同学说:现在不是可以执行命令么,通过单独进程把这些其他语言写的代码跑一下就行了啊。
确实,也就是这样:当进程跑一个子进程,就可以用 stdio(标准输入输出流,也就是键盘输入、控制台输出)这种方式通信。
还有的同学说:简单,用 HTTP 啊!本地跑个服务就好了。
也就是这样:通过 HTTP 连接远程服务进程。
现在解决了跨语言调用工具的问题。但新的问题来了:如果每个人都这样搞,它们提供的服务都不一样,我想接入别的 Tool,是不是要了解每个服务都是怎么定义的呢?
能不能定义一个统一的通信协议,我们都按照这个格式来沟通? 这样所有的跨进程工具调用就都可以接入了。
想跨进程调用某个工具,通过这个协议通信就行:本地工具直接跑那个进程,然后 stdio 通信;远程工具通过 HTTP 连接远程服务进程。
这个协议叫什么?它是给 Model 扩展 Context 上下文、让它能做的更多、知道的更多的 Protocol 协议——就叫 MCP 吧。
恭喜你,你发明了 MCP!
什么是 MCP
MCP 最大的特点就是可以跨进程调用工具:
- 跨本地的进程调用,用 stdio
- 跨远程的进程调用,用 HTTP
提到 MCP 都会提到这张经典的架构图:

你的 AI Agent 就是 MCP 客户端,可以通过 MCP 协议调用各种 MCP Server,实现跨进程的工具调用。
当然,在 LangChain 里,它也是 Tool——只不过是 Tool 的一种而已:你在 Tool 的函数里,调用下 MCP Client,访问下远程 MCP Server。它本质上还是 Tool,但是集成了 MCP 工具。
MCP 由 AI 巨头 Anthropic 公司发起并开发,2025 年 12 月交给了 Linux 基金会维护。也就是说,它现在是完全中立、不绑定任何一家模型的行业通用协议。
有一个很形象的类比:USB-C 是接口标准,不同设备可以通过 USB-C 接入电脑;MCP 是 AI 工具接口标准,不同工具服务可以通过 MCP 接入 AI 应用。MCP 让 AI 应用可以通过标准协议,调用运行在另一个进程里的工具。
大概知道 MCP 是啥就行,我们自己写个 MCP 服务就明白了。继续在 tool-test 这个项目里写:
pnpm install @modelcontextprotocol/sdk从包名就可以看出来,它是中立、不属于任何一家公司的。
写一个 MCP Server
创建 src/my-mcp-server.mjs:
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: 'zhangsan@example.com', role: 'admin' },
'002': { id: '002', name: '李四', email: 'lisi@example.com', role: 'user' },
'003': { id: '003', name: '王五', email: 'wangwu@example.com', role: 'user' },
}
};
const server = new McpServer({
name: 'my-mcp-server',
version: '1.0.0',
});
// 注册工具:查询用户信息
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: `用户信息:\n- ID: ${user.id}\n- 姓名: ${user.name}\n- 邮箱: ${user.email}\n- 角色: ${user.role}`,
},
],
};
});
// 注册资源:静态文档
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 会自动调用相应工具。`,
},
],
};
});
const transport = new StdioServerTransport();
await server.connect(transport);代码很容易看懂:
new McpServer创建了 MCP Server 实例server.registerTool注册了一个工具,声明name、description、schemaserver.registerResource注册了一个资源,就是静态数据
和我们写 Tool 的时候差不多,只不过这里分了 resource 和 tool:resource 一般返回静态数据(用来查询信息,read),tool 来做一些事情(执行功能,call)。
最后,可以提供 stdio 的本地进程调用方式,也可以提供 HTTP 的远程调用方式。这里用的是 stdio 的传输方式(Transport)。
这样我们的 MCP 服务就创建好了——是不是很简单?其实就是 Tool,加上了协议而已。
在 Cursor 里配置测试
我们在 Cursor 里配置下这个 MCP Server:
{
"mcpServers": {
"my-mcp-server": {
"command": "node",
"args": ["/path/to/tool-test/src/my-mcp-server.mjs"]
}
}
}配置好之后测试下,让 Cursor 查询一个用户的信息,确实能检测到这个 MCP 然后调用。
Cursor 有个坑注意下:点一下 Tool 是禁用,再点一下是启用。但 Cursor 这个状态颜色区分不明显——如果你没有调用 MCP 工具,可能是不小心关掉了。
这就是 MCP 的好处:写好之后可以插拔到任何地方当 Tool 用。因为有了 MCP,除了 Cursor,别的软件同样可以调用这个服务。
在 LangChain 里调用 MCP Server
用这个包:
pnpm install @langchain/mcp-adapters创建 src/langchain-mcp-test.mjs:
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';
const model = new ChatOpenAI({
modelName: "qwen-plus",
apiKey: process.env.OPENAI_API_KEY,
configuration: {
baseURL: process.env.OPENAI_BASE_URL,
},
});
const mcpClient = new MultiServerMCPClient({
mcpServers: {
'my-mcp-server': {
command: "node",
args: [
"/Users/guang/code/tool-test/src/my-mcp-server.mjs"
]
}
}
});
const tools = await mcpClient.getTools();
const modelWithTools = model.bindTools(tools);
async function runAgentWithTools(query, maxIterations = 30) {
const messages = [
new HumanMessage(query)
];
for (let i = 0; i < maxIterations; i++) {
console.log(chalk.bgGreen(`⏳ 正在等待 AI 思考...`));
const response = await modelWithTools.invoke(messages);
messages.push(response);
// 检查是否有工具调用
if (!response.tool_calls || response.tool_calls.length === 0) {
console.log(`\n✨ AI 最终回复:\n${response.content}\n`);
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;
}
await runAgentWithTools("查一下用户 002 的信息");
await mcpClient.close();我们用 @langchain/mcp-adapters 创建了 MCP Client,写法跟在 Cursor 里配置一样:用命令行启动这个进程,之后用 stdio 的方式做通信。
拿到 tools 之后绑定到模型。模型调用返回 tool_calls 消息,需要自己调用 Tool,调用完通过 ToolMessage 封装返回的消息,继续调用——这个循环我们写过很多次了。
调用试试:你让大模型查询用户,它识别到了工具调用,然后调用了 MCP 的工具。
这里进程没退出,因为你跑了一个子进程作为 MCP Server,需要把它关掉:
await mcpClient.close();Resource 怎么用
那种静态信息可以放到 SystemMessage 里。先查一下 resource:
const res = await mcpClient.listResources();
console.log(res);遍历依次读取 uri 内容:
const res = await mcpClient.listResources();
for (const [serverName, resources] of Object.entries(res)) {
for (const resource of resources) {
const content = await mcpClient.readResource(serverName, resource.uri);
console.log(content);
}
}然后把它放到 SystemMessage 里作为上下文就好了:
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 messages = [
new SystemMessage(resourceContent),
new HumanMessage(query)
];调用下试试(await runAgentWithTools("MCP Server 的使用指南是什么")),现在大模型就知道这个 resource 的信息,可以用来回答问题了。
Resource 可以用在 SystemMessage 里,也可以用在 HumanMessage 里,总之是作为信息引用的。我们主要还是用 MCP 的 tools。
这样,我们就写了一个 MCP Server,并分别在 Cursor、LangChain 里用上了它。
常见问题
大模型怎么知道要调用这个 tool? 启动 MCP 连接后,会自动读取 MCP Server 下每个 tool 的信息(description 和 schema),绑定到模型,和之前写 tool 然后 model.bindTools() 一样。
MCP Server 文件里没 export 任何东西,为什么能拿到工具? 因为 MCP 是通过进程通信(stdio)来交互的:你的代码启动了一个子进程运行 MCP Server,双方按协议在标准输入输出上通信,不需要模块导入。
MCP Server 只能用 Node 写吗? 任何语言都可以,MCP 是跨语言协议,Python、Java、Rust 都有对应的 SDK,这也是它最大的价值。
接入现成 MCP Server 有哪几种方式? 三种:本地调用(command 写开发语言如 node/python,args 填绝对路径)、安装使用(command 写 npx/uvx,args 填包名)、远程调用(直接配 url 连接远程服务)。
报 CallToolResultContentSchema 相关错误? 一般是 @langchain/mcp-adapters 版本和 Node 版本不匹配,降级到 0.4.0 或者升级 Node 版本即可。
总结
这节我们学了 MCP,它是可跨进程调用的 Tool:
- 可以是本地进程,用 stdio 进程通信
- 可以是远程进程,用 HTTP 通信
- 在 LangChain 里用
@langchain/mcp-adapters封装成 tools 来用,其实和其他 tool 没区别 - 跨进程就意味着不限语言,开发好之后可以被任意 MCP Client 调用,比如 Cursor、LangChain 等
当然,MCP 本质上还是 Tool,和之前的 Tool 的区别只不过是可以跨进程调用:当你不需要跨进程用的时候,还是之前那样写更好,还少了进程通信的成本。
除了自己写 MCP Server,现在也有很多现成的 MCP Server 可以直接用,下一节我们来用一下。
