12.1 低层 Server:直接处理协议对象 本节摘要:第 12 章开篇,我们回到第 2.3 节埋下的伏笔——两层服务端模型。前面十一章你用的都是高层 (装饰器层),它把协议细节藏得很好。但当你需要协议层做不到的事,就得下沉到低层 。本节讲透低层 Server:它直接处理协议对象(注册 / 处理器),没有类型推断、没有装饰器糖,是 的地基。读完本节,你理解了「装饰器糖之下是什么」,为第 12.2 节的对照与后续扩展打好基础。 一、低层 Server 是什么 回顾第 2.3 节:服务端分两层, (高层)构建于 (低层)之上。低层 是裸协议对象处理器: 低层 不读类型注解、不生成 Schema,你直接处理协议对象——收到的请求是协议参数对象,返回的响应是协议结果对象。
本节摘要:第 12 章开篇,我们回到第 2.3 节埋下的伏笔——两层服务端模型。前面十一章你用的都是高层
MCPServer(装饰器层),它把协议细节藏得很好。但当你需要协议层做不到的事,就得下沉到低层Server。本节讲透低层 Server:它直接处理协议对象(注册on_list_tools/on_call_tool处理器),没有类型推断、没有装饰器糖,是MCPServer的地基。读完本节,你理解了「装饰器糖之下是什么」,为第 12.2 节的对照与后续扩展打好基础。
回顾第 2.3 节:服务端分两层,MCPServer(高层)构建于 Server(低层)之上。低层 Server 是裸协议对象处理器:
高层 MCPServer(装饰器层): @mcp.tool() def add(a: int, b: int) -> int: return a + b ← 类型推断、Schema 生成、错误转换都自动 低层 Server(协议层): @server.on_call_tool() async def call_tool(ctx, params): if params.name == "add": return ToolResult(content=[...]) ← 手写分发、手写 Schema、手动包装
低层 Server 不读类型注解、不生成 Schema,你直接处理协议对象——收到的请求是协议参数对象,返回的响应是协议结果对象。
低层 Server 通过注册处理器响应协议方法:
from mcp.server import Server from mcp.server.lowlevel import helper_types server = Server("demo-lowlevel") # 处理 tools/list:返回工具清单 @server.on_list_tools() async def list_tools(ctx, params): return [ types.Tool( name="add", description="Add two numbers.", input_schema={ # 手写 JSON Schema "type": "object", "properties": { "a": {"type": "integer"}, "b": {"type": "integer"} }, "required": ["a", "b"] } ) ] # 处理 tools/call:分发到对应工具 @server.on_call_tool() async def call_tool(ctx, params): if params.name == "add": a = params.arguments["a"] # 手动取参数 b = params.arguments["b"] result = a + b return helper_types.ToolResult( content=[types.TextContent(type="text", text=str(result))] ) raise ValueError(f"Unknown tool: {params.name}")
看到差别了吗?
| 维度 | 高层 MCPServer | 低层 Server |
|---|---|---|
| 注册工具 | @mcp.tool() + 函数 |
注册 on_list_tools + on_call_tool |
| Schema | 从类型推断 | 手写 JSON Schema |
| 参数 | 函数参数(自动绑定) | params.arguments 手动取 |
| 返回 | 函数返回值(自动包装) | 手动构造 ToolResult |
| 分发 | 工具管理器自动 | 你手动 if/elif |
低层 Server 给你裸协议对象,一切自己来。
低层 Server 有针对每个协议方法的处理器:
| 处理器 | 响应什么 |
|---|---|
on_list_tools |
tools/list |
on_call_tool |
tools/call |
on_read_resource |
resources/read |
on_list_resources |
resources/list |
on_list_resource_templates |
resources/templates/list |
on_get_prompt |
prompts/get |
on_list_prompts |
prompts/list |
| (其他) | 各协议方法 |
每个处理器接收 (ctx, params),返回协议结果。ctx 是请求上下文(含 session、headers 等),params 是协议参数对象。
低层 Server 的存在不是冗余,它解决几个高层搞不定的问题:
| 场景 | 为什么用低层 |
|---|---|
| 自定义协议方法 | 高层只覆盖标准方法,自定义要低层 |
| 完全控制分发逻辑 | 高层按工具名自动分发,低层可任意 |
| 不依赖类型推断 | 某些场景 Schema 无法用注解表达 |
| 协议级调试 | 直接看协议对象,便于排查 |
| 二次封装 SDK | 在 SDK 之上再封装一层框架 |
最常见的场景是「自定义协议方法」——MCP 协议有标准方法(tools/call 等),但有时候你需要协议外的东西(如批量操作、自定义状态查询)。低层 Server 让你能定义这些,第 12.3 节会详讲。
低层 Server 的能力声明不自动——你要显式声明服务器支持哪些能力:
server = Server( "demo-lowlevel", capabilities=types.ServerCapabilities( tools=types.ToolsCapability(list_changed=True), resources=types.ResourcesCapability(...), # 显式声明每个能力 ) )
对比高层 MCPServer(从注册自动推断声明),低层要你自己声明。这给完全控制,但也意味着你要确保「声明与处理器一致」——声明了 tools 能力,就要注册 on_list_tools 与 on_call_tool,否则客户端请求会失败。
⚠️ 注意:低层 Server 要求你自己保证「声明与实现一致」。高层 MCPServer 替你保证(注册即声明),低层要你手动维护。这是低层灵活性的代价——自由多了,责任也多了。
低层 Server 也能跑成服务,只是配置更手动:
# 低层 Server 跑 stdio(概念性) from mcp.server.stdio import stdio_server async def main(): async with stdio_server() as (read, write): await server.run(read, write, initialization_options) # 或嵌入 ASGI 应用 app = server.streamable_http_app()
启动方式与高层类似,但「初始化选项」(initialization_options)要手动构造,因为低层不自动推断能力。
绝大多数场景用高层 MCPServer 就够。低层适合这些情况:
该用低层的判断: ├─ 需要自定义协议方法? → 低层 ├─ 需要完全控制分发逻辑? → 低层 ├─ Schema 无法用类型注解表达? → 低层 ├─ 在 SDK 之上做二次封装? → 低层 └─ 都不需要 → 高层 MCPServer(推荐)
💡 技巧:别为了「显得高级」用低层。高层 MCPServer 在 95% 的场景都够用,代码也更简洁。低层是「需要时的逃生口」,不是「默认选择」。第 12.2 节会用对照表让你看清两者,巩固「日常用高层,特殊才下沉」的判断。
on_list_tools / on_call_tool 等,接收 (ctx, params),返回协议结果。低层清楚了,下一节用对照表讲透「同一功能两种写法」。