Agent 如何通过 MCP 调用工具:完整链路详解
一句话概括:MCP(Model Context Protocol)是给 Agent 接上”手”和”眼”的标准插头。它定义了一套严格的握手协议,让 Agent 知道”有什么工具可用、怎么调用”——从建立连接到最终执行工具,每一步都有明确的分工。LLM 在其中只负责推理和决策,真正调用 MCP 的是 Host(宿主应用)。
背景:为什么需要 MCP?
LLM 本质上只会”说话”,它没有办法自己去查数据库、调接口、读文件。要让 LLM 具备这些能力,必须给它一个标准化的方式来”发现工具”并”使用工具”。
以前每家公司都自己搞一套,乱得很。Anthropic 提出了 MCP,相当于制定了一个通用的 USB 接口标准——不管是什么工具(数据库插件、网页抓取、代码执行器),只要实现了 MCP 协议,LLM 就能用统一的方式调用。
整体架构:三个角色
1 | ┌─────────────────────────────────┐ |
- Host(宿主):运行整个 Agent 的应用,比如 Claude Desktop、Cursor、你自己写的 Agent 程序。它同时持有 LLM 调用能力和 MCP Client。
- LLM:只负责推理和决策,输出
tool_useJSON 表达意图,从不直接调用任何工具。 - MCP Client:嵌在 Host 内部,负责实现 MCP 协议、与 MCP Server 通信。Host 解析 LLM 的
tool_use输出后,由 MCP Client 代为执行。 - MCP Server:一个独立进程(或远程服务),向外暴露”工具 / 资源 / 提示词”。
通信方式有两种:
- stdio(标准输入输出):Host 直接启动子进程,通过管道通信,本地工具常用这种。
- HTTP + SSE:通过网络通信,远程工具或云服务用这种。
完整链路:从开机到调用
下面按时间顺序,把每个步骤拆开说清楚。
第一步:建立传输层连接
这一步跟 MCP 协议本身没关系,纯粹是”建立通道”。
如果是 stdio 模式:
1 | Host 用命令行启动 MCP Server 进程 |
如果是 HTTP+SSE 模式:
1 | Client 先建立一个 SSE 长连接(用于接收服务器推送的消息) |
这一步做完了什么:双方有了一条能互相发消息的”管子”,但还什么协议都没跑——就像两个人拿起电话,却还没开口说话。
第二步:initialize 请求(握手核心)
Client 向 Server 发出第一条正式 JSON-RPC 消息:
1 | { |
这条消息在说什么:
protocolVersion:我支持的 MCP 协议版本,咱们得说同一种语言。capabilities:我(Client)支持哪些扩展能力。比如sampling表示”我支持让 Server 反过来请求我调用 LLM”。clientInfo:我是谁,方便 Server 记日志或做权限判断。
Server 回应:
1 | { |
Server 的回应在说什么:
- 确认协议版本(双方对齐)。
capabilities告诉 Client:我这里有tools(工具)、resources(资源)、prompts(提示词模板)。listChanged: true表示这些列表可能动态变化,Client 要能处理变更通知。serverInfo:我是谁。
这一步的本质:两个人第一次打电话互相报家门——“我是谁,我能做什么,我们说哪个版本的协议”。
第三步:notifications/initialized 通知
Client 收到 initialize 响应后,紧接着发一条通知:
1 | { |
注意:这是一条通知(notification),不是请求——没有 id 字段,不需要 Server 回复。
这条通知的意义:告诉 Server “我已经收到你的握手回应,准备好了,可以正式开始工作了”。
这是一个明确的状态转换信号。Server 在收到这条通知之前,不应该主动向 Client 发任何其他消息(比如工具变更通知)。这条消息就像裁判的发令枪:枪响之前,大家都别动;枪响之后,游戏正式开始。
第四步:tools/list 获取工具列表
握手完成后,Client(或者说宿主应用)通常会立刻来一波”摸底”,把 Server 上有什么东西都问清楚。
1 | { |
Server 返回所有可用工具:
1 | { |
这一步做了什么:
LLM 现在知道了”我有哪些工具可以用,每个工具叫什么名字、干什么的、需要传什么参数”。这些信息会被注入进 LLM 的 System Prompt,或以结构化方式传给模型,LLM 就能在对话中”看见”这些工具。
工具描述(description)极其重要:LLM 完全靠这段文字来判断”该不该用这个工具”。描述写得烂,LLM 就会用错或干脆不用。
第五步:prompts/list 获取提示词模板(可选)
1 | { |
Server 返回它预设的提示词模板:
1 | { |
这是什么:Server 可以预设一些常用的提示词模板,用户可以直接触发(比如在 Claude Desktop 里输入 / 弹出的那些命令)。LLM 应用会把这些模板展示给用户选择。
第六步:resources/list 获取资源列表(可选)
1 | { |
资源(Resource)和工具(Tool)的区别:
- 工具是让 LLM 主动去”做动作”的(读文件、查数据库、发请求)。
- 资源是 Server 暴露出来可以”被读取”的数据,更像是上下文附件(比如一份配置文件、一段代码库的说明)。
资源可以被订阅,当内容变化时,Server 会主动通知 Client。
第七步:LLM 如何决定调用工具(Agent 决策层)
这是整个链路里最容易被忽视、却最关键的一步——LLM 到底是怎么”决定”要用工具的?
LLM 不会主动调用工具,它只会输出结构化 JSON
本质上,LLM 调用工具的方式是:在生成回复时,输出一段特殊格式的 JSON,告诉 Host “我要调用某个工具”。Host 解析这段 JSON,代替 LLM 去真正执行工具调用。
LLM 的输出长这样(以 Claude 为例):
1 | { |
这不是 LLM “执行”了什么,它只是输出了一段 JSON。真正去调 MCP Server 的是 Host(宿主应用)。
LLM 怎么知道有哪些工具可以用?
第四步 tools/list 拿到的工具列表,会被 Host 注入到 LLM 的上下文里,通常是 System Prompt 的一部分:
1 | 你有以下工具可以使用: |
LLM 看到这段描述后,就”知道”有这些工具了。工具的 description 就是 LLM 的唯一认知来源——它不看代码,只看文字描述。
Agent Loop:多轮工具调用是怎么运转的?
一次用户请求,LLM 可能需要调用多次工具才能完成任务。这个循环叫 Agent Loop:
1 | 用户输入 |
一个具体例子:用户问”帮我整理一下 todo.md 里的任务,按优先级排序后写回去”
- LLM 推理:我需要先读文件 → 输出
tool_use: read_file - Host 调用 MCP,拿到文件内容,作为
tool_result返回给 LLM - LLM 看到内容,推理:我需要写回去 → 输出
tool_use: write_file - Host 再次调用 MCP,写入完成,返回结果
- LLM 看到写入成功 → 输出最终文字回复:”已按优先级整理完成”
整个过程,LLM 从未直接碰过文件系统,它只是在不断输出 JSON 和文字。
第八步:tools/call 实际执行
上一步 LLM 输出了 tool_use JSON,Host 解析后,代表 LLM 向 MCP Server 发起实际调用:
1 | { |
Server 执行工具,返回结果:
1 | { |
Host 把这个结果包装成 tool_result 消息,塞回给 LLM。LLM 看到结果后,进入下一轮推理——继续调用工具,或者输出最终回复(见第七步的 Agent Loop)。
完整流程图
1 | 用户启动应用 |
关键设计思想
1. JSON-RPC 2.0
MCP 所有通信都用 JSON-RPC 2.0 格式。有三种消息类型:
| 类型 | 特征 | 说明 |
|---|---|---|
| Request(请求) | 有 id,有 method |
需要对方回复 |
| Response(响应) | 有 id,有 result 或 error |
回复请求 |
| Notification(通知) | 无 id,有 method |
单向发送,不需要回复 |
notifications/initialized 是通知,tools/list 是请求——明白了这个区别,协议就清晰了很多。
2. 能力协商(Capability Negotiation)
initialize 阶段双方互报”我能做什么”,之后才会用对应功能。如果 Server 没有声明 tools 能力,Client 就不会去请求 tools/list,不做无效请求。
3. 工具描述驱动 LLM 决策
LLM 不认识工具的代码,它只看 description。整个工具调用链路里,工具描述文本是 LLM 和工具之间唯一的语义桥梁。所以写 MCP Server 时,工具描述写得清不清楚,直接决定 LLM 用得好不好。
4. 服务器可以反向请求 LLM
当 Server 声明了 sampling 能力,Server 可以在执行工具过程中,反过来请求 Client 让 LLM 做一次推理。这实现了”工具执行过程中嵌套调用 LLM”的能力,构成了更复杂的 Agent 链路。
常见问题
Q:initialize 和 notifications/initialized 为什么要分两步?
因为 Client 需要先处理 initialize 的响应(比如解析 Server 的能力列表、做一些初始化工作),处理完之后才发 notifications/initialized 告诉 Server “我就绪了”。如果合并成一步,Server 就不知道 Client 什么时候真的准备好了。
Q:为什么要先 list 工具,而不是用的时候再 list?
因为工具信息要提前注入到 LLM 的上下文里,LLM 才能在对话中”知道”有这些工具可以用。如果用的时候才 list,LLM 在生成回复时就已经”不知道有这个工具”了。
Q:tools/list 能分页吗?
可以。请求可以带 cursor 参数,响应里有 nextCursor 字段,支持游标分页,适合工具非常多的场景。
Q:工具列表变了怎么办?
如果 Server 的 capabilities 里声明了 tools: { listChanged: true },当工具列表变化时,Server 会主动发一条 notifications/tools/list_changed 通知给 Client,Client 收到后重新请求 tools/list 刷新列表。
总结
| 步骤 | 消息 | 作用 |
|---|---|---|
| 1 | 建立连接 | 打开通信管道 |
| 2 | initialize |
双方互报身份和能力,协商协议版本 |
| 3 | notifications/initialized |
Client 宣告”我就绪了”,正式开始工作 |
| 4 | tools/list |
获取所有可用工具的名称、描述、参数结构 |
| 5 | prompts/list |
获取预设的提示词模板(可选) |
| 6 | resources/list |
获取可读取的资源列表(可选) |
| 7 | Agent Loop | LLM 输出 tool_use JSON → Host 解析 → 循环调用工具,直到任务完成 |
| 8 | tools/call |
Agent Loop 中每次实际执行的工具调用 |
MCP 的本质是:用一套标准协议,让 Agent 在调用工具之前,先弄清楚”有什么工具、怎么用”,然后由 Host 代替 LLM 按标准格式去执行。
整个链路有三层分工:
- 握手层(MCP 协议):解决”能用什么工具”——initialize + tools/list
- 决策层(LLM + Agent Loop):解决”要不要用、用哪个”——LLM 输出 tool_use JSON,循环推理直到完成任务
- 执行层(MCP Client + Server):解决”怎么真正执行”——tools/call 实际调用工具
三层各司其职,LLM 只负责推理和决策,从不直接碰工具的执行细节。