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
5 changed files with 4821 additions and 2277 deletions

View file

@ -1,6 +1,6 @@
---
translation:
sections: [1062ef792791488a, 4be2b831547184a9, 374b049e770385f2, b72f6947089e6de0, b172c9db7831bb31, 70b9ece244ca1b0c, cba78e052898c3f6, f06bdb541cb0b469, fb82d526320b7cc3]
sections: [1062ef792791488a, 4be2b831547184a9, 374b049e770385f2, b72f6947089e6de0, b172c9db7831bb31, 10394c6f16601638, cba78e052898c3f6, f06bdb541cb0b469, 6dc898ccc5a903f9]
tool: 1
---
# In eine bestehende App einbinden {#add-to-an-existing-app}
@ -99,7 +99,7 @@ Das abschließende `/mcp` ist `streamable_http_path`. Setze es auf `"/"`, und da
--8<-- "docs_src/asgi/tutorial004.py"
```
Jetzt verbinden sich Clients mit `/notes`, nicht mit `/notes/mcp`.
Jetzt verbinden sich Clients mit `/notes/`, nicht mit `/notes/mcp`.
## CORS für Browser-Clients {#cors-for-browser-clients}
@ -142,4 +142,4 @@ Ein browserbasierter Client braucht zwei Erlaubnisse von dir: seine MCP-Request-
* Browser-Clients brauchen CORS: `allow_headers` für die `Mcp-*`-Request-Header, `expose_headers=["Mcp-Session-Id"]` für die Response.
* `@mcp.custom_route()` fügt einfache, nicht authentifizierte HTTP-Endpunkte neben `/mcp` hinzu.
Sobald der Server unter einer echten URL erreichbar ist, verbindet sich **[Der Client](../client/index.md)** über diese URL mit ihm statt über ein Server-Objekt.
Sobald der Server unter einer echten URL erreichbar ist, verbindet sich **[Der Client](../client/index.md)** über diese URL mit ihm.

View file

@ -1,6 +1,6 @@
---
translation:
sections: [d62c13457fc4a534, 80e73abaca6e0652, d1dc4c54cd00ec9c, 14ad3bc7904036bb, 5225f127bc1b9c77, fe1626fdd5aad1da, 4556cb7ea1a04a31]
sections: [d62c13457fc4a534, 80e73abaca6e0652, 128a492d18295f64, 14ad3bc7904036bb, 54a6697833fcc1d9, fe1626fdd5aad1da, 811d083c1da8bcf6]
tool: 1
---
# Autorisierung {#authorization}
@ -23,12 +23,12 @@ Das ist das ganze Dreieck. Alles auf dieser Seite betrifft den mittleren Punkt.
Das SDK hat keine Meinung dazu, wie ein gültiges Token aussieht. Das sagst du ihm, indem du **`TokenVerifier`** implementierst:
```python title="server.py" hl_lines="12-14 19-24"
```python title="server.py" hl_lines="14-16 21-27"
--8<-- "docs_src/authorization/tutorial001.py"
```
* `TokenVerifier` ist ein Protokoll mit einer einzigen asynchronen Methode. `verify_token` bekommt das rohe Token aus dem `Authorization`-Header und gibt ein **`AccessToken`** zurück, wenn es gültig ist, und `None`, wenn nicht. Mehr gibt es nicht zu implementieren.
* Dieser hier schlägt das Token in einer Tabelle nach. Ein echter prüft eine JWT-Signatur oder ruft den Token-Introspection-Endpunkt des Authorization Servers auf. Dieser Code gehört dir; das SDK ruft ihn nur auf.
* Dieser hier schlägt das Token in einer Tabelle nach; jeder Eintrag hält fest, für welche Ressource es ausgestellt wurde. Ein echter prüft eine JWT-Signatur oder ruft den Token-Introspection-Endpunkt des Authorization Servers auf und meldet in `AccessToken.resource`, für wen das Token ausgestellt wurde (sein `aud`). Dieser Code gehört dir; das SDK ruft ihn nur auf.
* `token_verifier=` und `auth=` treten immer gemeinsam auf. Übergibst du das eine ohne das andere, löst `MCPServer(...)` einen `ValueError` aus, bevor auch nur ein Request bedient wird.
`AuthSettings` ist das öffentliche Gesicht deines Resource Servers:
@ -36,6 +36,10 @@ Das SDK hat keine Meinung dazu, wie ein gültiges Token aussieht. Das sagst du i
* `issuer_url`: der Authorization Server, der deine Tokens ausstellt.
* `resource_server_url`: die öffentliche URL dieses MCP-Endpunkts. Sie benennt, für *welche* Ressource ein Token gilt, und unter ihr liegt das Discovery-Dokument.
* `required_scopes`: jedes Token muss alle davon tragen.
* `validate_token_resource`: lehnt jedes Token ab, dessen `AccessToken.resource` nicht `resource_server_url` ist. Lässt du es ungesetzt, während `resource_server_url` gesetzt ist, gibt es eine Warnung (`MCPDeprecationWarning`) und es verhält sich wie `False`; ab 3.0 ist `True` der Standardwert für Resource Server.
* Schalte es ein, wenn dein Authorization Server Tokens an die `resource` bindet, die der Client angefordert hat – MCP-Clients senden sie immer. Halte `resource_server_url` exakt auf der URL, mit der sich Clients verbinden.
* Lass es aus, wenn dein Authorization Server eigene Audience-Bezeichner verwendet (einen Auth0-API-Identifier, eine Entra-Application-ID), und prüfe `aud` stattdessen in deinem Verifier: Gib für ein Token, das nicht für diesen Server bestimmt ist, `None` zurück.
* Ist `aud` eine Liste, trage in `resource` den Eintrag ein, der `resource_server_url` entspricht.
!!! tip
`examples/servers/simple-auth/` im SDK-Repository enthält einen `IntrospectionTokenVerifier`, der den
@ -91,11 +95,11 @@ Schick ein `GET` an diesen Well-Known-Pfad, und du bekommst **Protected Resource
In jedem Handler ist **`get_access_token()`** das `AccessToken`, das dein Verifier für den aktuellen Request zurückgegeben hat:
```python title="server.py" hl_lines="4 32-35"
```python title="server.py" hl_lines="4 35-38"
--8<-- "docs_src/authorization/tutorial002.py"
```
* Es funktioniert in Tools, Ressourcen und Prompts, und du musst nichts herumreichen: Die Auth-Middleware speichert es pro Request in einer Context-Variablen.
* Es funktioniert in Tools, Ressourcen und Prompts, und du musst nichts herumreichen: Die Auth-Middleware speichert es pro Request in einer Kontextvariablen.
* Du bekommst **dasselbe Objekt zurück, das dein Verifier gebaut hat**: `client_id`, `scopes`, `subject`, `expires_at` und alle zusätzlichen `claims`, die du angehängt hast. Das ist der Ansatzpunkt für Regeln pro Tool: Lies die Scopes und lehne ab.
* Außerhalb eines authentifizierten HTTP-Requests gibt es `None` zurück. In-Memory und über `stdio` ist es immer `None`.
@ -125,6 +129,6 @@ Ein Authorization Server kann statt einer Person, die sich durch einen Consent-S
* `token_verifier=` und `auth=AuthSettings(issuer_url=..., resource_server_url=..., required_scopes=[...])` treten immer gemeinsam auf.
* Das SDK veröffentlicht Protected Resource Metadata nach [RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728) unter `/.well-known/oauth-protected-resource/...` und beantwortet nicht authentifizierte Requests mit einer 401, deren `WWW-Authenticate`-Header darauf zeigt. Das ist die ganze Discovery-Geschichte.
* `get_access_token()` in jedem Handler sagt dir, wer aufruft.
* Autorisierung ist eine HTTP-Angelegenheit. `stdio` und der In-Memory-Client bekommen sie nie zu sehen.
* Autorisierung ist eine HTTP-Angelegenheit. `stdio` und der In-Memory-Test-Client bekommen sie nie zu sehen.
Die Client-Hälfte (deinen Authorization Server finden und das Token für dich holen) steht unter **[OAuth-Clients](../client/oauth-clients.md)**. Und ein Client, der eine Identität *behauptet*, statt eine Person danach zu fragen, steht unter **[Identity Assertion](../client/identity-assertion.md)**.

View file

@ -1,6 +1,6 @@
---
translation:
sections: [28221886b198784f, f88ea1f1614f3a1d, ce926d686730b6d0, 3be24f8ad8bb5ab9, 3fad24032b2224ff, f25a7f860e579ecb, e758745df6fb7b0a]
sections: [28221886b198784f, f88ea1f1614f3a1d, 2e76f5cb9df15042, ce926d686730b6d0, 3be24f8ad8bb5ab9, 3fad24032b2224ff, f25a7f860e579ecb, 697b01d95080880d]
tool: 1
---
# Bereitstellen und skalieren {#deploy-scale}
@ -47,6 +47,22 @@ Hinter einem echten Hostnamen bereitgestellt, weist genau dieser Standard **jede
bereitgestellter Server, der jede Verbindung ablehnt, ist bis zum Beweis des Gegenteils eine Host-Allowlist.
Auch **[Fehlerbehebung](../troubleshooting.md)** fängt hier an.
## Hinter einem TLS-terminierenden Proxy {#behind-a-tls-terminating-proxy}
Wenn TLS an einem Proxy endet (einem Ingress, einem Load Balancer, Caddy, nginx) und uvicorn dahinter unverschlüsseltes HTTP ausliefert, weise uvicorn an, den `X-Forwarded-*`-Headern des Proxys zu vertrauen:
```console
uvicorn server:app --proxy-headers --forwarded-allow-ips='<proxy address>'
```
Ohne das glaubt die App, sie werde über `http://` ausgeliefert, und jeder Redirect, den sie auslöst (der übliche ist `/mcp` → `/mcp/`), zeigt auf `http://…`. Der Python-Client weigert sich, von einem HTTPS-Endpunkt zu unverschlüsseltem HTTP zu folgen, und sagt das auch:
```text
MCPError: Redirect to http://mcp.example.com/mcp/ not followed: it would downgrade this HTTPS endpoint to plain HTTP.
```
Die clientseitige Notlösung ist, genau die URL zu konfigurieren, die der Server ausliefert (`https://mcp.example.com/mcp/`, Schrägstrich inklusive), sodass kein Redirect passiert. Die eigentliche Lösung ist das Flag oben. `FORWARDED_ALLOW_IPS` ist die Schreibweise als Umgebungsvariable; `*` vertraut jedem Hop, was nur richtig ist, wenn nichts außer dem Proxy uvicorn erreichen kann.
## Worker – und wer sticky sein muss {#workers-and-who-has-to-be-sticky}
Sobald der Hostname antwortet, stellst du mehr als einen Worker dahinter. Dafür gibt es keinen Schalter im SDK; du skalierst eine Starlette-App wie jede andere ASGI-App, indem du das Objekt an etwas übergibst, das forken kann:
@ -170,6 +186,7 @@ Ein `MCPServer` ist eine Protokollimplementierung, kein Anwendungsserver. Die De
## Zusammenfassung {#recap}
* Ohne weitere Konfiguration beantwortet die App nur Requests an localhost. `transport_security=TransportSecuritySettings(allowed_hosts=[...], allowed_origins=[...])` ist die Schranke zum Livegang: Bis du es übergibst, ist jeder Request hinter einem echten Hostnamen ein `421`, und der Grund steht nur im Log des Servers.
* Hinter einem TLS-terminierenden Proxy startest du uvicorn mit `--proxy-headers --forwarded-allow-ips=...`, sonst zeigen seine Redirects auf `http://`, und der Client lehnt sie ab.
* Auf 2026-07-28 gibt es keine Session und nichts, woran ein Load Balancer sticky sein könnte. `stateless_http=True` ist ein reiner Legacy-Schalter, weil ein moderner Request geroutet und beantwortet ist, bevor dieses Flag überhaupt gelesen wird.
* Der Standardschlüssel für `requestState` ist `os.urandom(32)`, pro Prozess erzeugt. Ein Multi-Roundtrip-Retry, der bei einem anderen Worker landet, scheitert mit `-32602` *„Invalid or expired requestState“*.
* Die Lösung ist `RequestStateSecurity(keys=[...])` **und** derselbe Servername auf jeder Instanz. Der Name ist der Standard-Audience-Claim des Tokens. Dieselben Schlüssel, derselbe Name.

View file

@ -1,6 +1,6 @@
---
translation:
sections: [fea8d769ff9edeba, ce8e2ad42f29ef71, 0d705efb19cf99c2, 2fd7cf825e6d2b2c, 9adc400e8c88e854, 318893ad8e2e9924, 6b63ab96b34476c0]
sections: [fea8d769ff9edeba, ce8e2ad42f29ef71, 0d705efb19cf99c2, 9fd2357154a5b7e7, 9adc400e8c88e854, 318893ad8e2e9924, 6b63ab96b34476c0]
tool: 1
---
# Den Server betreiben {#running-your-server}
@ -75,6 +75,11 @@ Jeder Transport hat eigene Keyword-Argumente, alle an `run()`:
* `max_request_body_size`: größter akzeptierter Request-Body in Bytes. Standardwert 4 MiB; größere Requests
erhalten HTTP 413, bevor geparst oder eine Session angelegt wird. Erhöhe ihn nur, wenn legitime MCP-Nachrichten
diese Größe überschreiten.
* `session_idle_timeout`: Sekunden, die eine Legacy-Session ohne laufenden Request verbringen darf, bevor der
Server sie schließt. Standardwert 1800. `None` schaltet den Timeout ab. Siehe
[Lebensdauer und Limits von Sessions](legacy-clients.md#session-lifetime-and-limits).
* `max_sessions`: wie viele Legacy-Sessions ein Prozess gleichzeitig hält. Standardwert 10 000. `None`
hebt das Limit auf. Im selben Abschnitt beschrieben.
* `event_store`, `retry_interval`, `transport_security`: Wiederaufnahme und Schutz vor DNS-Rebinding. Sie können warten, bis du anderswo als auf localhost bereitstellst; **[Bereitstellen und skalieren](deploy.md)** behandelt `transport_security`.
!!! warning

View file

@ -1,6 +1,6 @@
---
translation:
sections: [3d1663c18edc824c, d4fd37009a13f03d, af9f398a5a8b679a, 470c2dd144294d69, 8e45827e6d24e8c8, 91dfd0ce98ebb03c]
sections: [3d1663c18edc824c, 90956965ae6a1ca1, af9f398a5a8b679a, 5ce83b1f9d88da62, 0d9b5d13fffc94e5, 8e45827e6d24e8c8, 91dfd0ce98ebb03c]
tool: 1
---
# Legacy-Clients unterstützen {#serving-legacy-clients}
@ -22,15 +22,25 @@ Ein Legacy-Client ist also nichts, *wofür* du etwas baust. Er ist etwas, das si
## Ein Handler, beide Generationen {#one-handler-both-eras}
Hier ist ein Tool, das die Person am Host etwas fragen muss, und Clients beider Generationen, die es aufrufen:
Hier ist ein Tool, das die Person am Host etwas fragen muss:
```python title="server.py" hl_lines="24 37-38"
```python title="server.py" hl_lines="21"
--8<-- "docs_src/legacy_clients/tutorial001.py"
```
`reserve` braucht eine Sache, die das Modell nicht geliefert hat: wie viele Exemplare. Mit `Annotated[..., Resolve(ask_quantity)]` deklariert ein Tool genau das (alles Weitere steht in **[Abhängigkeiten](../handlers/dependencies.md)**). Nichts in `reserve` nennt eine Version, prüft eine Capability oder verzweigt.
Die beiden Clients sind **gleichzeitig** offen, am selben `mcp`-Objekt. `mode="legacy"` führt den `initialize`-Handshake aus: genau die Verbindung, die ein Client von vor 2026 öffnet. Der andere nimmt den Standardwert und landet bei `2026-07-28`.
Stelle es über HTTP bereit, und hier sind Clients beider Generationen, die es aufrufen:
```console
uv run mcp run server.py --transport streamable-http
```
```python title="client.py" hl_lines="14-15"
--8<-- "docs_src/legacy_clients/tutorial001_client.py"
```
Die beiden Clients sind **gleichzeitig** offen, gegen denselben laufenden Server. `mode="legacy"` führt den `initialize`-Handshake aus: genau die Verbindung, die ein Client von vor 2026 öffnet. Der andere nimmt den Standardwert und landet bei `2026-07-28`. Führe `python client.py` in einem zweiten Terminal aus:
```text
2025-11-25 {'result': "Reserved 2 of 'Dune'."}
@ -44,7 +54,7 @@ Es lohnt sich, beim *Wie* kurz innezuhalten, denn den beiden Clients wurde diese
!!! tip
Genau diese Portabilität über Generationen hinweg ist der Grund, *warum* `Resolve` die API ist,
auf die du bauen solltest. Sein älterer Verwandter `ctx.elicit()`
(**[Elicitation](../handlers/elicitation.md)**, die Rückfrage bei der Person am Host) sendet
(**[Elicitation](../handlers/elicitation.md)** – Rückfrage bei der Person am Host) sendet
immer nur `elicitation/create` und funktioniert deshalb immer nur auf einer Legacy-Verbindung.
Auf einer `2026-07-28`-Verbindung schlägt der Aufruf fehl. Wenn ein Tool es noch verwendet, ist
die Lösung die, die du oben siehst, und kein Versionscheck.
@ -65,6 +75,41 @@ Auf einem Worker ist das unsichtbar. Auf zweien ist es das ganze Problem: Ein Re
verbindet), kein Session-Store. Es macht eine Session nie von einem anderen Prozess aus
erreichbar.
## Lebensdauer und Grenzen von Sessions {#session-lifetime-and-limits}
Eine Legacy-Session lebt nicht ewig, und ein Prozess hält nicht unbegrenzt viele davon. Zwei
Einstellungen steuern das. Beide sind Keyword-Argumente an `run()`, `streamable_http_app()` und
`Server.streamable_http_app()`. Moderne (`2026-07-28`-)Verbindungen und `stateless_http=True`
haben keine Sessions, also gilt keine der beiden Einstellungen für sie.
| Einstellung | Standardwert | Was sie tut | Was der Client sieht | Abschalten mit |
|---|---|---|---|---|
| `session_idle_timeout` | `1800` (30 min) | Schließt eine Session, bei der so lange nichts in Bearbeitung war. | `404 Session not found`. Er muss erneut `initialize` senden. | `None` |
| `max_sessions` | `10_000` | Lehnt es ab, über diese Anzahl hinaus eine Session zu öffnen. Bestehende Sessions bleiben unberührt, und nichts wird verdrängt. | `503 Too many open sessions` mit JSON-RPC-Code `-32603`. | `None` |
Was als „in Bearbeitung“ zählt:
* Ein offener `GET`-Stream. Die SDK-Clients halten einen offen, also läuft die Session eines
verbundenen Clients nie ab.
* Ein Request, der noch beantwortet wird. Ein Tool-Aufruf, der länger läuft als das Timeout, wird
nicht unterbrochen, und der Countdown beginnt erst, wenn er fertig ist.
* Sonst nichts. Zwischen Requests läuft die Uhr. Jeder Request auf der Session setzt sie zurück,
`ping` eingeschlossen. Ist eine Session einmal abgelaufen, belebt nichts sie wieder.
Ein Client, der seine Session mit `DELETE` beendet, gibt sie sofort frei. Dasselbe gilt für einen
Client, dessen eröffnender Request abgelehnt wurde.
```python
mcp.run(transport="streamable-http", session_idle_timeout=None, max_sessions=50_000)
```
Beide Ereignisse erscheinen im Server-Log. Ein Ablauf steht als `Session <id> idle timeout` auf
`INFO`. Ein abgelehntes Öffnen als `Refusing to open a new session: <n> sessions are already open`
auf `WARNING`.
Die Limits gelten pro Prozess. Mit vier Workern liegt die Obergrenze beim Vierfachen von
`max_sessions`, und jeder Worker lässt seine eigenen Sessions ablaufen.
## Die eine Stellschraube: `stateless_http` {#the-one-knob-stateless_http}
Wenn Stickiness ein Preis ist, den du nicht zahlen willst, gibt es genau eine Sache, die du ändern kannst.
@ -91,8 +136,8 @@ Zwei Dinge daran sind wichtiger als das, was es tut.
!!! check
Mach es absichtlich falsch. `reserve` ist genau das Tool, das eben beide Clients bedient hat.
Stelle es mit `stateless_http=True` bereit, verbinde dieselben zwei Clients über HTTP und rufe
es von jedem aus auf.
Stelle es mit `stateless_http=True` bereit, verbinde dieselben zwei Clients und rufe es von
jedem aus auf.
Der moderne Client bekommt weiterhin `Reserved 2 of 'Dune'.` Der moderne Zweig hat sich nicht
verändert.