各位 AI 探索者们,大家好!👋
在上一篇文章中,我们拒绝了"手搓"工具的原始方式,成功编写了一个符合 MCP (Model Context Protocol) 标准的 Server,并在其中模拟了一个数据库查询工具。
但是!写好了 Server 只是第一步。这就好比你造出了一把削铁如泥的宝剑(Server),但如果没人(Host)能挥舞它,它也就是块废铁。🧱
今天,我们要进行一次维度升级 。我们将跳出 Server 的视角,站在上帝视角(Host)来审视整个 MCP 生态,并且我们要干一件大事:不依赖 Cursor 或 Trae,我们要自己写一个 Agent,让它通过代码动态加载并使用我们昨天写的 MCP 工具!
准备好了吗?硬核干货,发车!🚀
🧩 第一部分:MCP 的"三国演义" ------ Host, Client, Server
很多刚接触 MCP 的朋友,容易被文档里的一堆名词绕晕:MCP Host, MCP Client, MCP Server......它们到底是谁?谁又是谁的爸爸?🤔
让我们来理清这个关系。
1.1 角色大揭秘
MCP 的架构其实非常清晰:
-
🏰 MCP Host (东道主)
- 谁是 Host? 你的编辑器(Cursor, Trae),或者我们今天要写的 LangChain 应用程序,它们都是 Host。
- 作用:它是发起者,是"甲方爸爸"。它负责发现工具、管理连接、向 LLM 发送 Prompt。
- 比喻 :Host 就像是一个大工头,手里拿着图纸(用户需求),指挥干活。
-
🔌 MCP Client (客户端)
- 谁是 Client? 这是一个存在于 Host 内部的组件。
- 作用:你可能会问,"Host 不就是客户端吗?" 严格来说,Host 是应用程序,而 Client 是 Host 内部负责和 Server 说"MCP 语言"的翻译官。Host 想要调用工具,必须通过 Client 去建立连接、发送请求。
- 比喻 :Client 就像是接口转换器 或翻译官。大工头(Host)说"我要那个锤子",翻译官(Client)把它翻译成 MCP 协议的标准指令发给工具箱。
-
🧰 MCP Server (服务器)
- 谁是 Server? 就是我们昨天写的
my-mcp-server.mjs。 - 作用:它是工具和资源的容器。它不知道谁在调用它,它只管提供能力。
- 比喻 :Server 就是超级工具箱。里面装着锤子(Tools)、说明书(Resources)和模版(Prompts)。
- 谁是 Server? 就是我们昨天写的
1.2 工作流程:一次握手的旅程 🤝
当我们在 Cursor 里输入"查询用户 002"时,后台发生了什么?
-
Initialize (初始化) : Host 启动,通过 Client 向 Server 发送握手请求:"兄弟,你那都有啥宝贝?" Server 回复:"我有
query-user这个工具,还有个使用指南。" -
Discovery (发现): Host 拿到了工具列表(Tools List),把它转换成 LLM 能看懂的 JSON Schema,喂给大模型。
-
Execution (执行) : LLM 思考后说:"我要调用
query-user,参数是userId: '002'"。 Host 收到指令,通过 Client 将这个请求通过Stdio(标准输入输出)管道发给 Server。 -
Response (响应): Server 干完活,把结果扔回给 Host。Host 再把结果喂给 LLM,最终生成人类可读的回复。
📚 第二部分:不仅仅是 Tool,还有 Resource(资源)
细心的同学可能发现了,MCP 的全称是 Model Context Protocol ,而不是 Model Tool Protocol。为什么叫 Context(上下文)?
因为 AI 不仅需要做事(Tools) ,还需要看书(Resources)!
2.1 什么是 Resource?
- Tool 是动态的,是函数调用(查天气、写库、发邮件)。
- Resource 是静态的(或准静态的),是数据内容(API 文档、日志文件、系统配置、代码片段)。
Resource 的意义在于:它允许 Server 主动把数据"喂"给 LLM,作为上下文的一部分,而不需要 LLM 去猜或者通过 Tool 去抓取。
2.2 实战:给我们的 Server 添加"使用指南"
让我们回到 my-mcp-server.mjs,给它增加一个 Resource 能力。
javascript
// 注册资源:使用指南
// URI: 统一资源定位符,这是资源的唯一身份证
server.registerResource(
'使用指南', // 资源名称
'docs://guide', // 资源 URI,类似于网页的 URL
{
description: 'MCP Server 使用文档', // 描述,LLM 会看这个
mimeType: 'text/plain', // 告诉 LLM 这是纯文本
},
async () => {
// 当 Client 请求读取这个资源时,返回的内容
return {
contents: [
{
uri: 'docs://guide',
mimeType: 'text/plain',
text: `MCP Server 使用指南
功能:提供用户查询等工具。
使用:在 Cursor 等 MCP Client 中通过自然语言对话,Cursor 会自动调用相应工具。`,
}
]
}
}
)
🔍 代码深度解析:
server.registerResource: 这是注册资源的入口。docs://guide: 自定义协议头。你可以随便定,比如mysql://table/users或者log://app/error。这让 LLM 可以通过 URI 引用特定的上下文。mimeType: 既然是传输内容,就得告诉对方格式。是文本(text/plain)?是图片(image/png)?还是 JSON?- Prompt Templates : 虽然代码里没写,但 MCP 还支持
Prompt。你可以把写好的优质 Prompt 模板预置在 Server 里,让小白用户也能直接调出高质量的回答。
总结: Context (上下文) = Tool (工具) + Resource (资源) + Prompt (提示词模板)。 这就是 MCP 强大的原因,它打包了一切 LLM 需要的东西。
🤖 第三部分:自制 Agent ------ 将 MCP 接入 LangChain
好了,理论知识储备完毕,Server 也升级了。现在我们来做一个真正的 Host!
我们将使用 Node.js + LangChain 来构建一个 Agent,让它连接我们本地运行的 my-mcp-server.mjs。
3.1 引入适配器:LangChain 的 MCP 魔法 🪄
首先,你需要安装一个关键的包:@langchain/mcp-adapters。
javascript
// adapters mcp 适配器
import {
MultiServerMCPClient
} from '@langchain/mcp-adapters';
这个 MultiServerMCPClient 非常强大,它帮我们屏蔽了底层协议的复杂性,支持同时连接多个 MCP Server(就像 Cursor 的配置列表一样)。
3.2 配置 Client:复刻 Cursor 的配置
记得我们在 Cursor 的 config.json 里是怎么配的吗?我们需要指定 command (node) 和 args (脚本路径)。在代码里,也是一样的:
javascript
// client 初始化
const mcpClient = new MultiServerMCPClient({
mcpServers: {
'my-mcp-server': { // 给这个 Server 起个 ID
command: 'node', // 运行命令
// ⚠️ 注意:这里必须是绝对路径!
args: ['C:\\Users\\MR\\Desktop\\workspace\\lesson_jp\\ai\\agent\\mini-cursor\\mcp\\mcp-tool\\my-mcp-server.mjs'],
},
},
});
💡 核心点 : 这里实例化了一个 mcpClient。此时,连接还没有建立,它只是记住了配置。这和你在 IDE 里填好配置但还没点击"Retry"是一样的状态。
3.3 动态获取工具:从 Server 到 Model
接下来是见证奇迹的时刻。我们要让 Client 去连接 Server,把工具"吸"过来,然后绑在 LLM 身上。
javascript
// 1. 获取工具列表
// 这一步会触发 initialize 流程,自动启动子进程并握手
const tools = await mcpClient.getTools();
console.log(tools, '////'); // 打印看看,你应该能看到 query-user 的详细 Schema
// 2. 绑定工具到模型
// 这里假设 model 是一个 ChatOpenAI 实例
const modelWithTools = model.bindTools(tools);
逻辑解析:
mcpClient.getTools():这行代码背后发生了巨大的工作量。它启动了 Node 子进程,建立了 Stdio 管道,发送了tools/list请求,接收了响应,并把 MCP 格式的 Tool 定义转换成了 LangChain/OpenAI 兼容的 Tool 定义。model.bindTools(tools):这是 LangChain 的标准操作,把工具描述告诉 LLM,让 LLM 知道它有了外挂。
3.4 编写 Agent 循环:让 AI 动起来 🔄
如果只是绑定了工具,LLM 只会返回一个"我想要调用工具"的信号(Tool Call)。真正执行工具并把结果喂回去,需要我们写一个循环。这其实就是 ReAct Agent 的雏形。
javascript
async function runAgentWithTools(query, maxIterations=30) {
const messages = [
new HumanMessage(query) // 初始问题
];
// 开启思考-执行循环
for (let i = 0; i < maxIterations; i++) {
console.log(chalk.bgGreen('⏳正在等待AI思考...'));
// 1. 调用模型
const response = await modelWithTools.invoke(messages);
// 把 AI 的回复(可能包含工具调用请求)加入历史记录
messages.push(response);
// 2. 判断是否结束
// 如果 AI 没有想调用的工具,说明它已经生成了最终答案
if (!response.tool_calls || response.tool_calls.length === 0) {
console.log(`\n AI 最终回复:\n ${response.content}\n`);
return response.content;
}
// 3. 处理工具调用
console.log(chalk.bgBlue(`🔍 检测到 ${response.tool_calls.length} 个工具调用`));
// 遍历所有工具调用请求(AI 可能一次想调多个工具)
for (const toolCall of response.tool_calls) {
// 在我们从 MCP 拉取回来的 tools 数组里找到对应的工具
const foundTool = tools.find(t => t.name === toolCall.name);
if (foundTool) {
// 4. 执行工具!
// 这里 foundTool.invoke 实际上会通过 MCP 协议
// 把参数发给 my-mcp-server.mjs,拿到结果
const toolResult = await foundTool.invoke(toolCall.args);
// 5. 将工具结果封装为 ToolMessage
messages.push(new ToolMessage({
content: toolResult,
tool_call_id: toolCall.id // 必须带上 ID,让 AI 知道这是哪个调用的结果
}));
}
}
// 循环继续,下一轮 invoke 会带上最新的 ToolMessage
}
return messages[messages.length - 1].content;
}
🧪 实验结果:
当我们运行 await runAgentWithTools("查一下用户 002 的信息") 时:
- 第一轮 :
- LLM 收到问题。
- LLM 思考:"我要用
query-user,参数userId='002'"。 - 代码捕获到
tool_calls。 - 代码调用
mcpClient-> 发送请求给my-mcp-server.mjs。 - Server 返回:"姓名:李四,邮箱..."。
- 代码把这个结果存入
ToolMessage。
注意第一轮有tool_calls,这个时候context为空,llm不会返回context只会返回需要调用哪些工具,与从用户提问中解析出的参数。
- 第二轮 :
- LLM 收到历史记录(问题 + 工具调用请求 + 工具返回结果)。
- LLM 思考:"我有信息了,组织语言回答"。
- LLM 输出:"用户 002 是李四,邮箱是..."。
- 代码检测到没有
tool_calls,循环结束,输出最终答案。
🎉 总结
今天我们不仅搞懂了 Host-Client-Server 的三角关系,还解锁了 Resource 这个新技能,最重要的是,我们亲手实现了一个 MCP Host!
现在,你不再只是 MCP 的使用者,你是 MCP 的掌控者。
想象一下未来的可能性:
- 你可以写一个 Python 的 MCP Server 处理数据分析。
- 写一个 Go 的 MCP Server 处理高并发任务。
- 然后用今天的这段 Node.js 代码(或者直接用 Cursor),把它们全部串联起来。
这就是 Agent 的未来,这就是 MCP 的魅力。
觉得文章有用?点个赞再走吧!👍 咱们下期见!👋
附:运行环境检查
- 确保安装了
@langchain/mcp-adapters,@langchain/openai,@modelcontextprotocol/sdk。- 确保
my-mcp-server.mjs路径正确。- 配置好
.env里的OPENAI_API_KEY。