简打卡
51.76M · 2026-03-28
各位 AI 探索者们,大家好!
在上一篇文章中,我们拒绝了“手搓”工具的原始方式,成功编写了一个符合 MCP (Model Context Protocol) 标准的 Server,并在其中模拟了一个数据库查询工具。
但是!写好了 Server 只是第一步。这就好比你造出了一把削铁如泥的宝剑(Server),但如果没人(Host)能挥舞它,它也就是块废铁。
今天,我们要进行一次维度升级。我们将跳出 Server 的视角,站在上帝视角(Host)来审视整个 MCP 生态,并且我们要干一件大事:不依赖 Cursor 或 Trae,我们要自己写一个 Agent,让它通过代码动态加载并使用我们昨天写的 MCP 工具!
准备好了吗?硬核干货,发车!
很多刚接触 MCP 的朋友,容易被文档里的一堆名词绕晕:MCP Host, MCP Client, MCP Server……它们到底是谁?谁又是谁的爸爸?
让我们来理清这个关系。
MCP 的架构其实非常清晰:
MCP Host (东道主)
MCP Client (客户端)
MCP Server (服务器)
my-mcp-server.mjs。当我们在 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,最终生成人类可读的回复。
细心的同学可能发现了,MCP 的全称是 Model Context Protocol,而不是 Model Tool Protocol。为什么叫 Context(上下文)?
因为 AI 不仅需要做事(Tools),还需要看书(Resources)!
Resource 的意义在于:它允许 Server 主动把数据“喂”给 LLM,作为上下文的一部分,而不需要 LLM 去猜或者通过 Tool 去抓取。
让我们回到 my-mcp-server.mjs,给它增加一个 Resource 能力。
// 注册资源:使用指南
// 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。你可以把写好的优质 Prompt 模板预置在 Server 里,让小白用户也能直接调出高质量的回答。总结: Context (上下文) = Tool (工具) + Resource (资源) + Prompt (提示词模板)。 这就是 MCP 强大的原因,它打包了一切 LLM 需要的东西。
好了,理论知识储备完毕,Server 也升级了。现在我们来做一个真正的 Host!
我们将使用 Node.js + LangChain 来构建一个 Agent,让它连接我们本地运行的 my-mcp-server.mjs。
首先,你需要安装一个关键的包:@langchain/mcp-adapters。
// adapters mcp 适配器
import {
MultiServerMCPClient
} from '@langchain/mcp-adapters';
这个 MultiServerMCPClient 非常强大,它帮我们屏蔽了底层协议的复杂性,支持同时连接多个 MCP Server(就像 Cursor 的配置列表一样)。
记得我们在 Cursor 的 config.json 里是怎么配的吗?我们需要指定 command (node) 和 args (脚本路径)。在代码里,也是一样的:
// 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”是一样的状态。
接下来是见证奇迹的时刻。我们要让 Client 去连接 Server,把工具“吸”过来,然后绑在 LLM 身上。
// 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 知道它有了外挂。如果只是绑定了工具,LLM 只会返回一个“我想要调用工具”的信号(Tool Call)。真正执行工具并把结果喂回去,需要我们写一个循环。这其实就是 ReAct Agent 的雏形。
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 的信息") 时:
query-user,参数 userId='002'”。tool_calls。mcpClient -> 发送请求给 my-mcp-server.mjs。ToolMessage。tool_calls,循环结束,输出最终答案。今天我们不仅搞懂了 Host-Client-Server 的三角关系,还解锁了 Resource 这个新技能,最重要的是,我们亲手实现了一个 MCP Host!
现在,你不再只是 MCP 的使用者,你是 MCP 的掌控者。
想象一下未来的可能性:
这就是 Agent 的未来,这就是 MCP 的魅力。
觉得文章有用?点个赞再走吧! 咱们下期见!