MCP 跨进程工具调用深度解析:从 LangChain Tool 的局限到自定义数据库查询 Server 实战
之前用 LangChain 的
tool()注册的工具,只能在本项目里用。如果你想用 Java 写一个数据库查询工具、用 Python 写一个数据分析工具,然后让同一个 AI Agent 调用它们------怎么办?MCP 协议就是答案。本文从 Tool 的局限性出发,深入解析 MCP 跨进程通信的本质,并用 45 行代码实现一个完整的数据库查询 MCP Server。
前言
在前面的文章中,我们用 LangChain 的 tool() 函数注册了 read_file、execute_command 等工具。它们工作得很好,但有一个根本性局限------只能在同一个 Node.js 进程中运行。
这意味着:
- 你用 Java 写的数据库连接工具,Node.js Agent 调不了
- 你用 Python 写的数据分析脚本,Claude Code 用不了
- 你在 A 项目里写的工具,B 项目里不能直接复用
MCP(Model Context Protocol) 正是为了解决这个问题而诞生的。它让 AI Agent 能够跨进程、跨语言地调用任何工具。
一、LangChain Tool 的局限性
1.1 回顾:LangChain tool() 的工作方式
javascript
import { tool } from '@langchain/core/tools';
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() })
}
);
// 工具直接注册到模型
const modelWithTools = model.bindTools([readFileTool]);
1.2 三个核心问题
| 问题 | 说明 |
|---|---|
| 项目绑定 | read_file 只能在当前 Node.js 项目中使用,换一个项目就得重写 |
| 语言绑定 | 只能用 JavaScript/TypeScript 编写,无法调用 Java/Python/Rust 工具 |
| 进程绑定 | 工具函数和 LLM 运行在同一个进程中,无法独立部署、独立扩展 |
yaml
LangChain Tool 的局限:
Agent(Node.js 进程)
├── tool1: read_file ← 写死在代码里
├── tool2: write_file ← 写死在代码里
└── tool3: exec_command ← 写死在代码里
想加一个 Java 工具?→ 不行,进程语言是 Node.js
想在另一个项目复用?→ 不行,代码耦合在项目里
1.3 理想方案:工具独立于 LLM
css
理想架构:
AI Agent(任意语言/框架)
↓ MCP 协议
Tool A(Node.js)── 查询数据库
Tool B(Python)─── 数据分析
Tool C(Java)───── 调用内部 API
Tool D(Rust)───── 高性能计算
工具独立开发、独立部署、独立运行
Agent 通过统一协议调用,不关心工具用什么语言写的
二、MCP 协议:跨进程工具调用的标准化方案
2.1 MCP 的核心定义
ini
MCP = Model Context Protocol(模型上下文协议)
目标:标准化 LLM 与 Tool / Resource 之间的通信
本质:让 LLM 和 Tool 解耦
MCP 不是工具、不是框架、不是 SDK------它是一个通信协议,就像 HTTP 是浏览器和服务器之间的通信协议一样。
2.2 MCP 的两种通信方式
| 通信方式 | 适用场景 | 底层技术 |
|---|---|---|
| stdio | 本地跨进程调用 | 标准输入输出流(stdin/stdout) |
| HTTP/SSE | 远程跨进程调用 | 网络请求 |
stdio 本地通信
c
AI Agent(父进程)
↓ spawn 启动子进程
MCP Server(子进程)
├── 通过 stdin 接收 Agent 的请求(JSON-RPC)
└── 通过 stdout 返回结果(JSON-RPC)
底层原理 :当 Agent 启动一个 MCP Server 子进程时(类似 child_process.spawn),两者通过标准输入输出流通信。Agent 向子进程的 stdin 写入 JSON-RPC 消息,子进程处理后将结果写入 stdout。
HTTP 远程通信
arduino
AI Agent(客户端)
↓ HTTP 请求
远程 MCP Server(服务器)
├── 接收请求
├── 调用工具
└── 返回结果
2.3 MCP 和 fetch 的区别
笔记中有一个精准的区分:
| 维度 | fetch / HTTP API | MCP |
|---|---|---|
| 目的 | 获取数据(接口调用) | 扩展 Context(工具 + 资源) |
| 协议 | HTTP + REST | JSON-RPC over stdio/HTTP |
| 通信方向 | 单次请求-响应 | 持续的双向通信 |
| 能力 | 只能传数据 | 可以注册工具、暴露资源、持续交互 |
MCP 不是"调接口拿数据",而是"给模型装上新的能力"------它扩展的是 Context(上下文),让 LLM 能做的更多(Tool)、知道的更多(Resource)。
2.4 MCP 的本质
MCP 的本质 = 给 Model 扩展 Context
Tool(工具) → 让 LLM 能做更多(读文件、查数据库、发邮件)
Resource(资源)→ 让 LLM 知道更多(内部文档、数据、配置)
三、实战:自定义数据库查询 MCP Server
3.1 项目结构
perl
my-mcp-server/
├── my-mcp-server.mjs # MCP Server 代码(45行)
├── package.json
└── .mcp.json # Claude Code 接入配置
3.2 完整代码
my-mcp-server.mjs:
javascript
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: 'zh@qq.com', role: 'admin' },
'002': { id: '002', name: '光光', email: 'gg@qq.com', role: 'user' },
'003': { id: '003', name: '小红', email: 'xh@qq.com', role: 'user' },
}
};
// 创建 MCP Server 实例
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: `用户 ${user.id} 的信息是:
姓名:${user.name}, 邮箱:${user.email}, 角色:${user.role}`
}
]
};
});
// 启动 stdio 传输
const transport = new StdioServerTransport();
await server.connect(transport);
3.3 代码逐行解析
第一步:模拟数据库
javascript
const database = {
users: {
'001': { id: '001', name: '祖豪', email: 'zh@qq.com', role: 'admin' },
// ...
}
};
这里用内存对象模拟数据库。实际项目中,可以替换为 MySQL、PostgreSQL、MongoDB 等真实数据库连接。MCP Server 的魅力就在于此------底层实现可以随意替换,对 Agent 来说接口不变。
第二步:注册工具(registerTool vs tool)
注意这里用的是 server.registerTool,不是 LangChain 的 tool():
| 对比项 | LangChain tool() |
MCP server.registerTool() |
|---|---|---|
| 注册位置 | LangChain 模型绑定 | MCP Server 实例 |
| 调用方式 | 同进程函数调用 | 跨进程 JSON-RPC |
| 语言限制 | JavaScript only | 任何语言(有 MCP SDK 即可) |
| 复用性 | 项目内 | 跨项目 |
第三步:输入参数
javascript
inputSchema: {
userId: z.string().describe('用户ID, 例如:001, 002, 003')
}
Zod Schema 定义了工具接受的参数类型和描述。MCP SDK 会将其自动转换为 JSON Schema 发送给 LLM。
第四步:返回格式
javascript
return {
content: [
{ type: 'text', text: '...' }
]
};
MCP 标准返回格式:content 数组,每个元素包含 type 和 text(或 image)。
3.4 接入 Claude Code
.mcp.json:
json
{
"mcpServers": {
"my-db": {
"type": "stdio",
"command": "node",
"args": ["/path/to/my-mcp-server.mjs"]
}
}
}
使用方式:
perl
⟩ 使用 my-db 工具查询用户 001 的信息
Claude Code 会自动:
- 分析请求 → 选择
my-dbMCP Client - 生成调用参数
{ "userId": "001" } - 通过 stdin 发送给我们的 MCP Server
- Server 查询数据库 → 返回结果
- LLM 基于结果生成回答
四、LangChain Tool vs MCP Tool 对比
4.1 架构对比
arduino
LangChain Tool(同进程):
┌─────────────────────────────┐
│ Node.js 进程 │
│ │
│ LLM ←→ tool() ←→ 函数体 │
│ 全部在一个进程内 │
└─────────────────────────────┘
MCP Tool(跨进程):
┌─────────────────────┐ ┌─────────────────────┐
│ Agent 进程(Host) │ │ MCP Server 进程 │
│ │ │ │
│ LLM ←→ MCP Client │ ←──→ │ MCP Server ←→ 工具 │
│ │ stdio│ │
└─────────────────────┘ └─────────────────────┘
4.2 完整对比表
| 维度 | LangChain Tool | MCP Tool |
|---|---|---|
| 通信方式 | 同进程函数调用 | 跨进程 JSON-RPC |
| 语言支持 | JavaScript only | Node.js / Python / Java / Rust ... |
| 复用性 | 项目内绑定 | 跨项目、跨 Agent 复用 |
| 部署 | 随 Agent 一起部署 | 独立部署、独立运行 |
| 数据源 | 内存中的函数 | 任意(数据库、API、文件) |
| 适用场景 | 快速原型、简单工具 | 生产环境、复杂工具链 |
4.3 何时用哪种?
| 场景 | 推荐 |
|---|---|
| 项目 Demo / 快速验证 | LangChain tool() |
| 只需在本项目使用的简单工具 | LangChain tool() |
| 需要跨项目复用的工具 | MCP Server |
| 需要用其他语言编写的工具 | MCP Server |
| 需要独立部署的数据库/API 工具 | MCP Server |
| 多个 Agent 共享同一套工具 | MCP Server |
五、从内存数据库到真实数据库
5.1 替换为 MySQL
javascript
import mysql from 'mysql2/promise';
const pool = mysql.createPool({
host: 'localhost',
user: 'root',
password: 'password',
database: 'my_app'
});
server.registerTool('query_user', {
description: '查询 MySQL 数据库中的用户信息',
inputSchema: {
userId: z.string().describe('用户ID')
}
}, async (userId) => {
const [rows] = await pool.execute(
'SELECT id, name, email, role FROM users WHERE id = ?', [userId]
);
if (rows.length === 0) {
return { content: [{ type: 'text', text: `用户 ${userId} 不存在` }] };
}
const user = rows[0];
return {
content: [{
type: 'text',
text: `姓名:${user.name}, 邮箱:${user.email}, 角色:${user.role}`
}]
};
});
关键点 :MCP Server 的接口(注册工具、返回格式)不变,只是底层实现从内存对象换成了 MySQL 连接。Agent 完全感知不到这个变化------这就是协议解耦的威力。
知识树
vbscript
MCP 跨进程工具调用深度解析
├── LangChain Tool 的局限
│ ├── 项目绑定(只能在本项目用)
│ ├── 语言绑定(只能用 JS)
│ └── 进程绑定(和 LLM 在同一进程)
├── MCP 协议
│ ├── Model Context Protocol(模型上下文协议)
│ ├── 核心目标:LLM 与 Tool/Resource 解耦
│ ├── 两种通信方式
│ │ ├── stdio(本地跨进程:stdin/stdout)
│ │ └── HTTP/SSE(远程跨进程)
│ ├── MCP vs fetch(扩展 Context vs 获取数据)
│ └── MCP 本质:给 Model 扩展 Context
├── 实战:数据库查询 MCP Server
│ ├── 模拟数据库 → 可替换为 MySQL/PG
│ ├── server.registerTool() 注册工具
│ ├── Zod Schema 参数定义
│ ├── MCP 标准返回格式(content 数组)
│ └── .mcp.json 接入 Claude Code
├── LangChain Tool vs MCP Tool 对比
│ ├── 同进程 vs 跨进程
│ ├── JS only vs 多语言
│ └── 何时用哪种
└── 进阶方向
├── 替换为真实数据库(MySQL)
├── 添加更多工具(增删改查)
└── 多 Agent 共享同一 MCP Server
结语
从 LangChain tool() 到 MCP server.registerTool(),表面上看只是换了一种注册方式,本质上是一次架构升级:
tool()是进程内函数调用------简单直接,但受限于语言和项目registerTool()是跨进程协议通信------复杂一些,但打破了所有边界
理解了这个区别,你就理解了 MCP 的核心价值:让工具独立于 LLM,让能力可以被任何 Agent 复用。
45 行代码,一个跨进程、可复用的数据库查询工具。这就是 MCP 协议的力量------工具不再被锁在某个项目里,而是成为所有 AI Agent 的公共能力。
参考与拓展阅读:
- MCP 官方规范(Model Context Protocol Specification)
@modelcontextprotocol/sdkGitHub 仓库- 《MCP 协议深度解析》------ AI 界的 USB-C 实战指南
- 《手写 MCP Server 全栈实战》------ Node.js + Zod 构建文件读取工具
- 《从零手写 AI Agent》------ LangChain + Tool 入门
如果本文帮你理解了 MCP 跨进程工具调用的原理和实战方法,欢迎点赞 + 收藏。你打算用 MCP 构建什么工具?欢迎在评论区分享你的想法 👇
#MCP #Node.js #跨进程通信 #AIAgent #ToolCalling #掘金技术社区