本节摘要:本章的核心命题是「类型即契约」,本节先讲输入这一半——函数的参数类型注解,如何变成给模型的输入 JSON Schema。背后是 SDK 的函数元数据(
func_metadata)机制:它在你用@mcp.tool()装饰的那一刻,扫描函数签名,把 Python 类型映射成 JSON Schema 的类型,把参数名映射成 Schema 的字段,把文档字符串映射成描述。我们会讲透这套映射规则——基础类型、容器类型、可选类型、Pydantic 模型各自怎么映射,以及为什么这套机制能替代手写 Schema。读完本节,你能向别人讲清楚「为什么a: int就够」。
先认识幕后的主角:函数元数据(func_metadata)。它是 SDK 内部的一个工具函数,负责扫描任意 Python 函数,提取出「这个函数需要什么参数、什么类型、有没有默认值」的完整描述。
@mcp.tool() 装饰时发生的事 │ ▼ 调用 func_metadata(add) │ ▼ 扫描函数签名 提取出: - 参数名:[a, b] - 类型:{a: int, b: int} - 必填性:{a: 必填, b: 必填} - 默认值:{} │ ▼ 转成 JSON Schema { "type": "object", "properties": { "a": {"type": "integer"}, "b": {"type": "integer"} }, "required": ["a", "b"] } │ ▼ 这个 Schema 发给模型
整个流程在你装饰的那一刻就完成了,不需要运行时反复扫描。这就是为什么第 1.2 节说「你不写 Schema,SDK 从函数读出来」——读的动作,就是 func_metadata 干的。
Python 的基础类型,直接映射到 JSON Schema 的基础类型:
| Python 类型注解 | 映射成的 JSON Schema | 示例参数 |
|---|---|---|
int |
{"type": "integer"} |
a: int |
float |
{"type": "number"} |
rate: float |
str |
{"type": "string"} |
name: str |
bool |
{"type": "boolean"} |
verbose: bool |
这几个映射是 1:1 的、可预测的。模型看到的 Schema 与你写的注解完全对应:
@mcp.tool() def set_rate(name: str, rate: float, enabled: bool) -> str: """Set a rate for a named target.""" ...
模型看到的 Schema 大致是:
{ "name": {"type": "string"}, "rate": {"type": "number"}, "enabled": {"type": "boolean"} }
容器类型的映射稍复杂,但仍是规则化的:
| Python 类型注解 | 映射成的 JSON Schema |
|---|---|
list[str] |
{"type": "array", "items": {"type": "string"}} |
list[int] |
{"type": "array", "items": {"type": "integer"}} |
dict[str, int] |
{"type": "object", "additionalProperties": {"type": "integer"}} |
tuple[str, int] |
定长数组(每个位置独立类型) |
@mcp.tool() def batch_process(items: list[str], mapping: dict[str, int]) -> list[str]: """Process a batch of items with a mapping.""" ...
模型据此知道:items 是字符串数组、mapping 是「键字符串、值整数」的对象。这种映射让模型能正确构造嵌套结构的参数,而不会传错类型。
可选类型(Optional / X | None)与默认值,决定参数的「必填性」:
@mcp.tool() def search( query: str, # 必填,无默认值 limit: int = 10, # 可选,有默认值 offset: int = 0, # 可选 filter: str | None = None, # 可选,可为 None ) -> list: """Search items.""" ...
映射规则:
| 写法 | 必填性 | Schema 里的体现 |
|---|---|---|
query: str(无默认值) |
必填 | 在 required 数组里 |
limit: int = 10(有默认值) |
可选 | 不在 required 里,Schema 标注 default: 10 |
filter: str | None = None |
可选 | 类型是 ["string", "null"] |
这套「默认值 = 可选参数」的规则,让模型知道「哪些参数必须填、哪些可以省略」。这是减少模型错误调用的关键——必填参数少了模型会瞎填,可选参数多了模型会困惑,清晰的必填性让模型准确判断。
💡 技巧:把「这个参数模型必须显式提供」的设为必填(无默认值),把「模型可以省略、用默认即可」的设为可选(有默认值)。这个判断直接影响模型调用的准确率。
当参数本身是个复杂结构,用 Pydantic BaseModel:
from pydantic import BaseModel class SearchQuery(BaseModel): text: str limit: int = 10 tags: list[str] = [] @mcp.tool() def advanced_search(query: SearchQuery) -> list: """Advanced search with structured query.""" ...
func_metadata 会把 SearchQuery 展开成嵌套的 JSON Schema:
{ "query": { "type": "object", "properties": { "text": {"type": "string"}, "limit": {"type": "integer", "default": 10}, "tags": {"type": "array", "items": {"type": "string"}} }, "required": ["text"] } }
模型据此能构造出嵌套的对象参数。这种「参数即模型」的写法,让你能用 Pydantic 表达任意复杂的参数结构,而不必手写嵌套 Schema。
现在退一步,看 func_metadata 替你省了什么。如果不用它,你要手写:
# 手写方式(不用 SDK 推断) tool_schema = { "name": "search", "description": "Search items.", "inputSchema": { "type": "object", "properties": { "query": {"type": "string"}, "limit": {"type": "integer", "default": 10}, # ... 几十行 }, "required": ["query"] } }
而用 SDK,你只写:
@mcp.tool() def search(query: str, limit: int = 10) -> list: """Search items.""" ...
手写 Schema 的所有信息,都在类型注解里。func_metadata 把它们读出来、转成 Schema,你不用维护两份(类型一份、Schema 一份),永远不会出现「类型改了、Schema 忘改」的割裂。
⚠️ 注意:这套机制的代价是——类型注解必须准确。如果你写
a: int但实际接受字符串,SDK 生成的 Schema 会告诉模型「这是整数」,模型传整数进来,你的代码可能出错。所以「类型即契约」也意味着「类型注解就是承诺」,必须如实写。
func_metadata 是扫描函数、提取元数据的引擎,装饰时运行一次。int→integer、float→number、str→string、bool→boolean。list[str]→array、dict[str, int]→object。required),有默认值→可选。输入这一半清楚了,下一节讲输出那一半——返回类型注解如何变成结构化输出。