在早先 AI Agent 的开发过程中,工程师们常常面临一个重复且繁琐的问题:如何让大模型连接外部工具?
过去,每接入一个新的数据源或功能(比如查询数据库、调用地图 API、操作本地文件),我们往往需要编写特定的适配代码。如果工具是用 Python 写的,而 Agent 运行在 Node.js 环境,跨语言调用更是增加了复杂度。随着工具数量的增加,维护成本呈指数级上升。
Model Context Protocol(MCP)的出现,旨在解决这一痛点。它试图成为 AI 领域的"USB-C 接口”,统一大模型与外部数据、工具的连接标准。
今天,我们将通过实际的代码案例,从编写一个 MCP 服务开始,到将其集成到 LangChain Agent 中,最后探讨混合部署(本地 + 远程)的实践方案,深入剖析 MCP 的架构设计与工程落地细节。
一、MCP 的核心架构:三方关系
在深入代码之前,我们需要理清 MCP 架构中的三个核心角色。理解它们的关系,是理解后续代码的基础。
- MCP Host(宿主):通常是 AI 应用或 IDE(如 Cursor、Claude Desktop)。它负责发起请求,承载用户意图,并决定调用哪些工具。
- MCP Client(客户端):在 Host 内部运行,负责与具体的 MCP Server 建立连接。它屏蔽了底层通信细节(是本地进程还是远程 HTTP)。
- MCP Server(服务端):实际提供工具、资源或提示词的服务。它暴露具体的功能接口,等待 Client 调用。
在我们的实践代码中,langchain-host.mjs 扮演了 Host 的角色(内部集成了 Client 适配器),而 my-mcp-server.mjs 则是标准的 MCP Server。
二、构建 MCP Server:标准化输出能力
编写 MCP Server 的核心在于“暴露能力”。我们来看 my-mcp-server.mjs 的实现逻辑。
1. 服务初始化与通信传输
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
const server = new McpServer({
name: 'my-mcp-server',
version: '1.0.0',
});
// 连接方式:本地进程调用
const transport = new StdioServerTransport();
await server.connect(transport);
这里有两个关键点值得注意:
- McpServer 实例:这是服务的容器。我们需要注册名称和版本,这有助于 Client 端进行服务发现和管理。
- StdioServerTransport:这是 MCP 支持的一种通信标准。它通过标准输入输出(stdin/stdout)进行通信。对于本地运行的工具(如 Node 脚本、Python 脚本),这种方式最为轻量,无需启动 HTTP 端口,减少了网络攻击面,且天然支持进程间隔离。
2. 注册工具(Tool)
MCP 的核心价值在于 Tool 的标准化。
server.registerTool('query-user', {
description: '查询数据库中的用户信息。输入用户 ID, 返回该用户的详细信息...',
inputSchema: {
userId: z.string().describe("用户 ID, 例如:001, 002, 003")
}
}, async ({ userId }) => {
// 业务逻辑实现
const user = database.users[userId];
// 返回符合 MCP 协议的内容结构
return {
content: [{ type: 'text', text: ... }]
}
})
这段代码体现了 MCP 设计的精妙之处:
- Schema 约束:使用
zod定义inputSchema。这不仅仅是参数校验,更重要的是,这个 Schema 会被发送给 LLM。LLM 根据这个结构化的描述,能够准确地生成符合要求的参数,减少了幻觉。 - 描述即文档:
description字段直接决定了 LLM 是否知道在何时调用此工具。编写清晰的描述是 MCP 开发中最重要的“提示词工程”。 - 返回结构:注意返回的不是纯字符串,而是
{ content: [{ type: 'text', text: ... }] }。这是 MCP 协议规定的标准格式,支持未来扩展图片、资源等多种类型。
3. 注册资源(Resource)
除了工具,MCP 还支持注册静态或动态资源(类似文件系统中的文件)。
server.registerResource('使用指南', 'docs://guide', { ... }, async () => { ... })
资源通过 URI(如 docs://guide)定位。这允许 Agent 像读取文件一样读取上下文信息,而无需将其定义为“工具调用”。这种区分让 Agent 的决策更加清晰:是需要“执行动作”(Tool)还是需要“获取信息”(Resource)。
三、Host 端集成:LangChain 与 MCP 的适配
有了 Server,我们需要在 Agent 中调用它。langchain-host.mjs 展示了如何通过 @langchain/mcp-adapters 将 MCP 工具转换为 LangChain 可识别的 Tool 对象。
1. 多服务器管理
const mcpClient = new MultiServerMCPClient({
mcpServers: {
'my-mcp-server': {
command: 'node',
args: ['D:\\code\\...\\my-mcp-server.mjs'],
},
},
});
这里体现了 MCP 的“进程隔离”优势。Host 不需要知道 Server 内部是用什么语言写的,只需要知道如何启动它(command + args)。MultiServerMCPClient 负责管理这些子进程的生命周期。
2. Agent 执行循环(ReAct 模式)
集成 MCP 工具后,核心难点在于 Agent 的推理循环。我们需要手动实现一个简化的 ReAct(Reasoning + Acting)循环:
async function runAgentWithTools(query, maxIterations=30) {
const messages = [new HumanMessage(query)];
for (let i = 0; i < maxIterations; i++) {
// 1. LLM 思考
const response = await modelWithTools.invoke(messages);
messages.push(response);
// 2. 判断是否结束
if (!response.tool_calls || response.tool_calls.length === 0) {
return response.content;
}
// 3. 执行工具
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);
// 4. 将结果反馈给 LLM
messages.push(new ToolMessage({
content: toolResult,
tool_call_id: toolCall.id
}));
}
}
}
}
代码深度解析:
- 消息历史管理:
messages数组不仅包含用户输入,还包含了 Assistant 的思考(response)和工具的返回结果(ToolMessage)。这是 LLM 能够进行多步推理的关键。 - ToolMessage 的重要性:在 LangChain 中,必须使用
ToolMessage并将tool_call_id与之前的toolCall.id对应。这告诉 LLM:“这是对上一次那个特定工具调用的回应”,防止上下文错乱。 - 循环终止条件:通过检测
tool_calls是否为空来判断 LLM 是否已经完成了任务并准备输出最终答案。设置maxIterations是为了防止死循环,这是生产环境必须考虑的安全兜底。
四、进阶实践:混合部署与远程调用
在 gaode.mjs 中,我们看到了 MCP 更强大的场景:混合部署。
const mcpClient = new MultiServerMCPClient({
mcpServers: {
// 远程 HTTP 服务
"amap-maps-streamableHTTP": {
url: `https://mcp.amap.com/mcp?key=${process.env.AMAP_MAPS_API_KEY}`
},
// 本地动态执行
"filesystem":{
"command":"npx",
"args":["-y", "@modelcontextprotocol/server-filesystem", "..."]
},
// 本地开发工具
"chrome-devtools":{ ... }
}
})
这段配置揭示了 MCP 的两大工程价值:
- 统一接口,异构实现:对于 Agent 代码而言,调用高德地图(HTTP)和调用本地文件系统(Stdio)的代码逻辑是完全一致的。
MultiServerMCPClient屏蔽了底层的传输协议差异。这意味着我们可以轻松地将本地开发工具与云端 SaaS 服务组合成一个超级 Agent。 - 按需加载:通过
npx动态运行@modelcontextprotocol/server-filesystem,我们无需在本地永久安装服务,降低了环境配置成本。
场景分析: 代码中的注释提到一个复杂任务:“北京南站附近的 2 个酒店,拿到酒店图片展开浏览器展示..."。 这需要串联多个工具:
- 调用 Amap MCP 查询酒店位置。
- 调用 Chrome DevTools MCP 打开浏览器并访问图片 URL。
- 调用 Filesystem MCP 保存路线文档。
如果没有 MCP,我们需要分别对接高德 API、Puppeteer/Playwright 和 Node FS 模块,并处理各自的认证和错误。通过 MCP,这些都被抽象为统一的 tools 列表,LLM 可以自主编排调用顺序。
五、深度思考:MCP 的优缺点与工程挑战
虽然 MCP 前景广阔,但在实际落地中,我们仍需保持清醒。
优势
- 解耦与标准化:工具提供者只需遵循 MCP 协议,无需关心 Host 端是 Python 还是 Node.js,是 Cursor 还是自研 Agent。
- 安全性隔离:通过 Stdio 启动子进程,工具运行在独立沙箱中。即使工具被攻击,也不容易直接影响 Host 主进程。
- 生态复用:随着大厂(如高德、微软等)开始提供官方 MCP Server,开发者可以直接复用这些高质量工具,无需重复造轮子。
挑战与风险
- 延迟问题:
- 在
langchain-host.mjs中,每次工具调用都涉及进程间通信(IPC)或网络请求。 - 如果是本地 Stdio,启动子进程有开销;如果是远程 HTTP,网络延迟不可避免。在高频调用场景下,这可能成为瓶颈。
- 在
- 安全隐患:
- 在
gaode.mjs中,我们赋予了 Agent 操作文件系统(filesystem)和浏览器(chrome-devtools)的权限。 - 风险:如果 Prompt 被注入,或者 LLM 产生幻觉,Agent 可能会误删文件或访问恶意网站。
- 对策:在生产环境中,必须引入“人类确认”(Human-in-the-loop)机制,对于敏感操作(如写文件、执行命令),需用户二次确认。
- 在
- 调试复杂度:
- 当 Agent 行为异常时,问题可能出在 LLM 推理、MCP Client 适配、网络传输或 Server 逻辑。链路变长,排查难度增加。需要完善的日志系统(如代码中使用的
chalk彩色日志)来追踪每一步的工具调用和参数。
- 当 Agent 行为异常时,问题可能出在 LLM 推理、MCP Client 适配、网络传输或 Server 逻辑。链路变长,排查难度增加。需要完善的日志系统(如代码中使用的
六、总结
MCP 不仅仅是一个协议,它代表了 AI 应用开发范式的转变:从“编写代码调用 API"转向“配置工具让 AI 自主调用”。
通过今天的代码实践,我们看到了一个完整的闭环:
- 使用
@modelcontextprotocol/sdk快速构建标准化服务。 - 利用
MultiServerMCPClient统一纳管本地与远程工具。 - 在 LangChain 中通过 ReAct 循环实现自主代理。
对于开发者而言,现在的重点不应仅仅是学习如何写 MCP Server,更应思考如何设计安全的工具边界,以及如何组合现有的 MCP 生态来解决复杂的业务问题。随着支持 MCP 的客户端(如 Cursor、IDE 插件)越来越多,掌握这一标准,意味着你开发的工具将能够被更广泛的 AI 生态系统所复用。
未来,或许正如笔记中所言,"80% 的 App 会消失”,取而代之的是无数个通过 MCP 连接的、按需组合的微服务与工具。而我们要做的,就是成为这些连接的设计者。