docs: refresh translations, and translate pages in parallel (#3458)
This commit is contained in:
parent
08a3bc8eaf
commit
fd66270926
277 changed files with 4821 additions and 2277 deletions
|
|
@ -11,10 +11,10 @@ The English pages under `docs/` are the source. This directory holds what steers
|
|||
|
||||
```text
|
||||
uv run --frozen python scripts/docs/translations.py status [--lang CODE]
|
||||
uv run --frozen --group translate python scripts/docs/translations.py translate --lang CODE [--pages PATH ...]
|
||||
uv run --frozen --group translate python scripts/docs/translations.py translate [--lang CODE ...] [--pages PATH ...] [--jobs N]
|
||||
uv run --frozen python scripts/docs/translations.py stage [--lang CODE]
|
||||
```
|
||||
|
||||
`status` is offline: per language it lists missing, outdated (with the sections that changed), current and removable pages (translations whose English page is gone — `git rm` them). `translate` calls the Claude API (`ANTHROPIC_API_KEY` in the environment; the registry's model, or `DOCS_TRANSLATE_MODEL` to trial another) for the missing and outdated pages, retranslating only the English sections that changed and keeping the rest byte for byte; `--pages` instead re-translates exactly the named pages from scratch, which is also how a glossary or instructions change reaches existing pages (each generated page records the English section hashes it reflects, so editing those inputs invalidates nothing). `stage` assembles the tree each language site is built from (every language's, or one with `--lang`): each generated page exactly as it was generated, under an "outdated" notice linking the current English page when the English has changed since, and the English page where nothing was generated yet; `scripts/docs/build.sh` runs it before building them. Commit the generated pages in an ordinary pull request.
|
||||
`status` is offline: per language it lists missing, outdated (with the sections that changed), current and removable pages (translations whose English page is gone — `git rm` them). `translate` calls the Claude API (`ANTHROPIC_API_KEY` in the environment; the registry's model, or `DOCS_TRANSLATE_MODEL` to trial another) for the missing and outdated pages of every language (or just the `--lang` ones), several pages at a time (`--jobs`, default 8; each page is its own request, so this changes how long the run takes, not what the model sees), retranslating only the English sections that changed and keeping the rest byte for byte; `--pages` instead re-translates exactly the named pages from scratch (in every language unless `--lang` narrows it), which is also how a glossary or instructions change reaches existing pages (each generated page records the English section hashes it reflects, so editing those inputs invalidates nothing). `stage` assembles the tree each language site is built from (every language's, or one with `--lang`): each generated page exactly as it was generated, under an "outdated" notice linking the current English page when the English has changed since, and the English page where nothing was generated yet; `scripts/docs/build.sh` runs it before building them. Commit the generated pages in an ordinary pull request.
|
||||
|
||||
To add a language, add an entry to `languages.yml`, write `<code>/instructions.md` (the sections the `pt` file has) and `<code>/glossary.json`, then run `translate --lang <code>`.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
translation:
|
||||
sections: [0355618e5f4d5fe4, 1821eaf50f2d0b64, 82e0b28ebd3abf5a, 8ac39614c094f2d0, dab6ff945501ab2a, bd5565c3b2d4f959, 96819ce3d63a0487]
|
||||
sections: [0355618e5f4d5fe4, 0fefa31fb7c7b585, 5c53e7487c9c70cc, 8ac39614c094f2d0, dab6ff945501ab2a, bd5565c3b2d4f959, 96819ce3d63a0487]
|
||||
tool: 1
|
||||
---
|
||||
# MCP Apps {#mcp-apps}
|
||||
|
|
@ -18,7 +18,7 @@ Das SDK liefert das als eingebaute Extension `Apps` (`io.modelcontextprotocol/ui
|
|||
|
||||
## Eine Uhr mit Gesicht {#a-clock-with-a-face}
|
||||
|
||||
```python title="server.py" hl_lines="19 22 30 32"
|
||||
```python title="server.py" hl_lines="17 20 28 30"
|
||||
--8<-- "docs_src/apps/tutorial001.py"
|
||||
```
|
||||
|
||||
|
|
@ -39,11 +39,31 @@ Nicht jeder Client rendert Apps. Die Spezifikation sagt unverblümt, was das fü
|
|||
|
||||
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"
|
||||
```python title="server.py" hl_lines="21-25"
|
||||
--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.
|
||||
`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. Hier ist die Client-Hälfte der Aushandlung:
|
||||
|
||||
```python title="client.py" hl_lines="8 12"
|
||||
--8<-- "docs_src/apps/tutorial001_client.py"
|
||||
```
|
||||
|
||||
Stelle `server.py` über HTTP bereit und starte dann den Client in einem zweiten Terminal:
|
||||
|
||||
```console
|
||||
uv run mcp run server.py --transport streamable-http
|
||||
```
|
||||
|
||||
```console
|
||||
python client.py
|
||||
```
|
||||
|
||||
```text
|
||||
2026-06-26T12:00:00Z
|
||||
```
|
||||
|
||||
Die reichhaltige Antwort kam zurück. Lass `extensions=[APPS_SUPPORT]` im `Client`-Aufruf weg, und dasselbe Programm gibt stattdessen `The time is 2026-06-26T12:00:00Z.` aus – das ist alles, was ein reiner Text-Client je zu sehen bekommt.
|
||||
|
||||
!!! 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.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
translation:
|
||||
sections: [05891e7cc1938a13, b3c01a6af28c51ee, 7ffc91f5e38bdfe0, 717d3f235a8333a7, f471a13b2fe5d737, ed6af2df4b656dff]
|
||||
sections: [05891e7cc1938a13, b3c01a6af28c51ee, 6eba6ce094f4417e, 125f28588c85dd7b, ddd8969b698ca11c, ed6af2df4b656dff]
|
||||
tool: 1
|
||||
---
|
||||
# Extensions {#extensions}
|
||||
|
|
@ -49,7 +49,7 @@ Nimm als Präfix eine Domain, die du kontrollierst. `io.modelcontextprotocol/*`
|
|||
|
||||
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"
|
||||
```python title="server.py" hl_lines="16 18-19 21-22 25"
|
||||
--8<-- "docs_src/extensions/tutorial003.py"
|
||||
```
|
||||
|
||||
|
|
@ -57,17 +57,23 @@ Die kleinste nützliche Extension besteht aus einem Tool und einer Settings-Map:
|
|||
* `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`:
|
||||
Stelle sie über HTTP bereit, und ein Client liefert den Beweis:
|
||||
|
||||
```python title="server.py" hl_lines="29-34"
|
||||
--8<-- "docs_src/extensions/tutorial003.py"
|
||||
```console
|
||||
uv run mcp run server.py --transport streamable-http
|
||||
```
|
||||
|
||||
```python title="client.py" hl_lines="7-11"
|
||||
--8<-- "docs_src/extensions/tutorial003_client.py"
|
||||
```
|
||||
|
||||
Jede `server.py` auf dieser Seite wird mit diesem Befehl bereitgestellt, und jede `client.py` läuft daneben mit `python client.py` aus einem zweiten Terminal.
|
||||
|
||||
### 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"
|
||||
```python title="server.py" hl_lines="14-20 24 33-41"
|
||||
--8<-- "docs_src/extensions/tutorial004.py"
|
||||
```
|
||||
|
||||
|
|
@ -83,14 +89,15 @@ Methoden sind **strikt additiv**. Das SDK erzwingt das bei der Konstruktion, nic
|
|||
|
||||
### Die Client-Seite {#the-client-side}
|
||||
|
||||
Das `main()` derselben Datei ist die ganze Client-Geschichte, beide Hälften davon:
|
||||
Der Client ist ein eigenes Programm und trägt beide Hälften der Client-Geschichte:
|
||||
|
||||
```python title="server.py" hl_lines="54-58"
|
||||
--8<-- "docs_src/extensions/tutorial004.py"
|
||||
```python title="client.py" hl_lines="21-23 27-30"
|
||||
--8<-- "docs_src/extensions/tutorial004_client.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.
|
||||
* `SearchRequest` und die beiden Models, die er trägt, sind der Vertrag der Extension auf der Leitung, also deklariert der Client sie für sich selbst. Eine veröffentlichte Extension würde sie in einem Paket ausliefern, das beide Seiten importieren.
|
||||
|
||||
### `tools/call` abfangen {#intercepting-toolscall}
|
||||
|
||||
|
|
@ -109,12 +116,18 @@ Der Hook umhüllt `tools/call` und sonst nichts. Für alles, was jede Nachricht
|
|||
|
||||
## 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:
|
||||
Eine **Client-Extension** ist derselbe Vertrag von der konsumierenden Seite: ein Bündel clientseitigen Verhaltens hinter einem einzigen Identifier. Der Server hier beantwortet `buy` mit einem einzulösenden Beleg statt mit der Ware, und das nur für einen Client, der die Extension deklariert hat:
|
||||
|
||||
```python title="client.py" hl_lines="66-68"
|
||||
```python title="server.py" hl_lines="22-25"
|
||||
--8<-- "docs_src/extensions/tutorial006.py"
|
||||
```
|
||||
|
||||
Übergib auf dem Client Instanzen an `Client(extensions=[...])` und rufe Tools ganz normal auf:
|
||||
|
||||
```python title="client.py" hl_lines="33-35"
|
||||
--8<-- "docs_src/extensions/tutorial006_client.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.
|
||||
|
|
@ -124,15 +137,15 @@ Um einen Identifier **ohne** clientseitiges Verhalten anzukündigen (der Server
|
|||
```python
|
||||
from mcp.client import advertise
|
||||
|
||||
client = Client(mcp, extensions=[advertise("com.example/search")])
|
||||
client = Client("http://localhost:8000/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"
|
||||
```python title="client.py" hl_lines="16-17 25-26 28-29"
|
||||
--8<-- "docs_src/extensions/tutorial006_client.py"
|
||||
```
|
||||
|
||||
* Der Identifier folgt derselben Grammatik wie auf dem Server und wird validiert, wenn die Klasse definiert wird.
|
||||
|
|
@ -153,12 +166,18 @@ Zwei stille Regeln. Claims sind nur auf 2026-07-28-Verbindungen aktiv, und die C
|
|||
|
||||
### 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`:
|
||||
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). Nimm einen Server, dessen Extension ein einziges Verb zu einem benannten Job bedient:
|
||||
|
||||
```python title="client.py" hl_lines="22-25 46-47"
|
||||
```python title="server.py" hl_lines="12-13 30"
|
||||
--8<-- "docs_src/extensions/tutorial007.py"
|
||||
```
|
||||
|
||||
Eine Ergänzung auf dem Client: 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="20-23 28-29"
|
||||
--8<-- "docs_src/extensions/tutorial007_client.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}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
translation:
|
||||
sections: [2c79b6338e09b7ac, 7edc43b3fae11314, 1086e77ce561cd7f, a3f71823df5efc31, 9fc7109f72201cae, d50fe7faead8cf68, 7bf25983df655b66, 6330e1f4c6029683, 2f1749c8c133fa1c, 8db7116fc8ddd0ee, ebc33704fbd74262, cd0e9c933350390e]
|
||||
sections: [2c79b6338e09b7ac, 9d5d10a5f0405d0a, 1086e77ce561cd7f, a3f71823df5efc31, 9fc7109f72201cae, d50fe7faead8cf68, 7bf25983df655b66, 6330e1f4c6029683, 2f1749c8c133fa1c, 8db7116fc8ddd0ee, ebc33704fbd74262, 0fde3bcea081ba3a]
|
||||
tool: 1
|
||||
---
|
||||
# Der Low-Level-Server {#the-low-level-server}
|
||||
|
|
@ -36,18 +36,22 @@ Drei Dinge haben sich geändert, und sie sind die ganze Low-Level-API:
|
|||
|
||||
### 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`:
|
||||
`mcp dev` und `mcp run` akzeptieren nur einen `MCPServer`, also betreibst du diesen hier selbst. Die letzte Zeile von `server.py` baut daraus eine gewöhnliche ASGI-App, und uvicorn führt sie aus:
|
||||
|
||||
```python title="main.py"
|
||||
```console
|
||||
uvicorn server:app --port 8000
|
||||
```
|
||||
|
||||
Richte den Inspector oder einen beliebigen Client auf `http://localhost:8000/mcp`:
|
||||
|
||||
```python title="client.py"
|
||||
import asyncio
|
||||
|
||||
from mcp import Client
|
||||
|
||||
from server import server
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
async with Client(server) as client:
|
||||
async with Client("http://localhost:8000/mcp") as client:
|
||||
result = await client.call_tool("search_books", {"query": "dune", "limit": 5})
|
||||
print(result.content)
|
||||
|
||||
|
|
@ -64,6 +68,8 @@ Derselbe Text, den die `@mcp.tool()`-Version erzeugt hat. Zwei ehrliche Untersch
|
|||
* `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.
|
||||
|
||||
In einem Test sparst du dir uvicorn und den Port: `Client(server)` nimmt einen Low-Level-`Server` im selben Prozess genauso entgegen wie einen `MCPServer`, und **[Testen](../get-started/testing.md)** ist genau dieses Muster.
|
||||
|
||||
## 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)**).
|
||||
|
|
@ -215,4 +221,4 @@ Jeder davon ist eine Idee, für die du jetzt das Vokabular hast; jeder hat seine
|
|||
* `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)**.
|
||||
Der Client 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)**.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
translation:
|
||||
sections: [a9aba7a026c7bd85, ed32bda7ba9ae33a, 7e64cc5646abb91f, 22a0129ee78b3c63, d875373c06d8d2f9]
|
||||
sections: [a9aba7a026c7bd85, 83e2a08b9d46a398, 9fd8a0aa384b3257, 22a0129ee78b3c63, d875373c06d8d2f9]
|
||||
tool: 1
|
||||
---
|
||||
# Paginierung {#pagination}
|
||||
|
|
@ -19,21 +19,25 @@ Paginierung ist für den Server gedacht, dessen Ressourcenliste in Wahrheit eine
|
|||
--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**.
|
||||
* Auf einem Low-Level-`Server` sind Handler Konstruktorargumente, keine Dekoratoren. `on_list_resources` beantwortet jeden `resources/list`-Request; das ist schon die ganze Anbindung.
|
||||
* Jeder paginierte Handler ist als `params: PaginatedRequestParams | None` typisiert, und das Beispiel akzeptiert beides. Über eine Verbindung übergibt dir das SDK allerdings 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.
|
||||
einzeiliger Ressourcen verträgt eine Seite mit 500 Einträgen; eine Liste üppiger 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.
|
||||
`mcp run` akzeptiert nur einen `MCPServer`, diesen hier stellst du also selbst bereit. Die letzte Zeile von `server.py` baut aus dem `Server` eine gewöhnliche ASGI-App, und uvicorn führt sie aus:
|
||||
|
||||
Rufe `list_resources()` ohne Argumente auf. Du bekommst zehn Ressourcen, `book-1` bis `book-10`, und `next_cursor` ist der String `"10"`.
|
||||
```console
|
||||
uvicorn server:app --port 8000
|
||||
```
|
||||
|
||||
Richte einen beliebigen Client (**[Der Client](../client/index.md)** oder den Inspector) auf `http://localhost:8000/mcp` und 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"`.
|
||||
|
||||
|
|
@ -43,7 +47,7 @@ Die zehnte Seite kommt mit `next_cursor` auf `None` zurück. Fertig.
|
|||
|
||||
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"
|
||||
```python title="client.py" hl_lines="9-15"
|
||||
--8<-- "docs_src/pagination/tutorial002.py"
|
||||
```
|
||||
|
||||
|
|
@ -51,7 +55,7 @@ Jede `list_*`-Methode auf `Client` (`list_tools`, `list_resources`, `list_resour
|
|||
* 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.
|
||||
Während uvicorn weiterhin `server.py` ausliefert, starte in einem zweiten Terminal `python client.py`. 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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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)**.
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)**.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
---
|
||||
translation:
|
||||
sections: [490237e61c3a7a44, 01262a123ad9501d, 429db5b574a2ac08, e2d0d273fbd2d74b, 64ab0331e868f3d4, 6c8878ce2d1f6d56, 4068f23e371bf0b3, eaef75b8725bc931]
|
||||
sections: [4c1dea378b2b1bf7, 01262a123ad9501d, 429db5b574a2ac08, e2d0d273fbd2d74b, 64ab0331e868f3d4, 6c8878ce2d1f6d56, c3d1099701156881, e185a1b8e53669f6]
|
||||
tool: 1
|
||||
---
|
||||
# Veraltete Features {#deprecated-features}
|
||||
|
||||
Die Spec 2026-07-28 mustert fünf Dinge aus. Das SDK implementiert jedes davon weiterhin, und jedes davon trägt jetzt eine **Deprecation-Warnung**. Ein SDK-Helfer ist unabhängig davon veraltet und steht [am Ende](#deprecated-sdk-helpers).
|
||||
Die Spec 2026-07-28 mustert fünf Dinge aus. Das SDK implementiert jedes davon weiterhin, und jedes davon trägt jetzt eine **Deprecation-Warnung**. Ein paar Dinge sind unabhängig davon nur auf SDK-Ebene veraltet; sie stehen [am Ende](#deprecated-sdk-helpers).
|
||||
|
||||
Die Tabelle unten nennt jedes veraltete Feature, den Grund, warum es verschwindet, und den Ersatz, auf dem du aufbauen solltest.
|
||||
|
||||
|
|
@ -138,11 +138,13 @@ Das ist die ganze API. Es gibt keinen Schalter pro Methode, und du willst auch k
|
|||
|
||||
## Veraltete SDK-Helfer {#deprecated-sdk-helpers}
|
||||
|
||||
Das sind keine Spec-Änderungen, sondern nur SDK-Interna mit einem besseren Ersatz. Sie warnen mit derselben `MCPDeprecationWarning` und werden in 3.0 entfernt.
|
||||
Das sind keine Spec-Änderungen, sondern nur Verwendungsweisen des SDK mit einem besseren Ersatz. Sie warnen mit derselben `MCPDeprecationWarning`, und 3.0 entfernt die alte Form.
|
||||
|
||||
| Veraltet | Was du stattdessen tust |
|
||||
|---|---|
|
||||
| `FuncMetadata.call_fn_with_arg_validation()` | `FuncMetadata.validate_arguments()` und danach `FuncMetadata.call_fn()`. Aufgerufen hat es ohnehin nur Code, der `FuncMetadata` direkt ansteuert (etwa eine eigene `Tool`-Unterklasse). |
|
||||
| `AuthSettings(resource_server_url=...)` ohne `validate_token_resource=` | Setze es: Mit `True` lehnt der Server Bearer-Tokens ab, die dein Verifier nicht als für `resource_server_url` ausgestellt meldet; `False` besagt, dass dein Verifier die Audience des Tokens selbst prüft (siehe **[Autorisierung](run/authorization.md#a-token-verifier)**). Nicht gesetzt verhält es sich wie `False`; 3.0 macht `True` zum Standardwert, sobald `resource_server_url` gesetzt ist. |
|
||||
| `ClientCredentialsOAuthProvider(...)` oder `PrivateKeyJWTOAuthProvider(...)` ohne `issuer=` | Übergib `issuer=` und benenne damit den Autorisierungsserver, der die Credentials ausgestellt hat (siehe **[OAuth-Clients schreiben](client/oauth-clients.md#machine-to-machine)**). Ohne ihn entscheidet der MCP-Server, welcher Autorisierungsserver sie erhält; 3.0 macht das Keyword-Argument verpflichtend. |
|
||||
|
||||
## Zusammenfassung {#recap}
|
||||
|
||||
|
|
@ -151,7 +153,7 @@ Das sind keine Spec-Änderungen, sondern nur SDK-Interna mit einem besseren Ersa
|
|||
* Veraltet ist ein Hinweis: keine Änderungen auf der Leitung, alles funktioniert weiterhin gegen Sessions von vor 2026, und du bekommst eine sichtbare `MCPDeprecationWarning` (eine `UserWarning`, also standardmäßig aktiv).
|
||||
* Sampling und Roots brauchen zusätzlich einen Rückkanal, den eine 2026-07-28-Session nicht hat. Auf einer modernen Verbindung warnen sie und werfen dann eine Exception.
|
||||
* `warnings.filterwarnings("ignore", category=MCPDeprecationWarning)` bringt die ganze Kategorie zum Schweigen; `"error::mcp.MCPDeprecationWarning"` in pytest macht daraus einen fehlschlagenden Test.
|
||||
* Ein SDK-Helfer, `FuncMetadata.call_fn_with_arg_validation()`, ist separat veraltet und wird in 3.0 entfernt.
|
||||
* Für die [veralteten SDK-Helfer](#deprecated-sdk-helpers) gilt dieselbe Regel: Sie warnen jetzt, und 3.0 entfernt die alte Form.
|
||||
* Neuer Code sollte auf nichts davon aufbauen.
|
||||
|
||||
Jede andere Seite dieser Dokumentation vermittelt die aktuelle API.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
translation:
|
||||
sections: [0d6c05bcbf836bf3, 59a7b14eeefc68c1, 7114d8d6daba203f, e8bbb56a98ba7bc9, 5138010f6159901c, f78da7c7c363d4c6, 220a939cab348686]
|
||||
sections: [0d6c05bcbf836bf3, 9a78b5f6b44b18ab, 7114d8d6daba203f, e8bbb56a98ba7bc9, bfd2fd1153e71dac, 1615a994ef071fdd, 65c599fae991f245]
|
||||
tool: 1
|
||||
---
|
||||
# Erste Schritte {#first-steps}
|
||||
|
|
@ -17,7 +17,7 @@ Drei Wörter, die dir ab hier auf jeder Seite begegnen:
|
|||
* Ein **Client** lebt im Host und spricht MCP. Der Host betreibt einen Client pro Server, mit dem er verbunden ist.
|
||||
* Ein **Server** ist das, was du mit diesem SDK baust. Er stellt Clients Dinge bereit. Mit dem Modell spricht er nie direkt.
|
||||
|
||||
Du schreibst den Server. Hosts sind das Produkt anderer. Das SDK gibt dir außerdem einen `Client`. Mit ihm testest du deine Server, und er taucht weiter unten auf dieser Seite auf.
|
||||
Du schreibst den Server. Hosts sind das Produkt anderer. Das SDK gibt dir außerdem einen `Client` – dieselbe Klasse, mit der ein Host einen Server per URL erreichen oder als Subprozess starten würde. Er taucht weiter unten auf dieser Seite auf, und mit ihm testest du auch deine Server.
|
||||
|
||||
## Die drei Primitive {#the-three-primitives}
|
||||
|
||||
|
|
@ -83,22 +83,20 @@ Du hast im Inspector drei Tabs gesehen. Woher wusste er, dass es drei sind?
|
|||
|
||||
Wenn sich ein Client verbindet, deklariert der Server seine **Capabilities**: welche Familien von Requests er beantwortet. Der Client entscheidet anhand dieser Deklaration, wonach er überhaupt fragt. Du hast sie nie geschrieben; `MCPServer` deklariert sie für dich.
|
||||
|
||||
Sieh es dir selbst an. Der `Client` des SDK nimmt das Server-Objekt direkt entgegen und verbindet sich **im Speicher** damit (kein Subprozess, kein Port):
|
||||
Sieh es dir selbst an. Lass `server.py` in einem Terminal über HTTP laufen:
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
```console
|
||||
uv run mcp run server.py --transport streamable-http
|
||||
```
|
||||
|
||||
from mcp import Client
|
||||
und richte aus einem anderen Terminal einen Client darauf:
|
||||
|
||||
from server import mcp
|
||||
```python title="client.py" hl_lines="7-8"
|
||||
--8<-- "docs_src/first_steps/tutorial001_client.py"
|
||||
```
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
async with Client(mcp) as client:
|
||||
print(client.server_capabilities.model_dump(exclude_none=True))
|
||||
|
||||
|
||||
asyncio.run(main())
|
||||
```console
|
||||
python client.py
|
||||
```
|
||||
|
||||
```text
|
||||
|
|
@ -115,11 +113,12 @@ Dieses Dictionary sind die deklarierten **Capabilities** deines Servers. Es ist
|
|||
|
||||
`MCPServer` bedient alle drei Primitive, also werden immer alle drei deklariert.
|
||||
|
||||
Achte darauf, was fehlt. `completions` (die automatische Vervollständigung von Argumenten für Ressourcen-Templates und Prompts) braucht einen Handler, den du schreibst. Dieser Server hat keinen, also fehlt die Capability, und ein wohlerzogener Client fragt gar nicht erst. Das ist die Regel für alles Optionale: Registriere das Ding, und die Capability erscheint; **[Vervollständigungen](../servers/completions.md)** zeigt es.
|
||||
Achte darauf, was fehlt. `completions` (die automatische Vervollständigung von Argumenten für Ressourcen-Templates und Prompts) braucht einen Handler, den du schreibst. Dieser Server hat keinen, also fehlt die Capability, und ein Client, der sich korrekt verhält, fragt gar nicht erst. Das ist die Regel für alles Optionale: Registriere das Ding, und die Capability erscheint; **[Vervollständigungen](../servers/completions.md)** zeigt es.
|
||||
|
||||
!!! info
|
||||
`Client(mcp)` ist derselbe In-Memory-Client, mit dem jedes Beispiel in dieser Dokumentation
|
||||
getestet wird, und so testest du auch deine. Er bekommt eine ganze Seite: **[Testen](testing.md)**.
|
||||
Dieses `client.py` ist ein vollständiger MCP-Client, und **[Der Client](../client/index.md)** ist seine Seite.
|
||||
In einem Test sparst du dir das Terminal und den Port und übergibst `Client` das Server-Objekt
|
||||
selbst: `Client(mcp)`. Auch das bekommt eine ganze Seite: **[Testen](testing.md)**.
|
||||
|
||||
## Was du nicht geschrieben hast {#what-you-did-not-write}
|
||||
|
||||
|
|
@ -128,7 +127,7 @@ Blick auf diese Seite zurück. Du hast drei kleine Python-Funktionen geschrieben
|
|||
* Ein JSON-Schema. `a: int, b: int` *ist* das Schema für `add`.
|
||||
* Einen Request-Handler. `tools/list`, `resources/read`, `prompts/get`: alles für dich bedient.
|
||||
* Eine Capability-Deklaration. `MCPServer` hat sie für dich erstellt.
|
||||
* Eine Zeile Protokoll. Die Versionsaushandlung, das JSON-RPC-Framing, der Austausch der Capabilities: Das alles passierte in `mcp dev` und `Client(mcp)`, und du hast es nie gesehen.
|
||||
* Eine Zeile Protokoll. Die Versionsaushandlung, das JSON-RPC-Framing, der Austausch der Capabilities: Das alles passierte in `mcp dev` und `client.py`, und du hast es nie gesehen.
|
||||
|
||||
Dieses Verhältnis ist der ganze Sinn des SDK.
|
||||
|
||||
|
|
@ -139,6 +138,6 @@ Dieses Verhältnis ist der ganze Sinn des SDK.
|
|||
* Ein Dekorator pro Primitiv: `@mcp.tool()`, `@mcp.resource(uri)`, `@mcp.prompt()`. Name, Beschreibung und Schema kommen aus der Funktion.
|
||||
* Ein URI mit einem `{param}` ergibt ein Ressourcen-**Template**, das getrennt von konkreten Ressourcen aufgelistet wird.
|
||||
* Die **Capabilities** des Servers werden für dich deklariert, und ein Client fragt nur nach dem, was ein Server deklariert.
|
||||
* `Client(mcp)` verbindet sich im Speicher mit dem Server-Objekt: deine Testumgebung vom ersten Tag an.
|
||||
* `Client("http://localhost:8000/mcp")` spricht mit deinem laufenden Server. Übergib ihm stattdessen das Server-Objekt, `Client(mcp)`, und er ist deine Testumgebung vom ersten Tag an.
|
||||
|
||||
Als Nächstes kommt **[Mit einem echten Host verbinden](real-host.md)**: dieser Server in Claude Desktop oder einer IDE, in echt. Danach **[Testen](testing.md)**: eine Seite, ein In-Memory-Client, und du musst nie raten, ob es funktioniert. Danach bekommt jedes Primitiv seine eigene Seite, angefangen mit dem, das das Modell steuert: **[Tools](../servers/tools.md)**.
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
---
|
||||
translation:
|
||||
sections: ['4926721070127497', c52a1de2b6b32f40, 8e792bf8c7489ec6, 627195f7159e24ef]
|
||||
sections: [5d13c2f0ba42c0d2, c52a1de2b6b32f40, 8e792bf8c7489ec6, 38552ea228b0a04f]
|
||||
tool: 1
|
||||
---
|
||||
# Testen {#testing}
|
||||
|
||||
Das Python SDK bringt eine Klasse `Client` mit einem **In-Memory-Transport** mit: Übergib ihr dein Server-Objekt, und sie verbindet sich direkt damit.
|
||||
Die Klasse `Client` des SDK – dieselbe, die sich mit einer URL verbindet oder einen Subprozess startet – verbindet sich auch **In-Memory**: Übergib ihr dein Server-Objekt, und sie spricht direkt mit ihm.
|
||||
|
||||
Kein Subprozess. Kein Port. Überhaupt kein Transport. Die Idee ist dieselbe wie bei FastAPIs `TestClient`.
|
||||
Kein Subprozess. Kein Port. Nichts auf der Leitung. Die Idee ist dieselbe wie bei FastAPIs `TestClient`.
|
||||
|
||||
## Grundlegende Verwendung {#basic-usage}
|
||||
|
||||
|
|
@ -97,12 +97,12 @@ statt der bereinigten.
|
|||
|
||||
Lass es in Tests eingeschaltet. In Produktionscode hat es keine Bedeutung.
|
||||
|
||||
## Standardmäßig im selben Prozess {#in-process-by-default}
|
||||
## Standardmäßig generationsneutral {#era-neutral-by-default}
|
||||
|
||||
!!! note
|
||||
`Client(mcp)` verbindet sich im selben Prozess und ist standardmäßig **generationsneutral** (era-neutral): Er prüft den Server und
|
||||
wählt den passenden Protokollpfad. Lege `mode="legacy"` fest, wenn dein Test Legacy-spezifische
|
||||
Semantik prüft (Sampling- oder Elicitation-Push – Elicitation ist die Rückfrage bei der Person am Host –, `message_handler`), und lass `raise_exceptions=True`
|
||||
Semantik prüft (Push von Sampling oder Elicitation – Rückfrage bei der Person am Host –, `message_handler`), und lass `raise_exceptions=True`
|
||||
dort weg: Eine Legacy-Verbindung bereinigt von vornherein nie, und das Flag löst den
|
||||
Fehler erneut in der Server-Task aus statt in deinem Test.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
translation:
|
||||
sections: [74011e683045eea9, 9b64cc175c18b6a9, 4b41be4824030397, e3b1502da786ec33, 71e41161f143c6a9, 9ec2c1eeb8c36378, 8dd027377d46448b, f81491125dcbfe8b]
|
||||
sections: [74011e683045eea9, 9b64cc175c18b6a9, 4b41be4824030397, e3b1502da786ec33, 71e41161f143c6a9, 9ec2c1eeb8c36378, b47667184ca5b516, f81491125dcbfe8b]
|
||||
tool: 1
|
||||
---
|
||||
# Multi-Roundtrip-Requests {#multi-round-trip-requests}
|
||||
|
|
@ -164,7 +164,7 @@ Der Low-Level-`Server` ist die Stufe ohne Extras: Anders als bei `MCPServer` wir
|
|||
|
||||
## Ein Ergebnis für 2026-07-28 {#a-2026-07-28-result}
|
||||
|
||||
`InputRequiredResult` gibt es nur bei Protokollversion **2026-07-28**. Der In-Memory-`Client(server)` handelt sie für dich aus; über die Leitung entdeckt `mode="auto"` sie. Nach dem Verbinden sagt dir `client.protocol_version`, was du bekommen hast.
|
||||
`InputRequiredResult` gibt es nur bei Protokollversion **2026-07-28**. `mode="auto"`, der Standardwert von `Client`, entdeckt sie auf jeder Verbindung. Nach dem Verbinden sagt dir `client.protocol_version`, was du bekommen hast.
|
||||
|
||||
!!! warning
|
||||
Eine Session vor 2026 hat keinen Platz für ein `InputRequiredResult`. Gibst du eines aus deinem Handler auf einer
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
translation:
|
||||
sections: [5315262fe26b33e1, 9d8e98840f1b78f0, 0284b215e85366c4, 8534d8dbb4053a70, 2966fac6fe697007]
|
||||
sections: [5315262fe26b33e1, 9d8e98840f1b78f0, 52d6009a07e770ea, 8534d8dbb4053a70, 2966fac6fe697007]
|
||||
tool: 1
|
||||
---
|
||||
# Fortschritt {#progress}
|
||||
|
|
@ -29,19 +29,17 @@ Drei Argumente, und du bestimmst, was sie bedeuten:
|
|||
|
||||
Der Client meldet sich **pro Aufruf** an, indem er `progress_callback=` an `call_tool` übergibt:
|
||||
|
||||
```python title="client.py" hl_lines="7 16"
|
||||
```python title="client.py" hl_lines="5 14"
|
||||
import anyio
|
||||
from mcp import Client
|
||||
|
||||
from server import mcp
|
||||
|
||||
|
||||
async def show(progress: float, total: float | None, message: str | None) -> None:
|
||||
print(f"{message} ({progress}/{total})")
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
async with Client(mcp) as client:
|
||||
async with Client("http://localhost:8000/mcp") as client:
|
||||
result = await client.call_tool(
|
||||
"import_catalog",
|
||||
{"urls": ["https://example.com/a.json", "https://example.com/b.json"]},
|
||||
|
|
@ -56,28 +54,31 @@ anyio.run(main)
|
|||
Der Callback ist eine `async`-Funktion, die genau das entgegennimmt, was der Server gemeldet hat: `progress`, `total`, `message`.
|
||||
|
||||
!!! info
|
||||
`Client(mcp)` verbindet sich direkt mit dem Server-Objekt, im Speicher – derselbe Client, auf dem die Seite
|
||||
**[Testen](../get-started/testing.md)** aufbaut. `progress_callback` ist derselbe Parameter, egal welchen
|
||||
Transport der `Client` nutzt; das *Timing*, das du gleich siehst, ist das der In-Memory-Verbindung. Sie führt
|
||||
deinen Callback inline aus, sodass jede Meldung eintrifft, bevor `call_tool` zurückkehrt. Über einen echten
|
||||
Transport liefern sich die Benachrichtigungen ein Rennen mit dem Ergebnis, und ein langsamer Callback kann noch
|
||||
laufen, nachdem `call_tool` bereits zurückgekehrt ist.
|
||||
`progress_callback` ist derselbe Parameter, egal was du `Client` übergeben hast: eine URL wie hier, ein
|
||||
`StdioServerParameters`-Objekt oder das Server-Objekt in einem Test. Achte über einen echten Transport
|
||||
allerdings auf das Timing. Jede Benachrichtigung wird für sich zugestellt, neben der Response, sodass ein
|
||||
langsamer Callback noch laufen kann, nachdem `call_tool` bereits zurückgekehrt ist. Nur die
|
||||
In-Process-Testverbindung führt den Callback inline aus und garantiert, dass jede Meldung vorher eintrifft.
|
||||
|
||||
### Ausprobieren {#try-it}
|
||||
|
||||
Lege `client.py` neben `server.py` und starte es:
|
||||
Stelle `server.py` über HTTP bereit und starte dann den Client aus einem zweiten Terminal:
|
||||
|
||||
```console
|
||||
uv run mcp run server.py --transport streamable-http
|
||||
```
|
||||
|
||||
```console
|
||||
python client.py
|
||||
```
|
||||
|
||||
```text
|
||||
Imported https://example.com/a.json (1/2)
|
||||
Imported https://example.com/b.json (2/2)
|
||||
Imported https://example.com/a.json (1.0/2.0)
|
||||
Imported https://example.com/b.json (2.0/2.0)
|
||||
{'result': 'Imported 2 records.'}
|
||||
```
|
||||
|
||||
Jedes `await ctx.report_progress(...)` auf dem Server wurde zu einem Aufruf von `show` auf dem Client, in derselben Reihenfolge, und beide Zeilen wurden ausgegeben, **bevor** `call_tool` zurückkehrte. Fortschritt wird nicht ins Ergebnis gepackt; er streamt, während das Tool noch arbeitet.
|
||||
Jedes `await ctx.report_progress(...)` auf dem Server wurde zu einem Aufruf von `show` auf dem Client, in derselben Reihenfolge. Fortschritt wird nicht ins Ergebnis gepackt. Er streamt, während das Tool noch arbeitet.
|
||||
|
||||
!!! warning
|
||||
`progress_callback` gehört zum **Aufruf**, nicht zum `Client`. Es gibt kein Konstruktorargument dafür,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
translation:
|
||||
sections: [478fd619e5f90ef8, aef094a00e44e248, bab8cbf3449fa7e9, df1809b15a58335b, 5f9d8c2336ed0239, f54974398e43ddef, b24443dd78584870]
|
||||
sections: [d98e337bf28845e0, dd642acbba36f615, f547b3e76da19c94, 149513378588da5c, 4e149ed992046709, f54974398e43ddef, b24443dd78584870]
|
||||
tool: 1
|
||||
---
|
||||
# Protokollversionen {#protocol-versions}
|
||||
|
|
@ -11,9 +11,17 @@ Server, die vor 2026-07-28 veröffentlicht wurden, eröffnen jede Verbindung mit
|
|||
|
||||
Darum musst du dich fast nie kümmern, denn `Client` handelt das für dich aus. Diese Seite behandelt das eine Konstruktorargument, das es steuert, `mode=`, und die drei Fälle, in denen du es änderst.
|
||||
|
||||
Jedes Snippet auf dieser Seite ist eine `client.py`, die mit der Bookshop-`server.py` aus **[Der Client](client/index.md)** spricht. Starte diesen Server in einem Terminal:
|
||||
|
||||
```console
|
||||
uv run mcp run server.py --transport streamable-http
|
||||
```
|
||||
|
||||
Führe dann jedes Snippet in einem zweiten Terminal mit `python client.py` aus.
|
||||
|
||||
## `mode="auto"` {#modeauto}
|
||||
|
||||
```python title="client.py" hl_lines="14-15"
|
||||
```python title="client.py" hl_lines="7-8"
|
||||
--8<-- "docs_src/protocol_versions/tutorial001.py"
|
||||
```
|
||||
|
||||
|
|
@ -31,13 +39,14 @@ So oder so bist du am Ende verbunden, und `client.protocol_version` sagt dir, we
|
|||
Das ist das ganze Feature. Ein `Client`, Server jeder Generation, keine Verzweigung in deinem Code.
|
||||
|
||||
!!! info
|
||||
`MCPServer` beantwortet `server/discover` auf jedem Transport – In-Memory, stdio, Streamable
|
||||
HTTP –, sodass `auto` gegen deinen eigenen Server immer bei `2026-07-28` landet. Der Fallback
|
||||
greift nur gegen einen echten Server von vor 2026, und genau dann willst du ihn auch.
|
||||
`MCPServer` beantwortet `server/discover` auf jedem Transport – Streamable HTTP, stdio und die
|
||||
In-Process-Verbindung, die deine Tests verwenden –, sodass `auto` gegen deinen eigenen Server
|
||||
immer bei `2026-07-28` landet. Der Fallback greift nur gegen einen echten Server von vor 2026,
|
||||
und genau dann willst du ihn auch.
|
||||
|
||||
## `mode="legacy"` {#modelegacy}
|
||||
|
||||
```python title="client.py" hl_lines="14"
|
||||
```python title="client.py" hl_lines="7"
|
||||
--8<-- "docs_src/protocol_versions/tutorial002.py"
|
||||
```
|
||||
|
||||
|
|
@ -61,7 +70,7 @@ Bei 2026-07-28 ist er weg. Der Server *gibt* seine Fragen *zurück*, und du wied
|
|||
|
||||
`mode` akzeptiert auch den String einer modernen Protokollversion. Heute ist diese Menge genau `["2026-07-28"]`.
|
||||
|
||||
```python title="client.py" hl_lines="14"
|
||||
```python title="client.py" hl_lines="7"
|
||||
--8<-- "docs_src/protocol_versions/tutorial003.py"
|
||||
```
|
||||
|
||||
|
|
@ -94,7 +103,7 @@ Die Sondierung ist billig, aber sie bleibt ein Roundtrip, den du bei jedem Neuve
|
|||
|
||||
Also heb sie auf. Nach einer `auto`-Verbindung enthält `client.session.discover_result` genau das `DiscoverResult`, das der Server gesendet hat: seine `supported_versions`, seine `capabilities`, seine `instructions` und die Identität, die der Server in das `_meta` des Ergebnisses gestempelt hat. Gib es beim nächsten Mal als `prior_discover=` zurück:
|
||||
|
||||
```python title="client.py" hl_lines="15 17"
|
||||
```python title="client.py" hl_lines="8 10"
|
||||
--8<-- "docs_src/protocol_versions/tutorial004.py"
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)**.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue