你有没有遇到过这样的窘境:用 LangChain 写了一堆 Tool,结果发现只能在当前项目里用?换个项目就得重新拷一遍?更糟糕的是,团队里有人用 Java、有人用 Python,大家的 Tool 完全没法共享?
这篇笔记,我们就从"传统 Tool 的痛点"出发,一步步搞懂 MCP 协议是如何解决这些问题的,并附上真实可跑的代码示例。
一、先看看我们现在写的 Tool 有什么问题?
在接触 MCP 之前,我们写 Tool 的方式大概是这样的:直接在项目里用 @langchain/core/tools 的 tool() 函数注册工具,然后和模型 bindTools 绑定完事。
javascript
// all-tools.mjs 片段
import { tool } from '@langchain/core/tools';
import fs from 'node:fs/promises';
const readFileTool = tool(
async({ filePath }) => {
const content = await fs.readFile(filePath, 'utf-8');
return content;
},
{
name: 'read_file',
description: '用此工具来读取文件内容...',
schema: z.object({
filePath: z.string().describe('要读取的文件路径')
})
}
)
// ... 还有 writeFileTool, listDirectoryTool, executeCommandTool
export { readFileTool, writeFileTool, ... };
这样写能用,但有两个致命问题:
问题 1:只能在当前项目用,不能复用
这些 Tool 跟 LangChain 的 tool() 强绑定,直接写在项目里。换一个项目(比如不用 LangChain、或者用 Python 的 LangChain),这些 Tool 就没法直接用了------你得拷代码、改依赖、改语法......
问题 2:只能用 Node.js 写,跨语言?想都别想
上面的 Tool 是 JavaScript 写的。那如果我有一个用 Rust 写的高性能日志分析工具、或者一个用 Python 写的机器学习推理脚本,怎么让 LLM 调用?传统做法是:你得在 Node 里用 child_process 去 spawn 子进程,自己处理输入输出、自己封装协议。各种边界情况全靠自己踩坑。
结论:Tool 应该独立于 LLM
理想状态下,Tool 应该是一个独立的服务------不管 LLM 用什么框架(LangChain/ LlamaIndex/ 自己写的 Agent)、不管用什么语言(JS/Java/Python/Rust)、不管在本地还是远程,都能统一调用。
这,就是 MCP 协议要做的事。
二、MCP 协议到底是什么?
MCP = Model Context Protocol,模型上下文协议。一句话总结:
MCP 是一个标准化 LLM 与 Tool / Resource 之间通信 的协议,目的是把 LLM 和 Tool 彻底解耦。
MCP 的核心思想:通信方式只有两种
MCP 不搞复杂的定制协议,它就用两种最通用的方式来通信:
| 通信方式 | 适用场景 | 说明 |
|---|---|---|
| stdio(标准输入输出流) | 本地跨进程调用 | 就是键盘输入、控制台输出那套东西。Agent(父进程)启动一个子进程(比如一个 node 脚本 / python 脚本),双方通过 stdin / stdout 传递 JSON 消息。 |
| HTTP | 远程跨进程调用 | 工具部署在远端服务器,Agent 通过 HTTP 请求调用。MCP 掌管统一的接口格式。 |
所以不管是:
- 本机一个 Node 写的 Tool
- 本机一个 Python 写的 Tool
- 局域网另一台机器上用 Rust 写的 Tool
- 云上部署的 Java Tool Service
只要它们都遵守 MCP 协议,Agent 就能统一调用。
MCP 在"拓展什么"?
MCP 的全称里有个关键词------Context(上下文)。它不是在给 LLM 加 API 接口,而是在拓展 LLM 的"上下文能力":
- Tool(工具) :让 LLM 能做的更多(比如查数据库、读文件、调用外部系统)
- Resource(资源) :让 LLM 知道的更多(比如把一份文档、一份配置直接注入上下文)
三、MCP 的最大特点:跨进程调用工具
这是 MCP 最核心的价值。我们用一张图来理解:
scss
┌──────────────────────────────────────────────────┐
│ AI Agent (Host) │
│ (LangChain / Claude Code / Trae / Cursor 等) │
│ │
│ MultiServerMCPClient(管理多个 MCP Server) │
└──────────┬───────────────────┬───────────────────┘
│ stdio │ HTTP
▼ ▼
┌──────────────────┐ ┌──────────────────────────┐
│ MCP Server #1 │ │ MCP Server #2 │
│ (Node 子进程) │ │ (远程 HTTP 服务) │
│ - query_user │ │ - 高复杂度计算工具 │
│ - 使用指南资源 │ │ - 企业内部系统对接 │
└──────────────────┘ └──────────────────────────┘
│ stdio
▼
┌──────────────────────────┐
│ MCP Server #3 │
│ (Python / Rust 子进程) │
│ - 机器学习推理 / 高性能 │
└──────────────────────────┘
AI Agent 作为 MCP Host(客户端),通过 clients 配置可以同时连接多个 MCP Server。每个 Server 可以是:
- 本地子进程:通过 stdio 通信(Node / Python / Rust 啥都行)
- 远程服务:通过 HTTP 通信
而这一切,对 LLM 来说完全透明------它只需要知道"我有这些 Tool 可以用",至于 Tool 在哪个进程、用什么语言写的、在本地还是远程,它根本不关心。
和普通的 API 调用(fetch)有什么区别?
fetch 是"拿接口数据",返回的是原始 JSON,LLM 不会主动去调用 fetch,你得在 prompt 里或者代码里明确告诉它什么时候调。
而 MCP 的 Tool:
- 自动注册到 LLM 的 tools 列表里
- LLM 自主决策什么时候调用、调哪个、传什么参数
- 返回结果自动注入下一轮对话上下文
MCP 不是一个简单的接口封装,它是直接给 Model 拓展 Context 能力的协议层。
四、MCP Tool 实战:写一个用户查询服务
光说不练假把式,我们直接看项目里的真实代码 my-mcp-server.mjs。
4.1 写 MCP Server(工具提供方)
javascript
// 1. 引入 MCP SDK(注意 SDK 1.30.0 的坑,要用子路径导入)
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';
// 2. 假数据(未来可以换成真实数据库查询)
const database = {
users: {
'001': { id: '001', name: '小a', email: 'a@qq.com', role: 'admin' },
'002': { id: '002', name: '小b', email: 'b@qq.com', role: 'user' },
'003': { id: '003', name: '小c', email: 'c@qq.com', role: 'user' },
}
}
// 3. 实例化 MCP Server
const server = new McpServer({
name: 'my-mcp-server',
version: '1.0.0',
});
// 4. 注册 Tool:query_user
server.registerTool('query_user', {
description: '查询数据库中的用户信息。输入用户ID,返回该用户的详细信息',
inputSchema: {
userId: z.string().describe('用户ID,例如:001,002,003')
}
}, async (args) => {
const userId = typeof args === 'string' ? args : args.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}`
}]
}
})
// 5. 用 stdio 方式启动(跨进程通信的核心!)
const transport = new StdioServerTransport();
await server.connect(transport);
就这么简单!注意几个关键点:
registerTool:注册工具,参数依次是工具名、描述、zod 参数 Schema、处理函数- 返回格式固定 :必须是
{ content: [{ type: 'text', text: '...' }] },这是 MCP 协议规定的 StdioServerTransport:启动后,这个脚本就变成了一个"从 stdin 读请求、往 stdout 写响应"的服务。它自己不监听端口------是 Host(Agent)启动它当子进程,通过标准流通信。
4.2 写 MCP Host(工具调用方,即 Agent)
然后看 Agent 端怎么用
javascript
import { MultiServerMCPClient } from '@langchain/mcp-adapters';
import { ChatOpenAI } from '@langchain/openai';
// 1. 初始化模型
const model = new ChatOpenAI({
modelName: 'deepseek-v4-flash',
apiKey: process.env.DEEPSEEK_API_KEY,
baseURL: 'https://api.deepseek.com/v1',
});
// 2. 配置 MCP Client------关键!告诉 Agent 有哪些 Server 可以用
const mcpClient = new MultiServerMCPClient({
mcpServers: {
'my-mcp-server': {
command: 'node',
args: ['C:/Users/xhj/Desktop/db_ai/xhj_ai/ai/agent_in_action/mcp-demo/src/my-mcp-server.mjs']
}
// ★ 这里可以继续加!
// 'python-mcp-server': { command: 'python', args: ['./my_tool.py'] },
// 'rust-mcp-server': { command: 'cargo', args: ['run', '-p', 'my-mcp'] },
// 'remote-mcp-server': { url: 'https://mcp.example.com/sse' },
}
});
// 3. 从所有 Server 获取 Tools
const tools = await mcpClient.getTools();
// 4. 把 Tools 绑定到模型上
const modelWithTools = model.bindTools(tools);
// 5. Agent 推理循环
async function runAgentWithTools(query, maxIterations = 30) {
const messages = [new HumanMessage(query)];
for (let i = 0; i < maxIterations; i++) {
const response = await modelWithTools.invoke(messages);
messages.push(response);
// 模型判断:不需要调工具 → 直接出最终答案
if (!response.tool_calls?.length) {
console.log(`最终回复:${response.content}`);
return;
}
// 模型判断:要调工具 → 自动找到对应 Tool 执行,结果回灌上下文
for (const toolCall of response.tool_calls) {
const foundTool = tools.find(t => t.name === toolCall.name);
const toolResult = await foundTool.invoke(toolCall.args);
messages.push(new ToolMessage({
content: toolResult,
tool_call_id: toolCall.id // ★ 必须带,不然模型不知道对应哪个调用
}));
}
}
}
// 使用!
await runAgentWithTools('查询用户002的信息');
// 用完要关,不然进程挂着
await mcpClient.close();
运行流程拆解:
MultiServerMCPClient根据配置,自动启动子进程node my-mcp-server.mjs(底层就是 Node 的child_process.spawn)- 父子进程通过 stdio 建立 MCP 协议通信
- Agent 通过
getTools()拿到所有注册的工具(内部通过 MCP 协议发tools/list请求) - 模型推理时自动决定是否调用工具,调用结果通过
ToolMessage回灌 - 最终模型综合工具返回结果,生成回答
4.3 另一个 MCP Server 示例:读文件工具
项目里还有一个写得非常详细的版本 server.js,用的是新版 server.tool() 语法(和 registerTool 功能等价,更简洁):
javascript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import fs from 'fs/promises';
const server = new McpServer({
name: 'simple-read-mcp',
version: '1.0.0'
});
// 新版语法:server.tool(name, desc, schema, handler)
// 一行顶旧版 60+ 行代码
server.tool(
"read_file",
"读取指定路径的本地文件内容",
{
path: z.string().describe("文件的绝对或相对路径")
},
async ({ path }) => {
try {
const content = await fs.readFile(path, 'utf-8');
return { content: [{ type: "text", text: content }] };
} catch (err) {
return {
isError: true,
content: [{ type: "text", text: `读取文件失败:${err.message}` }]
};
}
}
);
// ★ 调试日志必须打 stderr!stdout 是 MCP 协议通道
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("MCP read_file 服务已启动(stdio模式)");
}
main().catch(console.error);
⚠️ 踩坑提醒 :调试日志用
console.error(输出到 stderr),千万别用console.log!因为 stdout 被 MCP 协议占了,你打非协议内容上去,Client 解析 JSON-RPC 会直接炸掉。
五、MCP Resource:除了 Tool,还能给 LLM "喂知识"
MCP 不止 Tool,还有一个重要概念:Resource(资源) 。Resource 的作用是------把静态内容作为 SystemMessage 的一部分,直接注入 LLM 的上下文。
5.1 怎么注册 Resource?
javascript
// Server 端注册 Resource
server.registerResource(
'使用指南', // 名称
'docs://guide', // URI(自定义协议头,比如 docs://)
{
description: 'MCP Server 使用指南',
mimeType: 'text/plain'
},
async () => {
return {
contents: [{
uri: 'docs://guide',
mimeType: 'text/plain',
text: `
MCP Server 使用指南
功能:提供用户查询等工具。
使用:在 Cursor 等 MCP Client 中通过自然语言对话即可。
`
}]
}
}
)
5.2 Host 端怎么用 Resource?
在 Agent 启动时,把所有 Resource 的内容读出来,拼成 SystemMessage:
javascript
// langchain-mcp-test.mjs 片段
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;
}
}
console.log(resourceContent, '---------------');
// ★ 关键:作为 SystemMessage 注入上下文!
const messages = [
new SystemMessage(resourceContent),
new HumanMessage(query)
];
5.3 Resource 和 RAG 有什么区别?
这是个非常好的问题。它们都是"给 LLM 加知识",但定位不同:
| 维度 | MCP Resource | RAG |
|---|---|---|
| 数据规模 | 小而精的文档(比如使用指南、配置说明、API 文档) | 大规模知识库(百万级文档) |
| 触发时机 | Agent 启动时全部加载进 SystemMessage | 对话时根据用户问题实时检索,只取相关片段 |
| 上下文窗口 | 内容不能太长,否则撑爆 Token 限制 | 不受知识库大小限制,每次只塞入最相关的几条 |
| 使用场景 | 工具的使用说明、系统的基础配置、固定的业务规则等 | 长尾问答、企业知识库搜索等 |
一句话 :Resource 是"我一启动就该知道的常识",RAG 是"用户问了我才去查的大百科"。两者完全可以搭配使用。
六、踩过的坑 & 注意事项
6.1 MCP SDK 1.30.0 的入口 Bug
项目依赖的 @modelcontextprotocol/sdk@1.30.0 有个发布缺陷:主入口文件 ./dist/esm/index.js 缺失。所以不能用默认导入:
javascript
// ❌ 会报 ERR_MODULE_NOT_FOUND
import { McpServer } from '@modelcontextprotocol/sdk';
// ✅ 必须用子路径导入
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
6.2 pnpm 装不上依赖?换 npm
项目用 pnpm 时即使显示 Already up to date,node_modules 也可能没生成(linker/cache 的坑)。直接换 npm install 就完事。
6.3 stdio 模式下的进程管理
mcpClient.close() 千万不能忘!因为每个 MCP Server 都是一个独立的子进程,不手动 close 会导致脚本一直挂着不退出。
七、总结:MCP 的价值是什么?
回顾一下我们开头提出的两个问题:
| 痛点 | MCP 的解法 |
|---|---|
| Tool 只能在当前项目用,不能复用 | Tool 封装成独立的 MCP Server,任何支持 MCP 协议的 Agent 都能直接用 |
| 只能用 Node 写 Tool,跨语言难 | 通过 stdio 子进程通信,Python / Rust / Java / Go 写的程序都能包装成 MCP Server |
MCP 的本质是标准化:它没有发明什么新技术(stdio 和 HTTP 都是几十年的老东西了),但它定义了一套 LLM 和工具之间的"通用语言"。
有了 MCP 之后:
- 前端用 Node 写了一个文件操作工具 → 全团队的 LLM Agent 都能用
- 算法组用 Python 写了一个推理脚本 → 包个 MCP Server 即可接入
- 运维组用 Rust 写了个日志分析工具 → 同样只要实现 MCP 协议
Tool 不再是某个项目的私有财产,而是整个团队可共享的能力资产。
下一步行动
- 试着把项目里
all-tools.mjs的read_file/write_file工具改写成独立的 MCP Server - 试试用 Python 写一个 MCP Server(官方 SDK 支持 Python),用同一个 Agent 去调用它
- 试试在 Trae / Cursor 这类原生支持 MCP 的 IDE 里配置你的 MCP Server,直接和 AI 对话调用工具
欢迎在评论区分享你的 MCP 实践经验和踩过的坑