Pular para conteúdo

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

server.py
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 quer 17, 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:

server.py
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:

server.py
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.

server.py
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:

server.py
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:

server.py
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:

server.py
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:

server.py
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.

server.py
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/list como output_schema.
  • Escalares, listas, tuplas e uniões são envolvidos em {"result": ...}. Modelos, TypedDicts, dataclasses, classes anotadas e dict[str, ...] já são objetos e ficam como estão.
  • Todo resultado carrega content (texto, para o modelo) e structured_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=False deixa 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.