本节摘要:上一节认识了
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 │ └──────────────────────┘ └──────────────────────┘
这套哲学贯穿三个装饰器,差别只在「读出来的元数据用作什么」。下面逐个看。
工具装饰器把函数变成「模型可调用」的工具。它的核心是类型注解被读成给模型的 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显式覆盖。
资源装饰器把函数变成「应用可读」的资源。它的核心是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 章会详讲这套机制。
提示词装饰器把函数变成「用户可触发」的命名消息模板。它有一个特别限制:参数只能是扁平的字符串列表,没有 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 提示词的参数 table 和 question 都是字符串——印证了「提示词参数只能扁平字符串」的规则。
@mcp.tool() 把类型注解读成给模型的 JSON Schema,函数名当工具名,文档字符串当描述。@mcp.resource(uri) 的 URI 决定资源形态,无参数是具体资源,带 {param} 是模板。@mcp.prompt() 参数只能扁平字符串,因为 UI 要简单渲染输入框。注册清楚了,下一节讲这些注册如何变成「能力声明」,串起服务端与客户端。