docs: refresh translations, and translate pages in parallel (#3458)

This commit is contained in:
Max 2026-09-07 13:35:53 +01:00 • committed by GitHub
commit fd66270926
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 4821 additions and 2277 deletions

View file

@ -1,6 +1,6 @@
---
translation:
sections: [9e7b9a1710e5aeba, b74ca4c1d2ddddee, fa8714e61bf90c5a, 04db67a886b7271c, 857690fb8f876800]
sections: [9e7b9a1710e5aeba, 66a2e9acc9101d54, d3d25aa802f5145a, 04db67a886b7271c, 857690fb8f876800]
tool: 1
---
# Caching-Hinweise {#caching-hints}
@ -31,7 +31,7 @@ Ohne weitere Konfiguration sagt jedes Ergebnis `ttlMs: 0, cacheScope: "private"`
Auf dem Low-Level-`Server` bauen Handler ihre Ergebnisse von Hand, und `ttl_ms` / `cache_scope` sind einfach Felder der Ergebnismodelle. Ein Handler, der sie explizit setzt, gewinnt immer gegen die Map aus dem Konstruktor, Feld für Feld:
```python title="server.py" hl_lines="10 16"
```python title="server.py" hl_lines="11 17"
--8<-- "docs_src/caching/tutorial002.py"
```
@ -45,10 +45,24 @@ Ein Vorbehalt bei paginierten Listen: Das Protokoll verlangt **denselben `cacheS
In einer 2026-07-28-Session beachtet `Client` die Hinweise für dich: Er hat einen eingebauten Response-Cache, der standardmäßig aktiv ist. Ein Ergebnis, das mit einem `ttlMs` ankommt, wird gespeichert, und ein identischer Aufruf innerhalb dieser TTL wird ohne Roundtrip aus dem Cache bedient. Ein Ergebnis, das *keinen* Hinweis trägt, wird nicht gecacht: Ergebnisse ohne Hinweis bekommen `CacheConfig.default_ttl_ms`, dessen Standardwert `0` ist (sofort abgelaufen), sodass ein Server, der nichts deklariert, Aufruf für Aufruf genau denselben Verkehr sieht wie schon immer.
```python title="client.py" hl_lines="33 35 38"
Um das zu beobachten, stelle die `server.py` aus dem vorigen Abschnitt mit uvicorn bereit (ihre letzte Zeile baut die ASGI-App). Der Handler gibt jedes Mal eine Zeile aus, wenn er tatsächlich läuft:
```console
uvicorn server:app --port 8000
```
```python title="client.py" hl_lines="20 23 28"
--8<-- "docs_src/caching/tutorial003.py"
```
Führe `python client.py` in einem zweiten Terminal aus. Es gibt die Hinweise aus, die das erste Ergebnis trug: das `ttlMs` des Handlers neben dem `cacheScope` der Map:
```text
1000 public
```
Das Terminal des Servers erzählt den Rest: Zwischen den Request-Logs von uvicorn erscheint `tools/list served` dreimal.
Vier Aufrufe, drei Abrufe. Der zweite Aufruf fand einen frischen Eintrag und erreichte den Server nie; die (injizierte) Uhr über die TTL hinaus vorzustellen ließ den dritten wieder abrufen; der vierte gab `cache_mode="refresh"` an. Dieses Keyword-Argument gibt es auf den fünf cachenden Verben (`list_tools`, `list_prompts`, `list_resources`, `list_resource_templates`, `read_resource`):
* `"use"` (der Standardwert) liefert einen frischen Eintrag, wenn es einen gibt, und speichert andernfalls den Abruf.
@ -57,7 +71,7 @@ Vier Aufrufe, drei Abrufe. Der zweite Aufruf fand einen frischen Eintrag und err
Eine Regel steht über `"use"`: **Aufrufe mit `meta` erreichen immer den Server.** Ein Request mit gesetztem `meta` (ein Progress-Token, Tracing-Felder) erwartet einen Request auf der Leitung, deshalb wird er unter `cache_mode="use"` wie `"refresh"` behandelt: Das Lesen aus dem Cache entfällt, und das abgerufene Ergebnis ersetzt trotzdem den gecachten Eintrag. `"bypass"` und ein explizites `"refresh"` verhalten sich wie immer.
Um das Caching ganz abzuschalten, konstruiere mit `Client(server, cache=None)`: Jeder Aufruf ist wieder ein Roundtrip, und `cache_mode` wird zwar weiter akzeptiert, bewirkt aber nichts.
Um das Caching ganz abzuschalten, übergib `cache=None` bei der Konstruktion des `Client`: Jeder Aufruf ist wieder ein Roundtrip, und `cache_mode` wird zwar weiter akzeptiert, bewirkt aber nichts.
Auch der Scope wird automatisch beachtet: `"private"`-Einträge sind an die *Partition* des Caches gebunden (siehe unten), während `"public"`-Einträge sich für breiteres Teilen entscheiden können. Und **Benachrichtigungen schlagen die TTL** für genau die Einträge, die sie benennen: Eine `list_changed`-Benachrichtigung verdrängt die passende gecachte Liste, und `resources/updated` verdrängt den gecachten Lesevorgang, der unter exakt ihrem URI gespeichert ist – egal, wie frisch sie waren. Auf einer 2026-07-28-Verbindung kommen diese Benachrichtigungen auf einem `subscriptions/listen`-Stream an, den du mit `client.listen(...)` öffnest, und die Verdrängung ist abgeschlossen, bevor dein Watcher das Ereignis sieht; alles dazu steht in **[Abonnements](subscriptions.md)**.

View file

@ -1,6 +1,6 @@
---
translation:
sections: [adf3c545b5be46b6, 916cd3ab1c03f461, e9be7a8d0eb0a456, 565890a636288ecf, 6af7e49db9129ec3, 06b0238c174186af, 90c6043be435fcb0]
sections: [adf3c545b5be46b6, 916cd3ab1c03f461, 32ef568335dd95a7, 565890a636288ecf, 6af7e49db9129ec3, 06b0238c174186af, 0abc5ea5cb7ff6b3]
tool: 1
---
# Client-Callbacks {#client-callbacks}
@ -63,7 +63,7 @@ Ein `tools/call` von dir, ein `elicitation/create` zurück vom Server, beantwort
`mode="legacy"` im `Client(...)`-Aufruf leistet echte Arbeit. Standardmäßig handelt `Client(...)` den modernen
Protokollpfad aus, und dieser Pfad hat keinen Rückkanal (back-channel) für Requests vom Server an den Client: `ctx.elicit`
schlägt fehl, bevor dein Callback überhaupt läuft. Das entscheidet nicht der Transport, sondern das ausgehandelte
Protokoll – in-memory genauso wie über eine URL. Setze `mode="legacy"` fest, wann immer dein Client
Protokoll. Setze `mode="legacy"` fest, wann immer dein Client
einen solchen Request beantworten muss; jeder Test hinter dieser Seite tut das. Alles Weitere steht in **[Protokollversionen](../protocol-versions.md)**.
In einer 2026-07-28-Session ist der Callback nicht tot, er wird nur anders gespeist: Gibt ein Tool ein
@ -151,4 +151,4 @@ Zwei weitere. Keiner deklariert etwas.
* `sampling_callback` und `list_roots_callback` funktionieren genauso, bedienen aber veraltete Features; moderne Server verwenden stattdessen Multi-Roundtrip-Requests.
* `logging_callback` und `message_handler` empfangen Benachrichtigungen. Sie deklarieren nichts.
Das erste Argument von `Client(...)` ist ein Transport-Objekt. **[Client-Transporte](transports.md)** behandelt jede Art davon.
Das erste Argument von `Client(...)` wählt den Transport. **[Client-Transporte](transports.md)** behandelt jede Art davon.

View file

@ -1,6 +1,6 @@
---
translation:
sections: [ebef1e7a0df854f4, 8355cfaf1f76c9d5, 8e79141fc2985342, 46bdb07c7537e8a5, 80ce41579825a6fa, 5f0fa90494de8f65, 83d10514eaa62fa5, 9190555aa39a5d28, 84a4c9d8bf14dddb, 927d71cf40b58c30]
sections: [ebef1e7a0df854f4, 7e16449f66e7dfd6, eeb0682f7d2a1079, 5713f0196a34e6e7, 0e844597859e4248, 3a97d9195ddcc92e, 1da08c483e59c141, 84702cc6e0a1fd42, 8dee7a31c86ffc5c, 83a5bce168ef23d7]
tool: 1
---
# Der Client {#the-client}
@ -11,13 +11,23 @@ Er ist ein einziges Objekt mit einem einzigen Lebenszyklus: erzeugen, `async wit
## Der erste Client {#your-first-client}
```python title="client.py" hl_lines="14-18"
Ein Client braucht einen Server, mit dem er sprechen kann. Mit diesem Bookshop verbindet sich jedes Beispiel auf dieser Seite. Speichere ihn als `server.py` und lass ihn über HTTP laufen:
```python title="server.py"
--8<-- "docs_src/client/tutorial001.py"
```
Der Server oben ist nur da, damit du etwas hast, womit du dich verbinden kannst. Der Client sind die fünf hervorgehobenen Zeilen.
```console
uv run mcp run server.py --transport streamable-http
```
* `Client(mcp)` bekommt das **Server-Objekt selbst**. Das ist der In-Memory-Transport: kein Subprozess, kein Port, kein HTTP. So verbindet sich jedes Beispiel auf dieser Seite und jeder Test, den du schreibst.
Damit läuft er unter `http://localhost:8000/mcp`. Der Client ist ein eigenes Programm. Speichere ihn als `client.py` und führe in einem zweiten Terminal `python client.py` aus:
```python title="client.py" hl_lines="7-11"
--8<-- "docs_src/client/tutorial001_client.py"
```
* `Client("http://localhost:8000/mcp")` bekommt eine **URL** und verbindet sich deshalb über Streamable HTTP mit dem Server, den du gerade gestartet hast.
* `async with` ist der **Lebenszyklus**. Beim Betreten wird verbunden und ausgehandelt, beim Verlassen getrennt. Es gibt kein `connect()`/`close()`-Paar, und ein `Client` lässt sich nach dem Ende des Blocks nicht wiederverwenden.
* Innerhalb des Blocks liegen die Fakten zur Verbindung bereits als einfache Properties vor.
@ -25,10 +35,10 @@ Der Server oben ist nur da, damit du etwas hast, womit du dich verbinden kannst.
`Client` nimmt ein positionelles Argument und leitet den Transport aus dessen Typ ab:
* Eine Instanz von `MCPServer` (oder des Low-Level-`Server`): Verbindung **im selben Prozess**.
* Ein URL-String (`Client("http://localhost:8000/mcp")`): Streamable HTTP, der Weg für die Produktion.
* Ein `StdioServerParameters`: der Befehl, der als **Subprozess** gestartet wird und mit dem über dessen stdin und stdout gesprochen wird.
* Ein URL-String (`Client("http://localhost:8000/mcp")`): Streamable HTTP, der Transport, hinter dem du deinen Server bereitstellst.
* Ein `StdioServerParameters`: der Befehl, der als lokaler **Subprozess** gestartet wird und mit dem über dessen stdin und stdout gesprochen wird.
* Ein **Transport**: alles, was sich mit `async with ... as (read, write)` verwenden lässt, etwa `streamable_http_client(url, http_client=...)` um deinen eigenen HTTP-Client herum.
* Eine Instanz von `MCPServer` (oder des Low-Level-`Server`): Verbindung **im selben Prozess**, ohne Subprozess und ohne Port. Das ist die Variante für Tests, und **[Testen](../get-started/testing.md)** baut darauf auf.
Alles Übrige auf dieser Seite ist in allen vier Fällen identisch. Header, Subprozesse, Timeouts und das `Transport`-Protokoll haben ihre eigene Seite: **[Client-Transporte](transports.md)**.
@ -49,11 +59,11 @@ Eine Protokollversion hast du nie ausgewählt. Standardmäßig sondiert der `Cli
## Tools auflisten {#listing-tools}
```python title="client.py" hl_lines="15-20"
```python title="client.py" hl_lines="8-13"
--8<-- "docs_src/client/tutorial002.py"
```
`list_tools()` gibt ein `ListToolsResult` zurück; die Tools stehen in `.tools`. Jedes davon ist die vollständige Definition, die ein Host einem Modell übergeben würde:
`list_tools()` gibt ein `ListToolsResult` zurück; die Tools stehen in `.tools`. Jedes davon ist die vollständige Definition, die ein Host einem Modell übergeben würde. Hier ist das erste:
```python
tool.name # 'search_books'
@ -77,6 +87,8 @@ und `tool.input_schema` ist das JSON-Schema, das der Server aus den Type Hints d
Dieses Schema ist alles, was eine UI braucht, um ein Argumentformular zu rendern, und alles, was ein Modell braucht, um gültige Argumente zu erzeugen.
Das zweite Tool, `lookup_book`, wurde ohne `title=` registriert, sein `tool.title` ist also `None`.
!!! tip
`title` ist optional, also muss sich eine UI, die einem Menschen Tools anzeigt, entscheiden: den `title`, wenn es einen gibt,
sonst den `name`. `from mcp.shared.metadata_utils import get_display_name` macht genau das –
@ -86,7 +98,7 @@ Dieses Schema ist alles, was eine UI braucht, um ein Argumentformular zu rendern
`call_tool(name, arguments)` führt das Tool aus und gibt dir ein `CallToolResult` zurück.
```python title="client.py" hl_lines="27-34"
```python title="client.py" hl_lines="9-16"
--8<-- "docs_src/client/tutorial003.py"
```
@ -142,7 +154,7 @@ Ein Tool, das eine Exception auslöst, löst in deinem Client **keine** aus. Es
Die Ressourcen-Verben kommen paarweise: zwei Wege zum Auflisten, einer zum Lesen.
```python title="client.py" hl_lines="22-31"
```python title="client.py" hl_lines="9-18"
--8<-- "docs_src/client/tutorial004.py"
```
@ -156,7 +168,7 @@ Ein Client kann sich auch mitteilen lassen, wann sich eine Ressource ändert. Au
## Prompts {#prompts}
```python title="client.py" hl_lines="15-20"
```python title="client.py" hl_lines="8-13"
--8<-- "docs_src/client/tutorial005.py"
```
@ -181,7 +193,7 @@ Ein Host reicht diese Nachrichten direkt an das Modell weiter. Das ist das ganze
Ein Server mit einem Handler für Vervollständigungen kann Argumente von Prompts und Ressourcen-Templates automatisch vervollständigen, während die Person tippt.
```python title="client.py" hl_lines="27-31"
```python title="client.py" hl_lines="9-13"
--8<-- "docs_src/client/tutorial006.py"
```
@ -194,21 +206,21 @@ Die Antwort steht in `result.completion.values`. Tippe `"p"`, und der Server lie
Jede `list_*`-Methode nimmt ein Keyword-Argument `cursor=`, und jedes Ergebnis trägt einen `next_cursor`. Wenn `next_cursor` `None` ist, hast du alles.
```python title="client.py" hl_lines="22-30"
```python title="client.py" hl_lines="7-15"
--8<-- "docs_src/client/tutorial007.py"
```
Diese Schleife ist gegenüber jedem Server korrekt. `MCPServer` gibt alles auf einer Seite zurück, also ist `next_cursor` `None` und die Schleife läuft einmal – deshalb schreibt der meiste Code sie nie. Server, die wirklich paginieren, und die Regeln, denen Cursor gehorchen, stehen in **[Paginierung](../advanced/pagination.md)**.
`list_all_tools` ist gegenüber jedem Server korrekt. `MCPServer` gibt alles auf einer Seite zurück, also ist `next_cursor` `None` und die Schleife läuft einmal – deshalb schreibt der meiste Code sie nie. Server, die wirklich paginieren, und die Regeln, denen Cursor gehorchen, stehen in **[Paginierung](../advanced/pagination.md)**.
## In Tests {#in-tests}
`Client(mcp)` ohne Prozess und ohne Port ist bereits ein Test-Harness für deinen Server.
Jede `client.py` auf dieser Seite hat `server.py` über HTTP erreicht. In einem Test überspringst du das Netzwerk und übergibst `Client` das Server-Objekt selbst: `from server import mcp`, dann `Client(mcp)`. Kein Prozess, kein Port, und jede Methode von oben funktioniert genauso.
Dafür gibt es ein eigenes Konstruktor-Flag: `Client(mcp, raise_exceptions=True)`. Es wirkt nur auf In-Memory-Verbindungen, und **[Testen](../get-started/testing.md)** ist die Seite, die es erklärt und das ganze Muster darum herum aufbaut.
Dafür gibt es ein eigenes Konstruktor-Flag: `Client(mcp, raise_exceptions=True)`. Es wirkt nur auf Verbindungen im selben Prozess, und **[Testen](../get-started/testing.md)** ist die Seite, die es erklärt und das ganze Muster darum herum aufbaut.
## Zusammenfassung {#recap}
* `Client(x)` verbindet sich in-memory mit einem Server-Objekt, über Streamable HTTP mit einem URL-String und über alles andere per Transport.
* `Client(x)` verbindet sich über Streamable HTTP mit einem URL-String, startet für ein `StdioServerParameters` einen Subprozess, betritt einen Transport direkt und nimmt in Tests das Server-Objekt selbst.
* `async with` ist der ganze Lebenszyklus. Darin sind `server_capabilities` und `protocol_version` bereits gefüllt; `server_info` und `instructions` ebenfalls, wenn der Server sie liefert.
* `list_tools()` gibt dir für jedes Tool `name`, `title`, `description` und `input_schema`.
* `call_tool()` gibt `content` für das Modell, `structured_content` für deinen Code und `is_error` zurück. Ein Tool, das eine Exception auslöst, ist ein Ergebnis, keine Exception.

View file

@ -1,6 +1,6 @@
---
translation:
sections: [c6899d3892bd9fa0, 79372cff3cc48a88, 63878d29e87c3e73, 13175843d3588af4, e7e2b9fd516f77de, 758f06399b513c1f, a05d7278487d610b]
sections: [c6899d3892bd9fa0, 79372cff3cc48a88, c2dae1ebe2ebd543, 13175843d3588af4, df06056fb16b3846, 758f06399b513c1f, a05d7278487d610b]
tool: 1
---
# OAuth-Clients {#oauth-clients}
@ -81,18 +81,20 @@ Sieh dir `main()` an. Der Provider kommt an den **httpx2-Client**, der httpx2-Cl
Wenn `Client` zum ersten Mal einen Request schickt, antwortet der Server mit `401`. Der Provider übernimmt:
1. **Discovery.** Er liest den `WWW-Authenticate`-Header, holt die Protected Resource Metadata des Servers von `/.well-known/oauth-protected-resource`, erfährt, welcher Autorisierungsserver diese Ressource schützt, und holt die Metadaten *dieses* Servers.
1. **Discovery.** Er liest den `WWW-Authenticate`-Header, holt die Protected Resource Metadata des Servers von `/.well-known/oauth-protected-resource`, erfährt, welcher Autorisierungsserver diese Ressource schützt, und holt die Metadaten *dieses* Servers. (Ein älterer Server, der keine Ressourcen-Metadaten veröffentlicht, wird stattdessen an seinem eigenen Origin nach Autorisierungsserver-Metadaten gefragt.) So oder so müssen die Metadaten als `issuer` den Server nennen, für den sie geholt wurden; alles andere wird abgelehnt.
2. **Registrierung.** Nichts im Speicher? Er registriert dich dynamisch mit deiner `OAuthClientMetadata` und speichert das Ergebnis.
3. **Autorisierung.** Er generiert das PKCE-Paar und einen `state`, baut die Autorisierungs-URL, awaited deinen `redirect_handler` und awaited dann deinen `callback_handler` für den Code.
4. **Austausch.** Er tauscht den Code gegen ein `OAuthToken`, speichert es und wiederholt deinen ursprünglichen Request mit `Authorization: Bearer ...`.
Danach ist Ruhe. Tokens kommen aus dem Speicher, ein abgelaufenes Access-Token wird mit dem Refresh-Token erneuert, und erst wenn nichts davon klappt, führt er den Flow erneut aus.
Eine Transportregel gilt für alle diese Requests: Wie der MCP-Request, in dem sie laufen, folgen sie einem Redirect nur, wenn er auf demselben Origin bleibt und die Methode beibehält (etwa ein 307/308 wegen eines Schrägstrichs am Ende), und behandeln jeden anderen Redirect so, als würde diese URL nicht antworten.
Nichts davon hast du geschrieben. Zwei Keyword-Argumente bleiben übrig (`client_metadata_url` und `validate_resource_url`), und diese Datei braucht keines davon. `client_metadata_url` ist dasjenige, das man kennen sollte; es bekommt unten einen eigenen Abschnitt.
### Ausprobieren {#try-it}
Die meisten Beispiele in dieser Dokumentation kannst du mit einem In-Memory-`Client(server)` prüfen. Dieses nicht: Der ganze Sinn des Flows ist ein HTTP-`401`, und zwischen einem In-Memory-Client und seinem Server gibt es kein HTTP.
Der In-Memory-`Client(server)`, den deine Tests verwenden, hilft hier nicht: Der ganze Sinn des Flows ist ein HTTP-`401`, und zwischen einem In-Memory-Client und seinem Server gibt es kein HTTP.
Das Repository liefert die Live-Variante mit. `examples/servers/simple-auth/` betreibt einen eigenständigen Autorisierungsserver und einen geschützten MCP-Server; `examples/clients/simple-auth-client/` ist der Client dieser Seite, ausgebaut zu einem kleinen CLI. Sein README enthält die beiden Befehle: Starte die Server, lass den Client gegen sie laufen, und du siehst die vier Schritte vorbeiziehen.
@ -110,13 +112,14 @@ Ein nächtlicher Job, ein CI-Schritt, ein anderer Dienst. Es gibt keinen Browser
`ClientCredentialsOAuthProvider` ist dasselbe `httpx2.Auth`, ohne den Menschen:
```python title="client.py" hl_lines="4 27-33"
```python title="client.py" hl_lines="4 27-34"
--8<-- "docs_src/oauth_clients/tutorial002.py"
```
Was sich geändert hat:
* Keine `OAuthClientMetadata`, keine Handler. Du übergibst `client_id` und `client_secret`; der Provider baut eine minimale `client_credentials`-Registrierung darum herum und überspringt die dynamische Registrierung komplett.
* `issuer` benennt den Autorisierungsserver, der diese Zugangsdaten ausgestellt hat; verwende den `issuer`-Wert, den sein Dokument unter `/.well-known/oauth-authorization-server` zurückgibt. Die Discovery läuft weiterhin wie oben, aber Token-Requests werden ausschließlich aus Metadaten für *diesen* Issuer gebaut; zeigt der MCP-Server irgendwo anders hin, bricht der Flow stattdessen mit einem `OAuthFlowError` ab. Ihn wegzulassen ist veraltet, und ab 3.0 ist er erforderlich (siehe **[Veraltete Features](../deprecated.md#deprecated-sdk-helpers)**); bis dahin warnt der Provider und verwendet den Autorisierungsserver, den die Discovery findet.
* `scope` ist ein durch Leerzeichen getrennter String, das OAuth-Format auf der Leitung.
* Alles danach ist identisch: dasselbe `TokenStorage`, derselbe `httpx2.AsyncClient(auth=...)`, derselbe `streamable_http_client`.
@ -129,7 +132,7 @@ Standardmäßig reist das Secret als HTTP Basic Auth im Token-Request (`client_s
Ein weiterer Provider liegt in `mcp.client.auth.extensions.client_credentials`:
**`PrivateKeyJWTOAuthProvider`**, für Clients, die sich mit einem JWT statt einem
gemeinsamen Secret authentifizieren (`private_key_jwt`, die Variante mit Schlüsselpaar und Workload-Identität). Er folgt
demselben Muster: einen erzeugen, auf `auth=` setzen. Dasselbe Modul liefert
demselben Muster: einen erzeugen (er nimmt denselben optionalen `issuer` entgegen), auf `auth=` setzen. Dasselbe Modul liefert
`SignedJWTParameters` und `static_assertion_provider`, zwei Helfer, die seine Assertion bauen.
Es gibt noch eine Situation ohne Menschen: Der Client gehört zu einem Unternehmen, dessen Identity Provider – nicht die Person am Host – entscheidet, welche MCP-Server er erreichen darf. Das ist ein anderer Grant mit eigenem Vertrauensmodell und eigener Seite: **[Identity Assertion](identity-assertion.md)**.

View file

@ -1,6 +1,6 @@
---
translation:
sections: [09c857a25a9dc37a, 43bc6a76a243a50e, 0a716022a88768df, 4b7f78042bfcfff7, c112662e61b03315, 58974ba1f489a8b4, d18adbdbb835ea73]
sections: [09c857a25a9dc37a, 43bc6a76a243a50e, 0a716022a88768df, 4b7f78042bfcfff7, c112662e61b03315, 58974ba1f489a8b4, ed4d17e894864056]
tool: 1
---
# Session-Gruppen {#session-groups}
@ -78,7 +78,7 @@ Hältst du bereits eine verbundene `ClientSession` (`Client.session` ist eine),
## Zusammenfassung {#recap}
* `ClientSessionGroup` hält viele Server-Verbindungen und führt deren Tools, Ressourcen und Prompts in je ein `dict` zusammen.
* `connect_to_server(params)` pro Server. Es nimmt Transport-Parameter entgegen, nie das Server-Objekt oder die URL, die ein `Client` entgegennimmt.
* `connect_to_server(params)` pro Server. Es nimmt Transport-Parameter entgegen, nie die URL oder den `Transport`, die ein `Client` entgegennimmt.
* `group.call_tool(name, arguments)` leitet den Aufruf für dich an den zuständigen Server weiter.
* Namen müssen in der gesamten Gruppe eindeutig sein; zwei Server mit einem `search`-Tool können nicht ohne Weiteres nebeneinander bestehen.
* `component_name_hook=` schreibt jeden registrierten Namen um. Der Dict-Schlüssel ändert sich, der Name auf der Leitung nicht.

View file

@ -1,6 +1,6 @@
---
translation:
sections: [9cac816674181eb0, 0700f337babcd4dd, 2bde0dd58cdf00f5, 40b4916d82eaf1d4, 3d0832f39b0d7059, dfa4446556badef0, 5bd93be2ab2ecb9c]
sections: [9cac816674181eb0, 7c157764133fea1f, 40b4916d82eaf1d4, 10d151f2cc75317f, 3d0832f39b0d7059, 92742ba36533633d, 0aeca6145e7bd302]
tool: 1
---
# Client-Transporte {#client-transports}
@ -11,30 +11,15 @@ Du konfigurierst nie einen separat. `Client` nimmt ein einziges positionales Arg
Die *Server*-Seite jedes Transports (was `mcp.run()` tut und was du bereitstellst) steht in **[Den Server betreiben](../run/index.md)**.
## Im Speicher {#in-memory}
Übergib das Server-Objekt selbst:
```python title="client.py" hl_lines="14"
--8<-- "docs_src/client_transports/tutorial001.py"
```
Kein Subprozess, kein Port, keine Bytes auf einer Leitung. Client und Server sind zwei Objekte im selben Prozess, und der Aufruf läuft trotzdem durch die echte Protokollschicht: `search_books` wird genau so aufgelistet, validiert und aufgerufen, wie es über HTTP geschähe.
Damit ist es zwei Dinge zugleich:
* **Eine Testumgebung.** Jedes Beispiel in dieser Dokumentation wird so ausgeführt, und die Seite **[Testen](../get-started/testing.md)** baut das ganze Muster darauf auf.
* **Eine Embedding-API.** Eine Anwendung, die den Server selbst erzeugt, braucht keinen Netzwerk-Hop, um dessen Tools aufzurufen.
## Streamable HTTP {#streamable-http}
Übergib einen URL-String und du bekommst **Streamable HTTP**, den Transport, hinter dem du bereitstellst:
Übergib einen URL-String und du bekommst **Streamable HTTP** – den Transport, hinter dem du bereitstellst und zu dem du zuerst greifen solltest:
```python title="client.py" hl_lines="5"
--8<-- "docs_src/client_transports/tutorial002.py"
```
Das ist der ganze Produktions-Client. `Client` packt die URL für dich in `streamable_http_client(...)`, auf Basis eines `httpx2.AsyncClient`, der so konfiguriert ist, wie MCP es braucht: `follow_redirects=True`, ein Timeout von 30 Sekunden für connect/write/pool und ein Read-Timeout von 300 Sekunden, weil der Server einen Response-Stream offen halten kann.
Das ist der ganze Produktions-Client. `Client` packt die URL für dich in `streamable_http_client(...)`, auf Basis eines `httpx2.AsyncClient`, der so konfiguriert ist, wie MCP es braucht: ein Timeout von 30 Sekunden für connect/write/pool und ein Read-Timeout von 300 Sekunden, weil der Server einen Response-Stream offen halten kann.
!!! check
Ein `Client`, den du erzeugt hast, ist **nicht** verbunden. Das Erzeugen wählt nur den Transport;
@ -50,7 +35,7 @@ Das ist der ganze Produktions-Client. `Client` packt die URL für dich in `strea
Sobald du einen `Authorization`-Header, ein Cookie, einen Proxy, mTLS oder ein anderes Timeout brauchst, baust du den `httpx2.AsyncClient` selbst und übergibst ihn an `streamable_http_client`:
```python title="client.py" hl_lines="8-14"
```python title="client.py" hl_lines="8-13"
--8<-- "docs_src/client_transports/tutorial003.py"
```
@ -80,9 +65,30 @@ oder übergibst deinem `httpx2.AsyncClient` ein explizites `verify=ssl_context`
!!! info
`httpx2` behält die vertraute `httpx`-API bei. Wenn du `httpx` kennst, weißt du hier also bereits, wie Auth,
Proxys, Event-Hooks, Retries und Verbindungslimits gehen. Das SDK fügt nichts hinzu und nimmt
nichts weg. Hier dockt auch OAuth an:
nichts weg – außer bei der [Behandlung von Redirects](#redirects). Hier dockt auch OAuth an:
`httpx2.AsyncClient(auth=OAuthClientProvider(...))`. Der ganze Ablauf steht in **[OAuth-Clients](oauth-clients.md)**.
### Redirects {#redirects}
Der Transport verbindet sich mit der URL, die du ihm gegeben hast, und nur mit diesem Origin.
* Einem `307`/`308`-Redirect, der auf demselben Schema, Host und Port bleibt, wird gefolgt, ebenso `http://` → `https://` auf demselben Host. Das deckt den üblichen Trailing-Slash-Redirect `/mcp` → `/mcp/` ab.
* Einem Redirect irgendwo anders hin wird **nicht** gefolgt. Der Aufruf schlägt fehl mit:
```text
MCPError: Redirect to https://other.example.com/mcp not followed; use that URL as the endpoint if it is the intended server
```
Ist diese URL der Server, den du meintest, trag sie in deine Konfiguration ein. Wenn nicht, ist der Server oder ein Proxy davor falsch konfiguriert.
Das gilt für jeden `httpx2.AsyncClient`, den du übergibst: Seine Einstellung `follow_redirects` wird für MCP-Requests nicht herangezogen, in keine der beiden Richtungen. Die OAuth-Provider des SDK wenden dieselbe Regel auf ihre eigenen Requests an.
!!! tip
`Redirect to http://… not followed: it would downgrade this HTTPS endpoint to plain HTTP` bedeutet, dass der
Server hinter einem TLS-terminierenden Proxy sitzt, von dem er nichts weiß, und `http://`-Redirects ausgibt.
Das behebst du auf dem Server (**[Bereitstellen und skalieren](../run/deploy.md#behind-a-tls-terminating-proxy)**)
oder indem du genau die `https://…/`-URL verwendest, die die Meldung vorschlägt.
## stdio {#stdio}
Ein **stdio**-Server ist ein Subprozess. Der Client startet ihn, schreibt JSON-RPC in seine stdin und liest JSON-RPC aus seiner stdout. So betreibt ein Desktop-Host einen Server auf deinem Rechner: Ein Host *ist* dieser Code plus eine UI, und **[Mit einem echten Host verbinden](../get-started/real-host.md)** zeigt dieselbe Beziehung von der Seite des Hosts, als Konfigurationsdatei.
@ -105,6 +111,18 @@ Die stderr des Kindprozesses landet in deiner. Um sie woandershin zu leiten, bau
Ein Server, der einen API-Key braucht, findet ihn dort nicht. Übergib ihn explizit mit `env=`; diese
Variablen werden über die Allow-List gelegt. Genau das tut `BOOKSHOP_API_KEY` oben.
## Im Speicher {#in-memory}
In einem Test gibt es nichts bereitzustellen und nichts zu starten. Übergib das Server-Objekt selbst:
```python hl_lines="14"
--8<-- "docs_src/client_transports/tutorial001.py"
```
Kein Subprozess, kein Port, keine Bytes auf einer Leitung. Client und Server sind zwei Objekte im selben Prozess, und der Aufruf läuft trotzdem durch die echte Protokollschicht: `search_books` wird genau so aufgelistet, validiert und aufgerufen, wie es über HTTP geschähe. **[Testen](../get-started/testing.md)** baut das ganze Muster darauf auf.
Dieselbe Form dient zugleich als Embedding-API: Eine Anwendung, die den Server selbst erzeugt, kann dessen Tools ohne Netzwerk-Hop aufrufen.
## SSE {#sse}
`sse_client(url)` aus `mcp.client.sse` ist der HTTP-Transport, den Streamable HTTP abgelöst hat. Pack ihn genauso ein, `Client(sse_client("http://localhost:8000/sse"))`, um mit einem Server zu sprechen, der ihn noch verwendet – und bau nichts Neues darauf.
@ -113,15 +131,16 @@ Die stderr des Kindprozesses landet in deiner. Um sie woandershin zu leiten, bau
Für `Client` ist alles oben Genannte dasselbe.
Ein **Transport** ist ein beliebiger asynchroner Kontextmanager, der ein `(read, write)`-Paar von Nachrichten-Streams liefert: formal das `Transport`-Protokoll in `mcp.client`. `Client` löst sein Argument nach Typ auf: Ein Server-Objekt verbindet im Prozess, ein `str` wird zu `streamable_http_client(url)`, ein `StdioServerParameters` wird zu `stdio_client(params)`, und alles andere wird direkt als Transport betreten. Diese letzte Regel ist der Grund, warum `stdio_client(...)`, `streamable_http_client(...)` und `sse_client(...)` alle in denselben Platz passen – und warum du deinen eigenen schreiben kannst.
Ein **Transport** ist ein beliebiger asynchroner Kontextmanager, der ein `(read, write)`-Paar von Nachrichten-Streams liefert: formal das `Transport`-Protokoll in `mcp.client`. `Client` löst sein Argument nach Typ auf: Ein `str` wird zu `streamable_http_client(url)`, ein `StdioServerParameters` wird zu `stdio_client(params)`, ein Server-Objekt verbindet im Prozess, und alles andere wird direkt als Transport betreten. Diese letzte Regel ist der Grund, warum `stdio_client(...)`, `streamable_http_client(...)` und `sse_client(...)` alle in denselben Platz passen – und warum du deinen eigenen schreiben kannst.
## Zusammenfassung {#recap}
* `Client(mcp)` (das Server-Objekt) verbindet im Speicher. Nutze es für Tests und zum Einbetten.
* `Client("http://.../mcp")` (eine URL) verbindet über Streamable HTTP, den Produktions-Transport.
* Header, Auth, Proxys und Timeouts gehören auf einen `httpx2.AsyncClient`, den du an `streamable_http_client(url, http_client=...)` übergibst. Es gibt kein Keyword `headers=`.
* Redirects wird nur innerhalb des eigenen Origins der URL gefolgt (ein Trailing-Slash-`307`/`308`), plus `http`→`https` auf demselben Host. Alles andere schlägt mit `Redirect to … not followed` fehl; konfiguriere die endgültige URL.
* stdio ist `Client(StdioServerParameters(...))`. Pack es nur dann selbst in `stdio_client(...)` ein, wenn du die stderr des Kindprozesses umleiten willst.
* Der Subprozess bekommt eine Umgebung per Allow-List, nicht deine; `env=` ergänzt sie.
* `Client(mcp)` (das Server-Objekt) verbindet im Speicher. Nutze es in Tests oder um einen Server in die Anwendung einzubetten, die ihn gebaut hat.
* Ein Transport ist alles, womit du `async with x as (read, write)` schreiben kannst. Alles, was weder Server-Objekt noch URL noch `StdioServerParameters` ist, reicht `Client` direkt an dieses Protokoll weiter.
* Das Erzeugen eines `Client` wählt den Transport. `async with` öffnet ihn.