MCP(Model Context Protocol)深度解析:从面试概念到代码实现
带你从"面试官视角"理解 MCP 协议,再深入到
my-mcp-server.mjs+langchain-host.mjs的每一行代码。
第一部分:面试视角看 MCP------如果你被问到"什么是 MCP"
1.1 一句话回答
MCP(Model Context Protocol)是一个标准化的 C/S 协议,让 AI 模型通过统一的接口发现和调用外部工具、访问数据源。
如果面试官追问"跟 LangChain 的 Tool 有什么区别",核心区别在于:LangChain Tool 是框架私有的工具定义方式,MCP 是跨框架、跨平台的开放协议。一个 MCP Server 写完后,Cursor、Claude Desktop、LangChain 都能用。
1.2 用"点外卖"类比理解 MCP
arduino
你(Client / Host) 餐厅(MCP Server)
│ │
│── 我要菜单 ──────────────► │ ← tools/list
│◀── 菜单来了(宫保鸡丁、鱼香肉丝)│
│ │
│── 点菜:宫保鸡丁 ──────────► │ ← tools/call
│◀── 菜做好了,请享用 ──────── │
- 你 = AI 模型(通过 MCP Host 接入)
- 餐厅 = MCP Server(提供工具和资源的服务端)
- 菜单 = Server 暴露的 Tool 列表
- 点菜 = AI 调用某个 Tool
- 上菜 = Tool 返回执行结果
1.3 MCP 为什么诞生?------解决"组合爆炸"
在 MCP 出现之前:
css
每个 AI 平台 × 每个外部工具 = 都要写单独的集成代码
Cursor ⇢ 连接数据库 → 写一套代码
Cursor ⇢ 连接 GitHub → 再写一套
Claude Desktop ⇢ 连接数据库 → 又写一套
...
这是 N 个 AI × M 个工具 = N×M 种集成 的组合爆炸。
MCP 的做法:
arduino
每个工具只需实现一次 MCP Server
↓
所有兼容 MCP 的 AI 都能直接用
数据库 MCP Server → Cursor、Claude Desktop、LangChain 全能用
GitHub MCP Server → 同上
这是 N+M 的关系,不再相乘。
1.4 MCP 三大核心组件(面试高频考点)
MCP 协议规定了 Server 可以向 Client 暴露三类内容:
| 组件 | 英文 | 一句话理解 |
|---|---|---|
| 工具 | Tool | "我能为你做什么"------可调用的函数 |
| 资源 | Resource | "我能访问什么数据"------可读取的内容 |
| 提示模板 | PromptTemplate | "怎么更好地让我帮你"------预制的提示词 |
面试重点:为什么协议叫 "Model Context Protocol" 而不是 "Model Tool Protocol"?
因为 Context(上下文)= Tool + Resource + PromptTemplate 的总和 。协议设计的初衷不只是让模型调用工具,而是让模型在"理解世界"的基础上去做事------Resource 提供知识,Tool 提供能力,PromptTemplate 提供方法。
1.5 通信方式:stdio 和 HTTP
MCP 支持两种传输方式:
| 方式 | 原理 | 适用场景 |
|---|---|---|
| stdio | 标准输入输出流,Client 启动 Server 为子进程 | 本地工具 |
| HTTP | Server-Sent Events (SSE),Server 独立运行 | 远程服务、团队共享 |
本文的代码使用的是 stdio 方式 ------Host 通过 spawn 启动 MCP Server 作为子进程,通过标准输入输出通信。
第二部分:my-mcp-server.mjs------MCP Server 端详解
2.1 整体结构
js
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
两个核心组件:
- McpServer:创建 MCP Server 实例,注册工具和资源
- StdioServerTransport:通过标准输入输出与外部通信
注意:这个文件不直接导入 LangChain 的任何东西------这正是 MCP 的核心价值:Server 与调用方完全解耦。
2.2 模拟数据库------Server 拥有的"数据资产"
js
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" },
}
}
真实场景下,这里可以连接到 MySQL、PostgreSQL、外部 API。这个对象模拟了 Server 独有、Client 不知道也访问不到的数据------Tool 就是把这些数据"暴露"给 AI 的桥梁。
2.3 创建 Server 实例
js
const server = new McpServer({
name: 'my-mcp-server',
version: '1.0.0',
});
name + version 是 MCP 协议的强制字段,用于 Client 识别和版本管理。
2.4 registerTool------定义 Tool 的 MCP 方式
js
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} 不存在。` }]
}
}
return {
content: [{ type: 'text', text: `用户信息:n- ID: ${user.id}n- 姓名: ${user.name}n...` }]
}
})
与 LangChain tool() 的关键区别:
| 维度 | LangChain tool() |
MCP registerTool |
|---|---|---|
| 定义方式 | tool(handler, config) |
server.registerTool(name, config, handler) |
| 参数校验 | schema: z.object({...}) |
inputSchema: { key: z.string().describe(...) } |
| 返回值格式 | return "字符串" |
return { content: [{ type: 'text', text: '...' }] } |
| 所属框架 | LangChain 专有 | MCP 协议标准,跨框架通用 |
最关键的差异是返回值格式 。MCP 要求返回结构化的 { content: [...] } 数组,每个元素带 type 字段(text、image、resource 等)。这设计是为了支持多模态------一个 Tool 可以同时返回文字 + 图片 + 文件链接。
2.5 registerResource------不只是工具,还有"知识"
js
server.registerResource('使用指南', 'docs://guide', {
description: 'MCP Server 使用文档',
mimeType: 'text/plain',
}, async () => {
return {
contents: [{
uri: 'docs://guide',
mimeType: 'text/plain',
text: `MCP Server 使用指南n功能:提供用户查询工具。n...`
}]
}
})
Resource 和 Tool 的区别:
arduino
Tool → "做":查询用户、发送邮件、创建文件(行为)
Resource → "看":使用文档、数据库 schema、API 文档(知识)
为什么需要 Resource? 因为 AI 除了"做事"还需要"知道怎么做事"。比如这个 docs://guide 资源,AI 可以读取它来理解如何使用这个 Server 提供的工具------相当于"给工具配了使用说明书"。
注意 comments 中的点睛之笔:
js
// 为什么叫 Model Context Protocol 不叫 Model Tool/Resource/PromptTemplate Protocol
// Context = Tool + Resource + PromptTemplate
这就是 MCP 设计的哲学高度------不是做一个"工具调用协议",而是一个上下文交换协议。
2.6 启动:打开通信通道
js
const transport = new StdioServerTransport();
await server.connect(transport);
StdioServerTransport 做了什么?
c
MCP Host(langchain-host.mjs)
│
│ spawn 子进程 ──► my-mcp-server.mjs
│ │
│ ◀── stdout ────────────┘ Server 通过 stdout 发送响应
│ ── stdin ──────────────► Server 通过 stdin 接收请求
整个通信过程就是 JSON 序列化后的标准输入输出。不需要 HTTP 端口,不需要网络配置------一个进程,两根管道,协议就跑起来了。
第三部分:langchain-host.mjs------Host 端详解
3.1 核心问题:LangChain 不认识 MCP
LangChain 的模型只能绑定 LangChain 格式的 Tool。而 MCP Server 返回的是 MCP 格式的 Tool。需要一个适配器。
js
import { MultiServerMCPClient } from '@langchain/mcp-adapters';
@langchain/mcp-adapters 就是这座桥梁------它把 MCP Server 提供的工具转换成 LangChain 能理解的 Tool 对象。
3.2 配置 MCP Client:告诉 Host 怎么启动 Server
js
const mcpClient = new MultiServerMCPClient({
mcpServers: {
'my-mcp-server': {
command: 'node',
args: ['C:/Users/.../my-mcp-server.mjs']
}
}
})
这里的配置告诉 MultiServerMCPClient:
- 有一个叫
my-mcp-server的 MCP Server - 启动方式:执行
node my-mcp-server.mjs - Client 会自动
spawn这个子进程,建立 stdio 通信
关键设计 :MCP Server 是一个独立进程 ,可以被任何支持 MCP 的 Client 启动。同一个 my-mcp-server.mjs,Cursor 能用,Claude Desktop 能用,LangChain 也能用------这就是协议的力量。
3.3 动态获取工具:getTools() 的神奇之处
js
const tools = await mcpClient.getTools();
console.log(tools, '/////');
const modelWithTools = model.bindTools(tools);
对比之前的做法:
ini
之前(all_tools.mjs):
import { readFileTool, writeFileTool, ... } from './all_tools.mjs';
const tools = [readFileTool, writeFileTool, ...];
→ 工具是写死在 Host 代码里的
现在(MCP):
const tools = await mcpClient.getTools();
→ 工具是动态从 MCP Server 发现的!
getTools() 背后发生了什么:
c
Host MCP Server
│ │
│── stdout: tools/list ────────────► │ "请告诉我你能提供什么工具"
│◀── stdin: 工具列表 ────────────── │ 返回 query-user 工具的元数据
│ │ (name, description, inputSchema)
│ │
│ LangChain 适配器将 MCP 格式 │
│ 转换为 LangChain Tool 格式 │
│ │
这意味着:MCP Server 上新增或修改工具,Host 代码完全不用改。热插拔。
3.4 Agent 循环:与之前一模一样的逻辑
js
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 || response.tool_calls.length === 0) {
return response.content;
}
for (const tool_call of response.tool_calls) {
const foundTool = tools.find(t => t.name === tool_call.name);
if (foundTool) {
const toolResult = await foundTool.invoke(tool_call.args);
messages.push(new ToolMessage({
content: toolResult,
tool_call_id: tool_call.id
}));
}
}
}
}
这个循环和上一篇 mini-cursor 的循环完全一样。 这就是 MCP 最美的地方------它只改变了"工具从哪来",完全不改变"怎么用工具"。Agent 循环是一种通用模式,工具来源可以是本地定义、MCP Server、甚至 REST API,循环逻辑不变。
3.5 执行与清理
js
const result = await runAgentWithTools('查询用户ID为001的用户信息');
console.log(result);
await mcpClient.close();
mcpClient.close() 会:
- 关闭与 MCP Server 的 stdio 通信
- 终止 MCP Server 子进程
- 释放相关资源
这是生产级代码应有的资源管理意识------启动了别人的进程,就要负责把它关掉。
3.6 完整数据流:一次查询的全景
css
用户: "查询用户ID为001的用户信息"
│
▼
┌─ langchain-host.mjs ─────────────────────────────────────┐
│ │
│ 1. modelWithTools.invoke() → tool_calls: [query-user] │
│ 2. tools.find("query-user") → 找到(来自 MCP 适配器) │
│ 3. tool.invoke({userId: "001"}) │
│ │ │
│ ▼ │
│ 适配器将调用转发给 MCP Server 子进程 ─────────────┐ │
│ │ │
│ ┌─ my-mcp-server.mjs (子进程) ──────────────────┐ │ │
│ │ │ │ │
│ │ stdin 收到: {method: "tools/call", "query-user", args:{userId:"001"}}
│ │ database.users["001"] → { name:"张三", ... } │ │ │
│ │ stdout 返回: {content: [{type:"text", text:"用户信息:..."}]}
│ │ │ │ │
│ └────────────────────────────────────────────────┘ │ │
│ │ │
│ ◀── 适配器将结果转换为 ToolMessage ──────────────┘ │
│ │
│ 4. modelWithTools.invoke(messages) → "用户001是张三..." │
│ 5. mcpClient.close() → 关闭子进程 │
└───────────────────────────────────────────────────────────┘
四、三种工具集成方式的对比总结
结合前三篇文章,我们现在看到了三种定义 Tool 的方式:
| 方式 | 文章 | 工具定义 | 适用场景 |
|---|---|---|---|
| 本地 LangChain Tool | 1.md(第一篇) | tool(handler, config) 直接写在代码里 |
简单功能、原型验证 |
| 本地模块化 Tool | 2.md(第二篇) | 独立 all_tools.mjs 文件 + import |
多工具项目 |
| MCP 远程 Tool | 3.md(本篇) | MCP Server 独立进程 + getTools() 动态发现 |
生产级、跨平台复用 |
进化路径:
arduino
单体 Tool(写死)→ 模块化 Tool(import)→ MCP Tool(协议发现)
耦合度:高 中 零
复用性:无 项目内 跨框架跨平台
五、总结
MCP 做了什么?用一句话概括:
把"工具怎么被 AI 使用"和"工具怎么被实现"彻底解耦。
Server 只管"我能做什么",Host 只管"让 AI 用工具",中间通过 stdio 管道 + JSON 协议通信。从此你的数据库查询工具、文件操作工具、API 调用工具都可以写成 MCP Server,然后给 Cursor、Claude Desktop、LangChain 任何 MCP 兼容的平台用------写一次,到处跑。
对于面试来说,记住三个关键词:Tool(做)、Resource(看)、PromptTemplate(教)------合起来就是 Model 需要的全部 Context。