Abhängigkeiten
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.
Die Argumente eines Tools kommen vom Modell. Manche Werte sollten das nie: ein Preis, den du in deinen eigenen Datensätzen nachschlägst, eine Bestätigung, die nur ein Mensch geben kann, alles, bei dem das Modell danebenliegen könnte, wenn es den Wert erfindet.
Abhängigkeiten sind Parameter, die deine eigenen Funktionen füllen. Du annotierst den Parameter, nennst die Funktion, und das SDK ruft sie auf, bevor dein Tool läuft.
Eine Abhängigkeit deklarieren
Umschließe den Typ des Parameters mit Annotated[...] und füge Resolve(fn) hinzu:
from typing import Annotated
from pydantic import BaseModel
from mcp.server import MCPServer
from mcp.server.mcpserver import Resolve
mcp = MCPServer("Bookshop")
INVENTORY = {"Dune": 7, "Neuromancer": 0}
class Stock(BaseModel):
title: str
copies: int
async def check_stock(title: str) -> Stock:
return Stock(title=title, copies=INVENTORY.get(title, 0))
@mcp.tool()
async def reserve_book(title: str, stock: Annotated[Stock, Resolve(check_stock)]) -> str:
"""Reserve a copy of a book."""
if stock.copies == 0:
return f"{title!r} is out of stock."
return f"Reserved {title!r} ({stock.copies - 1} copies left)."
check_stockist ein Resolver: eine gewöhnliche Funktion, die das SDK vorreserve_bookausführt und deren Rückgabewert zum Argumentstockwird.- Sein Parameter
titleist dastitle-Argument des Tools selbst, zugeordnet über den Namen. Der Resolver sieht genau den validierten Wert, den auch der Tool-Rumpf sehen wird. - Der Tool-Rumpf beginnt mit einem
Stock, der bereits existiert. Kein Nachschlage-Code im Tool, keine „Was, wenn er fehlt“-Vorrede.
Info
Wenn du FastAPI kennst: Das ist Depends. Derselbe Kniff, derselbe Grund: Die Funktion
deklariert, was sie braucht, das Framework liefert es, und die Verdrahtung steckt in der Typannotation.
Für das Modell unsichtbar
Das ist das Eingabeschema, das tools/list für reserve_book meldet:
{
"type": "object",
"properties": {
"title": {"title": "Title", "type": "string"}
},
"required": ["title"],
"title": "reserve_bookArguments"
}
Eine einzige Property. Wie der Context in Der Context ist ein aufgelöster Parameter ein Vertrag zwischen dir und dem SDK: stock steht nicht im Schema, das Modell erfährt nie davon, und ein Client, der trotzdem einen stock-Wert schickt, wird ignoriert. Der Wert des Resolvers ist der einzige, den dein Tool empfangen kann.
Dieser letzte Teil ist der Kern. Ein Parameter, den das Modell nicht liefern kann, ist ein Parameter, bei dem das Modell nichts falsch machen kann.
Ausprobieren
Starte den Server mit dem MCP Inspector:
uv run mcp dev server.py
Das Formular für reserve_book hat ein einziges Feld title. stock taucht nirgends auf. Rufe es mit Dune auf:
Reserved 'Dune' (6 copies left).
Der Tool-Rumpf hat nichts nachgeschlagen: check_stock lief zuerst, und der zurückgegebene Stock kam als Argument an. Probiere Neuromancer, und derselbe Resolver reicht dem Tool eine Null.
Tip
Du kannst check_stock(title) auch einfach im Tool-Rumpf aufrufen. Deklariere es als Abhängigkeit,
wenn der Wert mehr verdient als einen Hilfsaufruf: Jedes Tool, das den Bestand braucht, deklariert
denselben Parameter, und das SDK führt den Resolver höchstens einmal pro Aufruf aus, egal wie viele
ihn deklarieren. Die nächsten Abschnitte liefern den Rest: Resolver, die voneinander abhängen, und
Resolver, die die Person am Host fragen.
Abhängigkeiten von Abhängigkeiten
Ein Resolver kann eigene Abhängigkeiten deklarieren, mit derselben Annotation:
from typing import Annotated
from pydantic import BaseModel
from mcp.server import MCPServer
from mcp.server.mcpserver import Resolve
mcp = MCPServer("Bookshop")
INVENTORY = {"Dune": 7, "Neuromancer": 0}
class Stock(BaseModel):
title: str
copies: int
async def check_stock(title: str) -> Stock:
return Stock(title=title, copies=INVENTORY.get(title, 0))
async def estimate_delivery(stock: Annotated[Stock, Resolve(check_stock)]) -> str:
return "tomorrow" if stock.copies > 0 else "in 2-3 weeks"
@mcp.tool()
async def order_book(
title: str,
stock: Annotated[Stock, Resolve(check_stock)],
delivery: Annotated[str, Resolve(estimate_delivery)],
) -> str:
"""Order a book from the shop."""
if stock.copies == 0:
return f"{title!r} is on backorder; it would arrive {delivery}."
return f"Ordered {title!r}; it arrives {delivery}."
estimate_deliveryhängt voncheck_stockab. Das SDK führt den Graphen der Reihe nach aus: erst der Bestand, dann die Schätzung, dann das Tool.- Sowohl
stockals auchdeliverybrauchen letztlichcheck_stock, aber es läuft einmal pro Aufruf. Eine Bestandsabfrage, zwei Konsumenten. - Es gibt nichts zu registrieren. Die Annotationen sind der Graph.
Check
Glaube das „einmal pro Aufruf“ nicht einfach. Setze ein print in check_stock und rufe
order_book aus dem Inspector auf: eine Zeile pro Aufruf. Zwei Konsumenten, eine Abfrage.
Das SDK analysiert den Graphen, wenn das Tool registriert wird, nicht wenn es aufgerufen wird. Ein Parameter, den es nicht einordnen kann – kein Context, kein Resolve(...), nicht der Name eines Tool-Arguments –, und ein Zyklus von Resolvern lösen beide beim Start InvalidSignature aus. Dein Server scheitert, bevor sich je ein Client verbindet, und der Fehler nennt den betreffenden Parameter oder Resolver.
Die Parameter eines Resolvers werden genau wie die eines Tools aufgelöst: ein weiteres Resolve(...), die eigenen Argumente des Tools über den Namen oder der Context – ctx.headers, das Lifespan-Objekt, alles davon.
Warning
Auf HTTP-Transporten enthält der Context auch ctx.headers. Header sind vom Client gelieferte
Eingaben, wie jedes Tool-Argument: in Ordnung für eine Locale oder ein Feature-Flag, nie für eine
Identität. Wer aufruft, bestimmt deine Autorisierungsschicht (Autorisierung),
nicht ein Header, der sich beliebig setzen lässt.
Tip
Einmal pro Aufruf heißt genau das: Der nächste tools/call führt check_stock erneut aus. Eine
Ressource, die einen Request überdauern soll – ein Datenbank-Pool, ein HTTP-Client –, gehört in den
Lifespan, und ein Resolver erreicht sie über ctx.request_context.lifespan_context.
Fragen, wenn es sein muss
Ein Resolver muss die Antwort nicht kennen. Er kann Elicit(message, Model) zurückgeben, und das SDK fragt die Person am Host – die Maschinerie der Elicitation (Rückfrage bei der Person am Host), für dich ausgeführt:
from typing import Annotated
from pydantic import BaseModel, Field
from mcp.server import MCPServer
from mcp.server.mcpserver import Elicit, Resolve
mcp = MCPServer("Bookshop")
INVENTORY = {"Dune": 7, "Neuromancer": 0}
class Stock(BaseModel):
title: str
copies: int
class Backorder(BaseModel):
confirm: bool = Field(description="Order anyway and wait?")
async def check_stock(title: str) -> Stock:
return Stock(title=title, copies=INVENTORY.get(title, 0))
async def confirm_backorder(
title: str,
stock: Annotated[Stock, Resolve(check_stock)],
) -> Backorder | Elicit[Backorder]:
if stock.copies > 0:
return Backorder(confirm=True) # in stock: nothing to ask
return Elicit(f"{title!r} is out of stock (2-3 weeks). Order anyway?", Backorder)
@mcp.tool()
async def order_book(
title: str,
stock: Annotated[Stock, Resolve(check_stock)],
backorder: Annotated[Backorder, Resolve(confirm_backorder)],
) -> str:
"""Order a book from the shop."""
if not backorder.confirm:
return "No order placed."
if stock.copies == 0:
return f"Backordered {title!r}; it ships in 2-3 weeks."
return f"Ordered {title!r}."
- Auf Lager:
confirm_backordergibt direkt einBackorderzurück. Keine Frage, kein Roundtrip. Die Person wird nur unterbrochen, wenn ihre Antwort zählt. - Nicht auf Lager: Das SDK sendet die Elicitation, validiert die Antwort gegen
Backorderund injiziert sie. Dein Resolver berührt das Protokoll nie. - Das Tool liest
backorder.confirmwie jedes andere Argument. Nein zu antworten ist trotzdem eine Antwort: Die Elicitation wird mitconfirm=Falseakzeptiert, das Tool läuft, und es wird keine Bestellung aufgegeben. Das Fragen ist zur Vorbedingung geworden, nicht zu Hilfscode im Tool-Rumpf.
Und wenn die Person gar nicht antwortet – die Frage ablehnt oder abbricht?
Check
Führe order_book für Neuromancer aus und lehne die Frage ab. Mit der Annotation
Annotated[Backorder, Resolve(...)] läuft der Tool-Rumpf nie; der Aufruf scheitert mit einem
Fehlerergebnis, das das Modell lesen kann:
Error executing tool order_book: Resolver for parameter 'backorder' could not resolve: elicitation was decline
Das ist der richtige Standardwert für eine Vorbedingung: keine Antwort, keine Bestellung. Wenn Ablehnen ein Ergebnis ist, das dein Tool behandeln will – die Nachbestellung überspringen, aber trotzdem einen anderen Titel vorschlagen –, annotiere stattdessen ElicitationResult[Backorder], und das Tool erhält das vollständige Ergebnis aus accept/decline/cancel, nach dem es verzweigen kann. Elicitation zeigt diese Form und alles Weitere zum Fragen: die Schema-Regeln, die drei Antworten, die Client-Seite des Gesprächs.
Info
Das Framework wählt den Transport der Frage anhand der ausgehandelten Protokollversion; der Code
oben ist in beiden Fällen identisch. Ab 2026-07-28 reist die Frage innerhalb eines
Multi-Roundtrip-tools/call (multi-round-trip) – der Server gibt sie zurück, der
elicitation_callback des Clients beantwortet sie, und der Client wiederholt den Aufruf für dich
(Multi-Roundtrip-Requests). Bei 2025-11-25 und früher ist es ein
synchroner Elicitation-Request mitten im Aufruf. Jede Frage wird genau einmal pro Aufruf gestellt –
eine Garantie über die Frage, nicht über den Resolver. In der Multi-Roundtrip-Form kann jeder
Resolver erneut laufen, sobald der Aufruf nach einer Frage fortgesetzt wird; Code vor einem
return Elicit(...) läuft also in jeder dieser Runden. Die aufgezeichnete Antwort erfüllt dann die
wiederholte Frage, ohne die Person erneut zu fragen. Eine aufgezeichnete Antwort wird überhaupt nur
herangezogen, wenn der Resolver fragt; ein Resolver, der antwortet, ohne zu fragen, wie
check_stock, liefert immer seinen selbst berechneten Wert. Weil jede Antwort ihrer Frage
zugeordnet wird, muss ein fragender Resolver seine Frage deterministisch aus den Argumenten des
Tools und früheren Antworten ableiten. Ein pro Aufruf erzeugter Wert (eine ID aus
default_factory, ein Zeitstempel) wird in jeder Runde neu abgeleitet und darf nicht in einer
Frage vorkommen, an die sich die Antwort binden soll. Eine Frage aus solch flüchtigen Daten lässt
jede aufgezeichnete Antwort veraltet aussehen, sodass der Server sie in jeder Runde erneut stellt,
bis das Rundenlimit des Clients den Aufruf beendet.
Den Client fragen, nicht die Person
Elicitation ist eine von drei Fragen, die ein Resolver stellen kann, und der Multi-Roundtrip-Ablauf lässt keine weiteren zu. Die beiden anderen gehen an den Client statt an die Person: Gib Sample(...) zurück, um einen LLM-Aufruf über den Client auszuführen (ein sampling/createMessage-Request), oder ListRoots(), um die aktuellen Roots (freigegebene Arbeitsverzeichnisse) des Clients abzurufen. Keine von beiden hat ein Ergebnis aus accept/decline; der Konsument annotiert direkt den Ergebnistyp, CreateMessageResult (CreateMessageResultWithTools, wenn der Request tools oder tool_choice trägt) oder ListRootsResult:
from typing import Annotated
from mcp.server import MCPServer
from mcp.server.mcpserver import Resolve, Sample
from mcp.types import CreateMessageResult, SamplingMessage, TextContent
mcp = MCPServer("Bookshop")
def suggest_title(genre: str) -> Sample:
prompt = f"Suggest one {genre} book title. Answer with the title only."
return Sample(
[SamplingMessage(role="user", content=TextContent(type="text", text=prompt))],
max_tokens=50,
)
@mcp.tool()
async def recommend_book(
genre: str,
suggestion: Annotated[CreateMessageResult, Resolve(suggest_title)],
) -> str:
"""Recommend a book in the given genre."""
title = suggestion.content.text if suggestion.content.type == "text" else "the classics"
return f"Today's {genre} pick: {title}"
- Das Framework leitet sie genau wie
Elicit: innerhalb des Multi-Roundtrip-tools/callbei 2026-07-28, über den eigenständigen Server-zu-Client-Request bei 2025-11-25. Eine nicht deklarierte Capability verweigert den Aufruf mit einem Protokollfehler-32021(sampling,roots,elicitationim Formularmodus;sampling.tools, wenn der Requesttoolsodertool_choiceträgt). - Alles, was der Info-Kasten oben über Fragen sagt, gilt unverändert: Ein
Sample-Request wird seinem aufgezeichneten Ergebnis über seine exakte Darstellung zugeordnet, baue ihn also deterministisch aus den Argumenten des Tools und früheren Antworten; der Client zahlt dann für den LLM-Aufruf einmal pro Tool-Aufruf, nicht einmal pro Runde. Das aufgezeichnete Ergebnis reist für den Rest des Aufrufs inrequest_statemit, sodass eine sehr große Completion jeden verbleibenden Roundtrip schwerer macht. - Die eigenständigen Features Sampling und Roots sind ab 2026-07-28 veraltet (SEP-2577). Neue Server, die das Modell des Clients brauchen, fragen über diesen Träger; Server, die es nicht brauchen, sollten direkt einen LLM-Anbieter anbinden. Andere
include_context-Werte als"none"sind selbst veraltet; vermeide sie.
Zusammenfassung
Annotated[T, Resolve(fn)]an einem Tool-Parameter: Das SDK führtfnaus und injiziert den Rückgabewert.- Ein aufgelöster Parameter ist für das Modell unsichtbar, und ein Client kann ihn nicht liefern. Werte, die das Modell nicht erfinden darf – Preise, Identitäten, Berechtigungen –, gehören hierher.
- Die Parameter eines Resolvers werden genauso aufgelöst: der
Context, ein weiteresResolve(...)oder ein Tool-Argument über den Namen. Der Graph führt jeden Resolver höchstens einmal pro Runde aus, egal wie viele Konsumenten er hat; jede Frage wird genau einmal gestellt, und jeder Resolver kann erneut laufen, wenn ein Aufruf nach einer Frage fortgesetzt wird. - Fehlerhafte Graphen scheitern bei der Registrierung mit
InvalidSignature, nicht mitten im Aufruf. - Gib
Elicit(message, Model)zurück, um die Person zu fragen – nur, wenn es sein muss. Unverpackte Annotationen brechen bei Ablehnung ab; mitElicitationResult[T]kann das Tool verzweigen. - Gib
Sample(...)oderListRoots()zurück, um den Client nach einer Antwort des Modells oder der Liste der Roots zu fragen; das reine Ergebnis wird injiziert.
Den Zustand, den dein Server einmal beim Start aufbaut, und wie ein Handler ihn erreicht, behandelt die Seite Lifespan.