Agent 如何通过 MCP 调用工具:完整链路详解

一句话概括:MCP(Model Context Protocol)是给 Agent 接上”手”和”眼”的标准插头。它定义了一套严格的握手协议,让 Agent 知道”有什么工具可用、怎么调用”——从建立连接到最终执行工具,每一步都有明确的分工。LLM 在其中只负责推理和决策,真正调用 MCP 的是 Host(宿主应用)。


背景:为什么需要 MCP?

LLM 本质上只会”说话”,它没有办法自己去查数据库、调接口、读文件。要让 LLM 具备这些能力,必须给它一个标准化的方式来”发现工具”并”使用工具”。

以前每家公司都自己搞一套,乱得很。Anthropic 提出了 MCP,相当于制定了一个通用的 USB 接口标准——不管是什么工具(数据库插件、网页抓取、代码执行器),只要实现了 MCP 协议,LLM 就能用统一的方式调用。


整体架构:三个角色

1
2
3
4
5
6
7
8
9
10
11
12
13
14
┌─────────────────────────────────┐
│ Host(宿主应用) │
│ │
│ ┌───────────┐ ┌─────────────┐ │
│ │ LLM │ │ MCP Client │ │
│ │ (推理决策) │ │ (协议通信) │ │
│ └───────────┘ └──────┬──────┘ │
└─────────────────────────┼───────┘
│ JSON-RPC

┌───────────────────┐
│ MCP Server │
│ (工具提供方) │
└───────────────────┘
  • Host(宿主):运行整个 Agent 的应用,比如 Claude Desktop、Cursor、你自己写的 Agent 程序。它同时持有 LLM 调用能力和 MCP Client。
  • LLM:只负责推理和决策,输出 tool_use JSON 表达意图,从不直接调用任何工具
  • MCP Client:嵌在 Host 内部,负责实现 MCP 协议、与 MCP Server 通信。Host 解析 LLM 的 tool_use 输出后,由 MCP Client 代为执行。
  • MCP Server:一个独立进程(或远程服务),向外暴露”工具 / 资源 / 提示词”。

通信方式有两种:

  • stdio(标准输入输出):Host 直接启动子进程,通过管道通信,本地工具常用这种。
  • HTTP + SSE:通过网络通信,远程工具或云服务用这种。

完整链路:从开机到调用

下面按时间顺序,把每个步骤拆开说清楚。


第一步:建立传输层连接

这一步跟 MCP 协议本身没关系,纯粹是”建立通道”。

如果是 stdio 模式

1
2
3
Host 用命令行启动 MCP Server 进程
比如:node /path/to/server.js
然后拿到这个进程的 stdin/stdout 作为通信管道

如果是 HTTP+SSE 模式

1
2
Client 先建立一个 SSE 长连接(用于接收服务器推送的消息)
Server 返回一个 endpoint URL(用于 Client 向 Server 发 POST 请求)

这一步做完了什么:双方有了一条能互相发消息的”管子”,但还什么协议都没跑——就像两个人拿起电话,却还没开口说话。


第二步:initialize 请求(握手核心)

Client 向 Server 发出第一条正式 JSON-RPC 消息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {
"roots": { "listChanged": true },
"sampling": {}
},
"clientInfo": {
"name": "ClaudeDesktop",
"version": "1.0.0"
}
}
}

这条消息在说什么

  • protocolVersion:我支持的 MCP 协议版本,咱们得说同一种语言。
  • capabilities:我(Client)支持哪些扩展能力。比如 sampling 表示”我支持让 Server 反过来请求我调用 LLM”。
  • clientInfo:我是谁,方便 Server 记日志或做权限判断。

Server 回应

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2024-11-05",
"capabilities": {
"tools": { "listChanged": true },
"resources": { "listChanged": true },
"prompts": { "listChanged": true }
},
"serverInfo": {
"name": "FileSystemServer",
"version": "2.1.0"
}
}
}

Server 的回应在说什么

  • 确认协议版本(双方对齐)。
  • capabilities 告诉 Client:我这里有 tools(工具)、resources(资源)、prompts(提示词模板)。listChanged: true 表示这些列表可能动态变化,Client 要能处理变更通知。
  • serverInfo:我是谁。

这一步的本质:两个人第一次打电话互相报家门——“我是谁,我能做什么,我们说哪个版本的协议”。


第三步:notifications/initialized 通知

Client 收到 initialize 响应后,紧接着发一条通知:

1
2
3
4
{
"jsonrpc": "2.0",
"method": "notifications/initialized"
}

注意:这是一条通知(notification),不是请求——没有 id 字段,不需要 Server 回复。

这条通知的意义:告诉 Server “我已经收到你的握手回应,准备好了,可以正式开始工作了”。

这是一个明确的状态转换信号。Server 在收到这条通知之前,不应该主动向 Client 发任何其他消息(比如工具变更通知)。这条消息就像裁判的发令枪:枪响之前,大家都别动;枪响之后,游戏正式开始。


第四步:tools/list 获取工具列表

握手完成后,Client(或者说宿主应用)通常会立刻来一波”摸底”,把 Server 上有什么东西都问清楚。

1
2
3
4
5
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/list"
}

Server 返回所有可用工具:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"tools": [
{
"name": "read_file",
"description": "读取指定路径的文件内容",
"inputSchema": {
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "文件的绝对路径"
}
},
"required": ["path"]
}
},
{
"name": "write_file",
"description": "向指定路径写入内容",
"inputSchema": {
"type": "object",
"properties": {
"path": { "type": "string" },
"content": { "type": "string" }
},
"required": ["path", "content"]
}
}
]
}
}

这一步做了什么

LLM 现在知道了”我有哪些工具可以用,每个工具叫什么名字、干什么的、需要传什么参数”。这些信息会被注入进 LLM 的 System Prompt,或以结构化方式传给模型,LLM 就能在对话中”看见”这些工具。

工具描述(description)极其重要:LLM 完全靠这段文字来判断”该不该用这个工具”。描述写得烂,LLM 就会用错或干脆不用。


第五步:prompts/list 获取提示词模板(可选)

1
2
3
4
5
{
"jsonrpc": "2.0",
"id": 3,
"method": "prompts/list"
}

Server 返回它预设的提示词模板:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"prompts": [
{
"name": "summarize_file",
"description": "生成一个总结指定文件内容的提示词",
"arguments": [
{
"name": "filePath",
"description": "要总结的文件路径",
"required": true
}
]
}
]
}
}

这是什么:Server 可以预设一些常用的提示词模板,用户可以直接触发(比如在 Claude Desktop 里输入 / 弹出的那些命令)。LLM 应用会把这些模板展示给用户选择。


第六步:resources/list 获取资源列表(可选)

1
2
3
4
5
{
"jsonrpc": "2.0",
"id": 4,
"method": "resources/list"
}

资源(Resource)和工具(Tool)的区别

  • 工具是让 LLM 主动去”做动作”的(读文件、查数据库、发请求)。
  • 资源是 Server 暴露出来可以”被读取”的数据,更像是上下文附件(比如一份配置文件、一段代码库的说明)。

资源可以被订阅,当内容变化时,Server 会主动通知 Client。


第七步:LLM 如何决定调用工具(Agent 决策层)

这是整个链路里最容易被忽视、却最关键的一步——LLM 到底是怎么”决定”要用工具的?

LLM 不会主动调用工具,它只会输出结构化 JSON

本质上,LLM 调用工具的方式是:在生成回复时,输出一段特殊格式的 JSON,告诉 Host “我要调用某个工具”。Host 解析这段 JSON,代替 LLM 去真正执行工具调用。

LLM 的输出长这样(以 Claude 为例)

1
2
3
4
5
6
7
8
{
"type": "tool_use",
"id": "toolu_01A09q90qw90lq917835lq9",
"name": "read_file",
"input": {
"path": "/Users/chenyuan/notes/todo.md"
}
}

这不是 LLM “执行”了什么,它只是输出了一段 JSON。真正去调 MCP Server 的是 Host(宿主应用)。

LLM 怎么知道有哪些工具可以用?

第四步 tools/list 拿到的工具列表,会被 Host 注入到 LLM 的上下文里,通常是 System Prompt 的一部分:

1
2
3
4
5
6
你有以下工具可以使用:

- read_file(path: string):读取指定路径的文件内容
- write_file(path: string, content: string):向指定路径写入内容

当你需要使用工具时,输出 tool_use 格式的 JSON。

LLM 看到这段描述后,就”知道”有这些工具了。工具的 description 就是 LLM 的唯一认知来源——它不看代码,只看文字描述。

Agent Loop:多轮工具调用是怎么运转的?

一次用户请求,LLM 可能需要调用多次工具才能完成任务。这个循环叫 Agent Loop

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
用户输入


LLM 推理 → 输出 tool_use JSON


Host 解析 → 调用 MCP Server(tools/call)


Server 返回结果


Host 把结果作为 tool_result 塞回给 LLM


LLM 继续推理 → 还需要工具?
├── 是 → 再次输出 tool_use JSON(循环)
└── 否 → 输出最终文字回复

一个具体例子:用户问”帮我整理一下 todo.md 里的任务,按优先级排序后写回去”

  1. LLM 推理:我需要先读文件 → 输出 tool_use: read_file
  2. Host 调用 MCP,拿到文件内容,作为 tool_result 返回给 LLM
  3. LLM 看到内容,推理:我需要写回去 → 输出 tool_use: write_file
  4. Host 再次调用 MCP,写入完成,返回结果
  5. LLM 看到写入成功 → 输出最终文字回复:”已按优先级整理完成”

整个过程,LLM 从未直接碰过文件系统,它只是在不断输出 JSON 和文字。


第八步:tools/call 实际执行

上一步 LLM 输出了 tool_use JSON,Host 解析后,代表 LLM 向 MCP Server 发起实际调用:

1
2
3
4
5
6
7
8
9
10
11
{
"jsonrpc": "2.0",
"id": 5,
"method": "tools/call",
"params": {
"name": "read_file",
"arguments": {
"path": "/Users/chenyuan/notes/todo.md"
}
}
}

Server 执行工具,返回结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"jsonrpc": "2.0",
"id": 5,
"result": {
"content": [
{
"type": "text",
"text": "# Todo\n- 买牛奶\n- 写 MCP 文章\n- 健身"
}
],
"isError": false
}
}

Host 把这个结果包装成 tool_result 消息,塞回给 LLM。LLM 看到结果后,进入下一轮推理——继续调用工具,或者输出最终回复(见第七步的 Agent Loop)。


完整流程图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
用户启动应用


[建立传输层连接]
stdin/stdout 或 HTTP+SSE


Client ──→ initialize ──→ Server
←── initialize响应 ───


Client ──→ notifications/initialized ──→ Server(无需回复)


Client ──→ tools/list ──→ Server
←── tools列表 ───


Client ──→ prompts/list ──→ Server(可选)
←── prompts列表 ───


Client ──→ resources/list ──→ Server(可选)
←── resources列表 ───


[把工具信息注入LLM上下文(System Prompt)]


用户输入 → LLM推理


┌─────────────────────────────────────┐
│ Agent Loop │
│ │
│ LLM输出 tool_use JSON │
│ │ │
│ ▼ │
│ Host解析 → MCP Client │
│ │ │
│ ▼ │
│ Client ──→ tools/call ──→ Server │
│ ←── 工具执行结果 ─── │
│ │ │
│ ▼ │
│ Host把结果作为tool_result给LLM │
│ │ │
│ ▼ │
│ LLM继续推理 ──→ 还需要工具? │
│ ├── 是 → 循环(回到顶部) │
│ └── 否 → 跳出循环 │
└─────────────────────────────────────┘


LLM输出最终文字回复


[等待下一轮用户输入...]

关键设计思想

1. JSON-RPC 2.0

MCP 所有通信都用 JSON-RPC 2.0 格式。有三种消息类型:

类型 特征 说明
Request(请求) id,有 method 需要对方回复
Response(响应) id,有 resulterror 回复请求
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 只负责推理和决策,从不直接碰工具的执行细节。