Saída estruturada
Tradução automática
Esta página foi traduzida automaticamente a partir da documentação em inglês, e a página em inglês é a versão de referência. Se algo parecer errado, Traduções explica como avisar.
Uma ferramenta (tool) que retorna uma str simples produz o resultado duas vezes: como texto em content e como {"result": "..."} em structured_content.
Esta página trata desse segundo canal: de onde ele vem, todas as formas que ele pode assumir e como o SDK garante que ele seja confiável.
A versão curta: a anotação do tipo de retorno é o schema de saída. Você já a escreveu.
O schema de saída
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]
A linha que importa é a assinatura: -> int.
Por causa dela, a ferramenta que o SDK envia durante tools/list carrega um output_schema ao lado do schema de entrada que ele monta a partir dos seus parâmetros (Ferramentas cobre esse):
{
"properties": {
"result": {"title": "Result", "type": "integer"}
},
"required": ["result"],
"title": "get_temperatureOutput",
"type": "object"
}
Um int sozinho não é um objeto JSON, então o SDK o envolve em {"result": ...}. Chame a ferramenta e os dois canais vêm preenchidos:
result.content # [TextContent(text="17")]
result.structured_content # {"result": 17}
Todo escalar recebe o mesmo wrapper: str, int, float, bool, bytes, None.
Dois canais
Por que enviar o mesmo valor duas vezes?
contenté para o modelo. Um modelo de linguagem lê texto; essa é a única parte do resultado que ele vê.structured_contenté para a aplicação dentro da qual o modelo roda: código que quer17, não uma frase contendo "17".output_schemaé o contrato entre os dois, publicado antes mesmo de a ferramenta ser chamada.
Você retorna um único valor Python. O SDK preenche os três.
Retorne um modelo
Declare a forma como um BaseModel do Pydantic e retorne uma instância:
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")
WeatherData agora é o schema. Sem wrapper, sem chave 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 é o objeto, campo por campo:
result.structured_content # {"temperature": 16.2, "humidity": 0.83, "conditions": "Overcast"}
E o modelo não fica de fora. O SDK serializa o mesmo objeto como texto JSON para content:
{
"temperature": 16.2,
"humidity": 0.83,
"conditions": "Overcast"
}
Repare que o Field(description=...) em temperature e humidity foi parar no schema. O mesmo Field que descrevia as suas entradas descreve as suas saídas.
Info
Se você já usou o response_model do FastAPI, já conhece isso: um modelo Pydantic como a resposta
declarada, serializado e documentado para você. A única diferença é que aqui a anotação de retorno
é a declaração inteira.
Um TypedDict
Nem toda forma merece uma classe. Um TypedDict produz o mesmo schema:
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")
Um TypedDict é um dict comum em tempo de execução, então é isso que você monta e retorna. O schema, a validação e o structured_content são idênticos aos da versão com BaseModel (menos as descrições, para as quais o TypedDict não tem lugar).
Uma dataclass
Dataclasses também funcionam, assim como qualquer classe comum cujos atributos tenham anotações de tipo. O SDK monta um modelo Pydantic a partir das anotações por baixo dos panos.
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")
Três formas de escrever, um schema só. Use a que a sua base de código já tem.
Listas
Uma list[...] também não é um objeto JSON, então ela recebe o wrapper {"result": ...}, com o tipo dos seus itens como uma referência $defs dentro dele:
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"
}
Peça uma previsão de dois dias e structured_content vem como {"result": [{...}, {...}]}. content vira dois blocos TextContent, um por item: uma lista é achatada para o modelo em vez de ser despejada como uma única string.
tuple[...], uniões e Optional[...] são envolvidos da mesma forma.
Dicionários
dict[str, ...] é o único genérico que já é um objeto JSON, então ele não é envolvido:
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}
As chaves precisam ser str. Um dict[int, float] não pode ser um objeto JSON, então ele recai no wrapper {"result": ...}.
Validação
output_schema não é documentação. O que quer que a sua função retorne é validado contra ele antes de sair do servidor.
Você não percebe enquanto monta o valor à mão: o Pydantic já garantiu que o seu WeatherData era um WeatherData. Você percebe no dia em que os dados vêm de algum lugar que você não controla:
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])
A anotação promete WeatherData. A resposta do serviço upstream parou de enviar humidity.
Check
Chame get_weather e ela não entrega discretamente ao cliente um objeto pela metade. A chamada falha,
e as primeiras linhas do erro dão o nome do 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]
Esse texto volta como o resultado da ferramenta com is_error=True, então o modelo sabe que a chamada
falhou em vez de ler, com toda a confiança, um clima que não existe.
Retornar um dict comum de uma ferramenta -> WeatherData não tem problema, aliás. É exatamente isso que json.loads produziu. A validação é feita sobre o valor, não sobre o tipo Python.
Desativando
Às vezes a anotação de retorno é para o seu verificador de tipos, não para o protocolo. Passe structured_output=False e a ferramenta fica só com 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."
Sem output_schema, sem wrapper, sem validação. structured_content é None e content é a string que você retornou.
O oposto, structured_output=True, transforma a detecção automática em exigência: uma ferramenta cujo tipo de retorno não consegue produzir um schema levanta uma exceção no momento do import em vez de recair para texto.
Uma classe sem anotações de tipo
Existe um jeito de acabar sem estrutura sem ter pedido por isso: retornar uma classe que não tem anotações no corpo.
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 define name e online dentro de __init__, mas a classe não declara nada. O SDK lê as anotações da classe, não encontra nenhuma e desiste.
Warning
Ele desiste em silêncio. output_schema é None, structured_content é None, e o texto
que o modelo lê é o repr do objeto:
"<server.Station object at 0x7f539d75b230>"
Nenhum erro, nenhum aviso, uma ferramenta inútil. Mova as anotações para o corpo da classe, ou passe
structured_output=True, que transforma isso em um erro de verdade no momento em que o módulo é importado:
Function get_station: return type <class 'server.Station'> is not serializable for structured output.
Tip
Precisa de controle total (montar o CallToolResult você mesmo, ou anexar um _meta que a
aplicação enxerga mas o modelo não)? Isso está em O Server de baixo nível.
Recapitulando
- A anotação do tipo de retorno é o schema de saída. Ela é publicada em
tools/listcomooutput_schema. - Escalares, listas, tuplas e uniões são envolvidos em
{"result": ...}. Modelos,TypedDicts, dataclasses, classes anotadas edict[str, ...]já são objetos e ficam como estão. - Todo resultado carrega
content(texto, para o modelo) estructured_content(dados, para a aplicação). - O que você retorna é validado contra o schema. Uma divergência vira um erro de ferramenta, não um resultado corrompido.
structured_output=Falsedeixa uma ferramenta de fora. Uma classe sem anotações de tipo fica de fora em silêncio; fique atento a isso.
Agora você domina tudo o que uma ferramenta pode dizer de volta. A seguir, a segunda primitiva: Recursos.