本节摘要:上一节看了服务端内部的两层模型,本节把视角拉高,俯瞰整个 SDK 的模块分层。SDK 不是一个扁平的包,而是按「协议类型 → 共享工具 → 服务端核心 / 客户端核心」分层组织,依赖方向严格自下而上。理解这个分层,你就能解释几个乍看奇怪的设计:为什么协议类型是个独立的包?为什么客户端和服务端能共用同一份类型?为什么改动协议类型要同时影响两端?本节用一张分层图与依赖铁律,把这些讲透。
SDK 的代码按职责分成四层,从下到上依赖:
┌─────────────────────────────────────────────┐ │ 应用层(你的代码) │ ← 你写的服务端/客户端 ├─────────────────────────────────────────────┤ │ 服务端核心模块 客户端核心模块 │ ← 两端各自的高层 API │ (MCPServer/Server、 (Client、 │ │ 传输、能力声明、 ClientSession、 │ │ 上下文、认证等) 会话组、认证等) │ ├─────────────────────────────────────────────┤ │ 共享工具模块 │ ← 两端共用的基础设施 │ (分发器、URI 模板、异常、 │ │ 传输抽象、订阅、可观测性) │ ├─────────────────────────────────────────────┤ │ 类型包(mcp-types) │ ← 协议类型定义 │ (JSON-RPC、方法枚举、 │ │ 协议版本、各修订的类型) │ └─────────────────────────────────────────────┘ 依赖方向:严格自下而上(禁止反向)
逐层看每层的职责:
| 层 | 内容 | 职责 |
|---|---|---|
| 类型包 | 协议类型、JSON-RPC、方法枚举、协议版本 | 定义「协议说什么」,纯数据,无业务 |
| 共享工具模块 | 分发器、URI 模板、传输抽象、异常、订阅 | 两端共用的基础设施 |
| 服务端核心 | MCPServer/Server、传输实现、能力声明、上下文、认证 |
服务端的高层 API |
| 客户端核心 | Client/ClientSession、传输实现、会话组、认证 |
客户端的高层 API |
这是分层里最值得讲的一个决策。协议类型(mcp-types)被拆成了独立的包,而不是藏在 mcp 主包里。为什么?
答案是两端必须共用同一份类型定义。考虑一个工具调用:客户端发出的请求、服务端收到的请求,必须是同一个类型;否则就会出现「客户端按自己的类型构造,服务端按另一个类型解析」的割裂。
mcp-types(独立包) ┌──────────────┐ │ 工具调用请求 │ │ 工具调用响应 │ │ 能力声明对象 │ │ ...所有协议类型│ └──────┬───────┘ ┌──────────┴──────────┐ ▼ ▼ 客户端核心 服务端核心 (引用同一份类型) (引用同一份类型)
把类型独立成包,带来三个好处:
| 好处 | 说明 |
|---|---|
| 类型一致 | 两端引用同一份定义,永远不会割裂 |
| 版本同步 | 协议类型有版本(对应协议修订),独立包便于锁版本 |
| 关注点分离 | 「协议说什么」与「怎么实现」分开,各自演进 |
这也是为什么第 1.1 节装 SDK 时,你会看到 mcp-types 作为独立依赖被拉进来——它是 SDK 的地基,两端都站在它上面。
整张分层图的核心约束是一条铁律:
依赖方向严格自下而上,禁止反向。
意思是:
| 层 | 可以依赖 |
|---|---|
| 类型包 | (无,它是最底层) |
| 共享工具模块 | 类型包 |
| 服务端/客户端核心 | 共享工具模块 + 类型包 |
| 应用层(你的代码) | 上面任意层 |
禁止反向意味着:类型包不能引用服务端核心的任何东西(否则循环依赖);共享工具模块不能引用服务端/客户端核心的特定逻辑。
⚠️ 注意:这条铁律不是文档约定,而是用代码结构强制的。如果你在写扩展或自定义实现时违反它(比如让类型包引用了服务端的某个类),会在导入时直接报循环依赖错误。这是 SDK 用工程手段守住的设计原则。
「共享工具模块」这一层容易被忽略,但它解释了 SDK 的很多能力。两端都需要的「跨切关注点」都放在这里:
| 模块 | 干什么 | 两端怎么用 |
|---|---|---|
| 分发器(Dispatcher) | 把进来的请求分发到对应处理器 | 服务端分发请求、客户端分发响应 |
| URI 模板 | 解析 greeting://{name} 这类 URI |
服务端注册资源模板、客户端构造读取请求 |
| 传输抽象 | 定义 Transport 协议 |
两端都基于它实现各自传输 |
| 异常 | MCPError 等协议级异常 |
两端共用同一套错误模型 |
| 订阅(Subscription) | 资源变更通知的发布订阅 | 服务端发布、客户端订阅 |
| 可观测性 | OpenTelemetry 钩子 | 两端都产生追踪数据 |
把跨切关注点放共享层,避免了两端各写一份的重复——这是「DRY(Don't Repeat Yourself)」在分层架构里的体现。
注意分层图里,服务端核心与客户端核心是并列的两层,互不依赖:
服务端核心 ◄──不依赖──► 客户端核心 │ │ └──── 都依赖共享层 + 类型包 ────┘
这个对称设计有两个好处:
它们之间的「对话」通过协议类型完成——服务端产出协议响应、客户端消费协议响应,中间不需要直接代码依赖。这与第 2.1 节讲的「服务端从不直接和模型对话」是一脉相承的设计哲学:两端通过协议对话,不通过代码耦合。
你可能会问:了解分层对我写代码有什么用?至少三点:
mcp.types(或 mcp_types),服务端 API 去 mcp.server,客户端 API 去 mcp.client(或顶层 mcp)。分层清楚了,最后一节我们用一次完整的「工具调用」旅程,把本章所有概念串起来。