本节摘要:本章前面四节分别讲了三角边界、三大原语、两层模型、模块分层,现在用一个具体的场景把这些概念全部串起来——一次「模型决定调用 add 工具」的完整旅程。我们会从模型在宿主里推理开始,跟着请求穿过客户端、跨过传输、到达服务端、被能力声明匹配、被工具管理器分发、执行函数、返回结果,最终喂回模型。每一步都标注它对应本章哪个概念,让抽象的分层变成可见的过程。读完本节,本章所有概念将形成一个连贯的整体心智模型。
设定一个具体场景:用户在 Claude Desktop(宿主)里问「1 加 2 等于几」。宿主连着一个跑着第 1.2 节 add 工具的服务端。我们追踪从「模型决定调用」到「结果回喂模型」的全过程。
涉及的各方:
┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ 宿主 │ │ 客户端 │ │ 服务端 │ │ (Claude │ ──► │ (说 MCP 的 │ ──► │ (你的 add │ │ Desktop) │ │ 那一半) │ │ 工具) │ │ │ ◄── │ │ ◄── │ │ └──────────────┘ └──────────────┘ └──────────────┘ 跑模型 转发请求 执行业务
旅程的起点在宿主内部。宿主把用户问题「1 加 2 等于几」连同工具清单(从服务端能力声明拿到的)一起喂给模型:
宿主 → 模型: 用户问:"1 加 2 等于几" 可用工具:[{name: "add", description: "Add two numbers.", schema: {a: integer, b: integer}}] 模型推理: "用户问加法,我有一个 add 工具正好能算, 我决定调用 add(a=1, b=2)"
关键点:模型看到的工具清单,是宿主从服务端能力声明里拿的(对应第 1.3 节的 server_capabilities、第 3 章的能力声明)。模型据此判断「该不该调、调哪个、传什么参数」。
💡 技巧:这一步解释了为什么工具的文档字符串和 Schema 这么重要——它们是模型在这个决策点的唯一依据。模糊的描述会导致模型乱调用,这就是第 4 章「类型即契约」的核心动机。
模型决定调用后,宿主把这次工具调用请求交给它的客户端实例:
模型 → 宿主:"请调用 add(a=1, b=2)" 宿主 → 客户端 A:转发这次调用
注意是宿主(不是模型)在驱动客户端——客户端是宿主的一部分,模型从不直接和客户端对话。这就是第 2.1 节讲的「三角边界」:模型只跟宿主对话,宿主跟客户端对话,客户端跟服务端对话。
客户端把这次调用包装成一个 JSON-RPC 请求(MCP 协议的消息格式),通过传输层发出去:
客户端构造的 JSON-RPC 请求(概念形态): { "method": "tools/call", "params": { "name": "add", "arguments": {"a": 1, "b": 2} } }
这一步对应第 2.4 节讲的「两端通过协议类型对话」——客户端用 mcp-types 里定义的 tools/call 请求类型构造消息。
请求通过传输层从客户端搬到服务端。具体怎么搬,取决于用哪种传输(第 8 章详讲):
| 传输 | 怎么搬 |
|---|---|
| 内存(开发用) | 直接函数调用,无序列化 |
| stdio(本地) | 写到服务端子进程的 stdin |
| 流式 HTTP(生产) | 发 HTTP POST 请求到服务端 |
关键洞见:无论哪种传输,服务端业务代码完全不变——它收到的都是同样的协议对象。这就是第 8 章会讲的「传输与业务解耦」。
请求到达服务端后,服务端先做能力声明匹配——确认自己声明了 tools 能力,愿意回应 tools/call:
服务端检查: 客户端发来 tools/call 请求 我声明了 tools 能力吗? → 是(因为注册了 @mcp.tool) → 接受请求,继续分发
如果服务端没声明 tools 能力(没注册任何工具),它会在连接初期就告诉客户端「我不支持 tools」,客户端根本不会发这种请求过来。这就是第 1.3 节看到的「能力是自动声明的,没注册就不声明」。
通过能力检查后,请求进入工具管理器(第 3 章详讲),由它分发到对应的工具:
工具管理器收到 tools/call add: 在注册表里查 "add" → 找到对应的函数 从 params 取 arguments {a: 1, b: 2} 把 arguments 按 Schema 校验 → 通过 调用 add(a=1, b=2)
这一步是「两层模型」的核心现场——你用 @mcp.tool() 装饰器注册的 add 函数,在低层被工具管理器包装成一个 on_call_tool 处理器。装饰器糖在这一步被「拆开」,变成实际的函数调用。这就是第 2.3 节说的「MCPServer 是写法,Server 是事实」。
add(1, 2) 执行,返回 3。SDK 把这个返回值包装成协议响应:
服务端构造的响应(概念形态): { "content": [{"type": "text", "text": "3"}], ← 给模型的文本 "structuredContent": {"result": 3} ← 给应用的结构化数据 }
注意返回里有两块内容——这是第 1.3 节看到的 content 与 structured_content,第 4 章会详讲为什么标量被包成 {"result": ...}。
响应沿着原路返回:服务端 → 传输层 → 客户端 → 宿主:
服务端 → 传输 → 客户端:tools/call 响应 客户端 → 宿主:工具调用的结果 宿主 → 模型:把结果作为「工具结果消息」喂回
最后一步,模型拿到工具结果,继续推理:
模型收到: 工具结果:{"content": [{"text": "3"}]} 模型推理: "工具返回 3,所以 1 加 2 等于 3, 我可以回答用户了" 模型 → 宿主:"1 加 2 等于 3" 宿主 → 用户:显示回复
旅程结束。从用户问问题到拿到答案,请求在三角之间走了一个完整的来回。
把这九步压成一张时序图,你会看到整条链路的全貌:
用户 宿主 客户端 传输 服务端 工具管理器 add函数 │ │ │ │ │ │ │ │──问──────►│ │ │ │ │ │ │ │──喂问题+工具清单───►│ │ │ │ │ │ (模型推理:决定调 add) │ │ │ │ │──转发调用─►│ │ │ │ │ │ │ │──JSON-RPC►│ │ │ │ │ │ │ │──搬运───►│ │ │ │ │ │ │ │──能力检查─►│ │ │ │ │ │ │ │──分发────►│ │ │ │ │ │ │ │─执行 │ │ │ │ │ │◄──结果───│ │ │ │ │ │◄──────────│ │ │ │ │ │◄──响应───│ │ │ │ │ │◄──响应──│ │ │ │ │ │◄──结果────│ │ │ │ │ │ │ (模型推理:用结果回答) │ │ │ │◄──答──────│ │ │ │ │ │
最后用一张表,把旅程每一步对应到本章的概念,巩固整体理解:
| 旅程步骤 | 对应本章概念 |
|---|---|
| 模型看工具清单 | 第 2.1 节 三角边界(模型只在宿主里) |
| 模型决定调用 add | 第 2.2 节 工具是「模型决定」的原语 |
| 宿主转发给客户端 | 第 2.1 节 一宿主多客户端 |
| 客户端构造 JSON-RPC | 第 2.4 节 协议类型独立成包 |
| 传输搬运消息 | 第 8 章(预告)传输与业务解耦 |
| 服务端能力声明匹配 | 第 3 章 能力声明机制 |
| 工具管理器分发 | 第 2.3 节 两层模型(装饰器拆开) |
| 返回 content + structured_content | 第 4 章 结构化输出 |
| 结果回喂模型继续推理 | 第 2.1 节 服务端从不直接对话模型 |
第 2 章结束。你已经有了完整的架构地图。从第 3 章开始,我们深入服务端主线,讲透三大原语的注册与能力声明机制。