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
6 changed files with 89737 additions and 54 deletions

View file

@ -0,0 +1,123 @@
---
translation:
sections: [0355618e5f4d5fe4, 1821eaf50f2d0b64, 82e0b28ebd3abf5a, 8ac39614c094f2d0, dab6ff945501ab2a, bd5565c3b2d4f959, 96819ce3d63a0487]
tool: 1
---
# MCP Apps {#mcp-apps}
Eine **MCP App** ist ein Tool mit Gesicht: Neben seinen Daten verweist das Tool auf ein HTML-Dokument, das der Host als interaktive Oberfläche rendert.
Zwei Teile, immer zwei Teile:
1. **Ein Tool**, das die Arbeit macht und Daten zurückgibt, wie jedes andere Tool auch.
2. **Eine `ui://`-Ressource** mit dem HTML, das der Host dafür anzeigt.
Das Tool trägt eine `_meta.ui.resourceUri`-Referenz auf die Ressource. Der Host holt sie mit `resources/read`, rendert sie in einem **Sandbox-iframe** und schiebt das Ergebnis des Tools per `postMessage` in diesen iframe. Dein Server sendet oder empfängt niemals `ui/*`-Nachrichten: Dieser Verkehr läuft zwischen Host und iframe. Du lieferst ein Tool und ein HTML-Dokument; das Theater übernimmt der Host.
Das SDK liefert das als eingebaute Extension `Apps` (`io.modelcontextprotocol/ui`) mit. Falls [Extensions](extensions.md) neu für dich sind, überfliege zuerst jene Seite. Eine Minute, dann komm zurück.
## Eine Uhr mit Gesicht {#a-clock-with-a-face}
```python title="server.py" hl_lines="19 22 30 32"
--8<-- "docs_src/apps/tutorial001.py"
```
Vier Schritte:
* `Apps()`: Eine Instanz hält deine UI-gebundenen Tools und ihre Ressourcen.
* `@apps.tool(resource_uri="ui://clock/app.html")`: ein normales Tool plus der `_meta.ui.resourceUri`-Stempel. Alles, was `@mcp.tool()` akzeptiert (name, title, description, ...), wird durchgereicht.
* `apps.add_html_resource("ui://clock/app.html", CLOCK_HTML)`: die passende Ressource, ausgeliefert als `text/html;profile=mcp-app`. Genau dieser MIME-Typ sagt einem Host „das ist eine App, rendere sie“.
* `MCPServer("clock", extensions=[apps])`: die Anmeldung. Der Server bewirbt jetzt `io.modelcontextprotocol/ui` unter `capabilities.extensions`.
Das HTML selbst lauscht auf das `postMessage` des Hosts und zeigt das Ergebnis an. Für echte Apps verwende das offizielle Browser-SDK [`@modelcontextprotocol/ext-apps`](https://github.com/modelcontextprotocol/ext-apps) in deinem HTML. Es gibt dir `ontoolresult`, `callServerTool`, `getHostContext` und `onhostcontextchanged` statt roher Message-Events.
## Graceful Degradation {#graceful-degradation}
Nicht jeder Client rendert Apps. Die Spezifikation sagt unverblümt, was das für dich bedeutet:
> Tools **MÜSSEN** ein sinnvolles `content`-Array zurückgeben, auch wenn eine UI verfügbar ist.
Das Modell liest `content`; der iframe ist für Menschen. Ein UI-fähiger Host füttert das Modell trotzdem mit dem Textergebnis, und ein reiner Text-Client bekommt *nur* das. Das kanonische Muster ist also: ein Tool, zwei Antworten. Sieh dir `get_time` noch einmal an:
```python title="server.py" hl_lines="23-27"
--8<-- "docs_src/apps/tutorial001.py"
```
`client_supports_apps(ctx)` ist nur dann `True`, wenn der Client die Extension `io.modelcontextprotocol/ui` deklariert **und** `text/html;profile=mcp-app` in seinen `mimeTypes`-Einstellungen aufgeführt hat. Das Feld ist Pflicht, ein Client, der es weglässt, zählt also nicht. Genau das deklariert `main()` in derselben Datei: die Client-Hälfte der Aushandlung – und die reichhaltige Antwort kommt zurück.
!!! warning
Gib niemals einen Platzhalter wie `"[Rendered UI]"` als einzigen Inhalt zurück. Wenn der Fallback-Text nutzlos ist, ist das Tool für jeden reinen Text-Client und für das Modell selbst nutzlos. Schreib den Satz.
## Den iframe abriegeln {#locking-the-iframe-down}
Die Ressourcenseite trägt die Sicherheitsmetadaten: was der iframe laden darf, welche Browser-Berechtigungen er möchte, wie er eingebettet werden will:
```python title="server.py" hl_lines="9 19-22"
--8<-- "docs_src/apps/tutorial002.py"
```
`csp` und `permissions` sind **Anfragen an den Host**, kein Serververhalten. Der Host baut daraus die Content-Security-Policy und die Permissions-Policy des iframes, und er darf ablehnen. Prüfe in deinem JS per Feature Detection, statt eine Zusage vorauszusetzen.
`ResourceCsp`, Feld für Feld (Python-Name, Schlüssel auf der Leitung, was der Host damit macht):
| Python | Leitung (`_meta.ui.csp`) | Steuert |
|---|---|---|
| `connect_domains` | `connectDomains` | `connect-src`: wohin `fetch`/XHR gehen darf |
| `resource_domains` | `resourceDomains` | `img-src`, `style-src`, ...: statische Assets |
| `frame_domains` | `frameDomains` | `frame-src`: verschachtelte iframes |
| `base_uri_domains` | `baseUriDomains` | `base-uri`: worauf `<base>` zeigen darf |
`ResourcePermissions`: Jedes Feld fordert eine Browser-Berechtigung für den iframe an.
| Python | Leitung (`_meta.ui.permissions`) |
|---|---|
| `camera` | `camera` |
| `microphone` | `microphone` |
| `geolocation` | `geolocation` |
| `clipboard_write` | `clipboardWrite` |
!!! note
CSP und Berechtigungen liegen auf der **Ressource**, nie auf dem Tool. Die Tool-Metadaten der Spezifikation haben keinen Platz dafür, und Hosts ignorieren sie dort. Das SDK macht den Fehler unmöglich: `@apps.tool()` hat schlicht keinen Parameter `csp`.
### Sichtbarkeit {#visibility}
`visibility=["app"]` an einem Tool sagt „das existiert für den iframe, nicht für das Modell“:
* `"model"`: Das Modell darf es aufrufen.
* `"app"`: Der iframe darf es aufrufen (über `callServerTool`).
* Weggelassen: beide, das ist der Standardwert.
Filtern ist Aufgabe des **Hosts**. Dein Server listet reine App-Tools in `tools/list` wie alle anderen; der Host verbirgt sie vor dem Modell. Filtere nicht serverseitig.
## Die Regeln, die das SDK durchsetzt {#the-rules-the-sdk-enforces}
All das schlägt beim Start fehl, nicht in Produktion:
* Ein `resource_uri` oder ein Ressourcen-URI, der nicht `ui://...` ist, ist ein `ValueError` zum Zeitpunkt der Dekoration bzw. Registrierung.
* Ein Tool, das an einen URI **ohne passende registrierte Ressource** gebunden ist, ist ein `ValueError`, wenn `MCPServer(extensions=[apps])` die Extension übernimmt. Ein Tool, das HTML bewirbt, das bei `resources/read` mit 404 antwortet, ist eine Fehlkonfiguration, also verweigert der Server die Konstruktion.
* `meta={"ui": ...}` an `@apps.tool()` ist ein `ValueError`. `_meta["ui"]` gehört dem Dekorator; sag es mit `resource_uri=` und `visibility=`. Andere `meta=`-Schlüssel werden daneben problemlos zusammengeführt.
Weder das TypeScript-ext-apps-SDK noch FastMCP fängt heute irgendetwas davon ab; uns ist lieber, du erfährst es, bevor ein Host es tut.
## Über Inline-HTML hinaus {#beyond-inline-html}
`add_html_resource` deckt den häufigen Fall ab: einen String mit HTML. Für alles andere, HTML auf der Platte oder generierte Inhalte, baust du die Ressource selbst und reichst sie weiter:
```python title="server.py" hl_lines="12 18"
--8<-- "docs_src/apps/tutorial003.py"
```
`add_resource` ergänzt den MIME-Typ `text/html;profile=mcp-app`, wenn die Ressource keinen explizit setzt, und weist einen expliziten Widerspruch zurück: Eine `ui://`-Ressource unter einem anderen MIME-Typ rendert kein Host.
!!! tip
Du zielst auf einen Pre-GA-Host, der noch den veralteten flachen Schlüssel `_meta["ui/resourceUri"]` liest? Führe ihn selbst zusammen:
`@apps.tool(resource_uri="ui://x", meta={"ui/resourceUri": "ui://x"})`.
Das verschachtelte `ui`-Objekt ist die Form der Spezifikation; der flache Schlüssel ist auf dem Weg nach draußen.
## Laufen sehen {#see-it-run}
Die Story `apps` in `examples/stories/` ist diese Seite als lauffähiges Paar: ein Server mit einem UI-gebundenen Uhr-Tool und ein Client, der Apps aushandelt, die `_meta.ui.resourceUri` des Tools liest, das HTML holt und das Tool aufruft.
```bash
uv run python -m stories.apps.client
```

View file

@ -0,0 +1,172 @@
---
translation:
sections: [05891e7cc1938a13, b3c01a6af28c51ee, 7ffc91f5e38bdfe0, 717d3f235a8333a7, f471a13b2fe5d737, ed6af2df4b656dff]
tool: 1
---
# Extensions {#extensions}
Eine **Extension** ist ein optionales Bündel von MCP-Verhalten hinter einem einzigen Identifier.
Auf einem Server kann sie Tools, Ressourcen und neue Request-Methoden beisteuern, und sie kann `tools/call` umhüllen. Auf einem Client kann sie zusätzliche Ergebnisformen von `tools/call` für sich beanspruchen und Vendor-Benachrichtigungen beobachten. Jede Seite kündigt sie unter ihrem eigenen `capabilities.extensions` an, und für alle, die nicht darum gebeten haben, ändert sich nichts. Das ist der Vertrag ([SEP-2133](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2133)), und er hat eine goldene Regel: **Extensions sind standardmäßig aus**.
## Eine Extension verwenden {#using-an-extension}
Übergib Instanzen bei der Konstruktion:
```python title="server.py"
--8<-- "docs_src/extensions/tutorial001.py"
```
Fertig. Der Server kündigt jetzt `io.modelcontextprotocol/ui` unter `capabilities.extensions` an und bedient alles, was die Extension beisteuert.
`Apps` ist die eingebaute Referenz-Extension und bekommt eine eigene Seite: **[MCP Apps](apps.md)**.
!!! note
Extensions stehen bei der Konstruktion fest. Es gibt kein `add_extension`, das du später aufrufen könntest: Die Capability-Map eines Servers sollte sich nicht ändern, während Clients mit ihm verbunden sind.
Die Capability-Map reist mit `server/discover`, und das ist ein Pfad von **2026-07-28**. Ein Legacy-`initialize`-Handshake hat keinen Platz dafür, also sieht ein Legacy-Client die Extension schlicht nicht. Plane das ein: Eine Extension *ergänzt* einen Server, sie darf nicht der einzige Weg sein, auf dem der Server nutzbar ist.
## Eine eigene Extension schreiben {#writing-your-own}
Leite von `Extension` ab und überschreibe nur, was du brauchst. Jede Methode hat eine Standardimplementierung.
### Der Identifier {#the-identifier}
```python
--8<-- "docs_src/extensions/tutorial002.py"
```
Der Identifier ist ein `vendor-prefix/name`-String nach der `_meta`-Schlüsselgrammatik der Spezifikation: durch Punkte getrennte Labels (jedes beginnt mit einem Buchstaben und endet mit einem Buchstaben oder einer Ziffer), ein Schrägstrich, dann der Name. Er wird **bei der Definition der Klasse** validiert, ein Tippfehler wartet also nicht darauf, dass ein Server startet:
```text
TypeError: Stamps.identifier must be a `vendor-prefix/name` string
(reverse-DNS prefix required), got 'stamps'
```
Nimm als Präfix eine Domain, die du kontrollierst. `io.modelcontextprotocol/*` ist Extensions vorbehalten, die das MCP-Projekt selbst spezifiziert.
### Tools beisteuern {#contributing-tools}
Die kleinste nützliche Extension besteht aus einem Tool und einer Settings-Map:
```python title="server.py" hl_lines="17 19-20 22-23 26"
--8<-- "docs_src/extensions/tutorial003.py"
```
* `tools()` gibt `ToolBinding`s zurück. Der Server registriert jedes einzelne genau so, als hättest du selbst `mcp.add_tool(...)` aufgerufen: dieselbe Schema-Generierung, dieselbe `Context`-Injection, alles gleich.
* `settings()` ist der Wert, der unter `capabilities.extensions["com.example/stamps"]` angekündigt wird. Gib `{}` zurück (den Standardwert), um die Extension ohne Settings anzukündigen.
* Die Extension bekommt den Server nie in die Hand. Sie deklariert ihre Beiträge als Daten; `MCPServer` verarbeitet sie. Es gibt kein `self.server`, das sie verändern könnte.
Und `main()` ist der Beweis, ein In-Memory-Client direkt gegen `mcp`:
```python title="server.py" hl_lines="29-34"
--8<-- "docs_src/extensions/tutorial003.py"
```
### Eigene Methoden bedienen {#serving-your-own-methods}
Eine Extension kann **neue Request-Methoden** registrieren: eigene Verben, bedient neben denen der Spezifikation:
```python title="server.py" hl_lines="16-22 31 40-48"
--8<-- "docs_src/extensions/tutorial004.py"
```
* `SearchParams` leitet von `RequestParams` ab, sodass der `_meta`-Umschlag von 2026 einheitlich geparst wird und dein Handler validierte Parameter bekommt, nie ein rohes Dict. Begrenze, was der Client kontrolliert: `Field(ge=1, le=100)` weist ein absurdes `limit` zurück, bevor dein Code irgendetwas dafür alloziert.
* `require_client_extension(ctx, EXTENSION_ID)` ist die Schranke: Ein Client, der die Extension nicht deklariert hat, bekommt den Fehler `-32021` (missing required client capability), samt der maschinenlesbaren `requiredCapabilities`-Payload, die die Spezifikation verlangt.
* `protocol_versions=frozenset({"2026-07-28"})` heftet die Methode an genau eine Protokollversion auf der Leitung. Bei jeder anderen Version bekommt der Client `METHOD_NOT_FOUND`, genau so, als gäbe es die Methode dort nicht. Für diesen Client gibt es sie auch nicht.
Methoden sind **strikt additiv**. Das SDK erzwingt das bei der Konstruktion, nicht zur Laufzeit:
* Ein `MethodBinding` für eine in der Spezifikation definierte Methode (`tools/list`, `completion/complete`, ...) löst `ValueError` aus, wenn das Binding konstruiert wird. Kernverben gehören dem Server.
* Zwei Extensions, die dieselbe Methode binden, lösen eine Exception aus, sobald sich die zweite registriert. Last-write-wins ist genau der Weg, auf dem Plugins einander beschädigen; das machen wir nicht.
* Ein leeres `protocol_versions`-Set löst ebenfalls eine Exception aus: Eine Methode, die nie bedient werden kann, ist ein Bug, keine Konfiguration.
### Die Client-Seite {#the-client-side}
Das `main()` derselben Datei ist die ganze Client-Geschichte, beide Hälften davon:
```python title="server.py" hl_lines="54-58"
--8<-- "docs_src/extensions/tutorial004.py"
```
* `Client(..., extensions=[advertise(EXTENSION_ID)])` deklariert die Extension. Die Deklarationen werden zu `ClientCapabilities.extensions`: Auf einer 2026-07-28-Verbindung reist die Map im `_meta`-Umschlag jedes einzelnen Requests, der Server sieht sie also bei **jedem** Request; auf einer Legacy-Verbindung reist sie mit dem `initialize`-Handshake. Dem Server-Code ist das egal: `require_client_extension(ctx, ...)` und `ctx.session.check_client_capability(...)` lesen auf beiden Pfaden die richtige Quelle.
* Vendor-Methoden steigen eine Schicht tiefer zu `client.session.send_request(...)` hinab; `Client` bekommt nur für Verben der Spezifikation eigene Methoden. `send_request` akzeptiert jede `Request`-Unterklasse, der Vendor-Request geht also unverändert durch.
### `tools/call` abfangen {#intercepting-toolscall}
Der eine eingreifende Hook. Überschreibe `intercept_tool_call`, um einen Tool-Aufruf zu beobachten, kurzzuschließen oder zu verhindern:
```python title="server.py" hl_lines="17-24"
--8<-- "docs_src/extensions/tutorial005.py"
```
* `params` sind die validierten `CallToolRequestParams`: Du bekommst `params.name` und `params.arguments`, ohne rohes JSON anzufassen. Sie entscheiden auch, welcher Tool-Aufruf läuft: Reichst du über `call_next` einen umgeschriebenen Kontext weiter, ändert das, was der Handler auf `ctx` sieht, nicht den Tool-Aufruf selbst. Das Umschreiben von Requests auf Leitungsebene gehört in die [Middleware](middleware.md).
* `call_next(ctx)` führt den Rest der Kette aus und gibt das Ergebnis des Handlers zurück. Gib es unverändert zurück (beobachten), gib etwas anderes zurück (ersetzen) oder löse einen `MCPError` aus (ablehnen). Was immer du zurückgibst, wird wie jedes Handler-Ergebnis serialisiert, einschließlich des `serverInfo`-Identitätsstempels der 2026er-Generation, ein kurzschließender Interceptor erzeugt also nie eine anonyme oder vom Schema abweichende Response.
* Bei mehreren Extensions schachteln sich die Interceptors in Registrierungsreihenfolge: Die erste Extension in `extensions=[...]` liegt ganz außen.
* Die Standardimplementierung reicht einfach durch, und ein Server, dessen Extensions diesen Hook nie überschreiben, behält den nackten `tools/call`-Handler unangetastet. Du zahlst nicht für das, was du nicht nutzt.
Der Hook umhüllt `tools/call` und sonst nichts. Für alles, was jede Nachricht betrifft, nimm [Middleware](middleware.md). Dafür ist sie da.
## Eine Client-Extension verwenden {#using-a-client-extension}
Eine **Client-Extension** ist derselbe Vertrag von der konsumierenden Seite: ein Bündel clientseitigen Verhaltens hinter einem einzigen Identifier. Übergib Instanzen an `Client(extensions=[...])` und rufe Tools ganz normal auf:
```python title="client.py" hl_lines="66-68"
--8<-- "docs_src/extensions/tutorial006.py"
```
`call_tool("buy", ...)` gibt ein gewöhnliches `CallToolResult` zurück, wie jeder andere Aufruf. Was die Extension geändert hat: Der Server darf `buy` jetzt mit einer `receipt`-**Ergebnisform** statt mit einem endgültigen Ergebnis beantworten, und `Receipts` bringt sie zu Ende (hier, indem sie den Beleg mit einem Folgeaufruf einlöst), bevor `call_tool` zurückkehrt. An der Aufrufstelle bewegt sich nichts.
Lass die Extension weg, und nichts davon existiert: Die Schranke des Servers weist einen Client ab, der sie nicht deklariert hat (Fehler -32021), und eine beanspruchte Form von einem Server, der die Schranke überspringt, fällt durch die Validierung, genau wie die Spezifikation es für einen unbekannten `resultType` verlangt. Standardmäßig aus, an beiden Enden der Leitung.
Um einen Identifier **ohne** clientseitiges Verhalten anzukündigen (der Server prüft die Capability, der Client tut nichts, wie beim Search-Client oben), nimm `advertise()`:
```python
from mcp.client import advertise
client = Client(mcp, extensions=[advertise("com.example/search")])
```
## Eine Client-Extension schreiben {#writing-a-client-extension}
Leite von `ClientExtension` ab und überschreibe nur, was du brauchst. Drei Arten von Beiträgen, jede mit einer Standardimplementierung: `settings()`, `claims()` und `notifications()`.
```python title="client.py" hl_lines="17-18 43-44 46-47"
--8<-- "docs_src/extensions/tutorial006.py"
```
* Der Identifier folgt derselben Grammatik wie auf dem Server und wird validiert, wenn die Klasse definiert wird.
* `claims()` gibt `ResultClaim`s zurück: ein Tag auf der Leitung, das Model, das es parst, und der Resolver, der es zu Ende bringt. Das Model muss das Tag mit `result_type: Literal["receipt"]` festlegen und darf nicht von den Kern-Ergebnistypen des Verbs ableiten; beides wird erzwungen, wenn der Claim konstruiert wird. Vendor-Felder wie `receipt_token` gehen unverändert über die Leitung: Eine ersetzte Form erreicht den Client wortwörtlich.
* Der Resolver erhält das geparste Model und einen `ClaimContext`; `ctx.session` ist derselbe öffentliche Griff wie `client.session`, Folgeaufrufe sind also gewöhnliche Session-Aufrufe. Er gibt das normale `CallToolResult` des Verbs zurück.
* `settings()` ist der Wert, der unter `ClientCapabilities.extensions[identifier]` angekündigt wird, einmal bei der Konstruktion von `Client` gelesen.
`notifications()` deklariert Vendor-Benachrichtigungen des Servers, die beobachtet werden sollen:
```python
def notifications(self) -> Sequence[NotificationBinding[Any]]:
return [NotificationBinding(method="notifications/receipts", params_type=ReceiptEvent, handler=self.on_receipt)]
```
Der Handler erhält validierte Parameter, eine Benachrichtigung nach der anderen, in Dispatch-Reihenfolge. Er beobachtet; ein Veto einlegen oder antworten kann er nicht.
Zwei stille Regeln. Claims sind nur auf 2026-07-28-Verbindungen aktiv, und die Capability-Ankündigung folgt ihnen: Auf einer Legacy-Verbindung lösen sich die Claims auf, und der Identifier fällt mit ihnen aus der Ankündigung heraus, der Client kündigt also nie eine Extension an, deren Formen er zurückweisen würde. Und wenn du die beanspruchte Form selbst statt des Resolvers haben willst, rufe `client.session.call_tool(..., allow_claimed=True)` auf; ohne dieses Flag löst eine beanspruchte Form, die bei einem Aufrufer auf Session-Ebene ankommt, `UnexpectedClaimedResult` aus.
### Extension-Verben {#extension-verbs}
Die eigenen Request-Methoden einer Extension brauchen keine clientseitige Registrierung. Ein Vendor-Request-Typ leitet von `mcp.types.Request` ab und geht durch `client.session.send_request`, wie in [Eigene Methoden bedienen](#serving-your-own-methods). Eine Ergänzung: Wenn ein Params-Schlüssel im `Mcp-Name`-Header mitreisen muss (Extension-Spezifikationen wie Tasks verlangen das für ihre Verben), deklariert der Request-Typ `name_param`:
```python title="client.py" hl_lines="22-25 46-47"
--8<-- "docs_src/extensions/tutorial007.py"
```
Die Session spiegelt `params["jobId"]` auf jedem Sendepfad in `Mcp-Name`, und ein fehlender Wert scheitert laut, statt einen erforderlichen Header stillschweigend wegzulassen.
## Was eine Extension nicht kann {#what-an-extension-cannot-do}
Die Fläche für Beiträge ist absichtlich **geschlossen**. Auf dem Server: Settings, Tools, Ressourcen, Methoden, ein `tools/call`-Interceptor. Auf dem Client: Settings, Result-Claims, Notification-Bindings. Eine Extension kann nicht:
* **In den Host hineingreifen.** Sie deklariert Daten; sie hält keine Referenz auf Server oder Client.
* **Kernverhalten ersetzen.** Methoden der Spezifikation und Kern-Ergebnistags werden bei der Konstruktion abgewiesen (`initialize` reserviert der Runner von vornherein für sich); ein Notification-Binding, das vom Kernvokabular überdeckt wird, verstummt stattdessen mit einer Warnung.
* **Sich nachträglich registrieren.** Sobald `MCPServer(...)` oder `Client(...)` zurückgekehrt ist, ist die Menge der Extensions, wie sie ist.
Wenn du gegen diese Wände ankämpfst, schreibst du keine Extension. Du schreibst einen Fork. Die Wände sind das Feature: Wer `extensions=[Apps(), Stamps()]` liest, weiß *alles*, was diese beiden angefasst haben können.

View file

@ -0,0 +1,34 @@
---
translation:
sections: [ca6988b7503cd2d3]
tool: 1
---
# Für Fortgeschrittene {#advanced}
Alles, was ein gewöhnlicher Server oder Client braucht, hat in den Abschnitten oben sein thematisches Zuhause.
Dieser Abschnitt versammelt die Notausgänge, zu denen du greifst, wenn die Komfortschicht
von `MCPServer` im Weg ist:
* **[Der Low-Level-Server](low-level-server.md)**: die Klasse, auf der `MCPServer` aufbaut.
Handgeschriebene Schemas, `on_*`-Handler, keine Prüfungen, die dir abgenommen werden, und
eigene JSON-RPC-Methoden.
* **[Paginierung](pagination.md)** und **[Middleware](middleware.md)**: zwei Dinge, die
*nur* auf dem Low-Level-`Server` gehen.
* **[Erweiterungen](extensions.md)** und **[MCP Apps](apps.md)**: die
Erweiterungsfläche des Protokolls. Kombiniere Erweiterungspakete zu einem Server oder schreibe deine eigenen.
Einiges, was du mit gutem Grund hier suchen könntest, steht stattdessen dort, wo du es
tatsächlich einsetzt:
* **Autorisierung** steht unter **[Den Server betreiben](../run/index.md)**, weil du
einen Server dort schützt, wo du ihn bereitstellst.
* **OAuth**, **Identity Assertion**, die Verbindung zu **mehreren Servern** und der
Response-**Cache** stehen alle unter **[Clients](../client/index.md)**.
* **Multi-Roundtrip-Requests** (multi-round-trip requests) und **Abonnements** stehen unter
**[Im Handler](../handlers/index.md)**, weil beides etwas ist, das ein
Handler *tut*.
* **URI-Templates** steht unter **[Server](../servers/index.md)**, neben den Ressourcen.
* **[Protokollversionen](../protocol-versions.md)** und
**[Veraltete Features](../deprecated.md)** haben jeweils eine eigene Seite auf oberster Ebene.
Wenn du nicht sicher bist, ob du diesen Abschnitt brauchst, brauchst du ihn nicht.

View file

@ -0,0 +1,207 @@
---
translation:
sections: [2c79b6338e09b7ac, 7edc43b3fae11314, 1086e77ce561cd7f, a3f71823df5efc31, 9fc7109f72201cae, 7bf25983df655b66, 6330e1f4c6029683, 2f1749c8c133fa1c, b3530fcf4d11fd56, ebc33704fbd74262, cd0e9c933350390e]
tool: 1
---
# Der Low-Level-Server {#the-low-level-server}
`@mcp.tool()` ist eine Schicht. Darunter liegt eine zweite Server-Klasse, `Server`, die rohes MCP spricht: Du gibst ihr die Protokollobjekte, und sie legt sie unverändert auf die Leitung.
`MCPServer` ist darauf aufgebaut. Du steigst hinab, wenn die Komfortschicht im Weg ist:
* Du musst ein **exaktes** Schema ausgeben (aus einer Datei geladen, aus einer Datenbank generiert), nicht eines, das aus einer Python-Signatur abgeleitet ist.
* Du brauchst die volle Kontrolle über das Ergebnis: `_meta`, `is_error`, jeden Schlüssel von `structured_content`.
* Du musst eine Methode behandeln, die MCP nicht definiert.
Für alles andere bleib bei `MCPServer`.
## Dasselbe Tool, von Hand {#the-same-tool-by-hand}
Das ist das Tool `search_books`, das **[Tools](../servers/tools.md)** in neun Zeilen `@mcp.tool()` schreibt, ohne den Zucker:
```python title="server.py" hl_lines="22 26 32"
--8<-- "docs_src/lowlevel/tutorial001.py"
```
Drei Dinge haben sich geändert, und sie sind die ganze Low-Level-API:
* **Handler sind Konstruktorparameter.** `on_list_tools=` und `on_call_tool=` wandern in `Server(...)`. Hier unten gibt es keine Dekoratoren, und jeder Handler hat dieselbe Form: `async (ctx, params) -> result`.
* **Du schreibst das Input-Schema.** `Tool.input_schema` ist ein schlichtes JSON-Schema-`dict`. Niemand leitet es aus Type Hints ab, denn es gibt keine Type Hints, aus denen man es ableiten könnte.
* **Du baust das Ergebnis.** `CallToolResult(content=[TextContent(...)])`, von Hand. Nichts wird verpackt, konvertiert oder aus einer Rückgabeannotation abgeleitet.
`params` ist der geparste Request: `CallToolRequestParams` gibt dir `.name` und `.arguments`. `ctx` ist ein `ServerRequestContext`: `ctx.session` zum Zurücksprechen an den Client, `ctx.lifespan_context`, `ctx.request_id` und `ctx.meta`, das eingehende `_meta` des Requests.
!!! info
Wenn du FastAPI kennst, kennst du diese Beziehung bereits. `MCPServer` ist die Schicht aus Dekoratoren und Type Hints; `Server` ist das Starlette darunter. Sie sind keine Rivalen: `MCPServer` erzeugt einen `Server` und registriert darauf genau solche Handler wie diese.
### Ausprobieren {#try-it}
Hierfür gibt es keinen Inspector: `mcp dev` und `mcp run` akzeptieren nur einen `MCPServer`. Dem In-Memory-`Client` ist das egal; er nimmt einen Low-Level-`Server` genauso wie einen `MCPServer`:
```python title="main.py"
import asyncio
from mcp import Client
from server import server
async def main() -> None:
async with Client(server) as client:
result = await client.call_tool("search_books", {"query": "dune", "limit": 5})
print(result.content)
asyncio.run(main())
```
```text
[TextContent(type='text', text="Found 3 books matching 'dune' (showing up to 5).", annotations=None, meta=None)]
```
Derselbe Text, den die `@mcp.tool()`-Version erzeugt hat. Zwei ehrliche Unterschiede:
* `result.structured_content` ist `None`. Der High-Level-Server verpackt ein `-> str` für dich in `{"result": ...}`; hier baut niemand, was du nicht gebaut hast.
* `list_tools` gibt das Schema zurück, das **du** getippt hast, Zeichen für Zeichen. Die High-Level-Version hatte `"title": "Query"` auf jeder Property und ein `"title": "search_booksArguments"` an der Wurzel: Pydantic-Artefakte. Hier unten gilt: Was auf der Leitung ist, hast du dort hingelegt.
## Nichts wird für dich geprüft {#nothing-is-checked-for-you}
`MCPServer` weist ein fehlerhaftes Argument ab, bevor deine Funktion überhaupt läuft, indem er den Aufruf gegen das generierte Schema validiert (**[Tools](../servers/tools.md)**).
`Server` tut das nicht. Dein `input_schema` wird dem Client *angekündigt*; es wird nie auf `params.arguments` *angewendet*.
!!! check
Ruf `search_books` ohne `limit` auf, und dein `args["limit"]` löst einen `KeyError` aus. Der Client sieht:
```text
MCPError: Internal server error
```
Ein JSON-RPC-Fehler, Code `-32603`, mit einer bewusst generischen Meldung: Das SDK gibt deinen Traceback nicht an einen entfernten Aufrufer preis. Das Modell erfährt nie, was es falsch gemacht hat, kann es also nicht erneut versuchen. (In einem Test bringt `raise_exceptions=True` stattdessen die echte Exception zum Vorschein; siehe **[Testen](../get-started/testing.md)**.)
Das lässt sich verallgemeinern. Eine Exception, die ein Low-Level-Handler auslöst, ist **immer** ein Protokollfehler, nie ein Tool-Ergebnis mit `is_error=True`. Wenn das Modell den Fehlschlag lesen und sich erholen soll, validiere `params.arguments` selbst und gib `CallToolResult(content=[TextContent(...)], is_error=True)` zurück. Die beiden Arten von Fehlschlägen sind das Thema von **[Fehler behandeln](../servers/handling-errors.md)**.
## Zwei Tools, ein Handler {#two-tools-one-handler}
`on_call_tool` ist der einzige Einstiegspunkt für jedes Tool auf dem Server. Du verzweigst über `params.name`:
```python title="server.py" hl_lines="38-43"
--8<-- "docs_src/lowlevel/tutorial002.py"
```
* `list_tools` kündigt beide an. `call_tool` verteilt nach dem Namen.
* Der `else`-Zweig ist wichtig: `Server` leitet ein `tools/call` für einen Namen, den du nie gelistet hast, bereitwillig direkt in deinen Handler weiter. Löst du dort eine Exception aus, wird aus dem Aufruf dasselbe `-32603` wie oben.
## Strukturierte Ausgabe, von Hand {#structured-output-by-hand}
Deklariere `output_schema` auf dem `Tool` und setze `structured_content` auf das Ergebnis. Beides liegt bei dir:
```python title="server.py" hl_lines="19-23 36"
--8<-- "docs_src/lowlevel/tutorial003.py"
```
Ruf es auf, und das Ergebnis trägt beide Darstellungen:
```json
{
"content": [{"type": "text", "text": "Found 3 books matching 'dune'."}],
"structuredContent": {"matches": 3, "query": "dune"},
"isError": false,
"resultType": "complete",
"_meta": {"io.modelcontextprotocol/serverInfo": {"name": "Bookshop", "version": "2.0.0"}}
}
```
Der `_meta`-Block ist der Identitätsstempel des Servers: Das SDK fügt ihn jedem Ergebnis der 2026er-Generation hinzu, mit der `version` aus dem Konstruktor (ein Server, der keine setzt, meldet einen leeren String). Ein Server, der sich nicht zu erkennen geben darf, kann den Schlüssel mit einer Middleware entfernen, der die Ergebnisse gehören, die sie zurückgibt.
Der Server vergleicht die beiden Felder nie. Der `Client` dieses SDK schon: Gibst du `structured_content` zurück, das das von dir deklarierte `output_schema` nicht erfüllt, löst `call_tool` einen `RuntimeError` aus, der mit `Invalid structured content returned by tool search_books` beginnt und dann den `jsonschema`-Fehler zitiert. Ein Schema zu versprechen ist billig; es einzuhalten liegt bei dir. Die ganze Stufenleiter der Rückgabetypen und Schemas steht in **[Strukturierte Ausgabe](../servers/structured-output.md)**.
## `_meta`: für die Anwendung, nicht für das Modell {#\_meta-for-the-application-not-the-model}
`content` ist der Teil der Antwort, den das Modell liest. `structured_content` ist dieselbe Antwort als typisierte Daten. `_meta` ist der dritte Kanal: Daten, die mit dem Ergebnis für die **Client-Anwendung** mitreisen, ohne überhaupt Teil der Antwort zu sein.
Nutze es für Datensatz-IDs, Trace-IDs, alles, was deine UI braucht und dein Prompt nicht:
```python title="server.py" hl_lines="37"
--8<-- "docs_src/lowlevel/tutorial004.py"
```
* Du erzeugst es als `_meta=`, dem Namen auf der Leitung. Der Client liest es als `result.meta` zurück.
* Versieh deine Schlüssel mit einem Namensraum (`bookshop/record_ids`). Die Schlüssel `io.modelcontextprotocol/*` sind vom Protokoll reserviert.
!!! warning
`_meta` ist eine Konvention zwischen dir und der Client-Anwendung, keine Garantie darüber, was
das Modell erreicht. Der Host entscheidet, was er darstellt. Lege niemals ein Geheimnis in irgendeinen Teil eines Tool-Ergebnisses.
## Capabilities folgen deinen Handlern {#capabilities-follow-your-handlers}
Ein `Server` kündigt genau die Methodenfamilien an, für die du ihm Handler gegeben hast. Der `Bookshop` oben übergibt `on_list_tools` und `on_call_tool` und sonst nichts, also sieht ein Client, der sich mit ihm verbindet:
```json
{"tools": {"listChanged": false}}
```
Kein `resources`, kein `prompts`: Es gibt nichts, was dahinter stünde. Übergib `on_list_prompts`, und `prompts` erscheint; übergib `on_completion`, und `completions` erscheint.
`MCPServer` kündigt Tools, Ressourcen und Prompts immer an, ob du welche registriert hast oder nicht, weil seine Manager immer existieren. Hier unten *ist* die Deklaration der Konstruktoraufruf.
## Der Lifespan-Generic {#the-lifespan-generic}
`Server` ist generisch im Typ, den sein Lifespan liefert. Annotiere ihn einmal, und das Objekt ist überall typisiert, wo es auftaucht:
```python title="server.py" hl_lines="24-26 44-45 50"
--8<-- "docs_src/lowlevel/tutorial005.py"
```
* Der Lifespan ist ein `Callable[[Server[Catalog]], AbstractAsyncContextManager[Catalog]]`; `@asynccontextmanager` auf einem `async`-Generator gibt dir genau das.
* Was immer er per `yield` liefert, wird zu `ctx.lifespan_context`, und weil die Handler mit `ServerRequestContext[Catalog]` annotiert sind, funktionieren Autovervollständigung und Typprüfung für `.search(...)`.
* Er wird einmal betreten, wenn der Server startet, und einmal verlassen, wenn er stoppt. Start, Abbau und die Variante derselben Idee in `MCPServer` stehen in **[Lifespan](../handlers/lifespan.md)**.
Ohne ein `lifespan=` ist `ctx.lifespan_context` ein leeres `dict`.
## Eine eigene Methode {#a-method-of-your-own}
Der Konstruktor deckt die Methoden ab, die MCP definiert. `add_request_handler` deckt alles andere ab:
```python title="server.py" hl_lines="35-36 39-40 43-44 48"
--8<-- "docs_src/lowlevel/tutorial006.py"
```
* Das erste Argument ist der Methoden-String. Benachrichtigungen haben ein Gegenstück, `add_notification_handler`.
* `params_type` ist das Modell, gegen das die eingehenden `params` validiert werden, **bevor** dein Handler läuft – eigene Methoden bekommen also *doch* die Validierung, die Tools nicht bekommen. Leite von `RequestParams` ab, damit das Feld `_meta` so geparst wird wie bei jeder anderen Methode.
* Der Handler gibt ein `BaseModel`, ein `dict` oder `None` zurück. Das SDK serialisiert es in das JSON-RPC-Ergebnis.
Ein ehrlicher Vorbehalt: Der High-Level-`Client` hat nur Verben für die Methoden, die MCP definiert, es gibt also kein `client.reindex()`. Eine Vendor-Methode ist für eine Gegenstelle gedacht, die bereits weiß, dass es sie gibt: ein Client, den du ebenfalls auslieferst, oder ein anderer deiner Dienste, der JSON-RPC spricht.
Eine Methode, die du nicht beanspruchen kannst:
```text
ValueError: 'initialize' is handled by the server runner and cannot be overridden;
use Server.middleware to observe or wrap initialization
```
Der Handshake gehört dem Runner. `server/discover`, `ping` und jeden anderen Built-in darfst du ersetzen.
!!! tip
`Server.middleware`, in dieser Fehlermeldung erwähnt, umhüllt **jede** eingehende Nachricht, `initialize` eingeschlossen. Wenn du Verkehr beobachten oder umschreiben willst, statt eine neue Methode zu beantworten, fang bei **[Middleware](middleware.md)** an.
## Die übrigen Handler {#the-other-handlers}
Jeder davon ist eine Idee, für die du jetzt das Vokabular hast; jeder hat seine eigene Seite.
* `on_call_tool`, `on_get_prompt` und `on_read_resource` dürfen statt ihres normalen Ergebnisses ein `InputRequiredResult` zurückgeben, um den Aufruf anzuhalten und den Client um Eingaben zu bitten; siehe **[Multi-Roundtrip-Requests](../handlers/multi-round-trip.md)** (multi-round-trip requests). Getreu dieser Ebene wird nichts für dich installiert: Wo `MCPServer` `requestState` standardmäßig versiegelt, geht hier der `request_state`, den du setzt, genau so über die Leitung, wie du ihn geschrieben hast, bis du dich mit `server.middleware.append(RequestStateBoundary(RequestStateSecurity(keys=[...]), default_audience=server.name))` dafür entscheidest: eine Zeile (beide Namen lassen sich aus `mcp.server.request_state` importieren) für genau die Versiegelung und Verifizierung, die `MCPServer` vornimmt (**[`requestState` schützen](../handlers/multi-round-trip.md#protecting-requeststate)**).
* `on_list_resources`, `on_read_resource`, `on_list_prompts`, `on_get_prompt`, `on_completion` haben dieselbe Form `(ctx, params) -> result` für die anderen Primitive.
* `on_subscriptions_listen` bedient den Stream `subscriptions/listen` aus 2026-07-28. Übergib einen `ListenHandler`, der auf einem `SubscriptionBus` aufgebaut ist, und veröffentliche Ereignisse aus deinen anderen Handlern auf dem Bus; die vollständige Zusammensetzung steht in **[Abonnements](../handlers/subscriptions.md)**.
* `server.streamable_http_app()` gibt dieselbe Starlette-App zurück wie die von `MCPServer`; stelle sie bereit, wie **[Den Server betreiben](../run/index.md)** jede andere ASGI-App bereitstellt. Hier unten gibt es kein `server.run(transport=...)`: `server.run(read_stream, write_stream, server.create_initialization_options())` treibt eine Verbindung über ein Paar Streams, und diese eine Zeile ist alles.
## Zusammenfassung {#recap}
* Der Low-Level-`Server` nimmt seine Handler als `on_*`-**Konstruktorparameter**; jeder Handler ist `async (ctx, params) -> result`.
* Du schreibst das `input_schema`-dict und du baust das `CallToolResult`. Nichts wird für dich abgeleitet, verpackt oder validiert.
* Eine Exception in einem Handler ist ein `-32603`-Protokollfehler. Ein Tool-Fehler, den das Modell lesen kann, ist ein `CallToolResult` mit `is_error=True`, das **du** zurückgibst.
* `_meta` auf dem Ergebnis richtet sich an die Client-Anwendung, nicht an das Modell.
* `Server[T]` ist generisch in dem, was sein Lifespan liefert; `ctx.lifespan_context` ist ein typisiertes `T`.
* `add_request_handler(method, params_type, handler)` bedient jede Methode. `initialize` ist reserviert.
* Die Capabilities, die ein `Server` ankündigt, leiten sich davon ab, welche Handler du registriert hast.
`Client(server)` hat beide Server identisch behandelt, weil sie dasselbe Protokoll *sind* – und genau darum geht es. Die nächste Schicht darunter ist gar keine Klasse: Es ist **[Middleware](middleware.md)**.

View file

@ -0,0 +1,127 @@
---
translation:
sections: [6048b4f308edbb8c, 068bda0f21ee9c1b, c3e565b61acd75c5, c62422b159c6ed09, 47204fab253cc45c]
tool: 1
---
# Middleware {#middleware}
Eine **Middleware** ist eine einzelne async-Funktion, die jede Nachricht umschließt, die dein Server empfängt.
Du schreibst sie als `async (ctx, call_next)` und hängst sie an `server.middleware` an. Das ist die ganze API.
!!! warning
Die Middleware-Liste ist im Quellcode als **provisorisch** markiert: Signatur und Semantik können
sich in einem 2.x-Minor-Release ändern. Nutze sie zum *Beobachten* (Timing, Logging, Tracing) und
zum *Ablehnen* von Nachrichten; mache sie nicht zum Fundament, auf dem dein Server steht.
`MCPServer` nimmt die Liste bei der Konstruktion entgegen (`MCPServer(name, middleware=[...])`) und stellt
sie als `mcp.middleware` bereit; der Low-Level-`Server` stellt dieselbe Liste als `server.middleware`
bereit. Das Beispiel unten verwendet den Low-Level-`Server`; wenn `Server(name, on_call_tool=...)` neu
für dich ist, lies zuerst **[Der Low-Level-Server](low-level-server.md)**.
## Eine Timing-Middleware {#a-timing-middleware}
Ein Server, ein Tool, eine Middleware, die loggt, wie lange jede Nachricht gedauert hat:
```python title="server.py" hl_lines="39-45 49"
--8<-- "docs_src/middleware/tutorial001.py"
```
* `ctx` ist derselbe `ServerRequestContext`, den deine Handler erhalten. `ctx.method` ist der rohe
Methoden-String; `ctx.params` sind die rohen Params, **vor** jeder Validierung.
* `call_next(ctx)` führt den Rest der Kette aus: Validierung, die Handler-Suche, deinen Handler.
Gib zurück, was es zurückgegeben hat, und die Response bleibt unverändert.
* Das `try`/`finally` ist Absicht: Ein Handler, der eine Exception auslöst, wird trotzdem gemessen,
denn der Fehlschlag erreicht deine Middleware als Exception aus `call_next`.
* `server.middleware.append(...)` registriert sie. Die Liste läuft von außen nach innen, also ist
`middleware[0]` diejenige, die am nächsten an der Leitung sitzt.
### Ausprobieren {#try-it}
Verbinde einen Client, liste die Tools auf, rufe eines auf. Dein Log hat **drei** Zeilen:
```text
server/discover took 18.3 ms
tools/list took 0.1 ms
tools/call took 0.1 ms
```
Du hast zwei Aufrufe gemacht und drei Zeilen bekommen. Die erste ist `server/discover`: der Request,
den der Client zum Aufbau der Verbindung geschickt hat, bevor du irgendetwas angefordert hast.
Genau darum geht es. Middleware umschließt **jede** eingehende Nachricht:
* Den Verbindungsaufbau: `server/discover`, oder `initialize` und `notifications/initialized`
in einer Legacy-Session.
* Jeden Request und jede Benachrichtigung. Bei einer Benachrichtigung gilt `ctx.request_id is None`,
`call_next(ctx)` gibt `None` zurück, und was immer du zurückgibst, wird verworfen.
* Sogar eine Methode, für die der Server keinen Handler hat: `call_next` wirft den
`MCPError(-32601, "Method not found")` *durch* deine Middleware hindurch auf dem Weg zum Client.
## Was du in einer Middleware tun kannst {#what-you-can-do-inside-one}
In aufsteigender Reihenfolge danach, wie sehr du zögern solltest:
* **Beobachten.** Miss es, zähle es, logge es. Das Beispiel oben.
* **Ablehnen.** Wirf einen `MCPError` *statt* `call_next(ctx)` aufzurufen, und diese eine Nachricht
wird mit einem JSON-RPC-Fehler beantwortet. Die Verbindung bleibt bestehen; die nächste Nachricht
geht durch. So beschränkt ein Server `subscriptions/listen` pro Aufrufer:
**[Entscheiden, wer zusehen darf](../handlers/subscriptions.md#deciding-who-may-watch)** auf der
Seite Abonnements führt es Schritt für Schritt vor.
* **Umschreiben.** `ctx` ist eine Dataclass: `await call_next(dataclasses.replace(ctx, params=...))`
reicht dem Rest der Kette andere Params weiter, als der Client geschickt hat. Tu das nie mit
`initialize`: Das Ergebnis, das der Client zurückbekommt, wird aus deinen umgeschriebenen Params
gebaut, aber der Server legt seinen Verbindungszustand anhand der ursprünglichen Params von der
Leitung fest. Beide Seiten können den Handshake beenden und sich dabei uneinig sein, was sie
ausgehandelt haben.
* **Antworten.** Gib ein Ergebnis zurück, ohne `call_next(ctx)` aufzurufen, und es geht als deine
Response an den Client. `call_next` reicht dir die fertige Form für die Leitung, und die Pipeline
bessert nie nach, was du zurückgibst – der ganze Umschlag gehört also dir: Auf einer Verbindung der
2026er-Generation gehört dazu der `serverInfo`-Stempel in `_meta`, den das SDK an Handler-Ergebnisse
anfügt, an deine aber nicht.
!!! check
`initialize` gehört zu dem, was Middleware umschließt, und es ist der *einzige* Hook, den du
dafür bekommst. Versuchst du, es mit `add_request_handler` zu übernehmen, weigert sich das SDK:
```text
ValueError: 'initialize' is handled by the server runner and cannot be overridden;
use Server.middleware to observe or wrap initialization
```
!!! warning
`initialize` wird inline behandelt: Der Server liest keine weiteren eingehenden Nachrichten, bis
deine Middleware-Kette zurückkehrt. Auf einen Server-zu-Client-Request zu warten
(`ctx.session.send_request(...)`, eine Elicitation – Rückfrage bei der Person am Host), während
`initialize` behandelt wird, **blockiert die Verbindung** daher **dauerhaft** (Deadlock): Die
Response, auf die du wartest, kann nie gelesen werden. Benachrichtigungen nach dem
Fire-and-forget-Prinzip sind in Ordnung.
## Die eine Middleware, die standardmäßig aktiv ist {#the-one-middleware-that-ships-on-by-default}
Das SDK liefert genau eine Middleware mit, und sie steht bereits auf der Liste deines Servers: die,
die für jede Nachricht einen OpenTelemetry-Span ausgibt. Du hängst sie nicht an, und meistens denkst
du gar nicht an sie. Sie tut nichts, bis du einen Exporter installierst, und sie hat ihre eigene Seite:
**[OpenTelemetry](../run/opentelemetry.md)**.
!!! info
Wenn du schon ASGI-Middleware geschrieben hast, kennst du diese Form bereits. Aus Starlettes
`(scope, receive, send)` wurde `(ctx, call_next)`, und sie läuft *nach* dem Transport, auf der
dekodierten Nachricht statt auf dem rohen HTTP-Request. Beide lassen sich kombinieren:
Starlette-Middleware auf `streamable_http_app()` sieht HTTP; diese hier sieht MCP.
## Zusammenfassung {#recap}
* Eine Middleware ist `async (ctx, call_next) -> result`, übergeben als `MCPServer(middleware=[...])`
(oder an `mcp.middleware` angehängt) und beim Low-Level-`Server` an `server.middleware` angehängt.
* Sie umschließt **jede** eingehende Nachricht (`server/discover`, `initialize`, Requests,
Benachrichtigungen, unbekannte Methoden) und läuft von außen nach innen.
* An `ctx.request_id is None` unterscheidest du eine Benachrichtigung von einem Request.
* Wirf eine Exception, statt `call_next` aufzurufen, um eine einzelne Nachricht abzulehnen; die
Verbindung überlebt.
* Das OpenTelemetry-Tracing des SDK ist ebenfalls eine Middleware und steht schon auf der Liste. Siehe
**[OpenTelemetry](../run/opentelemetry.md)**.
* Die ganze Oberfläche ist provisorisch. Beobachte damit; baue nicht darauf.
Das ist alles, was einen Request umschließt. **[Autorisierung](../run/authorization.md)** entscheidet,
ob der Request überhaupt laufen darf.

View file

@ -0,0 +1,85 @@
---
translation:
sections: [a9aba7a026c7bd85, ed32bda7ba9ae33a, 7e64cc5646abb91f, 22a0129ee78b3c63, d875373c06d8d2f9]
tool: 1
---
# Paginierung {#pagination}
Die meisten Server brauchen das nie.
`MCPServer` beantwortet jeden `list_*`-Request mit allem, was er hat, auf einer Seite, `next_cursor=None`. Bei ein paar Dutzend Tools, Ressourcen oder Prompts ist das die richtige Antwort, und es gibt nichts zu konfigurieren.
Paginierung ist für den Server gedacht, dessen Ressourcenliste in Wahrheit eine Datenbank ist: Tausende Zeilen, die er nicht in einer einzigen Response serialisieren will. Die Antwort des Protokolls darauf ist ein **Cursor**: Der Server gibt eine Seite plus ein opakes Token zurück, und der Client schickt dieses Token zurück, um die nächste Seite zu bekommen.
`@mcp.resource()` hat dafür keinen Einstiegspunkt. Um seitenweise auszuliefern, schreibst du den List-Handler selbst, auf dem **[Low-Level-Server](low-level-server.md)**.
## Ein Server, der paginiert {#a-server-that-pages}
```python title="server.py" hl_lines="12 15-16"
--8<-- "docs_src/pagination/tutorial001.py"
```
* Auf einem Low-Level-`Server` sind Handler Konstruktorargumente, keine Dekoratoren. `on_list_resources` beantwortet jeden `resources/list`-Request; mehr Verkabelung gibt es nicht.
* Jeder paginierte Handler ist als `params: PaginatedRequestParams | None` typisiert, und das Beispiel akzeptiert beides. Über eine Verbindung übergibt dir das SDK jedoch nie `None` (ein Request ohne `params`-Member erreicht den Handler als Modell mit seinen Standardwerten). Das Signal, auf das es ankommt, ist daher `params.cursor is None`: **von vorne beginnen**.
* Du entscheidest, was ein Cursor *ist*. Hier ist es ein Offset, als String dargestellt. Ein Zeitstempel, ein Primärschlüssel, ein Base64-Blob: alles, was du beim Herausgeben erzeugen und beim Zurückkommen wiedererkennen kannst.
* Mit `next_cursor=None` sagst du „das war die letzte Seite“. Es gibt keine Anzahl, keine Gesamtsumme, kein `has_more`. `None` ist das ganze Signal.
!!! tip
Eine `PAGE_SIZE` von 10 macht das Beispiel lesbar. Wähle deine pro Endpunkt: Eine Liste
einzeiliger Ressourcen verträgt eine Seite mit 500 Einträgen; eine Liste fetter Prompt-Templates nicht.
Der Client hat dabei nichts mitzureden, und das ist Absicht.
### Ausprobieren {#try-it}
`Client(server)` verbindet sich im Speicher mit einem Low-Level-`Server` genau so, wie er sich mit einem `MCPServer` verbindet.
Rufe `list_resources()` ohne Argumente auf. Du bekommst zehn Ressourcen, `book-1` bis `book-10`, und `next_cursor` ist der String `"10"`.
Gib ihn mit `list_resources(cursor="10")` zurück, und die erste Ressource ist `book-11`, der neue `next_cursor` ist `"20"`.
Die zehnte Seite kommt mit `next_cursor` auf `None` zurück. Fertig.
## Die Client-Schleife {#the-client-loop}
Jede `list_*`-Methode auf `Client` (`list_tools`, `list_resources`, `list_resource_templates`, `list_prompts`) nimmt ein Keyword-Argument `cursor=`. Eine paginierte Liste leerzulesen ist ein einziges `while True`:
```python title="client.py" hl_lines="26-32"
--8<-- "docs_src/pagination/tutorial002.py"
```
* `cursor` beginnt als `None`, der erste Request trägt also keinen Cursor.
* Erweitere die Liste, **bevor** du auf `next_cursor` schaust: Auch die letzte Seite enthält Ressourcen.
* `next_cursor is None` ist der Ausstieg. Alles andere geht unverändert direkt zurück in `cursor=`.
Führe sein `main()` aus, und es gibt `100 resources` aus: zehn Seiten zu je zehn, zusammengefügt von einer Schleife, die nie wusste, dass es zehn Seiten waren.
Das ist dieselbe Schleife, die **[Der Client](../client/index.md)** für jedes `list_*`-Verb zeigt, und sie kostet nichts gegenüber einem Server, der nicht paginiert: `next_cursor` ist schon in der ersten Response `None`, und die Schleife läuft genau einmal.
## Die drei Regeln {#the-three-rules}
**Cursor sind opak.** Ein Client darf einen Cursor nie parsen, bauen oder erraten. Die einzige zulässige Quelle eines Cursors ist der `next_cursor` der vorherigen Seite, wortwörtlich.
**Der Server bestimmt die Seitengröße.** Es gibt kein `limit=` im Protokoll. Wenn du eine andere Seitengröße brauchst, änderst du den Server.
**Ein Client, der Paginierung ignoriert, funktioniert trotzdem.** Er ruft `list_resources()` einmal auf, bekommt die ersten zehn und bemerkt den `next_cursor`, den er weggeworfen hat, nie. Nichts geht kaputt; er sieht nur weniger.
!!! check
Opak heißt opak. Erfinde einen Cursor (`list_resources(cursor="page-2")`), und das
Protokoll kann nichts für dich tun. Dieser Server versucht `int("page-2")`, der Handler löst eine Exception aus,
und beim Client kommt an:
```text
MCPError(-32603, 'Internal server error', None)
```
Ein Cursor, den du nicht vom Server bekommen hast, ist ein Bug, kein Feature-Wunsch.
## Zusammenfassung {#recap}
* `MCPServer` gibt alles auf einer Seite zurück. Paginierung ist optional, und du aktivierst sie auf dem Low-Level-`Server`.
* `on_list_resources` (und `on_list_tools`, `on_list_prompts`, `on_list_resource_templates`) erhält `PaginatedRequestParams | None`; `params.cursor` ist bei der ersten Seite `None`.
* Du gibst eine Seite plus `next_cursor` zurück: einen beliebigen String, den du später wiedererkennst, oder `None`, wenn nichts mehr übrig ist.
* Die Client-Schleife: `cursor=` übergeben, sammeln, wiederholen, bis `next_cursor is None`.
* Cursor sind opak, die Seitengröße gehört dem Server, und ein Client ohne Paginierung bekommt trotzdem Seite eins.
Der Rest der handgeschriebenen `Server`-API (`on_call_tool`, `input_schema`-Dicts, `_meta`) steht in **[Der Low-Level-Server](low-level-server.md)**.