python-sdk 结构化输出完全指南:让 MCP 工具返回类型注解即输出 Schema

发布时间:2026/9/20 16:36:19

python-sdk 结构化输出完全指南:让 MCP 工具返回类型注解即输出 Schema python-sdk 结构化输出完全指南让 MCP 工具返回类型注解即输出 Schema【免费下载链接】python-sdkThe official Python SDK for Model Context Protocol servers and clients项目地址: https://gitcode.com/gh_mirrors/pythonsd/python-sdk输出文章MCP Python SDK 结构化输出指南从返回类型注解到structured_content全流程解析本指南以官方 python-sdkModel Context Protocol 的 Python 官方实现中docs/servers/structured-output.md为核心骨架系统讲解 MCP 服务器工具Tool的**结构化输出Structured Output**机制output_schema从何而来、structured_content有哪些形态、SDK 如何保证输出与 Schema 一致。读完本文你将掌握用纯类型注解声明工具输出契约、在content与structured_content双通道间取舍、以及如何借助structured_outputFalse/True精确控制工具行为。一句话概括本机制的核心工具的返回类型注解就是输出 Schema——你早已把它写好了。什么是结构化输出返回类型注解即输出 Schema一个返回普通str的工具其结果会被同时产出两次以文本形式出现在content中以{result: ...}形式出现在structured_content中。本文讨论的就是这第二个通道它来自哪里、能呈现哪些形态、SDK 又是如何保证它诚实的。先看最简单的一个工具完整示例见 tutorial001.pyfrom mcp.server import MCPServer mcp MCPServer(Weather) READINGS {London: 17, Cairo: 34, Reykjavik: 4} mcp.tool() def get_temperature(city: str) - int: Current temperature in a city, in whole degrees Celsius. return READINGS[city]关键就在函数签名里的- int。正是这个注解让 SDK 在tools/list阶段发布工具时除了根据参数构建的输入 Schema参见 Tools 一文还会携带一份output_schema{ properties: { result: {title: Result, type: integer} }, required: [result], title: get_temperatureOutput, type: object }裸的int本身不是 JSON 对象因此 SDK 将其包装为{result: ...}。调用该工具后两个通道都会被填充result.content # [TextContent(text17)] result.structured_content # {result: 17}所有标量类型都使用同样的包装规则str、int、float、bool、bytes、None。两个通道给模型读的文本给应用读的数据为什么要把同一个值发送两次content是给模型model看的。语言模型读取的是文本这是模型能看到的结果的唯一部分structured_content是给**模型所运行的应用application**看的代码想要的是数字17而不是一句包含 17 的话output_schema是两者之间的契约在工具被调用之前就已对外发布。你只需返回一个 Python 值SDK 会为你填满全部三个部分。从源码实现看这一转换发生在 func_metadata.py 的FuncMetadata.convert_result()方法中它将函数返回值转换为CallToolResult(contentunstructured_content, structured_contentstructured_content)其中非字符串值通过_convert_to_content()序列化为 JSON 文本pydantic_core.to_json结构化部分则经由 PydanticTypeAdapter校验后按 JSON 模式导出。返回一个 Pydantic 模型声明即 Schema声明一个 PydanticBaseModel并返回其实例见 tutorial002.pyfrom pydantic import BaseModel, Field from mcp.server import MCPServer mcp MCPServer(Weather) class WeatherData(BaseModel): temperature: float Field(descriptionDegrees Celsius.) humidity: float Field(descriptionRelative humidity, 0 to 1.) conditions: str mcp.tool() def get_weather(city: str) - WeatherData: Current weather for a city. return WeatherData(temperature16.2, humidity0.83, conditionsOvercast)此时WeatherData本身就是 Schema——没有包装层也没有result键{ properties: { temperature: {description: Degrees Celsius., title: Temperature, type: number}, humidity: {description: Relative humidity, 0 to 1., title: Humidity, type: number}, conditions: {title: Conditions, type: string} }, required: [temperature, humidity, conditions], title: WeatherData, type: object }structured_content就是该对象本身字段一一对应result.structured_content # {temperature: 16.2, humidity: 0.83, conditions: Overcast}模型也没有被冷落。SDK 会把同一对象序列化为 JSON 文本放进content{ temperature: 16.2, humidity: 0.83, conditions: Overcast }注意temperature和humidity上的Field(description...)也进入了 Schema。描述输入参数的那套Field机制同样用于描述输出字段。如果你用过 FastAPI 的response_model对这个模式一定不陌生声明一个 Pydantic 模型作为响应由框架负责序列化与文档化。唯一的差别在于这里返回类型注解就是全部声明。从实现上看_create_output_model()对BaseModel子类走的是直接使用分支model type_annotation并且convert_result()在校验后通过validated.model_dump(modejson, by_aliasTrue)导出确保返回值与output_schema严格同构。使用 TypedDict不想写类时的轻量选择并非每种形状都值得定义一个类。TypedDict可以产出同样的 Schema见 tutorial003.pyfrom typing import TypedDict from mcp.server import MCPServer mcp MCPServer(Weather) class WeatherData(TypedDict): temperature: float humidity: float conditions: str mcp.tool() def get_weather(city: str) - WeatherData: Current weather for a city. return WeatherData(temperature16.2, humidity0.83, conditionsOvercast)TypedDict在运行时就是普通的dict所以你就直接构造并返回一个 dict。Schema、校验和structured_content遵循与BaseModel版本相同的规则添加类 docstring 或Annotated[..., Field(description...)]它们会变成字段描述用NotRequired标记的可选键如果你在 dict 中省略了它它也不会出现在structured_content中。实现细节上SDK 在 func_metadata.py 的_pydantic_readable_typeddict()中做了兼容处理在 Python 3.12 以下typing.TypedDict会被重建为typing_extensions.TypedDict以便 Pydantic 读取其键、docstring 与配置包括ReadOnly、NotRequired等限定符这样工具作者完全无需关心版本差异。使用 dataclass任何带类型注解的类都可以Dataclass 同样可用任何属性带类型提示的普通类也都可以。SDK 会在幕后根据这些注解构建一个 Pydantic 模型见 tutorial004.pyfrom dataclasses import dataclass from mcp.server import MCPServer mcp MCPServer(Weather) dataclass class WeatherData: temperature: float humidity: float conditions: str mcp.tool() def get_weather(city: str) - WeatherData: Current weather for a city. return WeatherData(temperature16.2, humidity0.83, conditionsOvercast)三种写法BaseModel/TypedDict/ dataclass产出同一套 Schema。用你代码库中已有的那一种就好。实现层面_create_output_model()对其他带注解的类走_create_model_from_class()它用get_type_hints()提取类属性注解通过create_model(cls.__name__, __config__ConfigDict(from_attributesTrue), ...)动态构建 Pydantic 模型类上带默认值的字段会成为可选字段没有默认值的字段进入 required 集合。返回列表{result: ...}包装与$defs引用list[...]本身也不是 JSON 对象因此同样会被包进{result: ...}而你的元素类型会以$defs引用的形式出现在包装内部见 tutorial005.pyfrom pydantic import BaseModel from mcp.server import MCPServer mcp MCPServer(Weather) class WeatherData(BaseModel): temperature: float humidity: float conditions: str mcp.tool() def get_forecast(city: str, days: int) - list[WeatherData]: Daily forecast for a city. return [WeatherData(temperature16.2 day, humidity0.83, conditionsOvercast) for day in range(days)]生成的 Schema{ $defs: { WeatherData: { properties: { temperature: {title: Temperature, type: number}, humidity: {title: Humidity, type: number}, conditions: {title: Conditions, type: string} }, required: [temperature, humidity, conditions], title: WeatherData, type: object } }, properties: { result: {items: {$ref: #/$defs/WeatherData}, title: Result, type: array} }, required: [result], title: get_forecastOutput, type: object }请求两天预报时structured_content是{result: [{...}, {...}]}而content则变成两个TextContent块——每个元素一块。列表会为模型展平而不是作为一个字符串整体倾倒。这一点在_convert_to_content()中有明确实现对list/tuple值会递归展平chain.from_iterable将每个元素分别转换为内容块。tuple[...]、联合类型union以及Optional[...]遵循相同的包装规则。返回字典dict[str, ...]是唯一不包装的泛型dict[str, ...]本身已经是一个 JSON 对象因此不会被包装见 tutorial006.pyfrom mcp.server import MCPServer mcp MCPServer(Weather) READINGS {London: 16.2, Cairo: 34.1, Reykjavik: 4.4} mcp.tool() def get_temperatures(cities: list[str]) - dict[str, float]: Current temperature for each city, in degrees Celsius. return {city: READINGS[city] for city in cities}生成的 Schema{ additionalProperties: {type: number}, title: get_temperaturesDictOutput, type: object }调用结果result.structured_content # {London: 16.2, Reykjavik: 4.4}注意键必须是str。dict[int, float]无法成为 JSON 对象因此会回退到{result: ...}包装。这一点在_create_output_model()的GenericAlias分支中有精确判断仅当get_origin(type_expr) is dict且键类型为str时才直接使用并打上Field(titlef{func_name}DictOutput)作为 Schema 标题其余情况一律走包装路径。实现层面字典型结果使用 Pydantic 的TypeAdapter进行校验与序列化。如果你检查某个工具的FuncMetadata.output_model它会持有该字典类型注解及其 Schema 标题——FuncMetadata在构造时若发现output_model非空而output_schema为空会立即用TypeAdapter(...).json_schema()派生 Schema见 func_metadata.py 的model_post_init。输出验证Schema 不是文档而是执行标准output_schema不是摆设。无论你的函数返回什么在离开服务器之前都会被拿来与它校验。当你亲手构建值时不会察觉到这一点——Pydantic 已经确保你的WeatherData就是WeatherData。真正遇到麻烦的是数据来自你无法控制的地方的那一天见 tutorial007.pyimport json from pydantic import BaseModel from mcp.server import MCPServer mcp MCPServer(Weather) UPSTREAM {London: {temperature: 16.2, conditions: Overcast}} class WeatherData(BaseModel): temperature: float humidity: float conditions: str mcp.tool() def get_weather(city: str) - WeatherData: Current weather for a city. return json.loads(UPSTREAM[city])注解承诺返回WeatherData但上游响应停止了发送humidity字段。调用get_weather时服务器不会悄悄地把一个残缺对象交给客户端而是让这次调用失败客户端收到is_errorTrue与Error executing tool get_weather模型因此知道调用失败了而不是自信地读出并不存在的天气数据。字段名则记录在服务器ERROR级别的日志中供你排查Tool get_weather raised an unexpected exception ... pydantic_core._pydantic_core.ValidationError: 1 validation error for WeatherData humidity Field required [typemissing, input_value{temperature: 16.2, conditions: Overcast}, input_typedict]顺带一提从一个- WeatherData的工具返回普通dict完全没问题——这正是json.loads产出的东西。校验针对的是值本身而不是 Python 类型。从源码看校验在convert_result()中执行adapter.validate_python(result, by_aliasTrue, by_nameTrue)会抛出pydantic_core.ValidationError进而以工具错误的形式反馈给客户端相关行为在 test_tool_manager.py 的TestStructuredOutput测试类中有覆盖如test_tool_with_basemodel_output断言structured_content {name: John, age: 30}。主动退出与强制开启structured_outputFalse / True有时返回类型注解是写给类型检查器看的而不是给协议用的。传入structured_outputFalse工具就变成纯文本模式见 tutorial008.pyfrom mcp.server import MCPServer mcp MCPServer(Weather) mcp.tool(structured_outputFalse) def weather_report(city: str) - str: A human-readable weather report for a city. return f{city}: 17 degrees, overcast, light rain easing by evening.此时没有output_schema、没有包装、没有校验。structured_content为Nonecontent就是你返回的字符串。与之相反structured_outputTrue会把自动检测升级为硬性要求一个返回类型无法生成 Schema 的工具会在导入时注册时直接报错而不是默默回退为文本。这一行为由 func_metadata.py 的func_metadata()实现它使用StrictJsonSchema一个遇到警告即抛异常的 JSON Schema 生成器emit_warning()直接 raiseValueError派生 Schema当structured_outputTrue而模型创建失败时抛出InvalidSignature(Function ... return type ... is not serializable for structured output)。参数structured_output的三种取值语义在func_metadata()的 docstring 中有明确说明None根据返回类型注解自动检测True强制创建结构化工具返回类型允许的前提下False无条件创建非结构化工具。内容块与媒体默认自动退出内容块与媒体TextContent、EmbeddedResource、Image、Audio及其同类无论是单独出现、作为list/tuple/Sequence的元素还是作为联合类型的成员都会为你自动退出结构化输出它们是给模型读的因此自动检测不会从它们身上派生 SchemaImage与Audio详见 Images, audio icons。不过structured_outputTrue仍会为这些内容块类强制生成一个 Schema。实现上_returns_content()专门检测返回注解是否为内容块类型裸类型、Annotated包裹、联合成员或list/tuple/Sequence元素命中即返回非结构化元数据FuncMetadata(arg_model...)且无output_model。相关注释明确说明若为其派生 Schema会把块自身模型当作output_schema发布并除非工具自行构建CallToolResult把每个块原样回显进structured_content因此默认予以排除。一个没有类型注解的类静默失守的陷阱有一种情况会让你无意间落入非结构化返回一个类体上没有任何注解的类见 tutorial009.pyfrom mcp.server import MCPServer mcp MCPServer(Weather) class Station: def __init__(self, name: str, online: bool): self.name name self.online online mcp.tool() def get_station(name: str) - Station: Look up a weather station by name. return Station(namename, onlineTrue)Station在__init__里设置了name和online但类本身没有声明任何注解。SDK 读取类注解时一无所获于是放弃。最危险的是它放弃得悄无声息。output_schema为Nonestructured_content为None模型读到的文本是对象的reprserver.Station object at 0x7f539d75b230没有报错、没有警告一个毫无用处的工具。解决方式有两种把注解移到类体上例如用 dataclass 或BaseModel重写传入structured_outputTrue把这种情况变成模块导入时的硬错误Function get_station: return type class server.Station is not serializable for structured output。从源码看_create_output_model()对其他类类型分支用get_type_hints(type_annotation)探测若返回空 dict即类无注解model保持None自动检测模式下静默回退为非结构化。需要完全控制自行构建CallToolResult或附加应用可见而模型不可见的_meta那是 The low-level Server 的范畴。小结结构化输出的五条要点返回类型注解就是输出 Schema并在tools/list中以output_schema形式对外发布标量、列表、元组与联合类型被包装进{result: ...}模型、TypedDict、dataclass、带注解的类以及dict[str, ...]本身就是对象保持原样每个结果都同时携带content给模型的文本与structured_content给应用的数据返回值会与 Schema 校验不匹配就是工具错误而不是损坏的结果structured_outputFalse让单个工具退出内容块、Image、Audio默认退出没有类型注解的类会静默退出务必留意。至此你已经掌握了一个工具能说回的全部内容。下一步请阅读第二个原语Resources。【免费下载链接】python-sdkThe official Python SDK for Model Context Protocol servers and clients项目地址: https://gitcode.com/gh_mirrors/pythonsd/python-sdk创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
延伸阅读

更多相关文章

2026/9/20 16:36:19

SystemView通信原理实验全解析:从抽样定理到数字调制

简介:北京邮电大学基于SystemView的通信原理软件实验报告,覆盖AM、SSB、FM的调制与解调,数字信号基带传输,OOK、2FSK、2PSK、16QAM调制与解调,以及抽样定理等九个完整实验。每个实验均包含实验目的、原理公式、SystemV…

2026/9/20 16:36:19

IATF16949全套文件实用指南:结构、改造与审核要点

简介:这份文档是IATF16949质量管理体系的全套文件与表格合辑,适合汽车行业质量管理人员、体系工程师、内审员以及正在推动IATF16949认证的企业使用。内容覆盖质量手册、质量方针、领导作用、策划、支持、运行、监视和测量资源、文件记录管理、过程设计和…

2026/9/20 17:31:27

PixiEditor · 自定义笔刷配置速通手册

PixiEditor 自定义笔刷配置速通手册 【免费下载链接】PixiEditor PixiEditor is a Universal Editor for all your 2D needs 项目地址: https://gitcode.com/GitHub_Trending/pi/PixiEditor PixiEditor 自定义笔刷只讲三件事:尺寸、抗锯齿、稳定化&#xff…

2026/9/20 17:31:27

开源可审计的LLM代码评审工作流:CLI+Git+OpenAI协议实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/20 17:26:26

radare2 libr 架构解析:一张图看懂核心库 API 依赖关系

逆向工程网络安全 【免费下载链接】radare2 UNIX-like reverse engineering framework and command-line toolset 项目地址: https://gitcode.com/gh_mirrors/ra/radare2 点击查看 免费下载 导读:radare2 的功能被拆分为多个以 r_ 前缀命名的核心库&…

2026/9/20 0:04:49

GAMP 5 基于风险的计算机化系统验证:软件分类与审计追踪实践

简介:《A Risk-Based Approach to Compliant GxP Computerized Systems》即业内熟知的GAMP 5指南,面向制药企业质量与IT合规人员、验证工程师及计算机化系统管理者,用于解决GxP法规环境下系统合规性难以科学落地的问题。文档以风险管理为主线…

2026/9/20 0:04:49

安全托管MSSP实战:从静态防御到人机协同的攻防运营与应急响应

简介:这份PPT围绕互联网业务安全托管服务展开,面向企业安全负责人、IT运维人员及关注MSSP/MSS选型的读者,重点回应传统安全过度依赖人工、碎片化静态防御难以对抗产业化攻击等痛点。资源共1个pptx文件,包体约30.63MB,以…

2026/9/20 0:04:49

GAMP 5 基于风险的计算机化系统验证:软件分类与审计追踪实践

简介:《A Risk-Based Approach to Compliant GxP Computerized Systems》即业内熟知的GAMP 5指南,面向制药企业质量与IT合规人员、验证工程师及计算机化系统管理者,用于解决GxP法规环境下系统合规性难以科学落地的问题。文档以风险管理为主线…

2026/9/20 0:04:49

安全托管MSSP实战:从静态防御到人机协同的攻防运营与应急响应

简介:这份PPT围绕互联网业务安全托管服务展开,面向企业安全负责人、IT运维人员及关注MSSP/MSS选型的读者,重点回应传统安全过度依赖人工、碎片化静态防御难以对抗产业化攻击等痛点。资源共1个pptx文件,包体约30.63MB,以…

2026/9/20 4:54:47

USB Type-C PCB布局分区设计:电源、高速信号与PD协议全攻略

做硬件这行,Type-C接口算是典型的“看着简单,做起来全坑”的东西。光引脚就24个,高低速信号、电源、控制线全部塞在一个小小的连接器里,如果PCB布局不做规划,打样回来基本就是“插上没反应”、“高速掉线”、“静电一打…

2026/9/20 5:01:23

系统编程学习原型如何补齐稳定性边界

系统编程学习原型如何补齐稳定性边界预算有限时&#xff0c;我先优化明显多余的复制&#xff0c;而不是猜测性地换容器。用借用传递只读数据通常就能减少分配&#xff1a; fn parse(line: &str) -> Result<Item, Error> { /* ... */ }用基准确认热点确实在分配&am…

2026/9/20 5:09:33

雨花区哪家财务公司代理记账比较好?

在雨花区&#xff0c;企业处理财税事务常常面临诸多挑战&#xff0c;选择一家靠谱的财务公司至关重要。湖南巨勤财务管理咨询有限公司就是本地正规实体财税服务机构&#xff0c;深耕本地工商财税行业多年&#xff0c;熟悉当地工商局、税务局最新政策与申报流程。主营公司注册、…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
咨询二维码