本节摘要:本节是全书最重要的架构判断。MCP Python SDK 的服务端被有意分成了两层:
MCPServer(高层,装饰器与类型推断的便捷层)构建于低层Server(裸协议对象层)之上。这个关系,有 Web 框架经验的人一看就懂——它和 FastAPI 之于 Starlette 的关系一模一样:FastAPI 在 Starlette 之上加了类型推断、自动文档、依赖注入等便捷;Starlette 本身是个纯粹 ASGI 框架。同样,MCPServer在Server之上加了装饰器、Schema 推断、能力自动声明;Server本身是个纯粹的协议处理器。理解这两层,你才能理解「为什么日常写法极简,却又保留了直接操作协议的逃生口」。
如果你用过 FastAPI,这个类比立刻让一切清晰:
FastAPI 生态 ┌─────────────────────────────────────────┐ │ FastAPI(高层) │ │ - 装饰器 @app.get("/items/{id}") │ │ - 类型推断(PyDantic 自动校验/文档) │ │ - 依赖注入 Depends() │ │ - 自动生成 OpenAPI 文档 │ └─────────────────────────────────────────┘ 构建于 ┌─────────────────────────────────────────┐ │ Starlette(低层) │ │ - 纯 ASGI 应用 │ │ - 路由、请求/响应对象 │ │ - 中间件 │ │ - 无类型推断、无自动文档 │ └─────────────────────────────────────────┘
FastAPI 没有重新发明轮子,它在 Starlette 之上加了「类型即文档」的便捷层。当你需要 FastAPI 没提供的东西,可以下沉到 Starlette 直接操作 ASGI。
MCP SDK 的两层模型,完全是同一个思路:
MCP Python SDK 服务端 ┌─────────────────────────────────────────┐ │ MCPServer(高层) │ │ - 装饰器 @mcp.tool / @mcp.resource │ │ - 类型推断(自动生成 JSON Schema) │ │ - 依赖注入 Resolve() │ │ - 自动声明能力 │ └─────────────────────────────────────────┘ 构建于 ┌─────────────────────────────────────────┐ │ Server(低层) │ │ - 纯协议对象处理 │ │ - 注册 on_list_tools / on_call_tool │ │ - 处理器收协议对象、返回协议对象 │ │ - 无类型推断、无装饰器糖 │ └─────────────────────────────────────────┘
到目前为止本书所有代码用的都是高层 MCPServer。它的核心特征是「函数即元数据来源」:
from mcp.server import MCPServer mcp = MCPServer("Demo") @mcp.tool() def add(a: int, b: int) -> int: """Add two numbers.""" return a + b
你写一个普通函数,装饰器替你做这些事:
| 你写的 | MCPServer 替你做 |
|---|---|
函数 add |
注册成工具,名字自动取 add |
| 文档字符串 | 当工具描述发给模型 |
类型注解 a: int, b: int |
推断成输入 JSON Schema |
返回注解 -> int |
推断成输出 Schema(结构化输出) |
| (无) | 自动声明 tools 能力 |
这一切的便捷,都建立在低层 Server 之上。MCPServer 内部维护工具管理器、资源管理器、提示词管理器,当你用装饰器注册函数时,它替你生成低层处理器,挂到底层 Server 上。
低层 Server 长什么样?它没有装饰器、没有类型推断,你直接注册协议处理器:
from mcp.server import Server from mcp.server.lowlevel import helper_types server = Server("demo-lowlevel") @server.on_call_tool() async def call_tool(ctx, params): # params 是协议对象(工具名 + 参数字典) # 你手动分发、手动校验、手动返回协议结果 if params.name == "add": a = params.arguments["a"] b = params.arguments["b"] return helper_types.ToolResult(content=[...]) raise ValueError(f"Unknown tool: {params.name}") @server.on_list_tools() async def list_tools(ctx, params): # 你手动返回工具清单(含手写的 JSON Schema) return [ # 手写 add 的完整 Schema ]
看到差别了吗?
| 维度 | 高层 MCPServer | 低层 Server |
|---|---|---|
| 写一个工具 | @mcp.tool() + 函数 |
手写 on_call_tool 处理器 + on_list_tools 返回 Schema |
| Schema | 从类型注解自动生成 | 你手写 JSON Schema |
| 参数校验 | SDK 自动 | 你手动 |
| 错误转换 | SDK 自动 | 你手动包装成协议错误 |
| 灵活性 | 受装饰器约束 | 完全自由,直接操作协议对象 |
低层 Server 给你的是裸协议对象——请求进来是什么参数、响应该返回什么结构,全在你手里。代价是你要自己写 Schema、自己分发、自己校验。
这是本节的核心理解点。分两层不是「历史包袱」,而是有意的工程决策,解决两个矛盾:
矛盾一:日常写法要极简 vs 协议要完整支持。
绝大多数人写服务端,就是「注册几个工具/资源/提示词」。对这种场景,装饰器 + 类型推断是最佳形态——零样板代码。但 MCP 协议本身很完整(版本协商、能力声明、各种方法),如果只有高层 API,协议的完整能力就被装饰器的「便捷」牺牲掉了。
矛盾二:便捷层 vs 逃生口。
高层 API 永远覆盖不到所有需求。当你需要协议层做不到的事(一个自定义方法、一条特殊处理逻辑、一次细粒度的协议操作),你要有地方下沉。低层 Server 就是这个逃生口。
两层模型的解法:高层提供 90% 场景的便捷,低层提供 100% 协议能力的兜底。这与 FastAPI/Starlette 的关系、SQLAlchemy 2.0 的 Core/ORM 关系,是同一种工程智慧。
💡 技巧:把这句话记下来——「MCPServer 是写法,Server 是事实」。你用 MCPServer 的写法,最终都编译成 Server 的协议处理器。理解这一点,第 12 章讲低层 API 时就不会觉得是「另一个 SDK」,而是「同一个 SDK 的另一面」。
为了帮你建立准确的边界感,列一张「这件事在哪层做」的表:
| 你想做的事 | 哪层做 |
|---|---|
| 注册一个普通工具/资源/提示词 | 高层 MCPServer 装饰器 |
| 自动生成 Schema | 高层(类型推断) |
| 注入 Context / Resolve 依赖 | 高层 |
| 跑成 stdio / HTTP 服务 | 高层 mcp.run() |
| 用自定义方法扩展协议 | 低层(或第 12 章的扩展机制) |
| 直接操作协议对象(如自定义握手) | 低层 Server |
| 写一个不依赖类型推断的纯协议服务端 | 低层 Server |
| 中间件、自定义分发逻辑 | 低层 / 第 12 章中间件 |
日常 95% 的场景,高层够用。只有当你做的是「SDK 之上再封装一层」或「协议级特殊处理」时,才需要下沉。
读完本节,你应该带走这两点:
带着这个心智模型,后续每一章看到装饰器的「魔法」,你都知道那背后是低层处理器在干活——这种「看穿糖」的能力,是从「会用」到「能改」的分水岭。
MCPServer(高层,装饰器/类型推断)构建于 Server(低层,裸协议对象)之上。两层模型清楚了,下一节我们把视角拉高,俯瞰整个 SDK 的模块分层与依赖方向。