3.2 三个装饰器:从函数到注册的原语


3.2 三个装饰器:从函数到注册的原语

本节摘要:上一节认识了 MCPServer 这个容器,本节逐个展开 @mcp.tool()@mcp.resource(uri)@mcp.prompt() 三个装饰器的注册语义。核心是体会「函数即元数据来源」这套统一哲学——三者都从函数读元数据,但读法不同:工具把类型注解读成给模型的 Schema,资源从 URI 占位符读参数,提示词只读一个扁平的字符串参数列表。我们会用一张对照表讲透三者的差异,再用代码示例分别演示。读完本节,你能不查文档就写出三类原语的注册。

一、统一哲学:函数即元数据来源

三个装饰器共享一套底层哲学:你写的普通 Python 函数,本身就是全部元数据的来源。SDK 不让你在别处再声明一遍名字、描述、参数——它直接从函数身上读:

你写的函数 SDK 读出来的元数据 ┌──────────────────────┐ ┌──────────────────────┐ │ def add(a: int, │ ───► │ 名字: add │ │ b: int) -> int: │ │ 描述: (文档字符串) │ │ """Add two...""" │ │ 参数: a, b │ │ return a + b │ │ 类型: integer │ └──────────────────────┘ └──────────────────────┘

这套哲学贯穿三个装饰器,差别只在「读出来的元数据用作什么」。下面逐个看。

二、@mcp.tool():类型注解即输入 Schema

工具装饰器把函数变成「模型可调用」的工具。它的核心是类型注解被读成给模型的 JSON Schema:

@mcp.tool() def add(a: int, b: int) -> int: """Add two numbers.""" return a + b

读出来的元数据:

元数据 来源 用作
工具名 add 函数名 模型调用时的标识
描述 Add two numbers. 文档字符串 模型判断「该不该调」的依据
输入 Schema {a: int, b: int} 参数类型注解 模型构造调用参数的依据
输出 Schema 返回类型注解 结构化输出(第 4 章详讲)

装饰器还能接受一些可选参数,覆盖默认推断:

@mcp.tool(name="add_numbers", # 覆盖默认工具名 description="把两个数相加") # 覆盖文档字符串当描述 def add(a: int, b: int) -> int: return a + b

💡 技巧:多数情况让 SDK 自动推断就好(函数名当工具名、文档字符串当描述)。只有当函数名不符合工具命名规则(如含中文、太长),或文档字符串不适合直接给模型看时,才用 name / description 显式覆盖。

三、@mcp.resource():URI 即接口

资源装饰器把函数变成「应用可读」的资源。它的核心是URI 决定了资源形态——无参数是具体资源,带 {param} 是资源模板:

# 具体资源:URI 无参数,可直接列举 @mcp.resource("config://app") def get_config() -> dict: """Return app configuration.""" return {"theme": "dark", "lang": "zh"} # 资源模板:URI 带 {name},需实例化才能读 @mcp.resource("greeting://{name}") def greeting(name: str) -> str: """Greet someone by name.""" return f"Hello, {name}!"

注意资源装饰器的第一个参数是 URI(必填),不是函数名——这是它与工具装饰器的关键差别。URI 既是资源的「地址」,也是参数来源。

URI 占位符 {name} 与函数参数 name 的对应关系:

URI: greeting://{name} │ ▼ 对齐 函数签名: def greeting(name: str) │ ▼ 执行: greeting(name=<URI 里填的值>)

SDK 在客户端读 greeting://World 时,从 URI 解析出 name="World",调用 greeting("World")。第 5 章会详讲这套机制。

四、@mcp.prompt():扁平字符串参数

提示词装饰器把函数变成「用户可触发」的命名消息模板。它有一个特别限制:参数只能是扁平的字符串列表,没有 JSON Schema:

@mcp.prompt() def summarize(text: str) -> str: """Summarize a piece of text in one sentence.""" return f"Summarize the following text in one sentence:\n\n{text}"

为什么提示词参数只能是字符串?这是协议设计的取舍——提示词由用户在界面上触发,UI 要为每个参数渲染一个输入框。复杂的 JSON Schema 在 UI 上渲染困难,而「每个参数一个文本框」简单可靠。所以协议规定提示词参数必须是扁平字符串。

原语 参数模型 为什么
工具 完整 JSON Schema(任意类型) 模型能处理复杂结构
资源 URI 占位符(字符串) URI 本身就是字符串
提示词 扁平字符串列表 UI 要简单渲染输入框

⚠️ 注意:提示词函数的返回值会变成一条用户消息注入对话(不是工具结果、不是资源内容)。所以提示词的本质是「渲染一段用户指令的函数」,这与工具(执行动作)、资源(提供数据)的定位完全不同。

五、三者对照表

把三个装饰器放一起对照,差异更清晰:

维度 @mcp.tool() @mcp.resource(uri) @mcp.prompt()
谁决定调用 模型 应用(宿主) 用户
第一个参数 (可选覆盖项) URI(必填) (可选覆盖项)
函数名当 工具名 处理器名(内部) 提示词名
参数模型 完整 JSON Schema URI 占位符 扁平字符串列表
返回值当 工具结果(content + structured) 资源内容 用户消息
类比 POST GET 保存的查询

这张表值得反复回看——它把三大原语的本质差别压缩在一张表里。

六、一个文件里同时用三者

实战中,一个服务端经常三类原语都有。它们可以共存于同一个 MCPServer:

from mcp.server import MCPServer mcp = MCPServer("Demo") @mcp.tool() def search(query: str, limit: int = 10) -> list: """Search items by query.""" ... # 执行搜索(有副作用:查数据库) @mcp.resource("schema://{table}") def get_schema(table: str) -> str: """Return schema of a table.""" ... # 返回表结构(只读) @mcp.prompt() def analyze(table: str, question: str) -> str: """Generate a prompt to analyze a table.""" return f"分析 {table} 表,回答:{question}"

注意 analyze 提示词的参数 tablequestion 都是字符串——印证了「提示词参数只能扁平字符串」的规则。

本节要点回顾

  1. 三个装饰器共享「函数即元数据来源」哲学,差别只在「读出来当什么」。
  2. @mcp.tool() 把类型注解读成给模型的 JSON Schema,函数名当工具名,文档字符串当描述。
  3. @mcp.resource(uri) 的 URI 决定资源形态,无参数是具体资源,带 {param} 是模板。
  4. @mcp.prompt() 参数只能扁平字符串,因为 UI 要简单渲染输入框。
  5. 提示词返回值变成用户消息,这与工具结果、资源内容的定位不同。
  6. 三者可共存于同一个 MCPServer,实战中一个服务端常三类原语都有。
  7. 对照表(控制权/参数模型/返回值)是理解三原语的最佳抓手

注册清楚了,下一节讲这些注册如何变成「能力声明」,串起服务端与客户端。


作者与出处
原作者: 灏天文库
来源:modelcontextprotocol
许可证:MIT
整理: 灏天文库整理
由灏天文库结构化整理,提供目录导航、全文检索与在线阅读,便于系统化学习
发布者: 作者: 灏天文库 转发
评论区 (0)
U