docs: publish translated docs in twelve languages and the tool that maintains them (#3280)

This commit is contained in:
Max 2026-08-14 17:07:34 +01:00 • committed by GitHub
commit 52ad0a8876
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 89737 additions and 54 deletions

View file

@ -0,0 +1,134 @@
---
translation:
sections: [b50152f05c81e786, b302059b22fb7cb4, 85682a1bf561243a, 53fc48838eb6837a, b24190e0842786ec, 85f93e150fc9b240]
tool: 1
---
# Der Context {#the-context}
Die Argumente eines Tools kommen vom Modell. Alles andere (der Request, den du gerade bearbeitest, der Server, in dem du lebst, ein Weg zurück zum Client) kommt aus einem einzigen Objekt: dem **`Context`**.
Du erzeugst ihn nicht selbst und konfigurierst ihn auch nicht. Du forderst ihn einfach an.
## Anfordern {#ask-for-it}
Füge einem beliebigen Tool einen Parameter hinzu, der mit `Context` annotiert ist:
```python title="server.py" hl_lines="2 8"
--8<-- "docs_src/context/tutorial001.py"
```
* Das SDK baut für jeden Request einen frischen `Context` und übergibt ihn.
* Der **Name des Parameters spielt keine Rolle**. `ctx`, `context`, `c`: Das SDK findet ihn über seine Annotation.
* Ressourcen und Prompts können ebenfalls einen deklarieren, auf dieselbe Weise.
* `ctx.request_id` ist die ID des Requests, den deine Funktion gerade bearbeitet.
!!! info
Wenn du FastAPI kennst, kennst du diesen Kniff: Deklariere einen Parameter mit dem
frameworkeigenen Typ (`Request` dort, `Context` hier), und das Framework liefert ihn. Nichts zu
registrieren, nichts zu konfigurieren: Die Typannotation ist der ganze Mechanismus.
### Für das Modell unsichtbar {#invisible-to-the-model}
Das ist der Teil, den du verinnerlichen solltest. Hier ist das Eingabeschema, das `tools/list` für `search_books` meldet:
```json
{
"type": "object",
"properties": {
"query": {"title": "Query", "type": "string"}
},
"required": ["query"],
"title": "search_booksArguments"
}
```
Eine Eigenschaft. `ctx` ist kein Argument: Es taucht nie im Schema auf, das Modell erfährt nie davon, und kein Client kann es ausfüllen. Es ist ein Vertrag zwischen dir und dem SDK, unsichtbar auf der Leitung.
### Ausprobieren {#try-it}
Starte den Server mit dem MCP Inspector:
```console
uv run mcp dev server.py
```
Das Formular für `search_books` hat ein einziges Feld `query`. Rufe es mit `dune` auf:
```text
[request 3] Found 3 books matching 'dune'.
```
Die Zahl ist die Nummer des Requests, der es zufällig war. Rufe das Tool noch einmal auf, und sie ändert sich: Jeder Request bekommt seinen eigenen `Context`.
## Was er dir bietet {#what-it-gives-you}
Das injizierte Objekt ist klein. Neben `request_id`:
* `await ctx.read_resource(uri)`: eine der **eigenen** Ressourcen des Servers aus einem Tool heraus lesen. Der nächste Abschnitt.
* `await ctx.report_progress(progress, total, message)`: während eines langen Aufrufs Fortschritt an den Aufrufer zurückstreamen. Alles Weitere steht in **[Fortschritt](progress.md)**.
* `await ctx.elicit(message, schema)` und `await ctx.elicit_url(...)`: das Tool anhalten und der Person am Host eine Frage stellen. Das ist **[Elicitation](elicitation.md)** (Rückfrage bei der Person am Host).
* `ctx.session`: die Server-Seite des Gesprächs mit diesem Client. Benachrichtigungen, die du an den Client schickst, leben hier; der letzte Abschnitt nutzt sie.
* `ctx.headers`: die Request-Header, die der Transport mitgebracht hat, oder `None` bei stdio. Einen eigenen Header liest du mit `(ctx.headers or {}).get("x-...")`. Header sind vom Client gelieferte Eingaben – in Ordnung für eine Locale oder ein Feature-Flag, nie für eine Identität.
* `ctx.request_context`: der rohe Datensatz pro Request. Das Feld, nach dem du greifen wirst, ist `lifespan_context`, das Objekt, das dein Startcode per yield geliefert hat (siehe **[Lifespan](lifespan.md)**).
Logging steht bewusst nicht auf dieser Liste. Ein Server loggt mit Pythons Modul `logging`, wie jedes andere Python-Programm. **[Logging](logging.md)** ist die kurze Seite, die erklärt, warum.
!!! tip
Injiziert wird nur in die Funktion, die du registriert hast. Eine Hilfsfunktion, die dein Tool
aufruft, bekommt keinen eigenen `Context`; reiche `ctx` als gewöhnliches Argument weiter. Es gibt
keinen umgebenden „aktuellen Kontext“, den du von woanders holen könntest.
## Eigene Ressourcen lesen {#read-your-own-resources}
Die Ressourcen eines Servers sind nicht nur für Clients da. Auch ein Tool kann sie lesen:
```python title="server.py" hl_lines="16"
--8<-- "docs_src/context/tutorial002.py"
```
`ctx.read_resource` löst den URI über dieselbe Registry auf, die auch `resources/read` bedient. Ein Tool bekommt also, was ein Client bekäme: ein Iterable von `ReadResourceContents`, eines pro Content-Block. Für diesen URI gibt es einen:
```python
contents.content # 'fiction, non-fiction, poetry'
contents.mime_type # 'text/plain'
```
* `content` ist genau das, was `genres()` zurückgegeben hat. Eine einzige Quelle der Wahrheit: Der Client durchstöbert die Ressource, deine Tools konsumieren sie, niemand kopiert den String.
* Der einzige Parameter von `describe_catalog` ist der `Context`, daher hat sein Eingabeschema **überhaupt keine Eigenschaften**. Das Modell ruft es mit `{}` auf.
## Dem Client mitteilen, dass sich die Liste geändert hat {#tell-the-client-the-list-changed}
Was ein Server anbietet, steht nicht zur Importzeit fest. Registriere ein Tool zur Laufzeit und teile es dann dem Client mit:
```python title="server.py" hl_lines="15-16"
--8<-- "docs_src/context/tutorial003.py"
```
* `mcp.add_tool(recommend_book)` registriert eine gewöhnliche Funktion als Tool: Name, Beschreibung und Schema werden genau so abgeleitet, wie `@mcp.tool()` es getan hätte.
* `await ctx.session.send_tool_list_changed()` sendet `notifications/tools/list_changed`. Ein Client, der das empfängt, ruft `tools/list` erneut auf und sieht `recommend_book`.
Die Geschwister sind `send_resource_list_changed()`, `send_prompt_list_changed()` und `send_resource_updated(uri)` für eine Änderung an einer bestimmten Ressource.
Auf einer Verbindung mit 2026-07-28 empfangen Clients Änderungsbenachrichtigungen nur auf einem `subscriptions/listen`-Stream, den sie selbst geöffnet haben. Die `send_*`-Methoden oben erreichen diese Streams daher nicht. Die Publish-Methoden des `Context` liefern an alle abonnierten Streams gleichzeitig aus: `await ctx.notify_tools_changed()`, `await ctx.notify_prompts_changed()`, `await ctx.notify_resources_changed()` und `await ctx.notify_resource_updated(uri)`. Alles Weitere, einschließlich der horizontalen Skalierung über Replikate, steht in **[Abonnements](subscriptions.md)**.
!!! check
Bevor jemand `enable_recommendations` ausführt, existiert das Tool, das du versprichst, nicht.
Rufst du es trotzdem auf, ist das Ergebnis ein Fehler, den das Modell lesen kann:
```text
Unknown tool: recommend_book
```
Führe `enable_recommendations` aus, und genau derselbe Aufruf gelingt. Die Tool-Liste ist
wirklich dynamisch: `tools/list` spiegelt wider, was *gerade jetzt* registriert ist.
## Zusammenfassung {#recap}
* Annotiere einen Parameter mit `Context` (in einem Tool, einer Ressource oder einem Prompt), und das SDK injiziert ihn. Der Name gehört dir.
* Er ist für das Modell unsichtbar: Das Eingabeschema enthält immer nur deine echten Argumente.
* `ctx.request_id` identifiziert den Request; `ctx.request_context.lifespan_context` ist das, was dein Startcode per yield geliefert hat.
* Mit `await ctx.read_resource(uri)` liest ein Tool die eigenen Ressourcen des Servers.
* `ctx.session` ist der Kanal zurück zum Client: `send_tool_list_changed()` und seine Geschwister sagen ihm, dass er eine Liste, die du geändert hast, erneut abrufen soll.
* Auch Fortschrittsmeldungen und Elicitation beginnen beim `Context`; beide haben ihre eigene Seite.
Parameter, die das Modell nie sieht und die deine eigenen Funktionen füllen, sind **[Abhängigkeiten](dependencies.md)**.

View file

@ -0,0 +1,167 @@
---
translation:
sections: [b0389403e98d25ad, e2cf58b43b285e86, a363e1a38e1a5971, 6cfac078feb18013, b4535bd61df337e6, e97ed44207f929fd]
tool: 1
---
# Abhängigkeiten {#dependencies}
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 {#declare-one}
Umschließe den Typ des Parameters mit `Annotated[...]` und füge `Resolve(fn)` hinzu:
```python title="server.py" hl_lines="18-19 23"
--8<-- "docs_src/dependencies/tutorial001.py"
```
* `check_stock` ist ein **Resolver**: eine gewöhnliche Funktion, die das SDK vor `reserve_book` ausführt und deren Rückgabewert zum Argument `stock` wird.
* Sein Parameter `title` ist das `title`-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 {#invisible-to-the-model}
Das ist das Eingabeschema, das `tools/list` für `reserve_book` meldet:
```json
{
"type": "object",
"properties": {
"title": {"title": "Title", "type": "string"}
},
"required": ["title"],
"title": "reserve_bookArguments"
}
```
Eine einzige Property. Wie der `Context` in **[Der Context](context.md)** 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 {#try-it}
Starte den Server mit dem MCP Inspector:
```console
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:
```text
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 {#dependencies-of-dependencies}
Ein Resolver kann eigene Abhängigkeiten deklarieren, mit derselben Annotation:
```python title="server.py" hl_lines="22 29-30"
--8<-- "docs_src/dependencies/tutorial002.py"
```
* `estimate_delivery` hängt von `check_stock` ab. Das SDK führt den Graphen der Reihe nach aus: erst der Bestand, dann die Schätzung, dann das Tool.
* Sowohl `stock` als auch `delivery` brauchen letztlich `check_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](../run/authorization.md)**),
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](lifespan.md)**, und ein Resolver erreicht sie über `ctx.request_context.lifespan_context`.
## Fragen, wenn es sein muss {#ask-when-you-must}
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](elicitation.md)** (Rückfrage bei der Person am Host), für dich ausgeführt:
```python title="server.py" hl_lines="26-32 39"
--8<-- "docs_src/dependencies/tutorial003.py"
```
* Auf Lager: `confirm_backorder` gibt direkt ein `Backorder` zurü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 `Backorder` und injiziert sie. Dein Resolver berührt das Protokoll nie.
* Das Tool liest `backorder.confirm` wie jedes andere Argument. **Nein** zu antworten ist trotzdem eine Antwort: Die Elicitation wird mit `confirm=False` akzeptiert, 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:
```text
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](elicitation.md)** 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](multi-round-trip.md)**). 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 {#ask-the-client-not-the-user}
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`:
```python title="server.py" hl_lines="10-15 21"
--8<-- "docs_src/dependencies/tutorial004.py"
```
* Das Framework leitet sie genau wie `Elicit`: innerhalb des Multi-Roundtrip-`tools/call` bei **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`, `elicitation` im Formularmodus; `sampling.tools`, wenn der Request `tools` oder `tool_choice` trä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 in `request_state` mit, 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 {#recap}
* `Annotated[T, Resolve(fn)]` an einem Tool-Parameter: Das SDK führt `fn` aus 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 weiteres `Resolve(...)` 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; mit `ElicitationResult[T]` kann das Tool verzweigen.
* Gib `Sample(...)` oder `ListRoots()` 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](lifespan.md)**.

View file

@ -0,0 +1,190 @@
---
translation:
sections: [335ca2a0b266f003, d1ad562d3fe87bc0, 0bb1396c86daeba4, d1cb1235bb9ee267, 833179c09d239c83, e5d6dec2d2e655e8]
tool: 1
---
# Elicitation {#elicitation}
Ein Tool, das mitten in seiner Arbeit steckt und dem eine Antwort fehlt, muss nicht scheitern.
Mit **Elicitation** (Rückfrage bei der Person am Host) kann es fragen. Mitten in einem Tool-Aufruf bekommt die Person eine Frage gestellt, und ihre Antwort landet wieder im selben Funktionsaufruf.
Es gibt zwei Modi:
* **Formular-Modus**: Du brauchst einen Wert (eine Bestätigung, ein Datum, eine Menge). Du beschreibst die Felder, der Client rendert das Formular.
* **URL-Modus**: Die Person muss woanders hin (ein OAuth-Zustimmungsbildschirm, eine Bezahlseite). Nichts von dem, was sie dort tut, läuft über das Protokoll.
Und es gibt zwei Wege zu fragen. Der Weg der Wahl ist ein **Resolver**: Du hängst die Frage an einen Parameter, und das SDK fragt – auf jeder Verbindung, egal welche Protokollgeneration der Client spricht. Der direkte Weg, `await ctx.elicit(...)`, ist ein Request vom *Server* an den *Client*, ein Kanal, den es nur für einen Client auf einer Legacy-Verbindung gibt (Spec-Version 2025-11-25 oder älter). Beide stehen auf dieser Seite; fang mit dem Resolver an.
## Mit einem Resolver fragen {#ask-with-a-resolver}
Eine Frage, die das ganze Tool blockiert – *bist du sicher? welches der drei passenden Konten?* – lässt sich aus dem Tool-Body in einen **Resolver** herausziehen, und das Framework stellt sie für dich.
Ein Parameter mit der Annotation `Annotated[T, Resolve(fn)]` wird befüllt, indem `fn` vor dem Tool-Body läuft. Der Resolver gibt den Wert direkt zurück, wenn er ihn schon kennt, oder gibt `Elicit(...)` zurück, damit das Framework fragt:
```python title="server.py" hl_lines="24-30 35-36"
--8<-- "docs_src/elicitation/tutorial004.py"
```
* `confirm_delete` liest das eigene Argument `path` des Tools über den Namen aus, listet den Ordner auf und **fragt nur, wenn es sein muss** – ein leerer Ordner wird zu `Confirm(ok=True)` aufgelöst, ohne Roundtrip zum Client.
* `delete_folder` annotiert `ElicitationResult[Confirm]`, also injiziert das Framework das ganze Ergebnis, und das Tool behandelt per `match` jeden Fall: annehmen und bestätigen, annehmen, aber behalten (`ok=False`), ablehnen, abbrechen.
* Der Parameter `confirm` taucht nie im Input-Schema des Tools auf – der Client liefert `path`, der Resolver liefert `confirm`.
Annotiere stattdessen das unverpackte Modell (`Annotated[Confirm, Resolve(confirm_delete)]`), wenn das Tool nicht verzweigen muss: Beim Annehmen bekommt es das Modell, bei Ablehnen oder Abbrechen bricht der Aufruf mit einem Fehler ab.
Ein Resolver funktioniert auf **jeder** Verbindung. Einem Client auf einer Legacy-Verbindung schickt das SDK die Frage direkt; auf einer **2026-07-28**-Verbindung *gibt* das SDK die Frage aus dem Aufruf *zurück*, und der nächste Versuch des Clients trägt die Antwort. Dein Resolver merkt den Unterschied nie; was unter der Haube passiert, steht in **[Multi-Roundtrip-Requests](multi-round-trip.md)** (multi-round-trip requests).
Fragen ist nur eines von dem, was ein Resolver kann. Der allgemeine Mechanismus – Abhängigkeiten, die rechnen, ohne zu fragen, Abhängigkeiten von Abhängigkeiten, was das Modell liefern kann und was nicht – ist die Seite **[Abhängigkeiten](dependencies.md)**.
## Aus dem Tool heraus fragen {#ask-from-inside-the-tool}
Ein Tool kann auch mitten in seinem eigenen Body anhalten und fragen.
!!! warning
`ctx.elicit()` und `ctx.elicit_url()` sind Requests vom *Server* an den *Client* – ein
Kanal, den es nur für einen Client auf einer Legacy-Verbindung gibt (Spec-Version **2025-11-25**
oder älter). Auf einer **2026-07-28**-Verbindung gibt es keine vom Server initiierten Requests,
also schlagen diese Aufrufe fehl. Ein Resolver funktioniert auf beiden. Alles Weitere steht in
**[Protokollversionen](../protocol-versions.md)**.
`await ctx.elicit()` nimmt eine Nachricht und ein Pydantic-Modell entgegen:
```python title="server.py" hl_lines="9-11 20-23 25"
--8<-- "docs_src/elicitation/tutorial001.py"
```
* Der **`Context`**-Parameter gibt dir `ctx.elicit`; jedes Tool kann einen entgegennehmen. Dieses Objekt hat seine eigene Seite: **[Der Context](context.md)**.
* `AlternativeDate` ist das **Schema** der Antwort, die du haben willst.
* Das Tool ist `async def`. Das muss es sein: Es hält mittendrin an und wartet auf einen Menschen.
* An jedem anderen Datum gibt das Tool sofort zurück. Es fragt nur, wenn es muss.
* Das Datum, das die Person annimmt, läuft wieder durch `book_table` selbst. Eine Antwort ist Eingabe wie jede andere: Ist die Alternative ebenfalls ausgebucht, wird erneut gefragt statt blind bestätigt.
### Was der Client erhält {#what-the-client-receives}
Der Client bekommt deine Nachricht und daneben ein JSON Schema, das aus dem Modell generiert wird:
```json
{
"properties": {
"accept_alternative": {
"description": "Try another date?",
"title": "Accept Alternative",
"type": "boolean"
},
"date": {
"default": "2025-12-26",
"description": "Alternative date (YYYY-MM-DD)",
"title": "Date",
"type": "string"
}
},
"required": ["accept_alternative"],
"title": "AlternativeDate",
"type": "object"
}
```
Dieses Schema ist das Formular. `Field(description=...)` ist die Beschriftung; ein Standardwert füllt das Eingabefeld vor und macht das Feld optional. Es ist dieselbe Pydantic-zu-JSON-Schema-Maschinerie, die **[Tools](../servers/tools.md)** für die Argumente eines Tools beschreibt.
!!! warning
Ein Elicitation-Schema ist nicht so ausdrucksstark wie das Input-Schema eines Tools. Nur flache,
primitive Felder: `str`, `int`, `float`, `bool` oder ein `Literal` aus Strings (daraus wird ein `enum`).
Steckst du ein Modell in das Modell, löst `ctx.elicit` eine Exception aus, bevor irgendetwas an den Client geht:
```text
TypeError: Elicitation schema field 'address' rendered as {'$ref': '#/$defs/Address'}, which is not a valid PrimitiveSchemaDefinition
```
Du unterbrichst einen Menschen mitten in einer Aufgabe. Wenn die Antwort Verschachtelung braucht,
hätte sie ein Argument des Tools sein sollen.
### Die drei Antworten {#the-three-answers}
`result.action` sagt dir, was die Person getan hat, und es gibt genau drei Möglichkeiten:
* `"accept"`: Sie hat das Formular abgeschickt. `result.data` ist eine `AlternativeDate`-Instanz, bereits validiert.
* `"decline"`: Sie hat Nein gesagt.
* `"cancel"`: Sie hat die Frage weggeklickt, ohne sich zu entscheiden.
`result.data` existiert nur bei `"accept"`, deshalb prüft das Beispiel zuerst `result.action`. Dein Type Checker erzwingt die Reihenfolge: Nach `result.action == "accept"` ist `result.data` ein `AlternativeDate`; davor gibt es gar kein `.data`.
Eine Absage ist kein Fehler. Das Tool entscheidet, was Ablehnen bedeutet (hier: keine Buchung), und antwortet dem Modell ganz normal.
!!! tip
Die Antwort wird gegen dein Modell validiert, bevor dein Code sie sieht. Ein Client, der
`"maybe"` für ein `bool` schickt, bringt deine Buchung nicht durcheinander: Der Aufruf schlägt mit einem
Schema-Mismatch-Fehler fehl, dein `if` läuft nie.
## Die Person zu einer URL schicken {#send-the-user-to-a-url}
Manche Dinge dürfen nicht durch das Modell oder den Client laufen: Zugangsdaten, Kartennummern, OAuth-Zustimmung. Dafür fragst du nicht nach Daten; du bittest die Person, irgendwohin zu gehen:
```python title="server.py" hl_lines="10-14 23"
--8<-- "docs_src/elicitation/tutorial002.py"
```
* `ctx.elicit_url()` nimmt die Nachricht, die zu besuchende **URL** und eine `elicitation_id` deiner Wahl entgegen: einen beliebigen String, der diese Elicitation innerhalb deines Servers identifiziert.
* Das Ergebnis hat eine Action und sonst nichts. `"accept"` heißt, die Person hat zugestimmt, die URL zu öffnen, **nicht**, dass sie das, was auf der anderen Seite wartet, abgeschlossen hat.
* Die Zahlung läuft außerhalb des Protokolls, zwischen dem Browser der Person und deinem Zahlungsanbieter. Über MCP kommt nie irgendein Inhalt zurück.
Sieh dir das zweite Tool an. Wenn dein Server erfährt, dass der externe Ablauf abgeschlossen ist (ein Webhook, ein Poll; hier als zweites Tool modelliert), sendet `ctx.session.send_elicit_complete(...)` die Benachrichtigung `notifications/elicitation/complete` mit derselben `elicitation_id`. So weiß der Client, dass er *„waiting for payment...“* nicht mehr anzeigen muss. Ohne sie kann der Client nur raten.
## Die Client-Seite {#the-client-side}
Server fragen. Clients antworten, indem sie `Client(...)` einen **`elicitation_callback`** übergeben:
```python title="client.py" hl_lines="6-7 18"
--8<-- "docs_src/elicitation/tutorial003.py"
```
* Ein Callback behandelt beide Modi. `params` ist eine Union aus `ElicitRequestFormParams` und `ElicitRequestURLParams`; `isinstance` ist die Verzweigung.
* Bei einer URL zeigst du der Person `params.url` und gibst die Action zurück, die sie gewählt hat. Niemals irgendein `content`.
* Bei einem Formular rendert eine echte Anwendung `params.requested_schema` und gibt die Eingabe der Person als `content` zurück. Dieser hier sagt immer Ja mit einer vorgefertigten Antwort – genau der Callback, den du in einem Test willst.
* Den Callback zu übergeben ist zugleich die **Capability-Deklaration**: So erfährt der Server, dass dieser Client gefragt werden kann. Was ein Client sonst noch für einen Server beantworten kann, steht in **[Client-Callbacks](../client/callbacks.md)**.
!!! info
Elicitation ist ein Request vom *Server* an den *Client*, und solche gibt es nur auf einer
Session mit klassischem Handshake, deshalb übergibt dieser Client `mode="legacy"`.
Auf einer **2026-07-28**-Verbindung fragt ein Tool stattdessen, indem es die Frage aus dem Aufruf
*zurückgibt*; dieser Ablauf steht in **[Multi-Roundtrip-Requests](multi-round-trip.md)**.
### Ausprobieren {#try-it}
Starte die `server.py` des `ctx.elicit`-Formular-Modus (die mit `book_table`) über Streamable HTTP (den Einzeiler dafür hat **[Den Server betreiben](../run/index.md)**), führe dann `main()` des Clients aus und frage `book_table` nach dem ersten Weihnachtstag.
Der Callback gibt die Frage aus, die er bekommen hat:
```text
No tables for 2 on 2025-12-25. Would you like to try another date?
```
Er antwortet mit `{"accept_alternative": True, "date": "2025-12-27"}`, und das Tool, das die ganze Zeit in `await ctx.elicit(...)` gewartet hat, schließt die Buchung ab:
```text
Booked a table for 2 on 2025-12-27.
```
Tausche nun die `server.py` des URL-Modus ein und richte dasselbe `main()` auf `pay_deposit`: Derselbe Callback nimmt den anderen Zweig, gibt den Bezahllink aus, und das Tool kommt mit *„Complete the payment in your browser.“* zurück. Ein Roundtrip, mitten im Aufruf, in beide Richtungen.
!!! check
Entferne nun `elicitation_callback=` aus dem `Client` und rufe `book_table` noch einmal für den ersten
Weihnachtstag auf. Der ganze Aufruf schlägt mit einem Protokollfehler fehl:
```text
Elicitation not supported
```
Ein Client, der keinen Callback registriert hat, hat die Capability `elicitation` nie deklariert, also gibt es
niemanden zum Fragen. Dein Tool hat kein `"decline"` bekommen; es hat eine Exception bekommen. Plane dafür: Jede
Elicitation braucht eine sinnvolle Antwort auf „Was, wenn ich nicht fragen kann?“.
## Zusammenfassung {#recap}
* Ein Parameter mit der Annotation `Annotated[T, Resolve(fn)]` wird von einem Resolver befüllt, der `Elicit(...)` zurückgibt, wenn er fragen muss. Das funktioniert auf jeder Verbindung.
* Das Schema ist ein flaches Pydantic-Modell: nur primitive Felder, auf dem Rückweg validiert.
* `result.action` ist `"accept"`, `"decline"` oder `"cancel"`; `result.data` existiert nur bei Accept.
* `await ctx.elicit(message, schema=Model)` fragt aus dem Tool-Body heraus, und `await ctx.elicit_url(message, url, elicitation_id)` ist für alles, was nicht durch das Modell laufen darf (`ctx.session.send_elicit_complete(elicitation_id)` meldet, dass der externe Teil erledigt ist). Beide sind Server-zu-Client-Requests: Sie brauchen den Client auf einer Legacy-Verbindung.
* Der Client antwortet mit einem einzigen `elicitation_callback`, der nach dem Typ der Params verzweigt; ihn zu registrieren deklariert die Capability.
* Auf einer 2026-07-28-Verbindung gibt der Server die Frage zurück, statt sie zu pushen; derselbe Callback wird von **[Multi-Roundtrip-Requests](multi-round-trip.md)** gespeist.
Alles unterhalb dieser Rückgabe (die Retry-Schleife, der Schutz von `requestState`, es selbst zu steuern) steht in **[Multi-Roundtrip-Requests](multi-round-trip.md)**.

View file

@ -0,0 +1,24 @@
---
translation:
sections: [424930166c4bc6f3]
tool: 1
---
# Im Handler {#inside-your-handler}
Die Argumente eines Handlers kommen vom Client. Alles *andere*, was er lesen kann, und alles, was er tun kann, während er läuft, steht hier.
Was er lesen kann:
* **[Der Context](context.md)** ist der eine zusätzliche Parameter, den jeder Handler anfordern kann: der laufende Request, seine Header, seine Session sowie die Verben für Fortschritt und Änderungsbenachrichtigungen.
* **[Abhängigkeiten](dependencies.md)** sind Parameter, die das Modell nie sieht – deine eigenen Funktionen füllen sie mit `Resolve`.
* **[Lifespan](lifespan.md)** behandelt Zustand, den dein Server einmal beim Start aufbaut, und wie ein Handler ihn über den `Context` erreicht.
Was er tun kann, während er läuft:
* Die Person am Host um weitere Eingaben bitten – mit **[Elicitation](elicitation.md)** (Rückfrage bei der Person am Host) und **[Multi-Roundtrip-Requests](multi-round-trip.md)** (multi-round-trip requests), dem Muster aus 2026-07-28, das sie transportiert.
* Den Client um die Antwort eines LLM oder um seine Arbeitsverzeichnisse bitten – mit **[Sampling und Roots](sampling-and-roots.md)**, veraltet, aber weiterhin bedient.
* **[Fortschritt](progress.md)** bei etwas Langsamem melden.
* Logs schreiben (auf die Standardfehlerausgabe, für alle, die den Server betreiben) – mit **[Logging](logging.md)**.
* Abonnierten Clients mitteilen, dass sich etwas geändert hat – mit **[Abonnements](subscriptions.md)**.
Wenn du noch keinen Handler registriert hast, beginne mit **[Tools](../servers/tools.md)**. Jede Seite hier setzt voraus, dass du einen hast.

View file

@ -0,0 +1,107 @@
---
translation:
sections: [f3ca8ac5f90f2dfa, 85a1ef3588ba0736, 563346d4d5804933, 9e3528340d0bab53]
tool: 1
---
# Lifespan {#lifespan}
Die meisten echten Server halten etwas für ihre gesamte Lebensdauer: einen Datenbank-Pool, einen HTTP-Client, ein geladenes Modell.
Du willst das nicht bei jedem Aufruf neu aufbauen, und du willst es sauber schließen. Genau dafür gibt es den **Lifespan** (Start- und Stopp-Phase des Servers).
## Ein typisierter Lifespan {#a-typed-lifespan}
Ein Lifespan ist ein `@asynccontextmanager`, der den Server erhält und per `yield` **ein Objekt** liefert. Was immer du dabei lieferst, steht jedem Handler zur Verfügung, solange der Server läuft.
```python title="server.py" hl_lines="25-31 34 38 40"
--8<-- "docs_src/lifespan/tutorial001.py"
```
Lies es von unten nach oben:
* `app_lifespan` verbindet die `Database` **vor** dem `yield` und trennt sie **danach**, in einem `finally`. Das sind Start und Stopp.
* Es liefert einen `AppContext`, eine schlichte Dataclass, die die eingerichteten Dinge hält. Heute ein Feld, morgen zehn.
* `MCPServer("Bookshop", lifespan=app_lifespan)` ist die ganze Verdrahtung.
* Im Tool ist das gelieferte Objekt `ctx.request_context.lifespan_context`.
Der Lifespan läuft **einmal**. Er wird betreten, wenn der Server startet (vor dem ersten Request), und verlassen, wenn der Server stoppt. Alle Requests dazwischen teilen sich denselben `AppContext`.
!!! info
Wenn du schon einmal einen FastAPI-`lifespan` geschrieben hast, kennst du das bereits. Derselbe Dekorator, dasselbe `yield`, dasselbe `finally`.
### Was das Modell sieht {#what-the-model-sees}
Nichts Neues. `ctx` ist ein **Context**-Parameter, also injiziert das SDK ihn, und er landet nie im Eingabeschema:
```json
{
"type": "object",
"properties": {
"genre": {"title": "Genre", "type": "string"}
},
"required": ["genre"],
"title": "count_booksArguments"
}
```
`genre` ist das einzige Argument, das das Modell übergeben kann. Der Lifespan ist Sache deines Servers.
Auch `@mcp.resource()`- und `@mcp.prompt()`-Funktionen können einen `ctx`-Parameter annehmen, geschrieben als bloßer `Context` – aus einem Grund, zu dem der nächste Abschnitt kommt. Alles, was `ctx` mitbringt, steht in **[Der Context](context.md)**.
### Es ist wirklich typisiert {#it-really-is-typed}
Sieh dir die Annotation noch einmal an: `ctx: Context[AppContext]`.
Dieser eine Typparameter ist der Grund, warum `ctx.request_context.lifespan_context` für deinen Type Checker ein `AppContext` **ist**. `.db` wird automatisch vervollständigt; `.dbb` ist ein Fehler, bevor du den Server überhaupt startest.
Schreibst du stattdessen einen bloßen `Context`, ist `lifespan_context` als `dict[str, Any]` typisiert: Der Type Checker kann nicht wissen, was dein Lifespan geliefert hat. Das Objekt ist zur Laufzeit immer noch da; du hast nur die Hilfe verloren.
!!! warning
`Context[AppContext]` ist eine Schreibweise **nur für Tools**. Setzt du sie auf eine `@mcp.resource()`- oder
`@mcp.prompt()`-Funktion, schlägt jeder Aufruf dieses Handlers fehl. Der Client bekommt einen Fehler zurück,
und das Server-Log zeigt, warum:
```text
Context is not available outside of a request
```
In Ressourcen und Prompts schreibst du das bloße `ctx: Context`. Das Objekt, das dein Lifespan geliefert hat, ist
zur Laufzeit immer noch `ctx.request_context.lifespan_context`; du gibst den Typparameter auf, nicht
das Objekt.
!!! tip
Es gibt immer einen Lifespan. Übergibst du keinen, liefert der Standard des SDK ein leeres `dict`,
also ist `ctx.request_context.lifespan_context` `{}`, nie `None`. Dieser Standard ist auch der Grund, warum ein
bloßer `Context` es als `dict[str, Any]` typisiert.
## Zusehen, wie es passiert {#watch-it-happen}
„Der Start läuft vor dem ersten Request“ ist die Art von Satz, die du nicht einfach glauben müssen solltest.
Reduziere den Server auf den Lebenszyklus: Gib `Database` ein `connected`-Flag, schalte es in `connect()` und `disconnect()` um und füge ein Tool hinzu, das es meldet.
```python title="server.py" hl_lines="11 14 17 25 44"
--8<-- "docs_src/lifespan/tutorial002.py"
```
`database` lebt aus einem Grund auf Modulebene: damit du es von *außerhalb* des Servers betrachten kannst.
!!! check
Drei Momente, drei Werte:
* Bevor der Server startet, ist `database.connected` `False`. Der Import des Moduls hat nichts verbunden.
* Während er läuft, rufe `database_status` auf, und das Ergebnis ist `"connected"`.
* Stoppe den Server, und der `finally`-Block läuft: `database.connected` ist wieder `False`.
Die Arbeit geschah genau dort, wo du sie hingelegt hast: rund um das `yield`, nicht beim Import und nicht pro Request.
## Zusammenfassung {#recap}
* `lifespan=` nimmt einen `@asynccontextmanager`, der den Server erhält und per `yield` ein Objekt liefert.
* Code vor dem `yield` ist der Start. Das `finally` danach ist der Stopp.
* Er läuft einmal, rund um die gesamte Lebensdauer des Servers, nicht pro Request.
* Was immer du per `yield` lieferst, ist `ctx.request_context.lifespan_context` in jedem Tool, jeder Ressource und jedem Prompt.
* `ctx: Context[AppContext]` macht diesen Zugriff in Tools vollständig typisiert. Ressourcen und Prompts nehmen den bloßen `Context`.
* Kein `lifespan=` bedeutet ein leeres `dict`, nie `None`.
Ein Handler, der mitten im Aufruf anhält, um die Person am Host nach etwas zu fragen, das nur sie weiß, ist **[Elicitation](elicitation.md)** (Rückfrage bei der Person am Host).

View file

@ -0,0 +1,87 @@
---
translation:
sections: [c93a3e1aefd77955, 7851abd5ec54393b, f49d1ca2f330f9cd, c03764bd9dfeef7b, 4a0391691a674ae4, 2df5cd279eabf9f5]
tool: 1
---
# Logging {#logging}
Logge aus einem Tool genauso wie aus jeder anderen Python-Funktion: mit der Standardbibliothek.
MCP hat auf Protokollebene eine **Capability für Logging**: Ein Server konnte seine Log-Meldungen über Methoden des `Context`-Objekts als Benachrichtigungen an den Client schicken. Die Revision 2026-07-28 der Spezifikation **erklärt diese Capability für veraltet und ersetzt sie nicht**, deshalb vermitteln diese Docs sie nicht. Die vollständige Liste dessen, was veraltet ist und was du stattdessen tust, steht in **[Veraltete Features](../deprecated.md)**.
Stattdessen tust du, was du in jedem anderen Python-Programm tust: Du nimmst die Standardbibliothek.
## Ein Tool, das loggt {#a-tool-that-logs}
```python title="server.py" hl_lines="1 5 13"
--8<-- "docs_src/logging/tutorial001.py"
```
* `logging.getLogger(__name__)` liefert dir einen Logger, der nach deinem Modul benannt ist. Leg ihn einmal an, ganz oben.
* Im Tool rufst du `logger.info(...)` auf wie in jeder anderen Funktion. Nichts zu injizieren, nichts mit `await`, nichts MCP-Spezifisches.
!!! check
Ruf das Tool auf und sieh dir das ganze Ergebnis an:
```python
result.content # [TextContent(text="Found 3 books matching 'dune'.")]
result.structured_content # {'result': "Found 3 books matching 'dune'."}
```
Die Log-Zeile taucht darin nirgends auf. Logging ist für **dich**, die Person, die den Server betreibt.
Das Modell sieht es nie. Wenn das Modell etwas lesen soll, gib es mit `return` zurück.
## Wohin die Ausgabe geht {#where-it-goes}
Bei einem **stdio**-Server ist diese Frage wichtiger als sonst. Der Host hat deinen Server als Subprozess gestartet und liest MCP-Nachrichten von dessen **stdout**. Standard Error gehört dir.
Die Standardbibliothek macht bereits das Richtige: Log-Ausgaben gehen standardmäßig nach `sys.stderr`. Deine `logger.info(...)`-Zeilen landen im Terminal (oder wo auch immer der Host das stderr des Subprozesses einsammelt), und der Protokoll-Stream bleibt sauber.
!!! tip
Verwende kein `print()` in einem stdio-Server. `print` schreibt nach **stdout**, und stdout gehört dem Protokoll.
Während der Server läuft, leitet das SDK stdout, das tatsächlich *geflusht* wird, nach stderr um, sodass es die
Leitung nicht beschädigen kann. Ein `print()` in einem blockgepufferten Prozess bleibt aber meist ungeflusht im
Puffer von `sys.stdout` liegen, bis der Interpreter ihn beim Beenden leert – direkt auf den Protokoll-Stream.
Selbst wenn die Zeile umgeleitet wird, landet sie roh zwischen den Log-Ausgaben, ohne Level, ohne Logger-Namen
und ohne Möglichkeit, sie zu filtern.
`logger.debug("got here")` ist dieselbe eine Zeile Aufwand und geht an die richtige Stelle.
## Das Level {#the-level}
Du musst `logging.basicConfig()` nicht selbst aufrufen. Das Erzeugen eines `MCPServer` hat das bereits getan, mit einem Handler, der auf Standard Error zeigt, auf dem Level, das du als `log_level=` übergibst. `MCPServer("Bookshop", log_level="DEBUG")` genügt also, um deine `logger.debug(...)`-Zeilen zu sehen.
Der Standardwert ist `"INFO"`.
`logging.basicConfig()` ersetzt nie Handler, die bereits existieren. Wenn du das Logging selbst konfigurierst, bevor du den Server erzeugst, gewinnt deine Konfiguration.
## Ausprobieren {#try-it}
Starte den Server mit dem MCP Inspector:
```console
uv run mcp dev server.py
```
Ruf `search_books` im Tab **Tools** auf. Der Inspector zeigt dir das Ergebnis: nur den Rückgabewert. Die Zeile
```text
Searching for 'dune'
```
ging nach Standard Error: ins Terminal, nicht auf die Leitung.
!!! info
Wenn du eigentlich *Tracing* willst (jeden Request, wie lange er gedauert hat, ob er fehlgeschlagen ist),
willst du keine Log-Zeilen, sondern Spans. Dein Server sendet sie bereits: Das SDK zeichnet ohne weitere
Konfiguration jede Nachricht mit OpenTelemetry auf. Siehe **[OpenTelemetry](../run/opentelemetry.md)**.
## Zusammenfassung {#recap}
* Die Logging-Capability des MCP-Protokolls ist mit der Spezifikation 2026-07-28 veraltet und wird nicht ersetzt. Bau nicht darauf auf.
* `logger = logging.getLogger(__name__)` auf Modulebene, `logger.info(...)` im Tool. Das ist das ganze Muster.
* Log-Ausgaben erreichen das Modell nie. Nur der Wert, den du mit `return` zurückgibst.
* Standard Error gehört dir; stdout gehört dem Protokoll. Das SDK leitet geflushtes, verirrtes stdout während des Betriebs nach stderr um, aber ein ungeflushtes `print()` kann beim Beenden trotzdem auf die Leitung gelangen, und umgeleitete Zeilen kommen ohne Kennzeichnung an. Nimm `logging`, dessen Handler jeden Eintrag flusht.
* `MCPServer(..., log_level="DEBUG")` setzt das Level, und eine Logging-Konfiguration, die du vorher angelegt hast, bleibt unangetastet.
Wie du verbundenen Clients mitteilst, dass sich auf deinem Server etwas geändert hat (die Tool-Liste, eine Ressource), steht in **[Abonnements](subscriptions.md)**.

View file

@ -0,0 +1,191 @@
---
translation:
sections: [74011e683045eea9, 9b64cc175c18b6a9, 4b41be4824030397, e3b1502da786ec33, 71e41161f143c6a9, 9ec2c1eeb8c36378, 8dd027377d46448b, f81491125dcbfe8b]
tool: 1
---
# Multi-Roundtrip-Requests {#multi-round-trip-requests}
Manchmal kann ein Tool nicht in einem einzigen Roundtrip fertig werden. Es braucht etwas, das nur die Person am Host hat: eine Auswahl, eine Bestätigung, Zugangsdaten.
Vor 2026-07-28 holte der Server sich das, indem er **zurückrief**: Mitten in der Bearbeitung des ursprünglichen Requests öffnete er einen eigenen Request an den Client (eine Elicitation – also eine Rückfrage bei der Person am Host – oder einen Sampling-Aufruf). Die Spec 2026-07-28 schafft diesen Rückkanal (back-channel) ab.
Stattdessen **gibt** der Server etwas **zurück**.
## Zurückgeben statt zurückrufen {#return-dont-call-back}
Der Server beantwortet `tools/call` mit einem **`InputRequiredResult`** statt mit einem `CallToolResult`. Zwei seiner Felder erledigen die Arbeit:
* **`input_requests`**: was der Server noch braucht, als Dict mit Schlüsseln, die der Server selbst gewählt hat. Jeder Wert ist ein `ElicitRequest`, ein `CreateMessageRequest` oder ein `ListRootsRequest`.
* **`request_state`**: ein opakes Token. Der Client schickt es beim Retry unverändert zurück. Dein Server ist der Einzige, der es liest.
Der Client erfüllt jeden Request und ruft dann **dasselbe Tool noch einmal** auf, mit seinen Antworten in `input_responses` und dem Token in `request_state`. Der Server hat jetzt, was ihm fehlte, und gibt ein normales `CallToolResult` zurück.
Das ist das ganze Protokoll. Jede Etappe ist ein gewöhnlicher Request vom Client an den Server. Nie fließt etwas in die andere Richtung.
## Die Serverseite {#the-server-side}
Auf `@mcp.tool()` baust du das selten von Hand: Deklariere eine Abhängigkeit, die bei der Person am Host zurückfragt (`Elicit`), das LLM des Clients per Sampling nutzt (`Sample`) oder seine Roots auflistet (`ListRoots`), und das SDK gibt das `InputRequiredResult` für dich zurück; diese Form beschreibt die Seite **[Abhängigkeiten](dependencies.md)**. Die beiden Formen lassen sich nicht mischen: Ein Aufruf hat genau einen `input_responses`/`request_state`-Kanal, deshalb kann ein Tool, das `Resolve(...)`-Parameter verwendet, nicht zusätzlich ein `InputRequiredResult` aus seinem Rumpf zurückgeben. Ein deklarierter `InputRequiredResult`-Rückgabetyp wird bei der Registrierung abgelehnt (`InvalidSignature`), ein nicht deklarierter lässt den Aufruf zur Laufzeit fehlschlagen. Die manuelle Form ist der **Low-Level**-`Server`, dessen Handler `on_call_tool` beide Ergebnistypen zurückgeben darf:
```python title="server.py" hl_lines="43-46"
--8<-- "docs_src/mrtr/tutorial001.py"
```
* `on_call_tool` ist typisiert als `-> CallToolResult | InputRequiredResult`. Das zweite zurückzugeben ist die gesamte serverseitige API.
* Beim ersten Aufruf ist `params.input_responses` `None`, also greift die Guard-Bedingung, und der Handler fragt, statt zu antworten.
* Beim Retry liegt das `ElicitResult`, das der Client geschickt hat, unter **demselben Schlüssel** (`"region"`), den der Server in `input_requests` verwendet hat.
Alles andere in dieser Datei (das explizite `input_schema`, das von Hand gebaute `CallToolResult`) ist der gewöhnliche Low-Level-`Server`, behandelt in **[Der Low-Level-Server](../advanced/low-level-server.md)**. Diese Seite fügt nur den zweiten Rückgabetyp hinzu.
## Über Tools hinaus {#beyond-tools}
`tools/call` ist nichts Besonderes: Unter 2026-07-28 darf ein Server `prompts/get` und `resources/read` genauso beantworten. Auf `MCPServer` gibt eine `@mcp.prompt()`-Funktion – oder eine `@mcp.resource()`-**Template**-Funktion – das `InputRequiredResult` selbst zurück und liest die Antworten des Retrys aus dem Context:
```python title="server.py" hl_lines="20 22 24"
--8<-- "docs_src/mrtr/tutorial004.py"
```
* Die erste Runde gibt das `InputRequiredResult` zurück. Beim Retry hält `ctx.input_responses` die Antworten unter denselben Schlüsseln bereit, und die Funktion gibt ihr gewöhnliches Ergebnis zurück – hier Prompt-Nachrichten, bei einer Template-Ressource Ressourceninhalt.
* Ein `request_state`, den du setzt, wird versiegelt, bevor er über die Leitung geht, und beim Echo verifiziert, wie alles andere auf dem Server; **[`requestState` schützen](#protecting-requeststate)** weiter unten beschreibt, was dir das Siegel bringt und wann du Schlüssel konfigurieren musst.
* Eine `@mcp.tool()`-Funktion kann das Ergebnis genauso direkt zurückgeben, wenn die Abhängigkeitsform nicht passt.
* Statische `@mcp.resource()`-Funktionen nehmen nicht teil: Sie bekommen keinen `Context` und könnten den Retry deshalb nie lesen. Nur Template-Ressourcen können fragen.
* Die Regeln zur Protokollgeneration weiter unten gelten unverändert: Ein `InputRequiredResult` auf einer Session vor 2026 zurückzugeben, ergibt denselben `-32603`, den die Warnung beschreibt.
## Die Clientseite {#the-client-side}
`Client` führt die Schleife für dich aus.
Registriere die Callbacks, nach denen der Server fragen könnte (`elicitation_callback`, `sampling_callback`, `list_roots_callback`), und rufe das Tool auf. Kommt ein `InputRequiredResult` an, verteilt `Client` jeden Eintrag in `input_requests` an den passenden Callback, wiederholt den Aufruf mit den Antworten und dem zurückgeschickten `request_state` und macht weiter, bis ein `CallToolResult` zurückkommt:
```python title="client.py" hl_lines="11 12"
--8<-- "docs_src/mrtr/tutorial003.py"
```
* Dieser `elicitation_callback` ist derselbe, den das `elicitation/create` eines Servers vor 2026 über den Rückkanal getroffen hätte. Dasselbe gilt für `sampling_callback` bei `sampling/createMessage` und für `list_roots_callback` bei `roots/list`: Unter 2026-07-28 sind die eigenständigen Server->Client-RPCs verschwunden, aber die identischen Payloads `ElicitRequest` / `CreateMessageRequest` / `ListRootsRequest` reisen in `input_requests` mit und landen bei denselben drei Callbacks. Ein Satz Callbacks bedient beide Generationen.
* `call_tool` gibt ein schlichtes `CallToolResult` zurück. Die Zwischenrunden sind für den aufrufenden Code unsichtbar.
* `get_prompt` und `read_resource` treiben dieselbe Schleife.
!!! check
Lässt du den Callback weg, scheitert die Schleife in der ersten Runde: Der Ersatz-Callback des SDK
beantwortet jede Elicitation mit einem Fehler, und `call_tool` löst `MCPError` mit der Meldung
*„Elicitation not supported“* aus.
Die Schleife ist begrenzt. `Client(..., input_required_max_rounds=10)` ist die Standardobergrenze; ein Server, der darüber hinaus weiter `InputRequiredResult` zurückgibt, lässt `call_tool` eine Exception auslösen. Trägt eine Runde nur `request_state` und keine `input_requests`, schläft `Client` kurz (50 ms, verdoppelt bis zu einer Obergrenze von 250 ms), bevor er es erneut versucht. So wird ein Server, der nur *„noch nicht fertig“* sagt, nicht in einer Dauerschleife abgefragt.
### Die Schleife selbst steuern {#driving-the-loop-yourself}
Die automatische Schleife genügt für einen Client in einem einzigen Prozess. Übernimm die Schleife stattdessen selbst, wenn:
* dein Client **verteilt** ist: Der Prozess, der der Person die Frage anzeigt, ist nicht der Prozess, der `call_tool` aufgerufen hat, also setzt ein anderer Worker den Retry ab. `request_state` ist das persistierbare Token, das du über diese Grenze trägst – durch deinen eigenen Speicher –, und `input_responses` ist das, was die andere Seite damit zurückschickt.
* du jede Runde **inspizieren** willst: jeden `input_requests`-Eintrag loggen oder auditieren, bestimmte Request-Arten ablehnen oder zwischen den Etappen ein eigenes Backoff anwenden.
* du eine Grenze nach **Uhrzeit** statt nach Rundenzahl willst: Umschließe deine eigene Schleife mit `anyio.fail_after(...)`, statt dich auf `input_required_max_rounds` zu verlassen.
Geh auf die darunterliegende Session hinunter, wo `allow_input_required=True` dir die Union direkt aushändigt:
```python title="client.py" hl_lines="12 13 19"
--8<-- "docs_src/mrtr/tutorial002.py"
```
* `client.session.call_tool(..., allow_input_required=True)` erweitert den Rückgabetyp auf `CallToolResult | InputRequiredResult`. Das `isinstance` engt ihn wieder ein.
* `request_state` liegt jetzt in deiner Hand. Schreib ihn zwischen den Etappen weg, und das Gespräch kann aus einem frischen Prozess fortgesetzt werden.
* Für jeden Eintrag in `input_requests` legst du eine `InputResponse` unter **demselben Schlüssel** in `input_responses` ab. `fulfil` ist die Stelle für deine UI; diese hier kodiert die Antwort fest.
* Derselbe Tool-Name, dieselben `arguments`, in jeder Etappe. Der Retry ist der ursprüngliche Aufruf, noch einmal ausgeführt, keine neue Methode.
## `requestState` schützen {#protecting-requeststate}
Alles oben behandelt `request_state` als Echo, und auf der Leitung ist er auch nichts anderes. Aber der Client hält ihn zwischen den Etappen (ihn über Prozesse hinweg wegzuschreiben ist genau das, was der vorige Abschnitt abgesegnet hat), also ist das, was zurückkommt, **vom Client gelieferte Eingabe**: Sie kann verändert, abgelaufen oder aus einem ganz anderen Aufruf entnommen sein. Die Spec verlangt von Servern, die Integrität dieses Zustands zu schützen und die Runde abzulehnen, wenn die Verifikation fehlschlägt – immer dann, wenn der Zustand Autorisierung, Ressourcenzugriff oder Geschäftslogik beeinflussen kann.
`MCPServer` schützt ihn standardmäßig. Jeder Server versiegelt ausgehenden `requestState` und verifiziert jedes Echo – Resolver-Zustand und von Hand gebauten Zustand gleichermaßen – unter einem Schlüssel, der beim Prozessstart erzeugt wird. Du konfigurierst nichts, schreibst Klartext und liest Klartext; über die Leitung geht immer nur ein opakes, verschlüsseltes Token.
Der Standardschlüssel lebt und stirbt mit dem Prozess – das ist das Eine, was du wissen musst, bevor du über einen einzelnen Prozess hinaus bereitstellst:
```python
from mcp.server.mcpserver import MCPServer, RequestStateSecurity
# Multi-instance or restart-surviving: one or more shared secret keys (>= 32 bytes each).
mcp = MCPServer("fleet", request_state_security=RequestStateSecurity(keys=[key]))
```
* **Der Standard (keine Konfiguration)** passt für einen einzelnen Prozess: stdio oder genau ein HTTP-Worker. Ein Retry, der bei einem anderen Worker, einer anderen Instanz hinter einem Load Balancer oder demselben Server nach einem Neustart landet, ist unter einem Schlüssel versiegelt, den dieser Prozess nicht hat – der Client bekommt die unten beschriebene feste Ablehnung und muss den Ablauf von vorn beginnen.
* **`keys=[...]`** ist erforderlich, sobald ein Retry eine **andere Instanz** erreichen kann (`uvicorn` mit mehreren Workern, HTTP hinter Lastverteilung) oder Neustarts überleben muss: Jede Instanz verifiziert, was irgendeine Schwesterinstanz ausgestellt hat. Dieselbe Maschinerie, dein Geheimnis statt eines erzeugten.
* Für eigene Kryptografie, etwa ein KMS oder einen vorhandenen Token-Dienst, übergib `RequestStateSecurity(codec=...)` statt `keys`; **[Eigene Kryptografie mitbringen](#bring-your-own-crypto)** weiter unten beschreibt den Vertrag.
### Was das Siegel trägt {#what-the-seal-carries}
Ob Standard oder konfiguriert: `requestState` auf der Leitung ist ein verschlüsseltes, authentifiziertes Token. Dein Code sieht es nie: Handler und Resolver schreiben Klartext und lesen Klartext (`ctx.request_state`); das SDK versiegelt auf dem Weg hinaus und verifiziert auf dem Weg hinein. Über die Integrität hinaus ist jedes Token gebunden an:
* **Ein Zeitfenster.** Jede Runde versiegelt neu mit frischem Ablaufzeitpunkt, deshalb begrenzt `RequestStateSecurity(ttl=...)` (Standardwert 600 Sekunden) die Bedenkzeit pro Runde, nicht den ganzen Ablauf.
* **Den authentifizierten Principal.** Trägt der Request ein OAuth-Access-Token, das das SDK validiert hat, wird der Zustand an Client, Issuer und Subject des Tokens gebunden: Zustand, der für eine Person ausgestellt wurde, scheitert unter einer anderen, selbst wenn beide denselben OAuth-Client teilen. Ein Verifier, der kein Subject liefert, schwächt die Bindung auf die Client-Identität allein ab, die bei URL-basierten Client-IDs alle teilen, die diese Client-Software verwenden. Wird die Authentifizierung außerhalb des SDK terminiert (ein vorgeschalteter Proxy) oder ist der Transport nicht authentifiziert, gibt es keinen Principal zum Binden, und diese Prüfung bleibt wirkungslos – es sei denn, `RequestStateSecurity(bind_principal=...)` liefert einen aus deinem eigenen Identitätssignal. Welche Bestandteile dein Token-Verifier auch liefert, er muss sie konsistent liefern: Ein Verifier, der das Subject bei manchen Requests einschließt und bei anderen weglässt, ändert den Principal mitten im Ablauf, und laufende Runden werden abgelehnt.
* **Den auslösenden Request.** Die Methode, den Tool- oder Prompt-Namen (oder den Ressourcen-URI) und einen Digest der Argumente. Ein Token, das gegen ein anderes Tool, andere Argumente oder eine andere Methode wieder eingespielt wird, scheitert.
* **Die genaue gestellte Frage.** Jede Resolver-Antwort ist an die gerenderte Frage geheftet, die dem Client gezeigt wurde, sowohl in der Runde, in der sie zuerst eintrifft, als auch wenn eine aufgezeichnete Antwort später wiederverwendet wird. Stellst du mit umformulierter Nachricht oder geändertem Schema neu bereit, fragt der Server erneut, statt eine veraltete Antwort zu verbrauchen. Dieselbe Bindung wirkt auch andersherum: Leite Nachrichten aus den Argumenten des Tools ab, nicht aus Daten pro Aufruf. Eine Nachricht, die aus einem Zeitstempel oder einem Live-Kurs gebaut ist, rendert in jeder Runde anders, sodass jede aufgezeichnete Antwort veraltet aussieht und der Server erneut fragt, bis das Rundenlimit des Clients den Aufruf beendet.
All das ist Aufgabe des SDK, nicht deine – und nicht die des Codecs, falls du deinen eigenen mitbringst.
### Schlüssel rotieren {#rotating-keys}
`keys[0]` versiegelt neuen Zustand; jeder Schlüssel in der Liste verifiziert. Eine Rotation ohne Ausfallzeit besteht aus drei Phasen, jede vollständig ausgerollt, bevor die nächste beginnt:
```python
RequestStateSecurity(keys=[OLD, NEW]) # 1: every instance learns to verify NEW; OLD still mints
RequestStateSecurity(keys=[NEW, OLD]) # 2: NEW mints; in-flight OLD state keeps verifying
RequestStateSecurity(keys=[NEW]) # 3: one ttl after phase 2 is fully out, retire OLD
```
Befördere niemals zuerst den ausstellenden Schlüssel: Unter einem Schlüssel auszustellen, den manche Instanz noch nicht verifizieren kann, lässt laufende Runden mitten im Rollout fallen.
Schlüssel gelten für genau einen Dienst. Der versiegelte Umschlag trägt außerdem den Namen des Servers als Audience-Claim, sodass ein Token, das ein anderer Dienst ausgestellt hat, der zufällig ein Geheimnis teilt, trotzdem abgelehnt wird. Der Claim ist nur so unterscheidungskräftig wie der Name, deshalb muss ein Server mit expliziter Policy einen echten Namen haben oder `RequestStateSecurity(audience=...)` setzen – ein unbenannter löst bei der Konstruktion eine Exception aus. `audience=` dient auch bewussten Multi-Service-Topologien, in denen ein Dienst Zustand akzeptieren muss, den ein anderer ausgestellt hat. (Der konfigurationsfreie Standard ist ausgenommen: Sein Schlüssel verlässt den Prozess nie, also hat der Audience-Claim nichts hinzuzufügen.)
### Eigene Kryptografie mitbringen {#bring-your-own-crypto}
`RequestStateSecurity(codec=...)` nimmt alles mit `seal(bytes) -> str` und `unseal(str) -> bytes`, das für jedes Token, das es nicht selbst ausgestellt hat, `InvalidRequestState` auslöst. Die klassische Form ist Envelope Encryption gegen ein KMS, bei der du beim Start einmal einen Datenschlüssel entpackst und die Kryptografie pro Token lokal hältst:
```python title="server.py" hl_lines="12 26-27 34-35 38"
--8<-- "docs_src/mrtr/tutorial005.py"
```
TTL, Principal-Bindung und Request-Bindung sind **nicht** Sache des Codecs: Das SDK stempelt sie vor `seal` in die Payload und verifiziert sie nach `unseal` erneut, für jeden Codec. Die einzigen Pflichten eines Codecs sind Integrität (manipuliert heißt: Exception auslösen) und idealerweise Vertraulichkeit.
### Wenn die Verifikation fehlschlägt {#when-verification-fails}
Jeder eingehende Fehlschlag – ob manipuliert, abgelaufen, gegen einen anderen Request oder Principal wieder eingespielt oder unter einem Schlüssel versiegelt, den dieser Server nicht kennt – bekommt dieselbe Antwort:
```json
{"code": -32602, "message": "Invalid or expired requestState"}
```
Eine feste Meldung für jede Ursache, damit die Leitung nie verrät, welche Prüfung fehlschlug; der wahre Grund geht ins Server-Log. Jeder eingehende `requestState` auf `tools/call`, `prompts/get` und `resources/read` wird geprüft, auch einer, der für einen Handler eintrifft, der nie Zustand ausstellt. Die in der Praxis häufigste Ablehnung ist kein Angriff – es ist der prozesslokale Standardschlüssel, der auf einen Retry von vor einem Neustart oder von einer anderen Instanz trifft; der Client startet den Ablauf neu, und `keys=[...]` ist die Lösung, wenn das ins Gewicht fällt.
### Von Hand gebauter Zustand {#hand-built-state}
Ein `request_state`, den du selbst setzt (indem du `InputRequiredResult` aus einer Tool-, Prompt- oder Ressourcen-Template-Funktion zurückgibst), wird von derselben Maschinerie versiegelt und verifiziert wie Resolver-Zustand, ganz ohne Codeänderungen: Klartext schreiben, Klartext lesen, und jede Bindung oben gilt.
Das Eine, was das SDK dir nicht festheften kann, selbst wenn konfiguriert, ist die Identität der Frage: Es weiß nicht, zu welcher *deiner* Fragen eine Antwort in deinem Zustand gehört. Speicherst du Antworten nach Fragen geschlüsselt, nimm deine eigene Fragekennung in den Zustand auf und prüfe sie beim Retry.
Der Low-Level-`Server` ist die Stufe ohne Extras: Anders als bei `MCPServer` wird nichts versiegelt, bis du die Grenze selbst anhängst, und bis dahin geht dein `request_state` genau so über die Leitung, wie du ihn geschrieben hast. Das einzeilige Opt-in zeigt **[Der Low-Level-Server](../advanced/low-level-server.md#the-other-handlers)**.
## Ein Ergebnis für 2026-07-28 {#a-2026-07-28-result}
`InputRequiredResult` gibt es nur bei Protokollversion **2026-07-28**. Der In-Memory-`Client(server)` handelt sie für dich aus; über die Leitung entdeckt `mode="auto"` sie. Nach dem Verbinden sagt dir `client.protocol_version`, was du bekommen hast.
!!! warning
Eine Session vor 2026 hat keinen Platz für ein `InputRequiredResult`. Gibst du eines aus deinem Handler auf einer
`mode="legacy"`-Verbindung zurück, kann der Runner es nicht in die ausgehandelte Version serialisieren; der
Client bekommt einen `-32603`-Fehler *„Handler returned an invalid result“* zurück. Ein Server, der
beide Generationen bedient, muss `ctx.protocol_version` prüfen, bevor er danach greift.
!!! info
**Elicitation im URL-Modus** nutzt auf einer 2026er-Verbindung genau diesen Mechanismus. Der Eintrag in
`input_requests` ist ein `ElicitRequest`, dessen Params `ElicitRequestURLParams` sind; die Person
schließt den Out-of-band-Ablauf ab, und dein Client wiederholt den Aufruf. Dieselbe Schleife, keine neue API. Die
Hälfte für den High-Level-Server steht in **[Elicitation](elicitation.md)**.
## Zusammenfassung {#recap}
* Unter 2026-07-28 **gibt** ein Server, der mitten im Aufruf Eingaben braucht, ein `InputRequiredResult` **zurück**. Er öffnet nie einen Request an den Client.
* `input_requests` ist, was er braucht. `request_state` ist ein opakes Wiederaufnahme-Token, das nur der Server liest.
* `Client` führt die Retry-Schleife für dich aus: Registriere `elicitation_callback` / `sampling_callback` / `list_roots_callback`, und `call_tool` gibt ein schlichtes `CallToolResult` zurück. `input_required_max_rounds` (Standardwert 10) begrenzt sie.
* Um Runden zu inspizieren oder zu persistieren, verwende `client.session.call_tool(..., allow_input_required=True)` und übernimm die Schleife `while isinstance(result, InputRequiredResult)` selbst.
* Auf `@mcp.tool()` erzeugt eine Abhängigkeit, die bei der Person am Host zurückfragt, dieses Ergebnis für dich (**[Abhängigkeiten](dependencies.md)**); der **Low-Level**-`Server` ist die manuelle Form.
* Prompts und Ressourcen nehmen ebenfalls teil: Eine `@mcp.prompt()`- oder Template-`@mcp.resource()`-Funktion gibt das `InputRequiredResult` selbst zurück und liest beim Retry `ctx.input_responses`.
* `requestState` kommt als vom Client gelieferte Eingabe zurück, deshalb versiegelt `MCPServer` ihn standardmäßig – Resolver-Zustand und von Hand gebauten Zustand gleichermaßen – unter einem prozesslokalen Schlüssel; Deployments mit mehreren Instanzen übergeben `RequestStateSecurity(keys=[...])` (oder einen eigenen Codec), damit jede Instanz verifizieren kann, was eine Schwesterinstanz ausgestellt hat. Das Siegel bindet jedes Token an ein Zeitfenster, den auslösenden Request und den authentifizierten Principal, wenn der Request eine vom SDK validierte Authentifizierung trägt oder `bind_principal=` dein eigenes Identitätssignal liefert (**[`requestState` schützen](#protecting-requeststate)**).
Das ist der Mechanismus, der serverinitiiertes Sampling und den Rest des Push-artigen Rückkanals ersetzt; siehe **[Veraltete Features](../deprecated.md)**.

View file

@ -0,0 +1,122 @@
---
translation:
sections: [5315262fe26b33e1, 9d8e98840f1b78f0, 0284b215e85366c4, 8534d8dbb4053a70, 2966fac6fe697007]
tool: 1
---
# Fortschritt {#progress}
Ein Tool, das dreißig Sekunden braucht und dreißig Sekunden lang schweigt, wirkt kaputt.
**Fortschrittsbenachrichtigungen** beheben das. Das Tool meldet, wie weit es ist; der Client entscheidet, was er daraus zeichnet: einen Balken, einen Spinner, eine Log-Zeile.
## Aus dem Tool melden {#report-it-from-the-tool}
Nimm einen **`Context`**-Parameter entgegen und rufe `report_progress` auf:
```python title="server.py" hl_lines="8 11"
--8<-- "docs_src/progress/tutorial001.py"
```
Drei Argumente, und du bestimmst, was sie bedeuten:
* `progress`: wie weit du bist. Die Spezifikation verlangt, dass der Wert mit jeder Meldung **steigt**; wiederhole nie einen Wert und geh nie rückwärts.
* `total`: wie viel es insgesamt ist, falls du es weißt. Optional.
* `message`: eine menschenlesbare Zeile über *diesen* Schritt. Optional.
`ctx` wird wegen seines Type Hints injiziert, und das Modell sieht ihn nie: Das Eingabeschema von `import_catalog` hat eine einzige Property, `urls`. Die Seite **[Der Context](context.md)** dreht sich ganz um dieses Objekt; Fortschritt ist eines der Dinge, die es dir bietet.
## Im Client darauf lauschen {#listen-for-it-from-the-client}
Der Client meldet sich **pro Aufruf** an, indem er `progress_callback=` an `call_tool` übergibt:
```python title="client.py" hl_lines="7 16"
import anyio
from mcp import Client
from server import mcp
async def show(progress: float, total: float | None, message: str | None) -> None:
print(f"{message} ({progress}/{total})")
async def main() -> None:
async with Client(mcp) as client:
result = await client.call_tool(
"import_catalog",
{"urls": ["https://example.com/a.json", "https://example.com/b.json"]},
progress_callback=show,
)
print(result.structured_content)
anyio.run(main)
```
Der Callback ist eine `async`-Funktion, die genau das entgegennimmt, was der Server gemeldet hat: `progress`, `total`, `message`.
!!! info
`Client(mcp)` verbindet sich direkt mit dem Server-Objekt, im Speicher – derselbe Client, auf dem die Seite
**[Testen](../get-started/testing.md)** aufbaut. `progress_callback` ist derselbe Parameter, egal welchen
Transport der `Client` nutzt; das *Timing*, das du gleich siehst, ist das der In-Memory-Verbindung. Sie führt
deinen Callback inline aus, sodass jede Meldung eintrifft, bevor `call_tool` zurückkehrt. Über einen echten
Transport liefern sich die Benachrichtigungen ein Rennen mit dem Ergebnis, und ein langsamer Callback kann noch
laufen, nachdem `call_tool` bereits zurückgekehrt ist.
### Ausprobieren {#try-it}
Lege `client.py` neben `server.py` und starte es:
```console
python client.py
```
```text
Imported https://example.com/a.json (1/2)
Imported https://example.com/b.json (2/2)
{'result': 'Imported 2 records.'}
```
Jedes `await ctx.report_progress(...)` auf dem Server wurde zu einem Aufruf von `show` auf dem Client, in derselben Reihenfolge, und beide Zeilen wurden ausgegeben, **bevor** `call_tool` zurückkehrte. Fortschritt wird nicht ins Ergebnis gepackt; er streamt, während das Tool noch arbeitet.
!!! warning
`progress_callback` gehört zum **Aufruf**, nicht zum `Client`. Es gibt kein Konstruktorargument dafür,
weil verschiedene Aufrufe verschiedene Callbacks wollen: Einer treibt einen Download-Balken an, der nächste
eine Log-Zeile.
!!! check
Lösche jetzt `progress_callback=show` und starte es erneut:
```text
{'result': 'Imported 2 records.'}
```
Kein Fehler, keine Warnung, dasselbe Ergebnis. `report_progress` ist ein **No-op, wenn der Aufrufer keinen
Fortschritt angefordert hat**. Du meldest also bedingungslos und musst dich nie fragen, ob überhaupt jemand
zuhört.
## Wenn du die Gesamtmenge nicht kennst {#when-you-dont-know-the-total}
`total` ist für den Fall, dass du den Nenner kennst. Oft kennst du ihn nicht: Du leerst einen Feed, läufst einen Cursor ab, lädst etwas ohne Längen-Header herunter.
Lass es weg:
```python title="server.py" hl_lines="20"
--8<-- "docs_src/progress/tutorial002.py"
```
Der Callback erhält `total=None`. Ein Client kann weiterhin *Aktivität* anzeigen („3 imported so far...“), aber keinen Prozentwert. Erfinde keine Gesamtmenge, nur um einen hübscheren Balken zu bekommen.
!!! tip
`progress` muss nichts Bestimmtes zählen. Bytes, Zeilen, Seiten: Wähle die Einheit, die die Person am Host
wiedererkennt, und versprich nur ein `total`, das du halten kannst.
## Zusammenfassung {#recap}
* `await ctx.report_progress(progress, total=None, message=None)` aus jedem Tool, das einen `Context` entgegennimmt.
* Der Client übergibt `progress_callback=` an `call_tool`: pro Aufruf, nie am `Client`.
* Der Callback ist `async (progress, total, message) -> None` und feuert, während das Tool noch läuft.
* Kein Callback am Aufruf heißt: `report_progress` tut nichts. Melde bedingungslos.
* Lass `total` weg, wenn du es nicht kennst; der Callback bekommt `None`.
Fortschritt ist das, was ein laufendes Tool der *Person am Host* zeigt. Die Zeilen, die es für *dich* loggt – für dich, weil du den Server betreibst –, sind ein anderer Kanal: **[Logging](logging.md)**.

View file

@ -0,0 +1,51 @@
---
translation:
sections: [5c82b20cbd65ded0, 9dc22632be79a533, 1fb8f452e990c456, 42666ab914ff0cb1, c4e0cb3667fd5ff9]
tool: 1
---
# Sampling und Roots {#sampling-and-roots}
Ein Handler kann den verbundenen Client um zwei weitere Dinge bitten: eine Completion vom eigenen Modell des Clients (**Sampling**) und die Arbeitsverzeichnisse des Clients (**Roots**, freigegebene Arbeitsverzeichnisse).
Beides funktioniert weiterhin, auf jeder Protokollversion, die das SDK spricht. Lies aber die Warnung, bevor du dein Design darauf aufbaust:
!!! warning "Veraltet seit der Spezifikation 2026-07-28"
Sampling und Roots gelten seit `2026-07-28` als veraltet ([SEP-2577](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/2577)). Sie bleiben voll funktionsfähig und stehen noch mindestens zwölf Monate in der Spezifikation, bevor sie entfernt werden dürfen, aber neue Implementierungen sollten nicht mehr darauf aufbauen. Die empfohlenen Migrationen: Binde statt Sampling direkt die API deines LLM-Anbieters an, und übergib Verzeichnisse statt über Roots per Tool-Parameter, Ressourcen-URI oder Serverkonfiguration. Die SDK-weite Liste steht in **[Veraltete Features](../deprecated.md)**.
## Sampling: das Modell des Clients ausleihen {#sampling-borrow-the-clients-model}
Ein Resolver gibt `Sample(...)` zurück, und das Tool erhält die Completion – über denselben Abhängigkeitsmechanismus, der in **[Abhängigkeiten](dependencies.md)** `Elicit` ausführt:
```python title="server.py" hl_lines="10-15 19"
--8<-- "docs_src/sampling_and_roots/tutorial001.py"
```
* `Sample(messages, max_tokens=...)` spiegelt die Parameter von `sampling/createMessage` wider. Der injizierte Wert ist das `CreateMessageResult` des Clients; übergibst du `tools` oder `tool_choice`, wird daraus stattdessen ein `CreateMessageResultWithTools`.
* Der Client muss die Capability `sampling` deklariert haben (`sampling.tools`, wenn du `tools` oder `tool_choice` übergibst). Hat er das nicht, schlägt der Aufruf mit einem Protokollfehler `-32021` fehl, statt einen Request zu senden, den der Client nicht verarbeiten kann. Eine Session aus der Zeit vor 2026 ohne Rückkanal (back-channel) schlägt mit ihrem üblichen No-Back-Channel-Fehler fehl, weil es nichts gibt, worüber gesendet werden könnte.
* Bei `2026-07-28` wird der Request innerhalb des Multi-Roundtrip-Ablaufs zugestellt (**[Multi-Roundtrip-Requests (multi-round-trip requests)](multi-round-trip.md)**); bei `2025-11-25` ist er ein eigenständiger Request an den Client. Der Code ist in beiden Fällen derselbe, beachte aber die Multi-Roundtrip-Regel: Der Request muss in jeder Wiederholungsrunde identisch aussehen. Baue ihn deshalb nur aus den Argumenten des Tools und anderen stabilen Daten.
* Lass `include_context` unangetastet: Andere Werte als `"none"` sind selbst veraltet (SEP-2596) und brauchen eine Capability, die fast kein Client deklariert.
## Roots: Wohin damit? {#roots-where-should-this-go}
Roots sind die Verzeichnisse, auf denen der Server laut Client arbeiten darf. Sie sind ein informativer Hinweis, kein Mechanismus zur Zugriffskontrolle. Ein Resolver gibt `ListRoots()` zurück:
```python title="server.py" hl_lines="10-11 15"
--8<-- "docs_src/sampling_and_roots/tutorial002.py"
```
* Das injizierte `ListRootsResult` enthält eine Liste von `Root`-Objekten: jeweils einen `file://`-URI und einen optionalen Anzeigenamen.
* Die Hürde ist dieselbe wie beim Sampling: Ohne deklarierte Capability `roots` schlägt der Aufruf mit `-32021` fehl, statt den Request zu senden.
Auf der anderen Seite der Leitung beantwortet der Client beide Requests mit den Callbacks, die er ohnehin schon hat: `sampling_callback` und `list_roots_callback`, beschrieben in **[Client-Callbacks](../client/callbacks.md)**.
## Auf Verbindungen der 2025er-Generation {#on-2025-era-connections}
`ctx.session.create_message(...)` und `ctx.session.list_roots()` gibt es weiterhin für Code, der die Session direkt ansteuert. Sie funktionieren nur dort, wo ein Rückkanal existiert (nicht zustandslose Verbindungen der 2025er-Generation), und ihr Aufruf löst eine Deprecation-Warnung aus. Die Resolver-Marker oben sind die unterstützte Form: Sie wählen die Zustellung anhand der ausgehandelten Version und warnen nicht.
## Zusammenfassung {#recap}
* Gib `Sample(...)` oder `ListRoots()` aus einem Resolver zurück; das Tool erhält das `CreateMessageResult` oder `ListRootsResult` wie jede andere Abhängigkeit.
* Der Client muss die passende Capability deklarieren, sonst schlägt der Aufruf mit `-32021` fehl, statt dass ein Request gesendet wird.
* Beide Features sind bei `2026-07-28` veraltet: vorerst voll funktionsfähig, aber falsch für neue Designs. Bevorzuge Anbieter-APIs gegenüber Sampling und explizite Parameter gegenüber Roots.
Wie ein langsames Tool seinen Fortschritt meldet: **[Fortschritt](progress.md)**.

View file

@ -0,0 +1,152 @@
---
translation:
sections: [60a9de8a0bdaa531, 317bbe7e4355cdcc, a61d660c8029e04a, 8f7e82fcb88df8a9, b165db51249ff8ed, 266f56fb798068a4, 7c0e57030b622139, df18d7c2417a9883]
tool: 1
---
# Abonnements {#subscriptions}
Der Katalog eines Servers ist nicht fest. Tools tauchen zur Laufzeit auf, und der Inhalt hinter einem Ressourcen-URI ändert sich.
Über **Abonnements** erfährt ein Client davon. Der Client sendet einen einzigen `subscriptions/listen`-Request, und die Response auf diesen Request *ist* der Stream: Er bleibt offen und trägt die Änderungsbenachrichtigungen, die der Client angefordert hat.
## Aus dem Tool heraus veröffentlichen {#publish-it-from-the-tool}
Dein Anteil daran ist eine Zeile: Veröffentliche die Änderung.
```python title="server.py" hl_lines="20 32"
--8<-- "docs_src/subscriptions/tutorial001.py"
```
* `await ctx.notify_resource_updated("board://sprint")` erreicht jeden offenen Stream, der diesen URI abonniert hat. Sonst niemanden.
* `await ctx.notify_tools_changed()` erreicht jeden Stream, der Änderungen an der Tool-Liste angefordert hat. Ein Client, der das empfängt, ruft `tools/list` erneut auf und sieht jetzt `sprint_report`.
* Die Geschwister heißen `notify_prompts_changed()` und `notify_resources_changed()`.
* Keine Abonnenten, keine Arbeit. Auf einem untätigen Server zu veröffentlichen ist ein No-op, deshalb prüfst du nie, ob jemand zuhört. Du gibst an, was sich geändert hat.
`MCPServer` bedient `subscriptions/listen` für dich. Die Pflichten auf der Leitung (die Bestätigung als erster Frame, das Filtern pro Stream, die Abonnement-ID auf jedem Frame) sind Sache des SDK.
!!! check
Auf der Leitung sieht ein Stream, dessen Filter `board://sprint` nannte, so aus, nachdem `complete_task` gelaufen ist:
```json
{"method": "notifications/subscriptions/acknowledged",
"params": {"notifications": {"resourceSubscriptions": ["board://sprint"]}, "_meta": {"io.modelcontextprotocol/subscriptionId": "listen-1"}}}
{"method": "notifications/resources/updated",
"params": {"uri": "board://sprint", "_meta": {"io.modelcontextprotocol/subscriptionId": "listen-1"}}}
```
Beachte, was das Update *nicht* trägt: das Board. Jeder Frame trägt die JSON-RPC-ID des listen-Requests unter `_meta`, und diese ID ist die Abonnement-ID. Der Client vergibt sie: Der Python-`Client` verwendet Strings wie `"listen-1"`; andere Clients verwenden vielleicht Ganzzahlen.
## Nur das, was angefordert wurde {#only-what-was-asked-for}
Der Filter ist ein Vertrag. Ein Stream, der Änderungen an der Tool-Liste und einen Ressourcen-URI angefordert hat, empfängt diese beiden Arten und nichts anderes. Veröffentlichst du eine Prompt-Änderung, bleibt dieser Stream still.
`MCPServer` vergleicht Ressourcen-URIs als exakte Strings, deshalb hört ein Stream, der `board://sprint` nannte, nichts über `board://sprint/tasks/1`. Die Spezifikation erlaubt einem Server, eine Änderung an einer Unterressource eines abonnierten URI zu melden; `MCPServer` tut das nie, aber Clients sind darauf ausgelegt, damit zu rechnen.
Zwei Dinge, die der Stream *nicht* ist:
* **Er ist kein Wiederholungsprotokoll.** Ein abgebrochener Stream ist weg, und Ereignisse, die veröffentlicht wurden, während niemand verbunden war, werden nicht zwischengespeichert. Clients horchen erneut und laden neu.
* **Er ist nicht der Pfad von 2025.** Clients, die `resources/subscribe` aufgerufen haben, werden über `ctx.session.send_resource_updated(uri)` bedient. Die `notify_*`-Methoden erreichen nur `subscriptions/listen`-Streams.
## Entscheiden, wer zusehen darf {#deciding-who-may-watch}
Standardmäßig wird jede angeforderte Art und jeder URI akzeptiert: Jeder Aufrufer darf jeden URI beobachten, den du veröffentlichst. Nichts befragt deinen Lese-Handler, weil niemand liest – ein Aufrufer, den dein `files://{name}`-Handler abweisen würde, kann trotzdem einen Stream auf `files://payroll.csv` öffnen und erfahren, dass und wann sich die Datei geändert hat. Er erfährt nie Inhalte, und er kann nicht ertasten, was existiert, denn ein unbekannter URI wird ebenfalls akzeptiert und feuert schlicht nie. Schmal, aber real – sichere es also ab, bevor du personenbezogene URIs von einem mandantenfähigen Server veröffentlichst.
Die Absicherung ist eine Middleware. Sie sieht den `subscriptions/listen`-Request, bevor das SDK ihn bestätigt, und lehnt ab, wenn der Aufrufer etwas anfordert, das er nicht lesen darf:
```python title="server.py" hl_lines="19-26 29"
--8<-- "docs_src/subscriptions/tutorial006.py"
```
* `ctx.params` ist der rohe Request, deshalb validiert die Middleware ihn selbst zu `SubscriptionsListenRequestParams` und liest den Filter, den der Client angefordert hat.
* Eine Ablehnung ist ein ausgelöster `MCPError` vor `call_next(ctx)`: Der Client bekommt diesen Fehler und keinen Stream, und die Verbindung läuft weiter. Halte die Meldung einheitlich und nenne keinen URI, damit eine Ablehnung nie bestätigt, welche URIs geschützt sind.
* Ein einziges `can_access(user, uri)` beantwortet beide Fragen. Der Ressourcen-Handler fragt es bei `resources/read`; die Middleware fragt es bei `subscriptions/listen`. Tausche die Tabelle gegen eine Datenbank oder dein RBAC-System aus, und beide bleiben im Gleichschritt.
* Die Entscheidung gilt für die Lebensdauer des Streams. Es gibt keine erneute Prüfung pro Ereignis. Kann der Zugriff eines Aufrufers also mitten im Stream erlöschen (ein ablaufendes Token), beende die Verbindung dieses Aufrufers, sobald das geschieht.
Der vollständige Middleware-Vertrag, einschließlich dessen, was sie sonst noch umschließt und warum sie als vorläufig markiert ist, steht auf **[Middleware](../advanced/middleware.md)**.
## Die Client-Seite {#the-client-end}
Hier ist ein Client auf der anderen Seite dieses Streams, der dem Board folgt:
```python title="client.py" hl_lines="15"
--8<-- "docs_src/subscriptions/tutorial003.py"
```
Beim Betreten von `client.listen(...)` wird der Request gesendet und auf deine Bestätigung gewartet, sodass der Stream aktiv ist, wenn der Block beginnt, und jedes typisierte Ereignis ist ein Signal zum Neuladen, nie eine Payload. Das ist der ganze Vertrag auf einem Bildschirm. Alles andere zur Client-Seite steht auf einer eigenen Seite: neben einem Hauptablauf beobachten, Stream-Enden und erneutes Horchen. Siehe **[Abonnements](../client/subscriptions.md)** unter *Clients*.
## Über einen Prozess hinaus skalieren {#scaling-past-one-process}
Veröffentlichungen wandern von deinem Handler über einen `SubscriptionBus` zu den offenen Streams. Der Standard arbeitet im Speicher: ein Prozess, jeder Stream darin. Das ist die richtige Antwort, bis du Replikate hinter einem Load Balancer betreibst, denn dann ist der Stream eines Clients an ein Replikat gebunden, und eine Veröffentlichung auf einem anderen Replikat muss ihn erreichen.
Diese Nahtstelle implementierst du selbst: zwei Methoden über deinem Pub/Sub-Backend.
```python
from collections.abc import Callable
from redis.asyncio import Redis
from mcp.server.mcpserver import MCPServer
from mcp.server.subscriptions import ServerEvent # SubscriptionBus is a Protocol: no base class
class RedisSubscriptionBus:
def __init__(self, redis: Redis) -> None:
self._redis = redis
self._listeners: dict[object, Callable[[ServerEvent], None]] = {}
async def publish(self, event: ServerEvent) -> None:
await self._redis.publish("mcp-events", encode(event)) # to every replica
def subscribe(self, listener: Callable[[ServerEvent], None]) -> Callable[[], None]:
token = object()
self._listeners[token] = listener
def unsubscribe() -> None:
self._listeners.pop(token, None)
return unsubscribe
mcp = MCPServer("Sprint Board", subscriptions=RedisSubscriptionBus(redis))
```
`encode` gehört dir, ebenso der Lese-Task auf jedem Replikat, der eintreffende Nachrichten dekodiert und jeden registrierten Listener aufruft. Listener sind synchron, dürfen keine Exception auslösen und laufen auf der Event-Loop des Servers.
Der Bus trägt typisierte `ServerEvent`-Werte, vier kleine Dataclasses, nie JSON-RPC. Stempeln, Filtern und Stream-Lebenszyklen bleiben im SDK, sodass eine Bus-Implementierung das Protokoll nicht brechen kann. Sie kann nur Ereignisse zwischen Prozessen bewegen.
Um außerhalb eines Requests zu veröffentlichen, erzeuge den Bus selbst, damit du die Referenz hältst. `MCPServer` baut intern einen, wenn du nichts übergibst, und legt ihn nicht offen.
```python
from mcp.server.subscriptions import InMemorySubscriptionBus, ToolsListChanged
bus = InMemorySubscriptionBus()
mcp = MCPServer("Sprint Board", subscriptions=bus)
async def tools_reloaded() -> None:
await bus.publish(ToolsListChanged()) # from a lifespan task, a webhook, anywhere
```
## Die Low-Level-Komposition {#the-low-level-composition}
Unten auf dem Low-Level-`Server` ist nichts vorverdrahtet, und dieselben Teile setzen sich in drei Zeilen zusammen:
```python title="server.py" hl_lines="8-9 47"
--8<-- "docs_src/subscriptions/tutorial002.py"
```
* Der Bus gehört dir, also veröffentlichst du direkt darauf: `await bus.publish(ResourceUpdated(uri=...))`. Lege ihn dorthin, wo deine Handler ihn erreichen: hier auf Modulebene, in einer größeren App im Lifespan.
* `ListenHandler(bus)` ist derselbe Handler, den `MCPServer` registriert, und `on_subscriptions_listen=` ist ein gewöhnlicher Handler-Slot. Setze dein eigenes Callable in diesen Slot für eine andere Semantik, und die Pflichten aus der Spezifikation gehen auf dich über: zuerst bestätigen, jeden Frame mit der Abonnement-ID stempeln, nichts außerhalb des Filters ausliefern.
* `ListenHandler.close()` beendet jeden offenen Stream geordnet. Jeder empfängt das Ergebnis des listen-Requests als letzten Frame – so sagt die Spezifikation, dass der Server das Abonnement absichtlich beendet hat. Die Methode kehrt zurück, bevor diese Streams fertig geleert sind, gib ihnen also einen Moment, bevor du den Transport abbaust. Ohne sie enden Streams, wenn der Client die Verbindung trennt.
## Zusammenfassung {#recap}
* Ein Client steigt mit einem einzigen `subscriptions/listen`-Request ein, und die Response ist der Stream. Ihn zu bedienen ist eingebaut.
* Du veröffentlichst mit `ctx.notify_*`, und das SDK übernimmt Stempeln, Filtern und die Lebenszyklus-Arbeit.
* Ereignisse sind Signale, keine Payloads. Beide Seiten laden neu.
* Die Client-Seite ist `async with client.listen(...)`: Alles Weitere steht in **[Abonnements](../client/subscriptions.md)** unter *Clients*.
* Auf dem Low-Level-`Server` setzt du dieselben Teile selbst zusammen: einen Bus, `ListenHandler(bus)`, den Slot `on_subscriptions_listen`.
* Horizontal skalieren heißt, `SubscriptionBus` zu implementieren, zwei Methoden, und ihn als `MCPServer(subscriptions=...)` zu übergeben.
Den Server zu betreiben, der all das bedient, hinter einem Replikat oder zwanzig, ist **[Bereitstellen und skalieren](../run/deploy.md)**.