12.2 同一功能两种写法:高层与低层对照 本节摘要:本节用并排对照表,把几个典型功能分别在 (高层)与低层 上实现,让你直观看到「装饰器糖替你做了什么」。理解了这个对照,你才算真正「看穿」两层模型——知道高层每个便捷背后,低层在干什么。这一节是理解第 2.3 节两层模型的关键深化,也是判断「何时该下沉」的依据。 一、功能一:定义一个工具 同一个「add 工具」,两层写法: 高层替你做的: 从类型注解推断 Schema(你写 ,它生成 ) 自动注册到工具管理器(你不用手动维护清单) 自动分发( 里你不用 ) 自动包装返回值成 ToolResult 二、功能二:定义一个资源 高层替你做的: 从 URI 占位符推断参数(你写 ,它对齐到函数参数) 自动解析 URI 提取参数(你不用手动 +
本节摘要:本节用并排对照表,把几个典型功能分别在
MCPServer(高层)与低层Server上实现,让你直观看到「装饰器糖替你做了什么」。理解了这个对照,你才算真正「看穿」两层模型——知道高层每个便捷背后,低层在干什么。这一节是理解第 2.3 节两层模型的关键深化,也是判断「何时该下沉」的依据。
同一个「add 工具」,两层写法:
# 高层 MCPServer(类型推断) from mcp.server import MCPServer mcp = MCPServer("Demo") @mcp.tool() def add(a: int, b: int) -> int: """Add two numbers.""" return a + b
# 低层 Server(手写一切) from mcp.server import Server from mcp.server.lowlevel import helper_types from mcp import types server = Server("demo-lowlevel") @server.on_list_tools() async def list_tools(ctx, params): return [types.Tool( name="add", description="Add two numbers.", input_schema={ "type": "object", "properties": { "a": {"type": "integer"}, "b": {"type": "integer"} }, "required": ["a", "b"] } )] @server.on_call_tool() async def call_tool(ctx, params): if params.name == "add": a = params.arguments["a"] b = params.arguments["b"] return helper_types.ToolResult( content=[types.TextContent(type="text", text=str(a + b))] ) raise ValueError(f"Unknown tool: {params.name}")
高层替你做的:
a: int,它生成 {"type": "integer"})on_call_tool 里你不用 if name == ...)# 高层 @mcp.resource("greeting://{name}") def greeting(name: str) -> str: """Greet someone.""" return f"Hello, {name}!"
# 低层 @server.on_list_resource_templates() async def list_templates(ctx, params): return [types.ResourceTemplate( uri_template="greeting://{name}", name="greeting", description="Greet someone." )] @server.on_read_resource() async def read_resource(ctx, params): if params.uri.startswith("greeting://"): name = params.uri[len("greeting://"):] # 手动解析 URI return types.ReadResourceResult( contents=[types.TextResourceContents( uri=params.uri, text=f"Hello, {name}!" )] ) raise ValueError(f"Unknown resource: {params.uri}")
高层替你做的:
{name},它对齐到函数参数)startswith + 切片)# 高层(自动推断) mcp = MCPServer("Demo") @mcp.tool() def add(...): ... # 自动声明 tools 能力,无需配置
# 低层(显式声明) server = Server( "demo-lowlevel", capabilities=types.ServerCapabilities( tools=types.ToolsCapability(list_changed=True), # 必须显式声明,否则客户端不知道你支持 tools ) )
高层替你做的:
list_changed 等标志把三个功能的对照压成一张总表:
| 任务 | 高层你写的 | 低层你写的 | 高层替你做的 |
|---|---|---|---|
| 工具 Schema | a: int, b: int |
手写 JSON Schema | 类型推断 |
| 工具分发 | 函数本身 | if name == ... 手动 |
自动分发 |
| 工具返回 | return a + b |
构造 ToolResult | 自动包装 |
| 资源参数 | URI 占位符 | 手动解析 URI | 自动对齐 |
| 能力声明 | (无,自动) | 显式 ServerCapabilities | 自动推断 |
| 错误转换 | 抛异常 | 手动构造错误响应 | 自动转换 |
一句话:高层把「协议细节」整套省掉,低层把它们都还给你。
通过对照,两者关系更清晰:
你写的高层代码 低层实际执行的 @mcp.tool() │ def add(a, b): │ return a + b │ ▼ on_list_tools 返回手写 Schema on_call_tool 手动分发到 add 构造 ToolResult 返回
高层代码「编译」成低层处理器。你用 @mcp.tool() 写的 5 行,低层要写 30 行。这就是「MCPServer 是写法,Server 是事实」(第 2.3 节)的字面含义。
基于对照表,下沉到低层的判断更清晰:
| 你的需求 | 高层能否 | 该不该下沉 |
|---|---|---|
| 注册标准工具/资源/提示词 | 能 | 不下沉 |
| 自动 Schema 推断 | 能 | 不下沉 |
| 自动能力声明 | 能 | 不下沉 |
| 自定义协议方法 | 不能 | 下沉 |
| 完全控制分发逻辑 | 不能 | 下沉 |
| 手写非标准 Schema | 不能 | 下沉 |
| 协议级调试 | 不便 | 下沉(临时) |
💡 技巧:对照表是「下沉决策」的依据。如果你的需求都在「高层能否」的「能」里,就别下沉;有「不能」的需求,才下沉。这个判断让你既享受高层便捷,又不丢失低层灵活性。
实战中常见「混合用法」——大部分用高层,少数特殊需求用低层或扩展:
# 大部分工具用高层 mcp = MCPServer("Demo") @mcp.tool() def add(a: int, b: int) -> int: ... # 标准,高层 @mcp.tool() def search(q: str) -> list: ... # 标准,高层 # 少数特殊需求,用扩展(第 12.3 节)或低层补充 # 比如自定义协议方法,用 Extension
这种「高层为主,低层/扩展补充」的模式,让你既保持大部分代码的简洁,又能处理特殊需求。第 12.3-12.5 节会讲扩展、中间件、Apps 这些「补充手段」。
对照清楚了,下一节讲扩展——如何在不改协议的前提下定义 MCP 未覆盖的方法。