Zum Inhalt

Strukturierte Ausgabe

Maschinelle Übersetzung

Diese Seite wurde automatisch aus der englischen Dokumentation übersetzt, und die englische Seite ist die maßgebliche Fassung. Wenn sich etwas falsch liest, erklärt Übersetzungen, wie du es melden kannst.

Ein Tool, das einen einfachen str zurückgibt, liefert das Ergebnis doppelt: als Text in content und als {"result": "..."} in structured_content.

Auf dieser Seite geht es um diesen zweiten Kanal: woher er kommt, welche Formen er annehmen kann und wie das SDK dafür sorgt, dass er hält, was er verspricht.

Die Kurzfassung: Die Annotation des Rückgabetyps ist das Ausgabeschema. Du hast sie schon geschrieben.

Das Ausgabeschema

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]

Die entscheidende Zeile ist die Signatur: -> int.

Ihretwegen trägt das Tool, das das SDK bei tools/list sendet, ein output_schema neben dem Eingabeschema, das es aus deinen Parametern baut (darum kümmert sich Tools):

{
  "properties": {
    "result": {"title": "Result", "type": "integer"}
  },
  "required": ["result"],
  "title": "get_temperatureOutput",
  "type": "object"
}

Ein nackter int ist kein JSON-Objekt, also verpackt das SDK ihn in {"result": ...}. Ruf das Tool auf, und beide Kanäle sind gefüllt:

result.content             # [TextContent(text="17")]
result.structured_content  # {"result": 17}

Jeder skalare Wert bekommt dieselbe Hülle: str, int, float, bool, bytes, None.

Zwei Kanäle

Warum denselben Wert zweimal senden?

  • content ist für das Modell. Ein Sprachmodell liest Text; das ist der einzige Teil des Ergebnisses, den es sieht.
  • structured_content ist für die Anwendung, in der das Modell läuft: Code, der 17 will und keinen Satz, in dem „17“ vorkommt.
  • output_schema ist der Vertrag zwischen beiden, veröffentlicht, bevor das Tool überhaupt aufgerufen wird.

Du gibst einen einzigen Python-Wert zurück. Das SDK füllt alle drei.

Ein Modell zurückgeben

Deklariere die Form als Pydantic-BaseModel und gib eine Instanz zurück:

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 ist jetzt das Schema. Keine Hülle, kein result-Schlüssel:

{
  "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 ist das Objekt, Feld für Feld:

result.structured_content  # {"temperature": 16.2, "humidity": 0.83, "conditions": "Overcast"}

Und das Modell geht nicht leer aus. Das SDK serialisiert dasselbe Objekt für content zu JSON-Text:

{
  "temperature": 16.2,
  "humidity": 0.83,
  "conditions": "Overcast"
}

Beachte, dass das Field(description=...) an temperature und humidity im Schema gelandet ist. Dasselbe Field, das deine Eingaben beschrieben hat, beschreibt auch deine Ausgaben.

Info

Wenn du FastAPIs response_model kennst, kennst du das hier schon: ein Pydantic-Modell als deklarierte Response, für dich serialisiert und dokumentiert. Der einzige Unterschied: Hier ist die Annotation des Rückgabetyps die ganze Deklaration.

Ein TypedDict

Nicht jede Form verdient eine Klasse. Ein TypedDict erzeugt dasselbe 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")

Ein TypedDict ist zur Laufzeit ein einfaches dict, also baust du genau das und gibst es zurück. Das Schema, die Validierung und structured_content sind identisch mit der BaseModel-Variante (abgesehen von den Beschreibungen, für die ein TypedDict keinen Platz hat).

Eine Dataclass

Dataclasses funktionieren auch, genauso wie jede gewöhnliche Klasse, deren Attribute Type Hints tragen. Das SDK baut unter der Haube aus den Annotationen ein Pydantic-Modell.

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")

Drei Schreibweisen, ein Schema. Nimm die, die deine Codebasis ohnehin schon verwendet.

Listen

Eine list[...] ist ebenfalls kein JSON-Objekt, also bekommt sie die {"result": ...}-Hülle, mit deinem Elementtyp als $defs-Referenz darin:

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"
}

Fordere eine Zwei-Tage-Vorhersage an, und structured_content ist {"result": [{...}, {...}]}. content wird zu zwei TextContent-Blöcken, einer pro Element: Eine Liste wird für das Modell aufgefächert, statt als ein einziger String ausgegeben zu werden.

tuple[...], Unions und Optional[...] werden genauso verpackt.

Dictionaries

dict[str, ...] ist der eine generische Typ, der bereits ein JSON-Objekt ist, und wird deshalb nicht verpackt:

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}

Die Schlüssel müssen str sein. Ein dict[int, float] kann kein JSON-Objekt sein und fällt deshalb auf die {"result": ...}-Hülle zurück.

Validierung

output_schema ist keine Dokumentation. Was auch immer deine Funktion zurückgibt, wird dagegen validiert, bevor es den Server verlässt.

Solange du den Wert von Hand baust, merkst du davon nichts: Pydantic hat schon sichergestellt, dass dein WeatherData ein WeatherData ist. Du merkst es an dem Tag, an dem die Daten von irgendwo kommen, das du nicht kontrollierst:

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])

Die Annotation verspricht WeatherData. Die Upstream-Response liefert humidity nicht mehr mit.

Check

Ruf get_weather auf, und es reicht dem Client nicht stillschweigend ein halb leeres Objekt weiter. Der Aufruf schlägt fehl, und die ersten Zeilen des Fehlers nennen das Feld:

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]

Dieser Text kommt als Tool-Ergebnis mit is_error=True zurück. So weiß das Modell, dass der Aufruf fehlgeschlagen ist, statt selbstbewusst Wetterdaten abzulesen, die gar nicht da sind.

Ein einfaches dict aus einem -> WeatherData-Tool zurückzugeben ist übrigens in Ordnung. Genau das hat json.loads erzeugt. Validiert wird der Wert, nicht der Python-Typ.

Abschalten

Manchmal ist die Annotation des Rückgabetyps für den Type Checker da, nicht für das Protokoll. Übergib structured_output=False, und das Tool liefert nur Text:

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."

Kein output_schema, keine Hülle, keine Validierung. structured_content ist None, und content ist der String, den du zurückgegeben hast.

Das Gegenteil, structured_output=True, macht aus der automatischen Erkennung eine Anforderung: Ein Tool, dessen Rückgabetyp kein Schema erzeugen kann, löst beim Import eine Exception aus, statt auf Text zurückzufallen.

Eine Klasse ohne Type Hints

Es gibt einen Weg, unstrukturiert zu enden, ohne es gewollt zu haben: eine Klasse zurückzugeben, die keine Annotationen im Klassenrumpf hat.

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 setzt name und online in __init__, aber die Klasse deklariert nichts. Das SDK liest die Klassenannotationen, findet keine und gibt auf.

Warning

Es gibt stillschweigend auf. output_schema ist None, structured_content ist None, und der Text, den das Modell liest, ist das repr des Objekts:

"<server.Station object at 0x7f539d75b230>"

Kein Fehler, keine Warnung, ein nutzloses Tool. Verschiebe die Annotationen in den Klassenrumpf oder übergib structured_output=True. Das macht daraus einen harten Fehler, sobald das Modul importiert wird: Function get_station: return type <class 'server.Station'> is not serializable for structured output.

Tip

Brauchst du die volle Kontrolle (das CallToolResult selbst bauen oder _meta anhängen, das die Anwendung sieht, das Modell aber nicht)? Das ist Der Low-Level-Server.

Zusammenfassung

  • Die Annotation des Rückgabetyps ist das Ausgabeschema. Sie wird in tools/list als output_schema veröffentlicht.
  • Skalare, Listen, Tupel und Unions werden in {"result": ...} verpackt. Modelle, TypedDicts, Dataclasses, annotierte Klassen und dict[str, ...] sind schon Objekte und bleiben, wie sie sind.
  • Jedes Ergebnis trägt content (Text, für das Modell) und structured_content (Daten, für die Anwendung).
  • Was du zurückgibst, wird gegen das Schema validiert. Eine Abweichung ist ein Tool-Fehler, kein kaputtes Ergebnis.
  • structured_output=False nimmt ein Tool davon aus. Eine Klasse ohne Type Hints nimmt sich stillschweigend aus; achte darauf.

Damit hast du alles in der Hand, was ein Tool zurückmelden kann. Als Nächstes das zweite Primitiv: Ressourcen.