Salida estructurada
Traducción automática
Esta página se tradujo automáticamente a partir de la documentación en inglés, y la página en inglés es la versión de referencia. Si algo no se lee bien, Traducciones explica cómo avisarnos.
Una herramienta que devuelve un simple str produce el resultado dos veces: como texto en content y como {"result": "..."} en structured_content.
Esta página trata de ese segundo canal: de dónde sale, todas las formas que puede tomar y cómo el SDK garantiza que sea fiel.
La versión corta: la anotación del tipo de retorno es el esquema de salida. Ya la escribiste.
El esquema de salida
from 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]
La línea que importa es la firma: -> int.
Gracias a ella, la herramienta que el SDK envía durante tools/list lleva un output_schema junto al esquema de entrada que construye a partir de tus parámetros (de ese se ocupa Herramientas):
{
"properties": {
"result": {"title": "Result", "type": "integer"}
},
"required": ["result"],
"title": "get_temperatureOutput",
"type": "object"
}
Un int suelto no es un objeto JSON, así que el SDK lo envuelve en {"result": ...}. Llama a la herramienta y se llenan los dos canales:
result.content # [TextContent(text="17")]
result.structured_content # {"result": 17}
Todos los escalares reciben el mismo envoltorio: str, int, float, bool, bytes, None.
Dos canales
¿Por qué enviar el mismo valor dos veces?
contentes para el modelo. Un modelo de lenguaje lee texto; es la única parte del resultado que ve.structured_contentes para la aplicación dentro de la que se ejecuta el modelo: código que quiere17, no una frase que contenga "17".output_schemaes el contrato entre ambos, publicado antes de que la herramienta se llame por primera vez.
Devuelves un único valor de Python. El SDK rellena los tres.
Devolver un modelo
Declara la forma como un BaseModel de Pydantic y devuelve una instancia:
from pydantic import BaseModel, Field
from mcp.server import MCPServer
mcp = MCPServer("Weather")
class WeatherData(BaseModel):
temperature: float = Field(description="Degrees Celsius.")
humidity: float = Field(description="Relative humidity, 0 to 1.")
conditions: str
@mcp.tool()
def get_weather(city: str) -> WeatherData:
"""Current weather for a city."""
return WeatherData(temperature=16.2, humidity=0.83, conditions="Overcast")
Ahora WeatherData es el esquema. Sin envoltorio, sin clave 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 es el objeto, campo por campo:
result.structured_content # {"temperature": 16.2, "humidity": 0.83, "conditions": "Overcast"}
Y el modelo no se queda fuera. El SDK serializa el mismo objeto como texto JSON para content:
{
"temperature": 16.2,
"humidity": 0.83,
"conditions": "Overcast"
}
Fíjate en que el Field(description=...) de temperature y humidity acabó en el esquema. El mismo Field que describía tus entradas describe tus salidas.
Info
Si has usado el response_model de FastAPI, esto ya lo conoces: un modelo de Pydantic como respuesta
declarada, serializado y documentado por ti. La única diferencia es que aquí la anotación de retorno
es toda la declaración.
Un TypedDict
No todas las formas merecen una clase. Un TypedDict produce el mismo esquema:
from 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(temperature=16.2, humidity=0.83, conditions="Overcast")
Un TypedDict es un dict normal en tiempo de ejecución, así que eso es lo que construyes y devuelves. El esquema, la validación y structured_content son idénticos a los de la versión con BaseModel (salvo las descripciones, para las que TypedDict no tiene sitio).
Una dataclass
Las dataclasses también funcionan, igual que cualquier clase normal cuyos atributos tengan anotaciones de tipo. El SDK construye internamente un modelo de Pydantic a partir de las anotaciones.
from 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(temperature=16.2, humidity=0.83, conditions="Overcast")
Tres formas de escribirlo, un solo esquema. Usa la que ya tenga tu código.
Listas
Un list[...] tampoco es un objeto JSON, así que recibe el envoltorio {"result": ...}, con tu tipo de elemento dentro como referencia en $defs:
from 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(temperature=16.2 + day, humidity=0.83, conditions="Overcast") for day in range(days)]
{
"$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"
}
Pide un pronóstico de dos días y structured_content es {"result": [{...}, {...}]}. content se convierte en dos bloques TextContent, uno por elemento: una lista se aplana para el modelo en lugar de volcarse como una sola cadena.
tuple[...], las uniones y Optional[...] se envuelven de la misma manera.
Diccionarios
dict[str, ...] es el único genérico que ya es un objeto JSON, así que no se envuelve:
from 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}
{
"additionalProperties": {"type": "number"},
"title": "get_temperaturesDictOutput",
"type": "object"
}
result.structured_content # {"London": 16.2, "Reykjavik": 4.4}
Las claves deben ser str. Un dict[int, float] no puede ser un objeto JSON, así que recurre al envoltorio {"result": ...}.
Validación
output_schema no es documentación. Lo que devuelva tu función se valida contra él antes de salir del servidor.
No lo notas mientras construyes el valor a mano: Pydantic ya se aseguró de que tu WeatherData fuera un WeatherData. Lo notas el día que los datos vienen de algún sitio que no controlas:
import 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])
La anotación promete WeatherData. La respuesta del servicio externo dejó de enviar humidity.
Check
Llama a get_weather y no le entrega al cliente en silencio un objeto medio vacío. La llamada falla,
y las primeras líneas del error nombran el campo:
Error executing tool get_weather: 1 validation error for WeatherData
humidity
Field required [type=missing, input_value={'temperature': 16.2, 'conditions': 'Overcast'}, input_type=dict]
Ese texto vuelve como resultado de la herramienta con is_error=True, así que el modelo sabe que la
llamada falló en lugar de leer con toda confianza un tiempo que no existe.
Por cierto, devolver un dict normal desde una herramienta -> WeatherData está bien. Es exactamente lo que produjo json.loads. La validación se aplica al valor, no al tipo de Python.
Desactivarlo
A veces la anotación de retorno es para tu verificador de tipos, no para el protocolo. Pasa structured_output=False y la herramienta es solo texto:
from mcp.server import MCPServer
mcp = MCPServer("Weather")
@mcp.tool(structured_output=False)
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."
Sin output_schema, sin envoltorio, sin validación. structured_content es None y content es la cadena que devolviste.
Lo contrario, structured_output=True, convierte la detección automática en un requisito: una herramienta cuyo tipo de retorno no pueda producir un esquema lanza una excepción al importar el módulo en lugar de recurrir al texto.
Una clase sin anotaciones de tipo
Hay una forma de acabar sin salida estructurada sin haberlo pedido: devolver una clase que no tiene anotaciones en su cuerpo.
from 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(name=name, online=True)
Station asigna name y online dentro de __init__, pero la clase no declara nada. El SDK lee las anotaciones de la clase, no encuentra ninguna y desiste.
Warning
Desiste en silencio. output_schema es None, structured_content es None y el texto
que lee el modelo es el repr del objeto:
"<server.Station object at 0x7f539d75b230>"
Ni error, ni aviso: una herramienta inútil. Mueve las anotaciones al cuerpo de la clase o pasa
structured_output=True, que convierte esto en un error inmediato en cuanto se importa el módulo:
Function get_station: return type <class 'server.Station'> is not serializable for structured output.
Tip
¿Necesitas control total (construir el CallToolResult tú mismo o adjuntar un _meta que la
aplicación pueda ver pero el modelo no)? Eso es El Server de bajo nivel.
Resumen
- La anotación del tipo de retorno es el esquema de salida. Se publica en
tools/listcomooutput_schema. - Los escalares, las listas, las tuplas y las uniones se envuelven en
{"result": ...}. Los modelos, losTypedDict, las dataclasses, las clases con anotaciones ydict[str, ...]ya son objetos y se quedan como están. - Cada resultado lleva
content(texto, para el modelo) ystructured_content(datos, para la aplicación). - Lo que devuelves se valida contra el esquema. Una discrepancia es un error de herramienta, no un resultado corrupto.
structured_output=Falseexcluye una herramienta. Una clase sin anotaciones de tipo queda excluida en silencio; vigílalo.
Ahora dominas todo lo que una herramienta puede responder. A continuación, la segunda primitiva: Recursos.