diff --git a/i18n/README.md b/i18n/README.md index 126f8bee03..948e95cc14 100644 --- a/i18n/README.md +++ b/i18n/README.md @@ -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 `/instructions.md` (the sections the `pt` file has) and `/glossary.json`, then run `translate --lang `. diff --git a/i18n/de/pages/advanced/apps.md b/i18n/de/pages/advanced/apps.md index 2df9303cec..266de0496d 100644 --- a/i18n/de/pages/advanced/apps.md +++ b/i18n/de/pages/advanced/apps.md @@ -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. diff --git a/i18n/de/pages/advanced/extensions.md b/i18n/de/pages/advanced/extensions.md index 79745141de..c47c7ae37d 100644 --- a/i18n/de/pages/advanced/extensions.md +++ b/i18n/de/pages/advanced/extensions.md @@ -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} diff --git a/i18n/de/pages/advanced/low-level-server.md b/i18n/de/pages/advanced/low-level-server.md index a84bc12e4e..0158d393fa 100644 --- a/i18n/de/pages/advanced/low-level-server.md +++ b/i18n/de/pages/advanced/low-level-server.md @@ -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)**. diff --git a/i18n/de/pages/advanced/pagination.md b/i18n/de/pages/advanced/pagination.md index a082b294a0..8b84af1074 100644 --- a/i18n/de/pages/advanced/pagination.md +++ b/i18n/de/pages/advanced/pagination.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. diff --git a/i18n/de/pages/client/caching.md b/i18n/de/pages/client/caching.md index 8860243502..43ce2df44b 100644 --- a/i18n/de/pages/client/caching.md +++ b/i18n/de/pages/client/caching.md @@ -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)**. diff --git a/i18n/de/pages/client/callbacks.md b/i18n/de/pages/client/callbacks.md index ba7103a335..57d3216d4a 100644 --- a/i18n/de/pages/client/callbacks.md +++ b/i18n/de/pages/client/callbacks.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. diff --git a/i18n/de/pages/client/index.md b/i18n/de/pages/client/index.md index 4a5a9361c2..a3ce8240de 100644 --- a/i18n/de/pages/client/index.md +++ b/i18n/de/pages/client/index.md @@ -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 +``` + +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: -* `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. +```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. diff --git a/i18n/de/pages/client/oauth-clients.md b/i18n/de/pages/client/oauth-clients.md index bc2a86dc23..d0b32ce805 100644 --- a/i18n/de/pages/client/oauth-clients.md +++ b/i18n/de/pages/client/oauth-clients.md @@ -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)**. diff --git a/i18n/de/pages/client/session-groups.md b/i18n/de/pages/client/session-groups.md index e94089b2f0..0f34e596ee 100644 --- a/i18n/de/pages/client/session-groups.md +++ b/i18n/de/pages/client/session-groups.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. diff --git a/i18n/de/pages/client/transports.md b/i18n/de/pages/client/transports.md index b605938f45..75de6f810f 100644 --- a/i18n/de/pages/client/transports.md +++ b/i18n/de/pages/client/transports.md @@ -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. diff --git a/i18n/de/pages/deprecated.md b/i18n/de/pages/deprecated.md index 0887f2a8ca..0655c7ec0e 100644 --- a/i18n/de/pages/deprecated.md +++ b/i18n/de/pages/deprecated.md @@ -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. diff --git a/i18n/de/pages/get-started/first-steps.md b/i18n/de/pages/get-started/first-steps.md index e841e02b2a..d9e2ba0dd5 100644 --- a/i18n/de/pages/get-started/first-steps.md +++ b/i18n/de/pages/get-started/first-steps.md @@ -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): - -```python -import asyncio - -from mcp import Client - -from server import mcp +Sieh es dir selbst an. Lass `server.py` in einem Terminal über HTTP laufen: +```console +uv run mcp run server.py --transport streamable-http +``` -async def main() -> None: - async with Client(mcp) as client: - print(client.server_capabilities.model_dump(exclude_none=True)) +und richte aus einem anderen Terminal einen Client darauf: +```python title="client.py" hl_lines="7-8" +--8<-- "docs_src/first_steps/tutorial001_client.py" +``` -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)**. diff --git a/i18n/de/pages/get-started/testing.md b/i18n/de/pages/get-started/testing.md index f550140247..7accf716e6 100644 --- a/i18n/de/pages/get-started/testing.md +++ b/i18n/de/pages/get-started/testing.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. diff --git a/i18n/de/pages/handlers/multi-round-trip.md b/i18n/de/pages/handlers/multi-round-trip.md index 37737b09e3..159fd89627 100644 --- a/i18n/de/pages/handlers/multi-round-trip.md +++ b/i18n/de/pages/handlers/multi-round-trip.md @@ -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 diff --git a/i18n/de/pages/handlers/progress.md b/i18n/de/pages/handlers/progress.md index cdefdfabc9..77e947a2ac 100644 --- a/i18n/de/pages/handlers/progress.md +++ b/i18n/de/pages/handlers/progress.md @@ -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, diff --git a/i18n/de/pages/protocol-versions.md b/i18n/de/pages/protocol-versions.md index 887ed4abba..b425073036 100644 --- a/i18n/de/pages/protocol-versions.md +++ b/i18n/de/pages/protocol-versions.md @@ -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" ``` diff --git a/i18n/de/pages/run/asgi.md b/i18n/de/pages/run/asgi.md index 23750a5c7d..ab304496ef 100644 --- a/i18n/de/pages/run/asgi.md +++ b/i18n/de/pages/run/asgi.md @@ -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. diff --git a/i18n/de/pages/run/authorization.md b/i18n/de/pages/run/authorization.md index 3cd5d35a4c..d717103c09 100644 --- a/i18n/de/pages/run/authorization.md +++ b/i18n/de/pages/run/authorization.md @@ -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)**. diff --git a/i18n/de/pages/run/deploy.md b/i18n/de/pages/run/deploy.md index ffb06c0447..66e451d118 100644 --- a/i18n/de/pages/run/deploy.md +++ b/i18n/de/pages/run/deploy.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='' +``` + +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. diff --git a/i18n/de/pages/run/index.md b/i18n/de/pages/run/index.md index 54e65f7ee5..031fbdd524 100644 --- a/i18n/de/pages/run/index.md +++ b/i18n/de/pages/run/index.md @@ -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 diff --git a/i18n/de/pages/run/legacy-clients.md b/i18n/de/pages/run/legacy-clients.md index 63fdbca6b4..e0606aeef0 100644 --- a/i18n/de/pages/run/legacy-clients.md +++ b/i18n/de/pages/run/legacy-clients.md @@ -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 idle timeout` auf +`INFO`. Ein abgelehntes Öffnen als `Refusing to open a new session: 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. diff --git a/i18n/de/pages/troubleshooting.md b/i18n/de/pages/troubleshooting.md index 84c61bb842..dc98239cc3 100644 --- a/i18n/de/pages/troubleshooting.md +++ b/i18n/de/pages/troubleshooting.md @@ -1,18 +1,24 @@ --- translation: - sections: [2efaecdef109a5c5, fcacd3e66b8635a4, 25323d737dcf0261, 8a6e351ec756904d, 137454d469c867f5, 6392596bd6df54f0, 41126fa9c4fe432f, 480b6d7897e30ab4, d83bb682e708dde0, ebbed3449c499db4, 323ef84f6b4bebde, 30fd31be74169d9a, 656943c6cb567218, c2dc3b1007d2e987, 7cf5386b997d04e9, 0b59feed8384456e, 0cba47bae78d04eb, e4355f4c7cf4fb2e] + sections: [3d58228e81b99543, 170514ce901c4139, 17d61fad0a50d62b, 8a6e351ec756904d, 137454d469c867f5, 6392596bd6df54f0, 41126fa9c4fe432f, 480b6d7897e30ab4, d83bb682e708dde0, ebbed3449c499db4, 525cdf1755e29d4c, 30fd31be74169d9a, d2e88333d4f7841f, c2dc3b1007d2e987, d6eabf60cc366341, f798e815252852c2, 0cba47bae78d04eb, 2c218ba829abf74e] tool: 1 --- # Fehlerbehebung {#troubleshooting} Jede Überschrift auf dieser Seite ist der exakte Text eines Fehlers, den das SDK erzeugt, gefolgt davon, was er bedeutet, und der Lösung in einem Schritt. Suche die letzte Zeile deines Tracebacks (oder deines Server-Logs) hier mit der Seitensuche des Browsers und lies nur diesen Eintrag. -Mehrere Einträge laufen gegen diesen einen Server: ein Tool und eine Ressource mit Template, die beide bei einer Stadt, die sie nicht kennen, eine Exception auslösen: +Mehrere Einträge laufen gegen diesen einen Server. Ein Tool und eine Ressource mit Template, die beide bei einer Stadt, die sie nicht kennen, eine Exception auslösen: ```python title="server.py" --8<-- "docs_src/troubleshooting/tutorial001.py" ``` +Diese Einträge erreichen ihn unter `http://localhost:8000/mcp`, lass ihn also über HTTP laufen: + +```console +uv run mcp run server.py --transport streamable-http +``` + Die Fehler, die diese Seite zitiert, sind echt: Die Testsuite des SDK selbst reproduziert jeden einzelnen. ## `ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception)` {#exceptiongroup-unhandled-errors-in-a-taskgroup-1-sub-exception} @@ -23,7 +29,7 @@ Das ist kein MCP-Fehler. Es ist Rauschen von anyio, und dein eigentlicher Fehler ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: await client.read_resource("weather://Atlantis") ``` @@ -49,7 +55,7 @@ Damit machst du zwei Dinge: ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: try: await client.read_resource("weather://Atlantis") except MCPError as e: @@ -67,7 +73,7 @@ async def main() -> None: ```python async def main() -> None: - client = Client(mcp) + client = Client("http://localhost:8000/mcp") tools = await client.list_tools() # RuntimeError ``` @@ -75,7 +81,7 @@ Betritt den Kontextmanager. `__aenter__` ist die Verbindung: ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: tools = await client.list_tools() ``` @@ -251,7 +257,7 @@ app = Starlette(routes=[Mount("/", app=mcp.streamable_http_app())], lifespan=lif ## `MCPError: Session not found` {#mcperror-session-not-found} -Der Server erkennt die `Mcp-Session-Id`, die dein Client gesendet hat, nicht – fast immer, weil der Server **neu gestartet** wurde (oder du zu einer anderen Instanz geroutet wurdest). Sessions leben im Speicher dieses einen Prozesses. +Der Server erkennt die `Mcp-Session-Id`, die dein Client gesendet hat, nicht. Entweder wurde der Server **neu gestartet** (oder du wurdest zu einer anderen Instanz geroutet), oder die Session ist **abgelaufen**, weil für die Dauer von `session_idle_timeout` – standardmäßig 30 Minuten – nichts unterwegs war. Siehe [Lebensdauer und Limits von Sessions](run/legacy-clients.md#session-lifetime-and-limits). Sessions leben im Speicher dieses einen Prozesses. Es gibt keinen Server-Bug zu finden. Die HTTP-Response ist ein `404`, dessen Body JSON-RPC *ist*, deshalb zeigt dir der Python-`Client` – anders als beim `421` oben – diese hier wörtlich: @@ -261,9 +267,9 @@ Es gibt keinen Server-Bug zu finden. Die HTTP-Response ist ein `404`, dessen Bod Die Lösung ist, dich neu zu verbinden: Verlasse den `async with Client(...)`-Block und betritt einen neuen, der eine frische Session aushandelt. Für einen langlebigen Client heißt das, `MCPError` um deine Aufrufe herum abzufangen und bei dieser Meldung neu zu verbinden, statt es in einer toten Session erneut zu versuchen. -Passiert es *ohne* Neustart, betreibst du mehr als einen Worker ohne Sticky Sessions: Jeder Worker hält seine eigene Session-Tabelle, sodass ein Request, der zum falschen geroutet wird, hier landet. **[Bereitstellen und skalieren](run/deploy.md)** und **[Legacy-Clients unterstützen](run/legacy-clients.md)** behandeln dieses Thema und seine beiden Lösungen (Sticky Routing oder `stateless_http=True`). +Passiert es *ohne* Neustart und ohne dass der Client so lange still war, betreibst du mehr als einen Worker ohne Sticky Sessions: Jeder Worker hält seine eigene Session-Tabelle, sodass ein Request, der zum falschen geroutet wird, hier landet. **[Bereitstellen und skalieren](run/deploy.md)** und **[Legacy-Clients unterstützen](run/legacy-clients.md)** behandeln dieses Thema und seine beiden Lösungen (Sticky Routing oder `stateless_http=True`). -Für alle, die den Server betreiben, lautet die passende Log-Zeile `Rejected request with unknown or expired session ID: `. Sie wird auf `INFO` geloggt, ist also bei der üblichen `WARNING`-Schwelle unsichtbar. Sie direkt nach einem Deployment stoßweise zu sehen ist normal; jeder verbundene Client verbindet sich neu. +Für alle, die den Server betreiben, lautet die passende Log-Zeile `Rejected request with unknown or expired session ID: `. Sie wird auf `INFO` geloggt, ist also bei der üblichen `WARNING`-Schwelle unsichtbar. Sie direkt nach einem Deployment stoßweise zu sehen ist normal; jeder verbundene Client verbindet sich neu. Ist die Session stattdessen abgelaufen, steht vor dieser Zeile `Session idle timeout`, ebenfalls auf `INFO`. ## `MCPError: Method not found` {#mcperror-method-not-found} @@ -275,7 +281,13 @@ Eines erzeugt diesen Fehler **nicht**, obwohl es ein Request ist, den das modern Dein Server möchte die Person am Host etwas fragen, und dieser Client hat nie gesagt, dass man ihn fragen kann. -Ein Resolver für Elicitation (Rückfrage bei der Person am Host) lehnt von vornherein ab, wenn der verbundene Client keine Form-Elicitation deklariert hat, und `e.error.data` nennt genau, was fehlt: +Dieses Bistro fragt über einen Resolver zurück, bevor es bucht: + +```python title="server.py" hl_lines="15-17 21" +--8<-- "docs_src/troubleshooting/tutorial007.py" +``` + +Betreibe es anstelle des Weather-Servers und rufe `book_table` von einem Client aus auf, der keinen `elicitation_callback` übergeben hat. Der Resolver lehnt von vornherein ab, weil der verbundene Client nie Form-Elicitation (Rückfrage bei der Person am Host) deklariert hat, und `e.error.data` nennt genau, was fehlt: ```json { @@ -289,7 +301,7 @@ Ein Resolver für Elicitation (Rückfrage bei der Person am Host) lehnt von vorn ```python async def main() -> None: - async with Client(mcp, elicitation_callback=handle_elicitation) as client: + async with Client("http://localhost:8000/mcp", elicitation_callback=handle_elicitation) as client: result = await client.call_tool("book_table", {"date": "Friday"}) ``` @@ -315,14 +327,14 @@ Du siehst diese bei `ctx.elicit()` auf einer Legacy-Verbindung, und auf jeder be Dein Handler hat versucht, den Client mitten im Request zu erreichen, auf einer Verbindung, deren Aufruf keinen Rückkanal (back-channel) hat, der einen Request vom Server tragen kann. Drei Server-Konfigurationen bringen einen Aufruf in diese Lage. -**Eine `2026-07-28`-Verbindung: jeder Transport, immer.** Das moderne Protokoll kennt überhaupt keine vom Server initiierten Requests, deshalb weigert sich der Server, bevor irgendetwas gesendet wird. `ctx.elicit()` innerhalb eines Tools ist der klassische Weg, dem zu begegnen (schon beim allerersten In-Memory-Test, denn `Client(server)` handelt ungefragt `2026-07-28` aus), und `elicitation_callback=` zu übergeben ändert nichts, weil nie ein Request beim Client ankommt, den er beantworten könnte: +**Eine `2026-07-28`-Verbindung: jeder Transport, immer.** Das moderne Protokoll kennt überhaupt keine vom Server initiierten Requests, deshalb weigert sich der Server, bevor irgendetwas gesendet wird. `ctx.elicit()` innerhalb eines Tools ist der klassische Weg, dem zu begegnen, meist im allerersten In-Memory-**[Test](get-started/testing.md)** dieses Tools, denn `Client(mcp)` handelt ungefragt `2026-07-28` aus. `elicitation_callback=` zu übergeben ändert nichts, weil nie ein Request beim Client ankommt, den er beantworten könnte: ```python title="server.py" hl_lines="16" --8<-- "docs_src/troubleshooting/tutorial006.py" ``` ```python -async def main() -> None: +async def test_book_table() -> None: async with Client(mcp) as client: await client.call_tool("book_table", {"date": "Friday"}) ``` @@ -364,7 +376,7 @@ Der Server konnte das `requestState`-Token, das dein Client zurückgespielt hat, ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: await client.call_tool("forecast", {"city": "London"}, request_state="round-1-from-worker-a") ``` @@ -418,7 +430,7 @@ mcp = MCPServer("Weather", request_state_security=RequestStateSecurity(keys=[key * `Tool already exists:` im Server-Log ist das einzige Zeichen, dass zwei gleichnamige Tools zu einem zusammengefallen sind. * Ein 421, drei Schreibweisen: `Server returned an error response` (der Python-`Client`), `421 Misdirected Request` / `Invalid Host header` (alles andere), `Invalid Host header: ` (das Server-Log). Lösung: `transport_security=TransportSecuritySettings(allowed_hosts=[...])`. * `Task group is not initialized` -> eine eingehängte App, deren Host-Lifespan nie `mcp.session_manager.run()` betreten hat. -* `Session not found` -> der Server wurde neu gestartet; verbinde dich neu. +* `Session not found` -> der Server wurde neu gestartet oder die Session ist abgelaufen (`session_idle_timeout`); verbinde dich neu. * `Cannot send 'elicitation/create': ... no back-channel ...` -> `ctx.elicit()` braucht einen Kanal vom Server zum Client: Eine `2026-07-28`-Verbindung hat nie einen, `stateless_http=True` nimmt den Legacy-Kanal weg, und `json_response=True` nimmt den Request-gebundenen weg. Verwende einen Resolver (ein Legacy-Client braucht außerdem einen Server, der den Kanal behält). Sein Nachbar `Method not found` ist ein Request für eine Methode, die die Protokollrevision der anderen Seite nicht hat. * `Client did not declare the form elicitation capability ...` und `Elicitation not supported` -> dem Client fehlt `elicitation_callback=`. * `Invalid or expired requestState` sagt auf der Leitung nie, warum. Das Server-Log schon; `unknown key` heißt: Teile `RequestStateSecurity(keys=[...])` über alle Worker hinweg. diff --git a/i18n/de/pages/whats-new.md b/i18n/de/pages/whats-new.md index a45f5a5918..44b57adfbc 100644 --- a/i18n/de/pages/whats-new.md +++ b/i18n/de/pages/whats-new.md @@ -1,6 +1,6 @@ --- translation: - sections: [cfe01c0c5863dfa2, 1c58c5cfcc37d455, a7392996acf1ad8f, 875eb2889263424e] + sections: [cfe01c0c5863dfa2, 0dbb68d8b210177b, 80cf193023af3ed4, 875eb2889263424e] tool: 1 --- # Was ist neu in v2 {#whats-new-in-v2} @@ -42,11 +42,11 @@ Nicht alles, was ein Tool braucht, sollte vom Modell kommen. Neu in v2: Ein Tool v1 gab dir drei verschachtelte Schichten: einen Transport-Kontextmanager, der rohe Streams liefert, eine darum gewickelte `ClientSession` und ein von Hand aufgerufenes `await session.initialize()`. v2 hat ein einziges Objekt: -```python title="client.py" hl_lines="14-18" ---8<-- "docs_src/client/tutorial001.py" +```python title="client.py" hl_lines="7-11" +--8<-- "docs_src/client/tutorial001_client.py" ``` -`Client` nimmt ein Server-Objekt (im Speicher, ohne Transport: das ist der Testansatz), eine URL (Streamable HTTP), ein `StdioServerParameters` (ein stdio-Subprozess) oder einen beliebigen anderen Transport-Kontextmanager wie `sse_client(...)`. Das Betreten von `async with` verbindet und handelt die Protokollversion aus, welche Generation der Server auch spricht; `client.server_capabilities` und `client.protocol_version` sind danach einfach da, ebenso `client.server_info`, wenn der Server sich zu erkennen gibt (das ist jetzt `Implementation | None`, weil die Identität in der 2026er-Generation optional ist). Die Sampling- und Elicitation-Callbacks, die du in v1 registriert hast, funktionieren weiter (ihre Bodies sehen dieselbe Umbenennung der Attribute auf snake_case wie alles andere auf dieser Seite), sie beantworten jetzt außerdem die Requests-in-Results im 2026er-Stil (unten), und sie laufen nebenläufig statt nacheinander. `ClientSession` liegt für alle, die die Low-Level-Oberfläche wollen, weiterhin darunter, und `client.session` reicht sie dir; auch sie hat sich bewegt (sie läuft auf der neuen Dispatcher-Engine, und einige ihrer eigenen Signaturen haben sich geändert), lies also den **[Migrationsleitfaden](migration.md#clientsession-now-runs-on-jsonrpcdispatcher-basesession-removed)**, bevor du hinabsteigst. +`Client` nimmt eine URL (Streamable HTTP), ein `StdioServerParameters` (ein stdio-Subprozess), einen beliebigen anderen Transport-Kontextmanager wie `sse_client(...)` oder, in Tests, das Server-Objekt selbst (im Speicher, ohne Transport). Das Betreten von `async with` verbindet und handelt die Protokollversion aus, welche Generation der Server auch spricht; `client.server_capabilities` und `client.protocol_version` sind danach einfach da, ebenso `client.server_info`, wenn der Server sich zu erkennen gibt (das ist jetzt `Implementation | None`, weil die Identität in der 2026er-Generation optional ist). Die Sampling- und Elicitation-Callbacks, die du in v1 registriert hast, funktionieren weiter (ihre Bodies sehen dieselbe Umbenennung der Attribute auf snake_case wie alles andere auf dieser Seite), sie beantworten jetzt außerdem die Requests-in-Results im 2026er-Stil (unten), und sie laufen nebenläufig statt nacheinander. `ClientSession` liegt für alle, die die Low-Level-Oberfläche wollen, weiterhin darunter, und `client.session` reicht sie dir; auch sie hat sich bewegt (sie läuft auf der neuen Dispatcher-Engine, und einige ihrer eigenen Signaturen haben sich geändert), lies also den **[Migrationsleitfaden](migration.md#clientsession-now-runs-on-jsonrpcdispatcher-basesession-removed)**, bevor du hinabsteigst. **[Der Client](client/index.md)** stellt ihn vor, **[Client-Transporte](client/transports.md)** behandelt die vier Verbindungsformen, **[Client-Callbacks](client/callbacks.md)** die Callbacks selbst, und **[Testen](get-started/testing.md)** zeigt das In-Memory-Muster, das den Helfer `create_connected_server_and_client_session()` aus v1 ersetzt. @@ -113,7 +113,7 @@ async def call_tool(name: str, arguments: dict[str, Any]) -> list[types.ContentB Das Beispiel ist das Muster. Allgemeiner: Jeder Handler hat dieselbe Form, mit typisierten Params hinein und einem vollständigen Result-Typ hinaus; die alte jsonschema-Prüfung der Tool-Argumente ist entfernt; eine Exception ist ein Protokollfehler, nie ein Tool-Result mit `is_error=True`; und die umgebende ContextVar `server.request_context` ist entfernt. Eigene Methoden mit Vendor-Namespace sind über `add_request_handler(method, params_type, handler)` vollwertig unterstützt; das validiert eingehende Params gegen dein Modell, bevor dein Handler läuft. Und eine `middleware`-Liste (bewusst als vorläufig markiert) umhüllt jede eingehende Nachricht und ersetzt die privaten `_handle_*`-Methoden, die früher überschrieben wurden. -Unter der Haube wurde die `BaseSession`-Empfangsschleife aus v1 durch eine Dispatcher-Engine ersetzt, die Client und Server sich jetzt teilen, und sie macht mehrere Dinge auf dieser Seite gleichzeitig wahr: Ein einziges `Server`-Objekt bedient beide Protokollgenerationen, `Client(server)` dispatcht im Prozess ohne JSON-RPC-Framing, und ein Client-Request, der in den Timeout läuft, bricht jetzt tatsächlich den serverseitigen Handler ab. +Unter der Haube wurde die `BaseSession`-Empfangsschleife aus v1 durch eine Dispatcher-Engine ersetzt, die Client und Server sich jetzt teilen, und sie macht mehrere Dinge auf dieser Seite gleichzeitig wahr: Ein einziges `Server`-Objekt bedient beide Protokollgenerationen, `Client(server)` verteilt Requests direkt im Prozess, ohne JSON-RPC-Framing, und ein Client-Request, der in den Timeout läuft, bricht jetzt tatsächlich den serverseitigen Handler ab. Die Seite dazu ist **[Der Low-Level-Server](advanced/low-level-server.md)**; der **[Migrationsleitfaden](migration.md#lowlevel-server-decorator-based-handlers-replaced-with-constructor-on_-params)** geht jeden entfernten Hook durch. Wenn du nie unter `MCPServer` hinabgestiegen bist, betrifft dich nichts davon. @@ -171,11 +171,15 @@ Jeder vom Server ausgehende Request ist bei 2026-07-28 entfernt: Push-Elicitatio Der Ersatz dreht den Aufruf um. Ein Tool, das etwas von der Person am Host braucht, *gibt* die Frage *zurück* (`InputRequiredResult`), der Client beantwortet sie mit denselben Callbacks, die er schon immer hatte, und der Aufruf wird mit angehängten Antworten erneut versucht. `Client` treibt diese Schleife für dich. Auf dem Server baust du das Result selten selbst, weil eine **[Abhängigkeit](handlers/dependencies.md)** das übernimmt: Annotiere einen Parameter mit `Resolve(ask_quantity)`, wobei `ask_quantity` eine gewöhnliche Funktion ist, die du schreibst, und das SDK fragt über den Mechanismus, den die Verbindung unterstützt – ein Live-Elicitation-Request auf einer Legacy-Session oder ein Multi-Roundtrip bei 2026. Ein Tool-Body, beide Generationen: -```python title="dual_era.py" hl_lines="24 37-38" +```python title="server.py" hl_lines="21" --8<-- "docs_src/legacy_clients/tutorial001.py" ``` -Diese Datei ist das ganze Versprechen an einem Ort: ein Server, ein Tool mit `Resolve` dahinter, und ein Legacy-Client plus ein moderner Client, die beide ihre Antwort bekommen, im Speicher. **[Multi-Roundtrip-Requests](handlers/multi-round-trip.md)** erklärt den Mechanismus (einschließlich `request_state`, den das SDK für dich versiegelt und verifiziert); **[Elicitation](handlers/elicitation.md)** behandelt das Fragen. +```python title="client.py" hl_lines="14-15" +--8<-- "docs_src/legacy_clients/tutorial001_client.py" +``` + +Diese beiden Dateien sind das ganze Versprechen: ein Server, ein Tool mit `Resolve` dahinter, und ein Legacy-Client plus ein moderner Client, die beide ihre Antwort von demselben laufenden Server bekommen (**[Legacy-Clients unterstützen](run/legacy-clients.md)** geht sie durch). **[Multi-Roundtrip-Requests](handlers/multi-round-trip.md)** erklärt den Mechanismus (einschließlich `request_state`, den das SDK für dich versiegelt und verifiziert); **[Elicitation](handlers/elicitation.md)** behandelt das Fragen. !!! warning "Das ist die eine Stelle, an der ein portierter v1-Server sein Verhalten ändert" Deine eigenen Tests treffen es zuerst: `Client(mcp)` handelt gegen deinen v2-Server standardmäßig diff --git a/i18n/es/pages/advanced/apps.md b/i18n/es/pages/advanced/apps.md index 682404a1b2..163ceb906e 100644 --- a/i18n/es/pages/advanced/apps.md +++ b/i18n/es/pages/advanced/apps.md @@ -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} @@ -25,7 +25,7 @@ y luego vuelve. ## Un reloj con cara visible {#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" ``` @@ -50,21 +50,42 @@ dentro de tu HTML. Te da `ontoolresult`, `callServerTool`, No todos los clientes muestran apps. La especificación es tajante sobre lo que eso significa para ti: -> Tools **MUST** return a meaningful `content` array even when UI is available. +> Las herramientas **DEBEN** devolver un array `content` significativo incluso cuando hay una UI disponible. El modelo lee `content`; el iframe es para humanos. Un host capaz de mostrar UI sigue entregando el resultado en texto al modelo, y un cliente solo de texto recibe *solo* eso. Así que el patrón canónico es una herramienta, dos respuestas. Mira `get_time` de nuevo: -```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)` es `True` solo cuando el cliente declaró la extensión `io.modelcontextprotocol/ui` **y** incluyó `text/html;profile=mcp-app` en su configuración `mimeTypes`. El campo es obligatorio, así que un cliente que lo omite -no cuenta. Eso es exactamente lo que declara `main()` en el mismo archivo: la -mitad cliente de la negociación, y vuelve la respuesta enriquecida. +no cuenta. Esta es la mitad cliente de la negociación: + +```python title="client.py" hl_lines="8 12" +--8<-- "docs_src/apps/tutorial001_client.py" +``` + +Sirve `server.py` por HTTP y luego ejecuta el cliente desde una segunda terminal: + +```console +uv run mcp run server.py --transport streamable-http +``` + +```console +python client.py +``` + +```text +2026-06-26T12:00:00Z +``` + +Volvió la respuesta enriquecida. Quita `extensions=[APPS_SUPPORT]` de la llamada a `Client` +y el mismo programa imprime `The time is 2026-06-26T12:00:00Z.` en su lugar, que es +todo lo que un cliente solo de texto llega a ver. !!! warning Nunca devuelvas un marcador de posición como `"[Rendered UI]"` como único contenido. Si el diff --git a/i18n/es/pages/advanced/extensions.md b/i18n/es/pages/advanced/extensions.md index 74a2163033..095ed236dd 100644 --- a/i18n/es/pages/advanced/extensions.md +++ b/i18n/es/pages/advanced/extensions.md @@ -1,6 +1,6 @@ --- translation: - sections: [05891e7cc1938a13, b3c01a6af28c51ee, 7ffc91f5e38bdfe0, 717d3f235a8333a7, f471a13b2fe5d737, ed6af2df4b656dff] + sections: [05891e7cc1938a13, b3c01a6af28c51ee, 6eba6ce094f4417e, 125f28588c85dd7b, ddd8969b698ca11c, ed6af2df4b656dff] tool: 1 --- # Extensiones {#extensions} @@ -62,7 +62,7 @@ especificadas por el propio proyecto MCP. La extensión útil más pequeña es una herramienta y un mapa de ajustes: -```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" ``` @@ -74,18 +74,25 @@ La extensión útil más pequeña es una herramienta y un mapa de ajustes: * La extensión nunca recibe el servidor. Declara sus aportaciones como datos; `MCPServer` las consume. No hay un `self.server` que mutar. -Y `main()` es la prueba, un cliente en memoria directamente contra `mcp`: +Sírvela por HTTP, y un cliente es la prueba: -```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" ``` +Cada `server.py` de esta página se sirve con ese comando, y cada `client.py` +se ejecuta a su lado con `python client.py` desde una segunda terminal. + ### Servir tus propios métodos {#serving-your-own-methods} Una extensión puede registrar **nuevos métodos de solicitud**: sus propios verbos, servidos junto a los de la especificación: -```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" ``` @@ -112,10 +119,10 @@ tiempo de ejecución: ### El lado del cliente {#the-client-side} -El `main()` del mismo archivo es toda la historia del cliente, sus dos mitades: +El cliente es su propio programa, y lleva las dos mitades de la historia del cliente: -```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)])` declara la extensión. Las @@ -127,6 +134,9 @@ El `main()` del mismo archivo es toda la historia del cliente, sus dos mitades: * Los métodos de proveedor bajan una capa hasta `client.session.send_request(...)`; `Client` solo incorpora métodos de primera clase para los verbos de la especificación. `send_request` acepta cualquier subclase de `Request`, así que la solicitud de proveedor pasa tal cual. +* `SearchRequest` y los dos modelos que lleva son el contrato de la extensión en el canal, + así que el cliente los declara por su cuenta. Una extensión publicada los distribuiría en + un paquete que ambos lados importan. ### Interceptar `tools/call` {#intercepting-toolscall} @@ -159,13 +169,20 @@ El hook envuelve `tools/call` y nada más. Para lo que afecta a cada mensaje, us ## Usar una extensión de cliente {#using-a-client-extension} Una **extensión de cliente** es el mismo contrato desde el lado que consume: un paquete de -comportamiento del lado del cliente detrás de un único identificador. Pasa las instancias a -`Client(extensions=[...])` y llama a las herramientas con normalidad: +comportamiento del lado del cliente detrás de un único identificador. Aquí el servidor responde a `buy` +con un recibo para canjear en lugar de la mercancía, y solo para un cliente que declaró la +extensión: -```python title="client.py" hl_lines="66-68" +```python title="server.py" hl_lines="22-25" --8<-- "docs_src/extensions/tutorial006.py" ``` +En el cliente, pasa las instancias a `Client(extensions=[...])` y llama a las herramientas con normalidad: + +```python title="client.py" hl_lines="33-35" +--8<-- "docs_src/extensions/tutorial006_client.py" +``` + `call_tool("buy", ...)` devuelve un `CallToolResult` normal, como cualquier otra llamada. Lo que cambió la extensión: el servidor ahora puede responder a `buy` con una **forma de resultado** `receipt` en lugar de un resultado final, y `Receipts` la termina (aquí canjeando el @@ -184,7 +201,7 @@ según la capacidad, el cliente no hace nada, como en el cliente de búsqueda de ```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")]) ``` ## Escribir una extensión de cliente {#writing-a-client-extension} @@ -192,8 +209,8 @@ client = Client(mcp, extensions=[advertise("com.example/search")]) Crea una subclase de `ClientExtension` y sobrescribe solo lo que necesites. Tres tipos de aportación, cada uno con un valor por defecto: `settings()`, `claims()` y `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" ``` * El identificador sigue la misma gramática que el del servidor y se valida cuando se @@ -231,14 +248,21 @@ forma reclamada que llega a un llamador del nivel de sesión lanza `UnexpectedCl Los métodos de solicitud propios de una extensión no necesitan registro del lado del cliente. Un tipo de solicitud de proveedor es una subclase de `mcp.types.Request` y pasa por `client.session.send_request`, -como en [Servir tus propios métodos](#serving-your-own-methods). Un añadido: cuando una -clave de los parámetros debe viajar en el header `Mcp-Name` (las especificaciones de extensión, como -tasks, lo exigen para sus verbos), el tipo de solicitud declara `name_param`: +como en [Servir tus propios métodos](#serving-your-own-methods). Toma un servidor cuya +extensión sirve un único verbo sobre un trabajo con nombre: -```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" ``` +Un añadido en el cliente: cuando una clave de los parámetros debe viajar en el header `Mcp-Name` +(las especificaciones de extensión, como tasks, lo exigen para sus verbos), el tipo de solicitud +declara `name_param`: + +```python title="client.py" hl_lines="20-23 28-29" +--8<-- "docs_src/extensions/tutorial007_client.py" +``` + La sesión replica `params["jobId"]` en `Mcp-Name` en cada ruta de envío, y un valor ausente falla de forma visible en lugar de omitir en silencio un header obligatorio. diff --git a/i18n/es/pages/advanced/low-level-server.md b/i18n/es/pages/advanced/low-level-server.md index a9fe35e242..71286b6677 100644 --- a/i18n/es/pages/advanced/low-level-server.md +++ b/i18n/es/pages/advanced/low-level-server.md @@ -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 --- # El Server de bajo nivel {#the-low-level-server} @@ -36,18 +36,22 @@ Cambiaron tres cosas, y son toda la API de bajo nivel: ### Pruébalo {#try-it} -Aquí no hay Inspector: `mcp dev` y `mcp run` solo aceptan un `MCPServer`. Al `Client` en memoria le da igual; acepta un `Server` de bajo nivel exactamente igual que acepta un `MCPServer`: +`mcp dev` y `mcp run` solo aceptan un `MCPServer`, así que este lo sirves por tu cuenta. La última línea de `server.py` construye una app ASGI común a partir de él, y uvicorn la ejecuta: -```python title="main.py" +```console +uvicorn server:app --port 8000 +``` + +Apunta el Inspector, o cualquier cliente, a `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 @@ El mismo texto que produjo la versión con `@mcp.tool()`. Dos diferencias honest * `result.structured_content` es `None`. El servidor de alto nivel envuelve un `-> str` en `{"result": ...}` por ti; aquí nadie construye lo que no construiste. * `list_tools` devuelve el esquema que escribiste **tú**, carácter por carácter. La versión de alto nivel tenía `"title": "Query"` en cada propiedad y un `"title": "search_booksArguments"` en la raíz: artefactos de Pydantic. Aquí abajo, si se transmite, es porque lo pusiste ahí. +En una prueba te ahorras uvicorn y el puerto: `Client(server)` acepta un `Server` de bajo nivel en el mismo proceso exactamente igual que acepta un `MCPServer`, y **[Pruebas](../get-started/testing.md)** es ese patrón. + ## Nada se comprueba por ti {#nothing-is-checked-for-you} `MCPServer` rechaza un argumento incorrecto antes de que tu función llegue a ejecutarse, validando la llamada contra el esquema que generó (**[Herramientas](../servers/tools.md)**). @@ -215,4 +221,4 @@ Cada uno de estos es una idea para la que ya tienes el vocabulario; cada uno tie * `add_request_handler(method, params_type, handler)` sirve cualquier método. `initialize` está reservado. * Las capacidades que anuncia un `Server` se derivan de los handlers que registraste. -`Client(server)` trató a ambos servidores de forma idéntica porque *son* el mismo protocolo, que es justamente la idea. La siguiente capa hacia abajo no es una clase: es **[Middleware](middleware.md)**. +El cliente trató a ambos servidores de forma idéntica porque *son* el mismo protocolo, que es justamente la idea. La siguiente capa hacia abajo no es una clase: es **[Middleware](middleware.md)**. diff --git a/i18n/es/pages/advanced/pagination.md b/i18n/es/pages/advanced/pagination.md index d1abdfc04f..49a007d592 100644 --- a/i18n/es/pages/advanced/pagination.md +++ b/i18n/es/pages/advanced/pagination.md @@ -1,6 +1,6 @@ --- translation: - sections: [a9aba7a026c7bd85, ed32bda7ba9ae33a, 7e64cc5646abb91f, 22a0129ee78b3c63, d875373c06d8d2f9] + sections: [a9aba7a026c7bd85, 83e2a08b9d46a398, 9fd8a0aa384b3257, 22a0129ee78b3c63, d875373c06d8d2f9] tool: 1 --- # Paginación {#pagination} @@ -31,9 +31,13 @@ La paginación es para el servidor cuya lista de recursos es en realidad una bas ### Pruébalo {#try-it} -`Client(server)` se conecta a un `Server` de bajo nivel en memoria exactamente igual que se conecta a un `MCPServer`. +`mcp run` solo acepta un `MCPServer`, así que este lo sirves tú mismo. La última línea de `server.py` construye una app ASGI normal a partir del `Server`, y uvicorn la ejecuta: -Llama a `list_resources()` sin argumentos. Obtienes diez recursos, de `book-1` a `book-10`, y `next_cursor` es la cadena `"10"`. +```console +uvicorn server:app --port 8000 +``` + +Apunta cualquier cliente (**[El cliente](../client/index.md)**, o el Inspector) a `http://localhost:8000/mcp` y llama a `list_resources()` sin argumentos. Obtienes diez recursos, de `book-1` a `book-10`, y `next_cursor` es la cadena `"10"`. Devuélvelo con `list_resources(cursor="10")` y el primer recurso es `book-11`; el nuevo `next_cursor` es `"20"`. @@ -43,7 +47,7 @@ La décima página vuelve con `next_cursor` en `None`. Listo. Cada método `list_*` de `Client` (`list_tools`, `list_resources`, `list_resource_templates`, `list_prompts`) acepta el argumento nombrado `cursor=`. Vaciar una lista paginada es un solo `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 @@ Cada método `list_*` de `Client` (`list_tools`, `list_resources`, `list_resourc * Extiende **antes** de mirar `next_cursor`: la última página también tiene recursos. * `next_cursor is None` es la salida. Cualquier otra cosa vuelve directamente a `cursor=`, sin tocarla. -Ejecuta su `main()` e imprime `100 resources`: diez páginas de diez, unidas por un bucle que nunca supo que había diez páginas. +Con uvicorn todavía sirviendo `server.py`, ejecuta `python client.py` en una segunda terminal. Imprime `100 resources`: diez páginas de diez, unidas por un bucle que nunca supo que había diez páginas. Es el mismo bucle que **[El cliente](../client/index.md)** muestra para cada verbo `list_*`, y no cuesta nada frente a un servidor que no pagina: `next_cursor` es `None` en la primera respuesta y el bucle se ejecuta una vez. diff --git a/i18n/es/pages/client/caching.md b/i18n/es/pages/client/caching.md index e8d1613c1a..34174862dc 100644 --- a/i18n/es/pages/client/caching.md +++ b/i18n/es/pages/client/caching.md @@ -1,6 +1,6 @@ --- translation: - sections: [9e7b9a1710e5aeba, b74ca4c1d2ddddee, fa8714e61bf90c5a, 04db67a886b7271c, 857690fb8f876800] + sections: [9e7b9a1710e5aeba, 66a2e9acc9101d54, d3d25aa802f5145a, 04db67a886b7271c, 857690fb8f876800] tool: 1 --- # Sugerencias de caché {#caching-hints} @@ -31,7 +31,7 @@ Por defecto, cada resultado dice `ttlMs: 0, cacheScope: "private"`: caducado de En el `Server` de bajo nivel, los handlers construyen sus resultados a mano, y `ttl_ms` / `cache_scope` son simplemente campos de los modelos de resultado. Un handler que los define explícitamente siempre gana al mapa del constructor, campo por campo: -```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 @@ Una salvedad sobre las listas paginadas: el protocolo exige el **mismo `cacheSco En una sesión 2026-07-28, `Client` respeta las sugerencias por ti: tiene una caché de respuestas integrada, activada por defecto. Un resultado que llega con un `ttlMs` se almacena, y una llamada idéntica dentro de ese TTL se sirve desde la caché sin ida y vuelta. Un resultado que llega *sin* sugerencia no se guarda en caché: los resultados sin sugerencia reciben `CacheConfig.default_ttl_ms`, que es `0` por defecto (caducado de inmediato), así que un servidor que no declara nada ve exactamente el mismo tráfico llamada por llamada de siempre. -```python title="client.py" hl_lines="33 35 38" +Para verlo en acción, sirve el `server.py` de la sección anterior con uvicorn (su última línea construye la app ASGI). El handler imprime una línea cada vez que se ejecuta de verdad: + +```console +uvicorn server:app --port 8000 +``` + +```python title="client.py" hl_lines="20 23 28" --8<-- "docs_src/caching/tutorial003.py" ``` +Ejecuta `python client.py` desde una segunda terminal. Imprime las sugerencias que llevaba el primer resultado: el `ttlMs` del handler junto al `cacheScope` del mapa: + +```text +1000 public +``` + +La terminal del servidor cuenta el resto: entre los logs de solicitudes de uvicorn, `tools/list served` aparece tres veces. + Cuatro llamadas, tres consultas al servidor. La segunda llamada encontró una entrada vigente y nunca llegó al servidor; adelantar el reloj (inyectado) más allá del TTL hizo que la tercera volviera a consultar; la cuarta dijo `cache_mode="refresh"`. Ese argumento nombrado existe en los cinco verbos con caché (`list_tools`, `list_prompts`, `list_resources`, `list_resource_templates`, `read_resource`): * `"use"` (el valor por defecto) sirve una entrada vigente si la hay, y almacena lo consultado si no. @@ -57,7 +71,7 @@ Cuatro llamadas, tres consultas al servidor. La segunda llamada encontró una en Hay una regla por encima de `"use"`: **las llamadas que llevan `meta` siempre llegan al servidor.** Una solicitud con `meta` definido (un token de progreso, campos de trazado) espera una solicitud real por el canal, así que con `cache_mode="use"` se trata como `"refresh"`: se omite la lectura de la caché, y el resultado obtenido sigue reemplazando la entrada en caché. `"bypass"` y un `"refresh"` explícito se comportan como siempre. -Para desactivar la caché por completo, construye con `Client(server, cache=None)`: cada llamada vuelve a ser una ida y vuelta, y `cache_mode`, aunque se sigue aceptando, no hace nada. +Para desactivar la caché por completo, pasa `cache=None` al construir el `Client`: cada llamada vuelve a ser una ida y vuelta, y `cache_mode`, aunque se sigue aceptando, no hace nada. El alcance también se respeta automáticamente: las entradas `"private"` se asocian a la *partición* de la caché (más abajo), mientras que las `"public"` pueden optar por compartirse más ampliamente. Y **las notificaciones ganan al TTL** para las entradas exactas que nombran: una notificación `list_changed` desaloja el listado en caché correspondiente, y `resources/updated` desaloja la lectura en caché almacenada exactamente bajo su URI, por muy vigentes que estuvieran. En una conexión 2026-07-28 esas notificaciones llegan por un stream `subscriptions/listen` que abres con `client.listen(...)`, y el desalojo se completa antes de que tu observador vea el evento; **[Suscripciones](subscriptions.md)** es esa página. diff --git a/i18n/es/pages/client/callbacks.md b/i18n/es/pages/client/callbacks.md index 2369552e08..a221b3240c 100644 --- a/i18n/es/pages/client/callbacks.md +++ b/i18n/es/pages/client/callbacks.md @@ -1,6 +1,6 @@ --- translation: - sections: [adf3c545b5be46b6, 916cd3ab1c03f461, e9be7a8d0eb0a456, 565890a636288ecf, 6af7e49db9129ec3, 06b0238c174186af, 90c6043be435fcb0] + sections: [adf3c545b5be46b6, 916cd3ab1c03f461, 32ef568335dd95a7, 565890a636288ecf, 6af7e49db9129ec3, 06b0238c174186af, 0abc5ea5cb7ff6b3] tool: 1 --- # Callbacks del cliente {#client-callbacks} @@ -63,7 +63,7 @@ Un `tools/call` tuyo, un `elicitation/create` de vuelta desde el servidor, respo `mode="legacy"` en la llamada a `Client(...)` hace trabajo real. Por defecto, `Client(...)` negocia la ruta moderna del protocolo, y esa ruta no tiene canal de retorno (back-channel) para las solicitudes del servidor al cliente: `ctx.elicit` falla antes de que tu callback llegue a ejecutarse. No lo decide el transporte; lo decide el protocolo - negociado, tanto en memoria como a través de una URL. Fija `mode="legacy"` siempre que tu cliente tenga + negociado. Fija `mode="legacy"` siempre que tu cliente tenga que responder a una; todas las pruebas detrás de esta página lo hacen. **[Versiones del protocolo](../protocol-versions.md)** tiene todos los detalles. En una sesión 2026-07-28 el callback no está muerto, se alimenta de otra forma: cuando una herramienta devuelve un @@ -151,4 +151,4 @@ Dos más. Ninguno declara nada. * `sampling_callback` y `list_roots_callback` funcionan igual pero atienden funcionalidades obsoletas; los servidores modernos usan solicitudes de varias idas y vueltas en su lugar. * `logging_callback` y `message_handler` reciben notificaciones. No declaran nada. -El primer argumento de `Client(...)` es un objeto de transporte. **[Transportes del cliente](transports.md)** cubre todos los tipos. +El primer argumento de `Client(...)` elige el transporte. **[Transportes del cliente](transports.md)** cubre todos los tipos. diff --git a/i18n/es/pages/client/index.md b/i18n/es/pages/client/index.md index f2bb09a509..a61c710402 100644 --- a/i18n/es/pages/client/index.md +++ b/i18n/es/pages/client/index.md @@ -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 --- # El cliente {#the-client} @@ -11,13 +11,23 @@ Es un solo objeto con un solo ciclo de vida: lo construyes, entras en `async wit ## Tu primer cliente {#your-first-client} -```python title="client.py" hl_lines="14-18" +Un cliente necesita un servidor con el que hablar. Este Bookshop es al que se conectan todos los fragmentos de esta página. Guárdalo como `server.py` y déjalo ejecutándose por HTTP: + +```python title="server.py" --8<-- "docs_src/client/tutorial001.py" ``` -El servidor del principio solo está ahí para que tengas algo a lo que conectarte. El cliente son las cinco líneas resaltadas. +```console +uv run mcp run server.py --transport streamable-http +``` + +Con eso queda disponible en `http://localhost:8000/mcp`. El cliente es un programa aparte. Guárdalo como `client.py` y ejecuta `python client.py` en una segunda terminal: -* `Client(mcp)` recibe **el propio objeto servidor**. Ese es el transporte en memoria: sin subproceso, sin puerto, sin HTTP. Así se conectan todos los ejemplos de esta página y todas las pruebas que escribas. +```python title="client.py" hl_lines="7-11" +--8<-- "docs_src/client/tutorial001_client.py" +``` + +* `Client("http://localhost:8000/mcp")` recibe una **URL**, así que se conecta por Streamable HTTP al servidor que acabas de iniciar. * `async with` es el **ciclo de vida**. Al entrar se conecta y negocia; al salir se desconecta. No hay un par `connect()` / `close()`, y un `Client` no se puede reutilizar una vez que termina el bloque. * Dentro del bloque, los datos de la conexión ya están ahí como propiedades simples. @@ -25,10 +35,10 @@ El servidor del principio solo está ahí para que tengas algo a lo que conectar `Client` recibe un solo argumento posicional y resuelve el transporte a partir de su tipo: -* Una instancia de `MCPServer` (o del `Server` de bajo nivel): se conecta **en el mismo proceso**. -* Una cadena con una URL (`Client("http://localhost:8000/mcp")`): Streamable HTTP, el camino de producción. -* Un `StdioServerParameters`: el comando que se lanza como **subproceso**, con el que se habla a través de su stdin y su stdout. +* Una cadena con una URL (`Client("http://localhost:8000/mcp")`): Streamable HTTP, el transporte con el que despliegas. +* Un `StdioServerParameters`: el comando que se lanza como **subproceso** local, con el que se habla a través de su stdin y su stdout. * Un **transporte**: cualquier cosa que puedas usar con `async with ... as (read, write)`, como `streamable_http_client(url, http_client=...)` envolviendo tu propio cliente HTTP. +* Una instancia de `MCPServer` (o del `Server` de bajo nivel): se conecta **en el mismo proceso**, sin subproceso y sin puerto. Ese caso es para las pruebas, y **[Pruebas](../get-started/testing.md)** se construye sobre él. Todo lo demás en esta página es idéntico en los cuatro casos. Los encabezados, los subprocesos, los timeouts y el protocolo `Transport` tienen su propia página: **[Transportes del cliente](transports.md)**. @@ -49,11 +59,11 @@ Nunca elegiste una versión del protocolo. Por defecto, el `Client` sondea el se ## Listar herramientas {#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()` devuelve un `ListToolsResult`; las herramientas están en `.tools`. Cada una es la definición completa que un host le entregaría a un modelo: +`list_tools()` devuelve un `ListToolsResult`; las herramientas están en `.tools`. Cada una es la definición completa que un host le entregaría a un modelo. Esta es la primera: ```python tool.name # 'search_books' @@ -77,6 +87,8 @@ y `tool.input_schema` es el JSON Schema que el servidor derivó de las anotacion Ese esquema es todo lo que una UI necesita para renderizar un formulario de argumentos, y todo lo que un modelo necesita para producir argumentos válidos. +La segunda herramienta, `lookup_book`, se registró sin `title=`, así que su `tool.title` es `None`. + !!! tip `title` es opcional, así que una UI que muestra herramientas a una persona tiene que elegir: el `title` si lo hay, el `name` si no. `from mcp.shared.metadata_utils import get_display_name` hace exactamente eso, @@ -86,7 +98,7 @@ Ese esquema es todo lo que una UI necesita para renderizar un formulario de argu `call_tool(name, arguments)` ejecuta la herramienta y te devuelve un `CallToolResult`. -```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 @@ Una herramienta que lanza una excepción **no** la lanza en tu cliente. Vuelve c Los verbos de recursos vienen en pares: dos formas de listar, una de leer. -```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 @@ A un cliente también se le puede avisar cuando cambia un recurso. En conexiones ## 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 @@ Un host le entrega esos mensajes directamente al modelo. Esa es toda la funciona Un servidor con un handler de autocompletado puede autocompletar argumentos de prompts y de plantillas de recursos mientras el usuario escribe. -```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 @@ La respuesta está en `result.completion.values`. Escribe `"p"` y el servidor de Cada método `list_*` acepta un argumento nombrado `cursor=` y cada resultado trae un `next_cursor`. Cuando `next_cursor` es `None`, ya lo tienes todo. -```python title="client.py" hl_lines="22-30" +```python title="client.py" hl_lines="7-15" --8<-- "docs_src/client/tutorial007.py" ``` -Este bucle es correcto contra cualquier servidor. `MCPServer` devuelve todo en una sola página, así que `next_cursor` es `None` y el bucle se ejecuta una vez; por eso la mayoría del código nunca lo escribe. Los servidores que realmente paginan, y las reglas que siguen los cursores, están en **[Paginación](../advanced/pagination.md)**. +`list_all_tools` es correcta contra cualquier servidor. `MCPServer` devuelve todo en una sola página, así que `next_cursor` es `None` y el bucle se ejecuta una vez; por eso la mayoría del código nunca lo escribe. Los servidores que realmente paginan, y las reglas que siguen los cursores, están en **[Paginación](../advanced/pagination.md)**. ## En las pruebas {#in-tests} -`Client(mcp)`, sin proceso y sin puerto, ya es un banco de pruebas para tu servidor. +Cada `client.py` de esta página llegó a `server.py` por HTTP. En una prueba te saltas la red y le pasas a `Client` el propio objeto servidor: `from server import mcp` y luego `Client(mcp)`. Sin proceso, sin puerto, y todos los métodos anteriores funcionan igual. -Hay una opción del constructor pensada para eso: `Client(mcp, raise_exceptions=True)`. Solo tiene efecto en conexiones en memoria, y **[Pruebas](../get-started/testing.md)** es la página que la explica y construye todo el patrón a su alrededor. +Hay una opción del constructor pensada para eso: `Client(mcp, raise_exceptions=True)`. Solo tiene efecto en conexiones en el mismo proceso, y **[Pruebas](../get-started/testing.md)** es la página que la explica y construye todo el patrón a su alrededor. ## Resumen {#recap} -* `Client(x)` se conecta en memoria a un objeto servidor, por Streamable HTTP a una cadena con una URL, y por cualquier otra cosa mediante un transporte. +* `Client(x)` se conecta por Streamable HTTP a una cadena con una URL, lanza un subproceso para un `StdioServerParameters`, entra directamente en un transporte y, en las pruebas, recibe el propio objeto servidor. * `async with` es todo el ciclo de vida. Dentro, `server_capabilities` y `protocol_version` ya están rellenas; `server_info` e `instructions` también, cuando el servidor las proporciona. * `list_tools()` te da el `name`, `title`, `description` e `input_schema` de cada herramienta. * `call_tool()` devuelve `content` para el modelo, `structured_content` para tu código, e `is_error`. Una herramienta que lanza una excepción es un resultado, no una excepción. diff --git a/i18n/es/pages/client/oauth-clients.md b/i18n/es/pages/client/oauth-clients.md index 9bccb9f8ae..6451495d65 100644 --- a/i18n/es/pages/client/oauth-clients.md +++ b/i18n/es/pages/client/oauth-clients.md @@ -1,6 +1,6 @@ --- translation: - sections: [c6899d3892bd9fa0, 79372cff3cc48a88, 63878d29e87c3e73, 13175843d3588af4, e7e2b9fd516f77de, 758f06399b513c1f, a05d7278487d610b] + sections: [c6899d3892bd9fa0, 79372cff3cc48a88, c2dae1ebe2ebd543, 13175843d3588af4, df06056fb16b3846, 758f06399b513c1f, a05d7278487d610b] tool: 1 --- # Clientes OAuth {#oauth-clients} @@ -81,18 +81,20 @@ Mira `main()`. El proveedor va en el **cliente httpx2**, el cliente httpx2 va en La primera vez que `Client` envía una solicitud, el servidor responde `401`. El proveedor toma el control: -1. **Descubrimiento.** Lee la cabecera `WWW-Authenticate`, obtiene los Protected Resource Metadata del servidor desde `/.well-known/oauth-protected-resource`, averigua qué servidor de autorización protege este recurso y obtiene los metadatos de *ese* servidor. +1. **Descubrimiento.** Lee la cabecera `WWW-Authenticate`, obtiene los Protected Resource Metadata del servidor desde `/.well-known/oauth-protected-resource`, averigua qué servidor de autorización protege este recurso y obtiene los metadatos de *ese* servidor. (A un servidor más antiguo que no publica metadatos del recurso se le piden, en su lugar, los metadatos del servidor de autorización en su propio origen.) En cualquier caso, los metadatos deben nombrar como `issuer` al servidor para el que se obtuvieron; cualquier otra cosa se rechaza. 2. **Registro.** ¿No hay nada en el almacenamiento? Te registra dinámicamente con tu `OAuthClientMetadata` y guarda el resultado. 3. **Autorización.** Genera el par PKCE y un `state`, construye la URL de autorización, espera tu `redirect_handler` y luego espera tu `callback_handler` para obtener el código. 4. **Intercambio.** Cambia el código por un `OAuthToken`, lo guarda y repite tu solicitud original con `Authorization: Bearer ...`. Después de eso, se queda callado. Los tokens salen del almacenamiento, un token de acceso caducado se renueva con el token de actualización y solo cuando nada de eso funciona vuelve a ejecutar el flujo. +Una regla de transporte se aplica a todas estas solicitudes: igual que la solicitud MCP dentro de la que se ejecutan, siguen una redirección solo cuando permanece en el mismo origen y conserva el método (un 307/308 por una barra final, por ejemplo), y tratan cualquier otra redirección como si esa URL no respondiera. + No escribiste nada de eso. Quedan dos argumentos nombrados (`client_metadata_url` y `validate_resource_url`), y este archivo no necesita ninguno. `client_metadata_url` es el que vale la pena conocer; tiene su propia sección más abajo. ### Pruébalo {#try-it} -La mayoría de los ejemplos de esta documentación puedes comprobarlos con un `Client(server)` en memoria. Este no: todo el sentido del flujo es un `401` HTTP, y no hay HTTP entre un cliente en memoria y su servidor. +El `Client(server)` en memoria que usan tus pruebas no sirve aquí: todo el sentido del flujo es un `401` HTTP, y no hay HTTP entre un cliente en memoria y su servidor. El repositorio incluye la versión real. `examples/servers/simple-auth/` ejecuta un servidor de autorización independiente y un servidor MCP protegido; `examples/clients/simple-auth-client/` es el cliente de esta página convertido en una pequeña CLI. Su README tiene los dos comandos: inicia los servidores, ejecuta el cliente contra ellos y verás pasar los cuatro pasos. @@ -110,13 +112,14 @@ Un trabajo nocturno, un paso de CI, otro servicio. No hay navegador ni nadie que `ClientCredentialsOAuthProvider` es el mismo `httpx2.Auth`, sin el humano: -```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" ``` Qué cambió: * Sin `OAuthClientMetadata`, sin handlers. Pasas `client_id` y `client_secret`; el proveedor construye un registro `client_credentials` mínimo en torno a ellos y se salta el registro dinámico por completo. +* `issuer` nombra el servidor de autorización que emitió esas credenciales; usa el valor `issuer` que devuelve su documento `/.well-known/oauth-authorization-server`. El descubrimiento sigue ejecutándose como arriba, pero las solicitudes de token solo se construyen a partir de los metadatos de *ese* emisor; si el servidor MCP apunta a cualquier otro sitio, el flujo se detiene con un `OAuthFlowError`. Omitirlo está obsoleto y pasa a ser obligatorio en 3.0 (consulta **[Funcionalidades obsoletas](../deprecated.md#deprecated-sdk-helpers)**); hasta entonces, el proveedor emite un aviso y usa el servidor de autorización que encuentre el descubrimiento. * `scope` es una cadena separada por espacios, el formato que OAuth usa en lo que se transmite. * Todo lo que viene después es idéntico: el mismo `TokenStorage`, el mismo `httpx2.AsyncClient(auth=...)`, el mismo `streamable_http_client`. @@ -129,7 +132,7 @@ Por defecto, el secreto viaja como autenticación HTTP Basic en la solicitud de Hay un proveedor más en `mcp.client.auth.extensions.client_credentials`: **`PrivateKeyJWTOAuthProvider`**, para clientes que se autentican con un JWT en lugar de un secreto compartido (`private_key_jwt`, la variante de par de claves e identidad de carga de trabajo). Sigue - el mismo patrón: construye uno y ponlo en `auth=`. El mismo módulo incluye + el mismo patrón: construye uno (acepta el mismo `issuer` opcional) y ponlo en `auth=`. El mismo módulo incluye `SignedJWTParameters` y `static_assertion_provider`, dos utilidades que construyen su aserción. Hay una situación más sin humanos: el cliente pertenece a una empresa cuyo proveedor de identidad, y no el usuario, decide a qué servidores MCP puede acceder. Ese es un grant distinto, con su propio modelo de confianza y su propia página, **[Aserción de identidad](identity-assertion.md)**. diff --git a/i18n/es/pages/client/session-groups.md b/i18n/es/pages/client/session-groups.md index 438e68c14b..164f49f750 100644 --- a/i18n/es/pages/client/session-groups.md +++ b/i18n/es/pages/client/session-groups.md @@ -1,6 +1,6 @@ --- translation: - sections: [09c857a25a9dc37a, 43bc6a76a243a50e, 0a716022a88768df, 4b7f78042bfcfff7, c112662e61b03315, 58974ba1f489a8b4, d18adbdbb835ea73] + sections: [09c857a25a9dc37a, 43bc6a76a243a50e, 0a716022a88768df, 4b7f78042bfcfff7, c112662e61b03315, 58974ba1f489a8b4, ed4d17e894864056] tool: 1 --- # Grupos de sesiones {#session-groups} @@ -78,7 +78,7 @@ Si ya tienes una `ClientSession` conectada (`Client.session` lo es), pásala a ` ## Resumen {#recap} * `ClientSessionGroup` mantiene muchas conexiones a servidores y reúne sus herramientas, recursos y prompts en un `dict` para cada tipo. -* `connect_to_server(params)` por servidor. Recibe parámetros de transporte, nunca el objeto servidor ni la URL que recibe un `Client`. +* `connect_to_server(params)` por servidor. Recibe parámetros de transporte, nunca la URL ni el `Transport` que recibe un `Client`. * `group.call_tool(name, arguments)` enruta por ti al servidor al que pertenece. * Los nombres deben ser únicos en todo el grupo; dos servidores con una herramienta `search` no pueden coexistir por sí solos. * `component_name_hook=` reescribe cada nombre registrado. La clave del dict cambia; el nombre que se transmite por el canal, no. diff --git a/i18n/es/pages/client/transports.md b/i18n/es/pages/client/transports.md index 4f25dc11f1..84f91ff240 100644 --- a/i18n/es/pages/client/transports.md +++ b/i18n/es/pages/client/transports.md @@ -1,6 +1,6 @@ --- translation: - sections: [9cac816674181eb0, 0700f337babcd4dd, 2bde0dd58cdf00f5, 40b4916d82eaf1d4, 3d0832f39b0d7059, dfa4446556badef0, 5bd93be2ab2ecb9c] + sections: [9cac816674181eb0, 7c157764133fea1f, 40b4916d82eaf1d4, 10d151f2cc75317f, 3d0832f39b0d7059, 92742ba36533633d, 0aeca6145e7bd302] tool: 1 --- # Transportes del cliente {#client-transports} @@ -11,30 +11,15 @@ Nunca configuras uno por separado. `Client` recibe un único argumento posiciona El lado del *servidor* de cada uno (lo que hace `mcp.run()` y lo que despliegas) está en **[Ejecutar el servidor](../run/index.md)**. -## En memoria {#in-memory} - -Pasa el propio objeto del servidor: - -```python title="client.py" hl_lines="14" ---8<-- "docs_src/client_transports/tutorial001.py" -``` - -Sin subproceso, sin puerto, sin bytes por ningún canal. El cliente y el servidor son dos objetos en el mismo proceso, y aun así la llamada pasa por la capa real del protocolo: `search_books` se lista, se valida y se invoca exactamente igual que por HTTP. - -Eso lo convierte en dos cosas a la vez: - -* **Un arnés de pruebas.** Todos los ejemplos de esta documentación se ejercitan así, y la página **[Pruebas](../get-started/testing.md)** construye todo el patrón en torno a ello. -* **Una API de integración.** Una aplicación que construye el servidor no necesita un salto de red para llamar a sus herramientas. - ## Streamable HTTP {#streamable-http} -Pasa una cadena con una URL y obtienes **Streamable HTTP**, el transporte con el que despliegas: +Pasa una cadena con una URL y obtienes **Streamable HTTP**, el transporte detrás del que despliegas y el primero al que deberías recurrir: ```python title="client.py" hl_lines="5" --8<-- "docs_src/client_transports/tutorial002.py" ``` -Ese es todo el cliente de producción. `Client` envuelve la URL en `streamable_http_client(...)` por ti, encima de un `httpx2.AsyncClient` configurado como MCP necesita: `follow_redirects=True`, un timeout de 30 segundos para connect/write/pool y un timeout de lectura de 300 segundos, porque el servidor puede mantener abierto un flujo de respuesta. +Ese es todo el cliente de producción. `Client` envuelve la URL en `streamable_http_client(...)` por ti, encima de un `httpx2.AsyncClient` configurado como MCP necesita: un timeout de 30 segundos para connect/write/pool y un timeout de lectura de 300 segundos, porque el servidor puede mantener abierto un flujo de respuesta. !!! check Un `Client` que has construido **no** está conectado. La construcción solo elige el transporte; @@ -50,7 +35,7 @@ Ese es todo el cliente de producción. `Client` envuelve la URL en `streamable_h En cuanto necesites un encabezado `Authorization`, una cookie, un proxy, mTLS o un timeout distinto, construye tú mismo el `httpx2.AsyncClient` y entrégaselo a `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 @@ estándar `SSL_CERT_FILE`/`SSL_CERT_DIR` o pasa un `verify=ssl_context` explíci !!! info `httpx2` conserva la API conocida de `httpx`, así que si conoces `httpx` ya sabes cómo hacer la autenticación, los proxies, los event hooks, los reintentos y los límites de conexión aquí. El SDK no añade nada encima ni quita - nada. También es donde se conecta OAuth: + nada, salvo el [manejo de redirecciones](#redirects). También es donde se conecta OAuth: `httpx2.AsyncClient(auth=OAuthClientProvider(...))`. Todo ese flujo está en **[Clientes OAuth](oauth-clients.md)**. +### Redirecciones {#redirects} + +El transporte se conecta a la URL que le diste, y solo a ese origen. + +* Una redirección `307`/`308` que se queda en el mismo esquema, host y puerto se sigue, y también `http://` → `https://` en el mismo host. Eso cubre la redirección habitual de barra final `/mcp` → `/mcp/`. +* Una redirección a cualquier otro sitio **no** se sigue. La llamada falla con: + + ```text + MCPError: Redirect to https://other.example.com/mcp not followed; use that URL as the endpoint if it is the intended server + ``` + + Si esa URL es el servidor que querías, ponla en tu configuración. Si no, el servidor o un proxy delante de él está mal configurado. + +Esto vale para cualquier `httpx2.AsyncClient` que pases: su ajuste `follow_redirects` no se consulta para las solicitudes MCP, en ningún sentido. Los proveedores OAuth del SDK aplican la misma regla a sus propias solicitudes. + +!!! tip + `Redirect to http://… not followed: it would downgrade this HTTPS endpoint to plain HTTP` significa que el + servidor está detrás de un proxy que termina TLS del que no sabe nada y está emitiendo redirecciones `http://`. + Eso se arregla en el servidor (**[Desplegar y escalar](../run/deploy.md#behind-a-tls-terminating-proxy)**), + o usando la URL `https://…/` exacta que sugiere el mensaje. + ## stdio {#stdio} Un servidor **stdio** es un subproceso. El cliente lo lanza, escribe JSON-RPC en su stdin y lee JSON-RPC de su stdout. Así es como un host de escritorio ejecuta un servidor en tu máquina: un host *es* este código más una interfaz de usuario, y **[Conectar a un host real](../get-started/real-host.md)** es la misma relación vista desde el lado del host, como archivo de configuración. @@ -105,6 +111,18 @@ El stderr del proceso hijo va al tuyo. Para enviarlo a otro sitio, construye tú Un servidor que necesita una clave de API no la encontrará ahí. Pásala explícitamente con `env=`; esas variables se fusionan encima de la lista de permitidos. Eso es lo que hace `BOOKSHOP_API_KEY` arriba. +## En memoria {#in-memory} + +En una prueba no hay nada que desplegar ni nada que lanzar. Pasa el propio objeto del servidor: + +```python hl_lines="14" +--8<-- "docs_src/client_transports/tutorial001.py" +``` + +Sin subproceso, sin puerto, sin bytes por ningún canal. El cliente y el servidor son dos objetos en el mismo proceso, y aun así la llamada pasa por la capa real del protocolo: `search_books` se lista, se valida y se invoca exactamente igual que por HTTP. **[Pruebas](../get-started/testing.md)** construye todo el patrón en torno a ello. + +La misma forma sirve además como API de integración: una aplicación que construye ella misma el servidor puede llamar a sus herramientas sin un salto de red. + ## SSE {#sse} `sse_client(url)`, de `mcp.client.sse`, es el transporte HTTP al que reemplazó Streamable HTTP. Envuélvelo igual, `Client(sse_client("http://localhost:8000/sse"))`, para hablar con un servidor que todavía lo usa, y no construyas nada nuevo sobre él. @@ -113,15 +131,16 @@ El stderr del proceso hijo va al tuyo. Para enviarlo a otro sitio, construye tú Para `Client`, todo lo anterior es lo mismo. -Un **transporte** es cualquier gestor de contexto asíncrono que produce un par `(read, write)` de flujos de mensajes: formalmente, el protocolo `Transport` de `mcp.client`. `Client` resuelve su argumento por tipo: un objeto de servidor se conecta dentro del proceso, un `str` se convierte en `streamable_http_client(url)`, un `StdioServerParameters` se convierte en `stdio_client(params)` y cualquier otra cosa se entra directamente como transporte. Esa última regla es la razón por la que `stdio_client(...)`, `streamable_http_client(...)` y `sse_client(...)` encajan todos en el mismo hueco, y por la que puedes escribir el tuyo. +Un **transporte** es cualquier gestor de contexto asíncrono que produce un par `(read, write)` de flujos de mensajes: formalmente, el protocolo `Transport` de `mcp.client`. `Client` resuelve su argumento por tipo: un `str` se convierte en `streamable_http_client(url)`, un `StdioServerParameters` se convierte en `stdio_client(params)`, un objeto de servidor se conecta dentro del proceso y cualquier otra cosa se entra directamente como transporte. Esa última regla es la razón por la que `stdio_client(...)`, `streamable_http_client(...)` y `sse_client(...)` encajan todos en el mismo hueco, y por la que puedes escribir el tuyo. ## Resumen {#recap} -* `Client(mcp)` (el objeto del servidor) se conecta en memoria. Úsalo para pruebas y para integración. * `Client("http://.../mcp")` (una URL) se conecta por Streamable HTTP, el transporte de producción. * Los encabezados, la autenticación, los proxies y los timeouts van en un `httpx2.AsyncClient` que pasas a `streamable_http_client(url, http_client=...)`. No existe el argumento nombrado `headers=`. +* Las redirecciones se siguen solo dentro del propio origen de la URL (un `307`/`308` de barra final), más `http`→`https` en el mismo host. Cualquier otra cosa falla con `Redirect to … not followed`; configura la URL final. * stdio es `Client(StdioServerParameters(...))`. Envuélvelo tú mismo en `stdio_client(...)` solo para redirigir el stderr del proceso hijo. * El subproceso recibe un entorno con lista de permitidos, no el tuyo; `env=` se añade a él. +* `Client(mcp)` (el objeto del servidor) se conecta en memoria. Úsalo en pruebas, o para integrar un servidor en la aplicación que lo construyó. * Un transporte es cualquier cosa con la que puedas hacer `async with x as (read, write)`. `Client` entrega directamente a ese protocolo todo lo que no sea un objeto de servidor, una URL ni un `StdioServerParameters`. * Construir un `Client` elige el transporte. `async with` lo abre. diff --git a/i18n/es/pages/deprecated.md b/i18n/es/pages/deprecated.md index 43beb87a8c..9e93dfe7ae 100644 --- a/i18n/es/pages/deprecated.md +++ b/i18n/es/pages/deprecated.md @@ -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 --- # Funcionalidades obsoletas {#deprecated-features} -La especificación 2026-07-28 retira cinco cosas. El SDK sigue implementando todas y cada una, y todas llevan ahora un **aviso de obsolescencia**. Una función auxiliar del SDK queda obsoleta por su cuenta y aparece [al final](#deprecated-sdk-helpers). +La especificación 2026-07-28 retira cinco cosas. El SDK sigue implementando todas y cada una, y todas llevan ahora un **aviso de obsolescencia**. Algunas partes del SDK quedan obsoletas por su cuenta y aparecen [al final](#deprecated-sdk-helpers). La tabla siguiente nombra cada funcionalidad obsoleta, explica por qué desaparece e indica el reemplazo sobre el que construir. @@ -137,11 +137,13 @@ Esa es toda la API. No hay un interruptor por método, y tampoco lo quieres: la ## Funciones auxiliares del SDK obsoletas {#deprecated-sdk-helpers} -No son cambios de la especificación, solo detalles internos del SDK con un reemplazo mejor. Avisan con el mismo `MCPDeprecationWarning` y se eliminarán en 3.0. +No son cambios de la especificación, solo usos del SDK que tienen un reemplazo mejor. Avisan con el mismo `MCPDeprecationWarning`, y 3.0 elimina la forma antigua. | Obsoleto | Qué hacer en su lugar | |---|---| | `FuncMetadata.call_fn_with_arg_validation()` | `FuncMetadata.validate_arguments()` y después `FuncMetadata.call_fn()`. Solo lo llamaba el código que maneja `FuncMetadata` directamente (una subclase propia de `Tool`, por ejemplo). | +| `AuthSettings(resource_server_url=...)` sin `validate_token_resource=` | Defínelo: `True` hace que el servidor rechace los tokens bearer que tu verificador no declara como emitidos para `resource_server_url`; `False` indica que tu verificador comprueba por sí mismo la audiencia del token (consulta **[Autorización](run/authorization.md#a-token-verifier)**). Sin definir se comporta como `False`; 3.0 hace que `True` sea el valor por defecto siempre que `resource_server_url` esté definido. | +| `ClientCredentialsOAuthProvider(...)` o `PrivateKeyJWTOAuthProvider(...)` sin `issuer=` | Pasa `issuer=` con el servidor de autorización que emitió las credenciales (consulta **[Escribir clientes OAuth](client/oauth-clients.md#machine-to-machine)**). Sin él, el servidor MCP decide qué servidor de autorización las recibe; 3.0 hace obligatorio el argumento nombrado. | ## Resumen {#recap} @@ -150,7 +152,7 @@ No son cambios de la especificación, solo detalles internos del SDK con un reem * Obsoleto es solo un aviso: no hay cambios en lo que se transmite, todo sigue funcionando contra sesiones anteriores a 2026 y recibes un `MCPDeprecationWarning` visible (un `UserWarning`, así que está activo por defecto). * El muestreo y los roots necesitan además un canal de retorno que una sesión 2026-07-28 no tiene. En una conexión moderna avisan y después lanzan una excepción. * `warnings.filterwarnings("ignore", category=MCPDeprecationWarning)` silencia toda la categoría; `"error::mcp.MCPDeprecationWarning"` en pytest la convierte en un fallo de prueba. -* Una función auxiliar del SDK, `FuncMetadata.call_fn_with_arg_validation()`, queda obsoleta por separado y se eliminará en 3.0. +* Las [partes del SDK obsoletas](#deprecated-sdk-helpers) siguen la misma regla: avisan ahora, y 3.0 elimina la forma antigua. * El código nuevo no debería construirse sobre nada de esto. Todas las demás páginas de esta documentación enseñan la API actual. diff --git a/i18n/es/pages/get-started/first-steps.md b/i18n/es/pages/get-started/first-steps.md index 86fce40692..a0f3b3e890 100644 --- a/i18n/es/pages/get-started/first-steps.md +++ b/i18n/es/pages/get-started/first-steps.md @@ -1,6 +1,6 @@ --- translation: - sections: [0d6c05bcbf836bf3, 59a7b14eeefc68c1, 7114d8d6daba203f, e8bbb56a98ba7bc9, 5138010f6159901c, f78da7c7c363d4c6, 220a939cab348686] + sections: [0d6c05bcbf836bf3, 9a78b5f6b44b18ab, 7114d8d6daba203f, e8bbb56a98ba7bc9, bfd2fd1153e71dac, 1615a994ef071fdd, 65c599fae991f245] tool: 1 --- # Primeros pasos {#first-steps} @@ -17,7 +17,7 @@ Tres palabras que verás en cada página a partir de aquí: * Un **cliente** vive dentro del host y habla MCP. El host ejecuta un cliente por cada servidor al que está conectado. * Un **servidor** es lo que construyes con este SDK. Expone cosas a los clientes. Nunca habla directamente con el modelo. -Tú escribes el servidor. Los hosts son el producto de otra persona. El SDK también te da un `Client`. Lo usarás para probar tus servidores, y aparece más adelante en esta página. +Tú escribes el servidor. Los hosts son el producto de otra persona. El SDK también te da un `Client`, la misma clase que usaría un host para llegar a un servidor por URL o lanzarlo como subproceso. Aparece más adelante en esta página, y es también la forma en que probarás tus servidores. ## Las tres primitivas {#the-three-primitives} @@ -83,22 +83,20 @@ Viste tres pestañas en el Inspector. ¿Cómo supo que había tres? Cuando un cliente se conecta, el servidor declara sus **capacidades**: qué familias de solicitudes va a responder. El cliente usa esa declaración para decidir qué vale la pena pedir siquiera. Nunca la escribiste; `MCPServer` la declara por ti. -Míralo tú mismo. El `Client` del SDK acepta el objeto servidor directamente y se conecta a él **en memoria** (sin subproceso, sin puerto): - -```python -import asyncio - -from mcp import Client - -from server import mcp +Míralo tú mismo. Deja `server.py` ejecutándose sobre HTTP en una terminal: +```console +uv run mcp run server.py --transport streamable-http +``` -async def main() -> None: - async with Client(mcp) as client: - print(client.server_capabilities.model_dump(exclude_none=True)) +y apunta un cliente hacia él desde otra: +```python title="client.py" hl_lines="7-8" +--8<-- "docs_src/first_steps/tutorial001_client.py" +``` -asyncio.run(main()) +```console +python client.py ``` ```text @@ -118,17 +116,18 @@ Ese diccionario son las **capacidades** declaradas de tu servidor. Es lo primero Fíjate en lo que no está. `completions` (autocompletado de argumentos para plantillas de recurso y prompts) necesita un handler que escribes tú, este servidor no tiene uno, así que la capacidad está ausente y un cliente bien hecho no la pedirá. Esa es la regla para todo lo opcional: registra la cosa y la capacidad aparece; **[Autocompletado](../servers/completions.md)** lo demuestra. !!! info - `Client(mcp)` es el mismo cliente en memoria con el que se prueba cada ejemplo de esta - documentación, y es como probarás los tuyos. Tiene una página entera: **[Pruebas](testing.md)**. + Ese `client.py` es un cliente MCP completo, y **[El cliente](../client/index.md)** es su página. + En una prueba te saltas la terminal y el puerto y le pasas a `Client` el propio objeto servidor, + `Client(mcp)`. Eso también tiene una página entera: **[Pruebas](testing.md)**. ## Lo que no escribiste {#what-you-did-not-write} Repasa esta página. Escribiste tres pequeñas funciones de Python. **No** escribiste: * Un JSON Schema. `a: int, b: int` *es* el esquema de `add`. -* Un handler de solicitudes. `tools/list`, `resources/read`, `prompts/get`: todos servidos por ti. +* Un handler de solicitudes. `tools/list`, `resources/read`, `prompts/get`: el SDK los atiende todos por ti. * Una declaración de capacidades. `MCPServer` la hizo por ti. -* Una línea de protocolo. La negociación de versión, el encuadre JSON-RPC, el intercambio de capacidades: todo ocurrió dentro de `mcp dev` y `Client(mcp)`, y nunca lo viste. +* Una línea de protocolo. La negociación de versión, el encuadre JSON-RPC, el intercambio de capacidades: todo ocurrió dentro de `mcp dev` y `client.py`, y nunca lo viste. Esa proporción es la razón de ser del SDK. @@ -139,6 +138,6 @@ Esa proporción es la razón de ser del SDK. * Un decorador por primitiva: `@mcp.tool()`, `@mcp.resource(uri)`, `@mcp.prompt()`. Nombre, descripción y esquema salen de la función. * Una URI con un `{param}` crea una **plantilla** de recurso, que se lista aparte de los recursos concretos. * Las **capacidades** del servidor se declaran por ti, y un cliente solo pide lo que un servidor declara. -* `Client(mcp)` se conecta al objeto servidor en memoria: tu entorno de pruebas desde el primer día. +* `Client("http://localhost:8000/mcp")` habla con tu servidor en ejecución. Pásale en su lugar el objeto servidor, `Client(mcp)`, y es tu entorno de pruebas desde el primer día. Lo siguiente es **[Conectar a un host real](real-host.md)**: este servidor dentro de Claude Desktop o un IDE, de verdad. Después, **[Pruebas](testing.md)**: una página, un cliente en memoria, y nunca más adivinas si funciona. Tras eso, cada primitiva tiene su propia página, empezando por la que maneja el modelo: **[Herramientas](../servers/tools.md)**. diff --git a/i18n/es/pages/get-started/testing.md b/i18n/es/pages/get-started/testing.md index 2c11eaacf2..4d67310c29 100644 --- a/i18n/es/pages/get-started/testing.md +++ b/i18n/es/pages/get-started/testing.md @@ -1,13 +1,13 @@ --- translation: - sections: ['4926721070127497', c52a1de2b6b32f40, 8e792bf8c7489ec6, 627195f7159e24ef] + sections: [5d13c2f0ba42c0d2, c52a1de2b6b32f40, 8e792bf8c7489ec6, 38552ea228b0a04f] tool: 1 --- # Pruebas {#testing} -El SDK de Python incluye una clase `Client` con un **transporte en memoria**: le pasas tu objeto servidor y se conecta a él directamente. +La clase `Client` del SDK, la misma que se conecta a una URL o lanza un subproceso, también se conecta **en memoria**: le pasas tu objeto servidor y habla con él directamente. -Sin subproceso. Sin puerto. Sin transporte alguno. Es la misma idea que el `TestClient` de FastAPI. +Sin subproceso. Sin puerto. Nada que se transmita por ningún canal. Es la misma idea que el `TestClient` de FastAPI. ## Uso básico {#basic-usage} @@ -97,11 +97,11 @@ remoto. En una prueba eso es exactamente lo que *no* quieres, y es lo que cambia Déjalo activado en las pruebas. No tiene ningún sentido en código de producción. -## En proceso por defecto {#in-process-by-default} +## Neutral respecto a la generación por defecto {#era-neutral-by-default} !!! note `Client(mcp)` se conecta en proceso y es **neutral respecto a la generación** por defecto: sondea - el servidor y elige la ruta de protocolo adecuada. Fija `mode="legacy"` si tu prueba ejercita + el servidor y elige la ruta de protocolo adecuada. Fija `mode="legacy"` si tu prueba comprueba comportamientos específicos de las conexiones heredadas (envío de muestreo (sampling) o elicitación (elicitation), `message_handler`), y quita `raise_exceptions=True` en ese caso: una conexión heredada nunca depura los errores en primer lugar, y el indicador relanza el fallo diff --git a/i18n/es/pages/handlers/multi-round-trip.md b/i18n/es/pages/handlers/multi-round-trip.md index 75555cb49d..4c0e10c6c4 100644 --- a/i18n/es/pages/handlers/multi-round-trip.md +++ b/i18n/es/pages/handlers/multi-round-trip.md @@ -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 --- # Solicitudes de varias idas y vueltas {#multi-round-trip-requests} @@ -164,7 +164,7 @@ El `Server` de bajo nivel es el nivel sin pilas incluidas: a diferencia de `MCPS ## Un resultado de 2026-07-28 {#a-2026-07-28-result} -`InputRequiredResult` solo existe en la versión del protocolo **2026-07-28**. El `Client(server)` en memoria la negocia por ti; a través del canal, `mode="auto"` la descubre. Tras conectar, `client.protocol_version` te dice qué obtuviste. +`InputRequiredResult` solo existe en la versión del protocolo **2026-07-28**. El `mode="auto"` por defecto de `Client` la descubre en cualquier conexión. Tras conectar, `client.protocol_version` te dice qué obtuviste. !!! warning Una sesión anterior a 2026 no tiene dónde poner un `InputRequiredResult`. Devuelve uno desde tu diff --git a/i18n/es/pages/handlers/progress.md b/i18n/es/pages/handlers/progress.md index 8f454dedc9..6e84b938e1 100644 --- a/i18n/es/pages/handlers/progress.md +++ b/i18n/es/pages/handlers/progress.md @@ -1,6 +1,6 @@ --- translation: - sections: [5315262fe26b33e1, 9d8e98840f1b78f0, 0284b215e85366c4, 8534d8dbb4053a70, 2966fac6fe697007] + sections: [5315262fe26b33e1, 9d8e98840f1b78f0, 52d6009a07e770ea, 8534d8dbb4053a70, 2966fac6fe697007] tool: 1 --- # Progreso {#progress} @@ -29,19 +29,17 @@ Tres argumentos, y tú decides qué significan: El cliente lo activa **por llamada**, pasando `progress_callback=` a `call_tool`: -```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) El callback es una función `async` que recibe exactamente lo que reportó el servidor: `progress`, `total`, `message`. !!! info - `Client(mcp)` se conecta directamente al objeto servidor, en memoria; es el mismo cliente sobre el que se construye la página **[Pruebas](../get-started/testing.md)**. - `progress_callback` es el mismo parámetro sea cual sea el transporte que use el `Client`; - los *tiempos* que vas a ver son los de la conexión en memoria. Ejecuta tu callback - de forma directa, así que cada reporte llega antes de que `call_tool` devuelva. Con un transporte real, - las notificaciones compiten con el resultado, y un callback lento puede seguir ejecutándose después de que `call_tool` - haya devuelto. + `progress_callback` es el mismo parámetro le pases lo que le pases a `Client`: una URL como aquí, un + `StdioServerParameters` o el objeto servidor en una prueba. Eso sí, ten en cuenta los tiempos con un + transporte real. Cada notificación se entrega por su cuenta, al margen de la respuesta, así que un callback + lento puede seguir ejecutándose después de que `call_tool` haya devuelto. Solo la conexión de prueba en el + mismo proceso ejecuta el callback de forma directa y garantiza que cada reporte llegue antes. ### Pruébalo {#try-it} -Pon `client.py` junto a `server.py` y ejecútalo: +Sirve `server.py` por HTTP y luego ejecuta el cliente desde una segunda 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.'} ``` -Cada `await ctx.report_progress(...)` en el servidor se convirtió en una llamada a `show` en el cliente, en orden, y ambas líneas se imprimieron **antes** de que `call_tool` devolviera. El progreso no va empaquetado en el resultado; se transmite mientras la herramienta sigue trabajando. +Cada `await ctx.report_progress(...)` en el servidor se convirtió en una llamada a `show` en el cliente, en orden. El progreso no va empaquetado en el resultado. Se transmite mientras la herramienta sigue trabajando. !!! warning `progress_callback` pertenece a la **llamada**, no al `Client`. No hay un argumento del constructor diff --git a/i18n/es/pages/protocol-versions.md b/i18n/es/pages/protocol-versions.md index 9d7211f1eb..a9f035a9d9 100644 --- a/i18n/es/pages/protocol-versions.md +++ b/i18n/es/pages/protocol-versions.md @@ -1,6 +1,6 @@ --- translation: - sections: [478fd619e5f90ef8, aef094a00e44e248, bab8cbf3449fa7e9, df1809b15a58335b, 5f9d8c2336ed0239, f54974398e43ddef, b24443dd78584870] + sections: [d98e337bf28845e0, dd642acbba36f615, f547b3e76da19c94, 149513378588da5c, 4e149ed992046709, f54974398e43ddef, b24443dd78584870] tool: 1 --- # Versiones del protocolo {#protocol-versions} @@ -11,9 +11,17 @@ Los servidores publicados antes de 2026-07-28 abren cada conexión con el **hand Casi nunca tienes que preocuparte por esto, porque `Client` negocia por ti. Esta página trata del único argumento del constructor que lo controla, `mode=`, y de las tres ocasiones en que lo cambias. +Cada fragmento de esta página es un `client.py` que habla con el `server.py` de Bookshop de **[El cliente](client/index.md)**. Inicia ese servidor en una terminal: + +```console +uv run mcp run server.py --transport streamable-http +``` + +Luego ejecuta cada fragmento en una segunda terminal con `python client.py`. + ## `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 @@ En cualquier caso terminas conectado, y `client.protocol_version` te dice cuál Esa es toda la funcionalidad. Un solo `Client`, servidores de cualquier generación, sin ramificaciones en tu código. !!! info - `MCPServer` responde a `server/discover` en todos los transportes (en memoria, stdio, Streamable - HTTP), así que contra tu propio servidor `auto` siempre llega a `2026-07-28`. El mecanismo de - respaldo solo se activa contra un servidor real anterior a 2026, que es exactamente cuando quieres que lo haga. + `MCPServer` responde a `server/discover` en todos los transportes (Streamable HTTP, stdio y la + conexión en proceso que usan tus pruebas), así que contra tu propio servidor `auto` siempre llega + a `2026-07-28`. El mecanismo de respaldo solo se activa contra un servidor real anterior a 2026, + que es exactamente cuando quieres que lo haga. ## `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 @@ En 2026-07-28 ya no existe. El servidor *devuelve* sus preguntas y tú repites l `mode` también acepta una cadena con una versión moderna del protocolo. Hoy ese conjunto es exactamente `["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 @@ El sondeo es barato, pero sigue siendo una ida y vuelta que pagas en cada recone Así que guárdala. Tras una conexión `auto`, `client.session.discover_result` contiene el `DiscoverResult` exacto que envió el servidor: sus `supported_versions`, sus `capabilities`, sus `instructions` y la identidad que el servidor grabó en el `_meta` del resultado. Devuélveselo como `prior_discover=` la próxima vez: -```python title="client.py" hl_lines="15 17" +```python title="client.py" hl_lines="8 10" --8<-- "docs_src/protocol_versions/tutorial004.py" ``` diff --git a/i18n/es/pages/run/asgi.md b/i18n/es/pages/run/asgi.md index 5d9363860a..9133ebfa5e 100644 --- a/i18n/es/pages/run/asgi.md +++ b/i18n/es/pages/run/asgi.md @@ -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 --- # Añadir a una app existente {#add-to-an-existing-app} @@ -100,7 +100,7 @@ Ese `/mcp` final es `streamable_http_path`. Ponlo en `"/"` y el prefijo de monta --8<-- "docs_src/asgi/tutorial004.py" ``` -Ahora los clientes se conectan a `/notes`, no a `/notes/mcp`. +Ahora los clientes se conectan a `/notes/`, no a `/notes/mcp`. ## CORS para clientes de navegador {#cors-for-browser-clients} @@ -143,4 +143,4 @@ Un cliente basado en navegador necesita dos permisos de tu parte: **enviar** sus * Los clientes de navegador necesitan CORS: `allow_headers` para los encabezados de solicitud `Mcp-*`, `expose_headers=["Mcp-Session-Id"]` para la respuesta. * `@mcp.custom_route()` añade endpoints HTTP simples, sin autenticación, junto a `/mcp`. -Una vez que el servidor es accesible en una URL real, **[El cliente](../client/index.md)** se conecta a él con esa URL en lugar de con un objeto servidor. +Una vez que el servidor es accesible en una URL real, **[El cliente](../client/index.md)** se conecta a él con esa URL. diff --git a/i18n/es/pages/run/authorization.md b/i18n/es/pages/run/authorization.md index c39fcdcec8..432031a143 100644 --- a/i18n/es/pages/run/authorization.md +++ b/i18n/es/pages/run/authorization.md @@ -1,13 +1,13 @@ --- translation: - sections: [d62c13457fc4a534, 80e73abaca6e0652, d1dc4c54cd00ec9c, 14ad3bc7904036bb, 5225f127bc1b9c77, fe1626fdd5aad1da, 4556cb7ea1a04a31] + sections: [d62c13457fc4a534, 80e73abaca6e0652, 128a492d18295f64, 14ad3bc7904036bb, 54a6697833fcc1d9, fe1626fdd5aad1da, 811d083c1da8bcf6] tool: 1 --- # Autorización {#authorization} -Sobre Streamable HTTP, tu servidor MCP es un servicio web común y corriente, y lo proteges igual que proteges cualquier servicio web: con tokens bearer de OAuth 2.1. +Sobre Streamable HTTP tu servidor MCP es un servicio web común y corriente, y lo proteges igual que proteges cualquier servicio web: con tokens bearer de OAuth 2.1. -En términos de OAuth, el servidor es un **servidor de recursos**. Nunca inicia la sesión de nadie y nunca emite un token. Hace una sola cosa: mirar el header `Authorization` de cada solicitud y decidir si el token que trae es válido. +En términos de OAuth, el servidor es un **servidor de recursos**. Nunca inicia la sesión de nadie y nunca emite un token. Hace una sola cosa: mirar el header `Authorization` de cada solicitud y decidir si el token que trae es bueno. Esta página es el lado del servidor. Un cliente que descubre tu servidor de autorización y obtiene el token está en **[Clientes OAuth](../client/oauth-clients.md)**. @@ -21,21 +21,25 @@ Ese es todo el triángulo. Todo lo que hay en esta página es el punto del medio ## Un verificador de tokens {#a-token-verifier} -El SDK no opina sobre cómo debe ser un token válido. Se lo dices tú, implementando **`TokenVerifier`**: +El SDK no tiene opinión sobre cómo es un token válido. Se lo dices tú, implementando **`TokenVerifier`**: -```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` es un protocolo con un solo método asíncrono. `verify_token` recibe el token en bruto del header `Authorization` y devuelve un **`AccessToken`** si es válido, `None` si no lo es. No hay nada más que implementar. -* Este busca el token en una tabla. Uno real verifica la firma de un JWT o llama al endpoint de introspección de tokens del servidor de autorización. Ese código es tuyo; el SDK solo lo llama. -* `token_verifier=` y `auth=` siempre van juntos. Pasa uno sin el otro y `MCPServer(...)` lanza un `ValueError` antes de atender ninguna solicitud. +* `TokenVerifier` es un protocolo con un único método asíncrono. `verify_token` recibe el token en bruto del header `Authorization` y devuelve un **`AccessToken`** si es válido, `None` si no lo es. No hay nada más que implementar. +* Este busca el token en una tabla; cada entrada registra el recurso para el que se emitió. Uno real verifica la firma de un JWT o llama al endpoint de introspección de tokens del servidor de autorización, e informa para quién se emitió el token (su `aud`) en `AccessToken.resource`. Ese código es tuyo; el SDK solo lo llama. +* `token_verifier=` y `auth=` siempre van juntos. Pasa uno sin el otro y `MCPServer(...)` lanza un `ValueError` antes de atender una sola solicitud. `AuthSettings` es la cara pública de tu servidor de recursos: * `issuer_url`: el servidor de autorización que emite tus tokens. -* `resource_server_url`: la URL pública de este endpoint MCP. Indica *para qué* recurso es un token y es donde vive el documento de descubrimiento. +* `resource_server_url`: la URL pública de este endpoint MCP. Indica para *qué* recurso es un token, y es donde vive el documento de descubrimiento. * `required_scopes`: todo token debe traerlos todos. +* `validate_token_resource`: rechaza cualquier token cuyo `AccessToken.resource` no sea `resource_server_url`. Dejarlo sin definir mientras `resource_server_url` está definido emite un aviso (`MCPDeprecationWarning`) y se comporta como `False`; la versión 3.0 hace que `True` sea el valor por defecto para los servidores de recursos. + * Actívalo cuando tu servidor de autorización vincula los tokens al `resource` que pidió el cliente, que los clientes MCP siempre envían. Mantén `resource_server_url` como la URL exacta a la que se conectan los clientes. + * Déjalo desactivado cuando tu servidor de autorización usa sus propios identificadores de audiencia (un identificador de API de Auth0, un ID de aplicación de Entra) y comprueba `aud` en tu verificador, devolviendo `None` para un token que no sea para este servidor. + * Si `aud` es una lista, pon en `resource` la entrada que sea igual a `resource_server_url`. !!! tip `examples/servers/simple-auth/` en el repositorio del SDK tiene un `IntrospectionTokenVerifier` que llama @@ -43,7 +47,7 @@ El SDK no opina sobre cómo debe ser un token válido. Se lo dices tú, implemen ## Lo que obtienes sobre HTTP {#what-you-get-over-http} -La autorización vive en los headers HTTP, así que solo existe en los transportes HTTP. Ejecútala en el que despliegues: `mcp.run(transport="streamable-http")` la pone en `http://127.0.0.1:8000/mcp`, y **[Ejecutar el servidor](index.md)** tiene el resto. La app ahora tiene dos rutas: +La autorización vive en los headers HTTP, así que solo existe en los transportes HTTP. Ejecútala en el que despliegues: `mcp.run(transport="streamable-http")` la pone en `http://127.0.0.1:8000/mcp`, y **[Ejecutar tu servidor](index.md)** tiene el resto. La app ahora tiene dos rutas: ```text /mcp @@ -65,7 +69,7 @@ Haz un `GET` a esa ruta well-known y obtienes los **Protected Resource Metadata } ``` -Este documento es la forma en que un cliente que nunca ha oído hablar de tu servidor encuentra la entrada: lee `authorization_servers` y va ahí a buscar un token. No escribiste nada de él. +Este documento es la forma en que un cliente que nunca ha oído hablar de tu servidor encuentra el camino de entrada: lee `authorization_servers` y va allí a buscar un token. No escribiste nada de él. !!! check Llama a `/mcp` sin token (o con uno para el que tu verificador devolvió `None`) y la solicitud @@ -78,25 +82,25 @@ Este documento es la forma en que un cliente que nunca ha oído hablar de tu ser {"error": "invalid_token", "error_description": "Authentication required"} ``` - No se analizó nada ni se ejecutó ninguna herramienta. Y ese puntero `resource_metadata` en `WWW-Authenticate` es + No se analizó nada y no se ejecutó ninguna herramienta. Y ese puntero `resource_metadata` en `WWW-Authenticate` es lo que hace automático el descubrimiento: 401 -> documento de metadatos -> servidor de autorización -> token -> reintento. !!! warning - Nada de esto protege a `stdio`. Una tubería no tiene header `Authorization`, así que ahí nunca se - consulta `token_verifier`. La frontera de seguridad de un servidor `stdio` es el proceso que lo lanzó. Lo mismo - vale para el `Client(mcp)` en memoria que usas en las pruebas: se conecta directamente al objeto servidor + Nada de esto protege `stdio`. Una tubería no tiene header `Authorization`, así que `token_verifier` nunca + se consulta ahí. El límite de seguridad de un servidor `stdio` es el proceso que lo lanzó. Lo mismo + vale para el `Client(mcp)` en memoria que usas en las pruebas: se conecta directamente al objeto del servidor y se salta la capa HTTP, autorización incluida. ## La identidad de quien llama {#the-callers-identity} Dentro de cualquier handler, **`get_access_token()`** es el `AccessToken` que tu verificador devolvió para la solicitud actual: -```python title="server.py" hl_lines="4 32-35" +```python title="server.py" hl_lines="4 35-38" --8<-- "docs_src/authorization/tutorial002.py" ``` * Funciona en herramientas, recursos y prompts, y no hay nada que pasar de un lado a otro: el middleware de autenticación lo guarda en una variable de contexto por solicitud. -* Recibes el **mismo objeto que construyó tu verificador**: `client_id`, `scopes`, `subject`, `expires_at` y cualquier `claims` extra que hayas añadido. Ese es el punto de enganche para reglas por herramienta: lee los scopes y rechaza. +* Recibes el **mismo objeto que construyó tu verificador**: `client_id`, `scopes`, `subject`, `expires_at` y cualquier `claims` extra que hayas adjuntado. Ese es el gancho para reglas por herramienta: lee los scopes y rechaza. * Fuera de una solicitud HTTP autenticada devuelve `None`. En memoria y sobre `stdio` siempre es `None`. Llama a `whoami` con `Authorization: Bearer alice-token` y el modelo lee: @@ -109,14 +113,14 @@ alice (scopes: notes:read) El SDK te da la mitad del servidor de recursos: verificar, anunciar, rechazar. No te da una página de inicio de sesión, una pantalla de consentimiento ni un token. -Para ver a las tres partes en movimiento, ejecuta `examples/servers/simple-auth/` del repositorio del SDK (un pequeño servidor de autorización y un servidor de recursos configurado exactamente como en esta página) y luego apunta `examples/clients/simple-auth-client/` hacia él para ver el recorrido completo de descubrimiento y token. +Para ver moverse a las tres partes, ejecuta `examples/servers/simple-auth/` del repositorio del SDK (un pequeño servidor de autorización y un servidor de recursos configurados exactamente como en esta página) y luego apunta `examples/clients/simple-auth-client/` hacia él para ver la coreografía completa de descubrimiento y token. !!! info Hay un segundo argumento del constructor, `auth_server_provider=`, que incrusta un servidor de autorización - completo dentro de tu servidor MCP. Es anterior a la separación AS/RS sobre la que se construye la especificación - de autorización de MCP. Los servidores nuevos no deberían recurrir a él. + completo dentro de tu servidor MCP. Es anterior a la separación AS/RS alrededor de la cual está construida + la especificación de autorización de MCP. Los servidores nuevos no deberían recurrir a él. -Un servidor de autorización también puede aceptar la aserción firmada de un proveedor de identidad empresarial en lugar de que un usuario haga clic en una pantalla de consentimiento, y el SDK admite los dos lados de ese intercambio. El grant, y el cliente que lo presenta, están en **[Aserción de identidad](../client/identity-assertion.md)**. +Un servidor de autorización también puede aceptar la aserción firmada de un proveedor de identidad empresarial en lugar de que un usuario pase por una pantalla de consentimiento, y el SDK admite ambos lados de ese intercambio. El grant, y el cliente que lo presenta, está en **[Aserción de identidad](../client/identity-assertion.md)**. ## Resumen {#recap} @@ -124,7 +128,7 @@ Un servidor de autorización también puede aceptar la aserción firmada de un p * `TokenVerifier` es toda la superficie de integración: un método asíncrono, entra un token, sale `AccessToken | None`. * `token_verifier=` y `auth=AuthSettings(issuer_url=..., resource_server_url=..., required_scopes=[...])` siempre van juntos. * El SDK publica los Protected Resource Metadata de [RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728) en `/.well-known/oauth-protected-resource/...` y responde a las solicitudes no autenticadas con un 401 cuyo header `WWW-Authenticate` apunta a ellos. Ese es todo el mecanismo de descubrimiento. -* `get_access_token()` en cualquier handler te dice quién llama. -* La autorización es un asunto de HTTP. `stdio` y el cliente en memoria nunca la ven. +* `get_access_token()` en cualquier handler es quién está llamando. +* La autorización es asunto de HTTP. `stdio` y el cliente de prueba en memoria nunca la ven. La mitad del cliente (descubrir tu servidor de autorización y obtener el token por ti) está en **[Clientes OAuth](../client/oauth-clients.md)**. Y un cliente que *afirma* una identidad en lugar de pedírsela a un usuario está en **[Aserción de identidad](../client/identity-assertion.md)**. diff --git a/i18n/es/pages/run/deploy.md b/i18n/es/pages/run/deploy.md index 3ff88dc1ae..53cd02cb7b 100644 --- a/i18n/es/pages/run/deploy.md +++ b/i18n/es/pages/run/deploy.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 --- # Desplegar y escalar {#deploy-scale} @@ -47,6 +47,22 @@ Desplegada detrás de un nombre de host real, esa misma configuración por defec desplegado que rechaza todas las conexiones es una lista de hosts permitidos hasta que se demuestre lo contrario. **[Solución de problemas](../troubleshooting.md)** también empieza por aquí. +## Detrás de un proxy que termina TLS {#behind-a-tls-terminating-proxy} + +Si TLS termina en un proxy (un ingress, un balanceador de carga, Caddy, nginx) y uvicorn sirve HTTP plano detrás de él, dile a uvicorn que confíe en los encabezados `X-Forwarded-*` del proxy: + +```console +uvicorn server:app --proxy-headers --forwarded-allow-ips='' +``` + +Sin eso, la app cree que se está sirviendo por `http://`, y cualquier redirección que emita (la habitual es `/mcp` → `/mcp/`) apunta a `http://…`. El cliente de Python se niega a seguir un endpoint HTTPS hacia HTTP plano y lo dice: + +```text +MCPError: Redirect to http://mcp.example.com/mcp/ not followed: it would downgrade this HTTPS endpoint to plain HTTP. +``` + +El parche del lado del cliente es configurar la URL exacta que sirve el servidor (`https://mcp.example.com/mcp/`, barra incluida) para que no haya ninguna redirección. La solución es el flag de arriba. `FORWARDED_ALLOW_IPS` es la forma en variable de entorno; `*` confía en todos los saltos, lo que solo es correcto cuando nada salvo el proxy puede llegar a uvicorn. + ## Workers, y quién tiene que ser sticky {#workers-and-who-has-to-be-sticky} Una vez que el nombre de host responde, pon más de un worker detrás. No hay ningún ajuste del SDK para eso; una app Starlette se escala como cualquier app ASGI, entregando el objeto a algo que sepa hacer fork: @@ -170,6 +186,7 @@ Un `MCPServer` es una implementación del protocolo, no un servidor de aplicacio ## Resumen {#recap} * Por defecto, la app responde solo a las solicitudes dirigidas a localhost. `transport_security=TransportSecuritySettings(allowed_hosts=[...], allowed_origins=[...])` es la puerta de salida a producción: hasta que lo pases, cada solicitud detrás de un nombre de host real es un `421` y el motivo solo está en el log del servidor. +* Detrás de un proxy que termina TLS, ejecuta uvicorn con `--proxy-headers --forwarded-allow-ips=...`, o sus redirecciones apuntarán a `http://` y el cliente las rechazará. * En 2026-07-28 no hay sesión ni nada sobre lo que un balanceador de carga pueda ser sticky. `stateless_http=True` es un ajuste solo para lo heredado porque una solicitud moderna se enruta y se responde antes de que ese flag llegue a leerse. * La clave por defecto de `requestState` es `os.urandom(32)`, acuñada por proceso. Un reintento de varias idas y vueltas que llega a otro worker falla con `-32602` *"Invalid or expired requestState"*. * La solución es `RequestStateSecurity(keys=[...])` **y** el mismo nombre de servidor en cada instancia. El nombre es el claim de audiencia por defecto del token. Las mismas claves, el mismo nombre. diff --git a/i18n/es/pages/run/index.md b/i18n/es/pages/run/index.md index a8964b70e7..8dd948d07d 100644 --- a/i18n/es/pages/run/index.md +++ b/i18n/es/pages/run/index.md @@ -1,6 +1,6 @@ --- translation: - sections: [fea8d769ff9edeba, ce8e2ad42f29ef71, 0d705efb19cf99c2, 2fd7cf825e6d2b2c, 9adc400e8c88e854, 318893ad8e2e9924, 6b63ab96b34476c0] + sections: [fea8d769ff9edeba, ce8e2ad42f29ef71, 0d705efb19cf99c2, 9fd2357154a5b7e7, 9adc400e8c88e854, 318893ad8e2e9924, 6b63ab96b34476c0] tool: 1 --- # Ejecutar el servidor {#running-your-server} @@ -75,6 +75,11 @@ Cada transporte tiene sus propios argumentos nombrados, todos en `run()`: * `max_request_body_size`: el cuerpo de solicitud más grande que se acepta, en bytes. Es 4 MiB por defecto; las solicitudes mayores reciben HTTP 413 antes del análisis o de la creación de la sesión. Súbelo solo cuando los mensajes MCP legítimos superen ese tamaño. +* `session_idle_timeout`: segundos que una sesión heredada puede pasar sin nada en curso antes de que el + servidor la cierre. Por defecto 1800. `None` lo desactiva. Consulta + [Duración de la sesión y límites](legacy-clients.md#session-lifetime-and-limits). +* `max_sessions`: cuántas sesiones heredadas mantiene un proceso a la vez. Por defecto 10 000. `None` + quita el límite. Se trata en la misma sección. * `event_store`, `retry_interval`, `transport_security`: reanudabilidad y protección contra DNS rebinding. Pueden esperar hasta que despliegues en algún lugar que no sea localhost; **[Desplegar y escalar](deploy.md)** cubre `transport_security`. !!! warning diff --git a/i18n/es/pages/run/legacy-clients.md b/i18n/es/pages/run/legacy-clients.md index 8b1e2bf28c..f7a1414c51 100644 --- a/i18n/es/pages/run/legacy-clients.md +++ b/i18n/es/pages/run/legacy-clients.md @@ -1,6 +1,6 @@ --- translation: - sections: [3d1663c18edc824c, d4fd37009a13f03d, af9f398a5a8b679a, 470c2dd144294d69, 8e45827e6d24e8c8, 91dfd0ce98ebb03c] + sections: [3d1663c18edc824c, 90956965ae6a1ca1, af9f398a5a8b679a, 5ce83b1f9d88da62, 0d9b5d13fffc94e5, 8e45827e6d24e8c8, 91dfd0ce98ebb03c] tool: 1 --- # Atender clientes heredados {#serving-legacy-clients} @@ -21,15 +21,25 @@ Así que un cliente heredado (legacy) no es algo *para* lo que construyes. Es al ## Un handler, ambas generaciones {#one-handler-both-eras} -Aquí tienes una herramienta que necesita preguntarle algo al usuario, y clientes de ambas generaciones que la llaman: +Aquí tienes una herramienta que necesita preguntarle algo al usuario: -```python title="server.py" hl_lines="24 37-38" +```python title="server.py" hl_lines="21" --8<-- "docs_src/legacy_clients/tutorial001.py" ``` `reserve` necesita una cosa que el modelo no proporcionó: cuántos ejemplares. `Annotated[..., Resolve(ask_quantity)]` es la forma en que una herramienta lo declara (**[Dependencias](../handlers/dependencies.md)** tiene todos los detalles). Nada en `reserve` nombra una versión, comprueba una capacidad ni se bifurca. -Los dos clientes están abiertos **al mismo tiempo**, sobre el mismo objeto `mcp`. `mode="legacy"` ejecuta el handshake `initialize`: exactamente la conexión que abre un cliente anterior a 2026. El otro toma el valor por defecto y queda en `2026-07-28`. +Sírvela por HTTP, y aquí tienes clientes de ambas generaciones que la llaman: + +```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" +``` + +Los dos clientes están abiertos **al mismo tiempo**, contra el mismo servidor en ejecución. `mode="legacy"` ejecuta el handshake `initialize`: exactamente la conexión que abre un cliente anterior a 2026. El otro toma el valor por defecto y queda en `2026-07-28`. Ejecuta `python client.py` desde una segunda terminal: ```text 2025-11-25 {'result': "Reserved 2 of 'Dune'."} @@ -62,6 +72,41 @@ Con un solo worker eso no se nota. Con dos, es todo el problema: una solicitud q perdidos a un cliente que se reconecta a la *misma* sesión), no un almacén de sesiones. Nunca hace que una sesión sea alcanzable desde otro proceso. +## Duración y límites de la sesión {#session-lifetime-and-limits} + +Una sesión heredada no vive para siempre, y un proceso no mantiene un número ilimitado de +ellas. Dos ajustes controlan esto. Ambos son argumentos nombrados de `run()`, `streamable_http_app()` +y `Server.streamable_http_app()`. Las conexiones modernas (`2026-07-28`) y `stateless_http=True` +no tienen sesiones, así que ninguno de los dos ajustes se les aplica. + +| Ajuste | Por defecto | Qué hace | Qué ve el cliente | Desactivarlo | +|---|---|---|---|---| +| `session_idle_timeout` | `1800` (30 min) | Cierra una sesión que no ha tenido nada en curso durante ese tiempo. | `404 Session not found`. Tiene que volver a hacer `initialize`. | `None` | +| `max_sessions` | `10_000` | Se niega a abrir una sesión más allá de esa cantidad. Las sesiones existentes no se tocan y no se expulsa ninguna. | `503 Too many open sessions` con el código JSON-RPC `-32603`. | `None` | + +Qué cuenta como "en curso": + +* Un stream `GET` abierto. Los clientes del SDK mantienen uno abierto, así que la sesión de un + cliente conectado nunca expira. +* Una solicitud que todavía se está respondiendo. Una llamada a herramienta que dura más que el + timeout no se interrumpe, y el plazo solo empieza a contar cuando termina. +* Nada más. Entre solicitudes el reloj corre. Cualquier solicitud en la sesión lo reinicia, + `ping` incluido. Una vez que una sesión ha expirado, nada la revive. + +Un cliente que termina su sesión con `DELETE` la libera de inmediato. También uno cuya +solicitud de apertura fue rechazada. + +```python +mcp.run(transport="streamable-http", session_idle_timeout=None, max_sessions=50_000) +``` + +Ambos eventos aparecen en el log del servidor. Una expiración es `Session idle timeout` en +nivel `INFO`. Una apertura rechazada es `Refusing to open a new session: sessions are already open` +en nivel `WARNING`. + +Los límites son por proceso. Con cuatro workers el techo es cuatro veces `max_sessions`, y cada +worker expira sus propias sesiones. + ## El único ajuste: `stateless_http` {#the-one-knob-stateless_http} Si la afinidad es un costo que te niegas a pagar, hay exactamente una cosa que puedes cambiar. @@ -87,7 +132,7 @@ Dos cosas sobre él importan más que lo que hace. !!! check Haz lo incorrecto. `reserve` es exactamente la herramienta que acaba de atender a ambos clientes. - Despliégala con `stateless_http=True`, conecta los mismos dos clientes por HTTP y llámala desde + Despliégala con `stateless_http=True`, conecta los mismos dos clientes y llámala desde cada uno. El cliente moderno sigue recibiendo `Reserved 2 of 'Dune'.` El tramo moderno no cambió. diff --git a/i18n/es/pages/troubleshooting.md b/i18n/es/pages/troubleshooting.md index 3779ff7237..04bf6378ab 100644 --- a/i18n/es/pages/troubleshooting.md +++ b/i18n/es/pages/troubleshooting.md @@ -1,6 +1,6 @@ --- translation: - sections: [2efaecdef109a5c5, fcacd3e66b8635a4, 25323d737dcf0261, 8a6e351ec756904d, 137454d469c867f5, 6392596bd6df54f0, 41126fa9c4fe432f, 480b6d7897e30ab4, d83bb682e708dde0, ebbed3449c499db4, 323ef84f6b4bebde, 30fd31be74169d9a, 656943c6cb567218, c2dc3b1007d2e987, 7cf5386b997d04e9, 0b59feed8384456e, 0cba47bae78d04eb, e4355f4c7cf4fb2e] + sections: [3d58228e81b99543, 170514ce901c4139, 17d61fad0a50d62b, 8a6e351ec756904d, 137454d469c867f5, 6392596bd6df54f0, 41126fa9c4fe432f, 480b6d7897e30ab4, d83bb682e708dde0, ebbed3449c499db4, 525cdf1755e29d4c, 30fd31be74169d9a, d2e88333d4f7841f, c2dc3b1007d2e987, d6eabf60cc366341, f798e815252852c2, 0cba47bae78d04eb, 2c218ba829abf74e] tool: 1 --- # Solución de problemas {#troubleshooting} @@ -13,6 +13,12 @@ Varias entradas se ejecutan contra este mismo servidor. Una herramienta y un rec --8<-- "docs_src/troubleshooting/tutorial001.py" ``` +Esas entradas se conectan a él en `http://localhost:8000/mcp`, así que déjalo ejecutándose por HTTP: + +```console +uv run mcp run server.py --transport streamable-http +``` + Los errores que cita esta página son reales: la propia suite de pruebas del SDK reproduce cada uno de ellos. ## `ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception)` {#exceptiongroup-unhandled-errors-in-a-taskgroup-1-sub-exception} @@ -23,7 +29,7 @@ Esto no es un error de MCP. Es ruido de anyio, y tu error real es la **última l ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: await client.read_resource("weather://Atlantis") ``` @@ -49,7 +55,7 @@ Dos cosas que hacer con eso: ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: try: await client.read_resource("weather://Atlantis") except MCPError as e: @@ -67,7 +73,7 @@ async def main() -> None: ```python async def main() -> None: - client = Client(mcp) + client = Client("http://localhost:8000/mcp") tools = await client.list_tools() # RuntimeError ``` @@ -75,7 +81,7 @@ Entra en él. `__aenter__` es la conexión: ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: tools = await client.list_tools() ``` @@ -251,7 +257,7 @@ app = Starlette(routes=[Mount("/", app=mcp.streamable_http_app())], lifespan=lif ## `MCPError: Session not found` {#mcperror-session-not-found} -El servidor no reconoce el `Mcp-Session-Id` que envió tu cliente, casi siempre porque el servidor **se reinició** (o te enrutaron a otra instancia). Las sesiones viven en la memoria de ese único proceso. +El servidor no reconoce el `Mcp-Session-Id` que envió tu cliente. O bien el servidor **se reinició** (o te enrutaron a otra instancia), o bien la sesión **expiró** porque no hubo nada en curso durante `session_idle_timeout`, que es de 30 minutos por defecto. Consulta [Duración y límites de la sesión](run/legacy-clients.md#session-lifetime-and-limits). Las sesiones viven en la memoria de ese único proceso. No hay ningún bug del servidor que encontrar. La respuesta HTTP es un `404` cuyo cuerpo *sí* es JSON-RPC, así que, a diferencia del `421` de arriba, el `Client` de python te muestra este tal cual: @@ -261,9 +267,9 @@ No hay ningún bug del servidor que encontrar. La respuesta HTTP es un `404` cuy La solución es reconectar: sal del bloque `async with Client(...)` y entra en uno nuevo, que negocia una sesión nueva. Para un cliente de larga duración, eso significa capturar `MCPError` alrededor de tus llamadas y reconectar ante este mensaje en lugar de reintentar dentro de una sesión muerta. -Si ocurre *sin* un reinicio, estás ejecutando más de un worker sin sticky sessions: cada worker mantiene su propia tabla de sesiones, así que una solicitud enrutada al equivocado acaba aquí. **[Desplegar y escalar](run/deploy.md)** y **[Atender clientes heredados](run/legacy-clients.md)** tienen todos los detalles y las dos soluciones (enrutamiento sticky o `stateless_http=True`). +Si ocurre *sin* un reinicio y sin que el cliente haya estado en silencio tanto tiempo, estás ejecutando más de un worker sin sticky sessions: cada worker mantiene su propia tabla de sesiones, así que una solicitud enrutada al equivocado acaba aquí. **[Desplegar y escalar](run/deploy.md)** y **[Atender clientes heredados](run/legacy-clients.md)** tienen todos los detalles y las dos soluciones (enrutamiento sticky o `stateless_http=True`). -Para quien opera el servidor, la línea de log correspondiente es `Rejected request with unknown or expired session ID: `. Se registra con nivel `INFO`, así que es invisible con el umbral habitual de `WARNING`. Verla en ráfagas justo después de un despliegue es normal; todos los clientes conectados están reconectando. +Para quien opera el servidor, la línea de log correspondiente es `Rejected request with unknown or expired session ID: `. Se registra con nivel `INFO`, así que es invisible con el umbral habitual de `WARNING`. Verla en ráfagas justo después de un despliegue es normal; todos los clientes conectados están reconectando. Cuando lo que ocurrió es que la sesión expiró, a esa línea la precede `Session idle timeout`, también con nivel `INFO`. ## `MCPError: Method not found` {#mcperror-method-not-found} @@ -275,7 +281,13 @@ Hay una cosa que **no** produce este error, aunque es una solicitud que el proto Tu servidor quiere preguntarle algo al usuario, y este cliente nunca dijo que se le pudiera preguntar. -Un resolutor de elicitación (elicitation) se niega de entrada cuando el cliente conectado no declaró la elicitación por formulario, y `e.error.data` nombra exactamente lo que falta: +Este Bistro pregunta antes de reservar, mediante un resolutor: + +```python title="server.py" hl_lines="15-17 21" +--8<-- "docs_src/troubleshooting/tutorial007.py" +``` + +Sírvelo en lugar del servidor Weather y llama a `book_table` desde un cliente que no pasó ningún `elicitation_callback`. El resolutor se niega de entrada, porque el cliente conectado nunca declaró la elicitación (elicitation) por formulario, y `e.error.data` nombra exactamente lo que falta: ```json { @@ -289,7 +301,7 @@ Pasa `elicitation_callback=` a `Client(...)`. Registrar el callback *es* la decl ```python async def main() -> None: - async with Client(mcp, elicitation_callback=handle_elicitation) as client: + async with Client("http://localhost:8000/mcp", elicitation_callback=handle_elicitation) as client: result = await client.call_tool("book_table", {"date": "Friday"}) ``` @@ -314,14 +326,14 @@ Este lo ves desde `ctx.elicit()` en una conexión heredada, y en cualquier conex Tu handler intentó contactar con el cliente a mitad de solicitud, en una conexión cuya llamada no tiene ningún canal capaz de llevar una solicitud desde el servidor. Hay tres configuraciones de servidor que ponen una llamada en esa situación. -**Una conexión `2026-07-28`: cualquier transporte, siempre.** El protocolo moderno no tiene solicitudes iniciadas por el servidor en absoluto, así que el servidor se niega antes de enviar nada. `ctx.elicit()` dentro de una herramienta es la forma clásica de toparse con esto (en la primera prueba en memoria, ya que `Client(server)` negocia `2026-07-28` sin que se lo pidas), y pasar `elicitation_callback=` no cambia nada, porque ninguna solicitud llega nunca al cliente para que la responda: +**Una conexión `2026-07-28`: cualquier transporte, siempre.** El protocolo moderno no tiene solicitudes iniciadas por el servidor en absoluto, así que el servidor se niega antes de enviar nada. `ctx.elicit()` dentro de una herramienta es la forma clásica de toparse con esto, normalmente en la primerísima **[prueba](get-started/testing.md)** en memoria de esa herramienta, ya que `Client(mcp)` negocia `2026-07-28` sin que se lo pidas. Pasar `elicitation_callback=` no cambia nada, porque ninguna solicitud llega nunca al cliente para que la responda: ```python title="server.py" hl_lines="16" --8<-- "docs_src/troubleshooting/tutorial006.py" ``` ```python -async def main() -> None: +async def test_book_table() -> None: async with Client(mcp) as client: await client.call_tool("book_table", {"date": "Friday"}) ``` @@ -363,7 +375,7 @@ El servidor no pudo verificar el token `requestState` que tu cliente devolvió c ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: await client.call_tool("forecast", {"city": "London"}, request_state="round-1-from-worker-a") ``` @@ -416,7 +428,7 @@ mcp = MCPServer("Weather", request_state_security=RequestStateSecurity(keys=[key * `Tool already exists:` en el log del servidor es la única señal de que dos herramientas con el mismo nombre se fundieron en una. * Un 421, tres formas de escribirlo: `Server returned an error response` (el `Client` de python), `421 Misdirected Request` / `Invalid Host header` (todo lo demás), `Invalid Host header: ` (el log del servidor). Solución: `transport_security=TransportSecuritySettings(allowed_hosts=[...])`. * `Task group is not initialized` -> una app montada cuyo lifespan de la app host nunca entró en `mcp.session_manager.run()`. -* `Session not found` -> el servidor se reinició; reconecta. +* `Session not found` -> el servidor se reinició o la sesión expiró (`session_idle_timeout`); reconecta. * `Cannot send 'elicitation/create': ... no back-channel ...` -> `ctx.elicit()` necesita un canal de servidor a cliente: una conexión `2026-07-28` nunca lo tiene, `stateless_http=True` quita el heredado y `json_response=True` quita el ligado a la solicitud. Usa un resolutor (un cliente heredado necesita además un servidor que conserve el canal). Su vecino `Method not found` es una solicitud de un método que la revisión del protocolo del otro lado no tiene. * `Client did not declare the form elicitation capability ...` y `Elicitation not supported` -> al cliente le falta `elicitation_callback=`. * `Invalid or expired requestState` nunca dice por qué en lo que se transmite. El log del servidor sí; `unknown key` significa compartir `RequestStateSecurity(keys=[...])` entre los workers. diff --git a/i18n/es/pages/whats-new.md b/i18n/es/pages/whats-new.md index 417f1eedbc..01a07527f6 100644 --- a/i18n/es/pages/whats-new.md +++ b/i18n/es/pages/whats-new.md @@ -1,6 +1,6 @@ --- translation: - sections: [cfe01c0c5863dfa2, 1c58c5cfcc37d455, a7392996acf1ad8f, 875eb2889263424e] + sections: [cfe01c0c5863dfa2, 0dbb68d8b210177b, 80cf193023af3ed4, 875eb2889263424e] tool: 1 --- # Novedades de la v2 {#whats-new-in-v2} @@ -43,11 +43,11 @@ No todo lo que una herramienta necesita debería venir del modelo. Novedad de la La v1 te entregaba tres capas anidadas: un gestor de contexto de transporte que producía flujos en crudo, una `ClientSession` envolviéndolos y un `await session.initialize()` llamado a mano. La v2 tiene un solo objeto: -```python title="client.py" hl_lines="14-18" ---8<-- "docs_src/client/tutorial001.py" +```python title="client.py" hl_lines="7-11" +--8<-- "docs_src/client/tutorial001_client.py" ``` -`Client` acepta un objeto servidor (en memoria, sin transporte: la historia de las pruebas), una URL (Streamable HTTP), un `StdioServerParameters` (un subproceso stdio) o cualquier otro gestor de contexto de transporte como `sse_client(...)`. Entrar en `async with` conecta y negocia la versión del protocolo, sea cual sea la generación que hable el servidor; `client.server_capabilities` y `client.protocol_version` simplemente están ahí después, y `client.server_info` también cuando el servidor se identifica (ahora es `Implementation | None`, porque la identidad en la generación 2026 es opcional). Los callbacks de muestreo y elicitación que registraste en la v1 siguen funcionando (sus cuerpos ven el mismo renombramiento de atributos a snake_case que todo lo demás en esta página), ahora también responden a las solicitudes dentro de resultados al estilo 2026 (más abajo) y se ejecutan concurrentemente en lugar de una a una. `ClientSession` sigue debajo para quien quiera la superficie de bajo nivel, y `client.session` te la entrega; también cambió (se ejecuta sobre el nuevo motor de despacho, y algunas de sus propias firmas cambiaron), así que lee la **[Guía de migración](migration.md#clientsession-now-runs-on-jsonrpcdispatcher-basesession-removed)** antes de bajar a ese nivel. +`Client` acepta una URL (Streamable HTTP), un `StdioServerParameters` (un subproceso stdio), cualquier otro gestor de contexto de transporte como `sse_client(...)` o, en las pruebas, el propio objeto servidor (en memoria, sin transporte). Entrar en `async with` conecta y negocia la versión del protocolo, sea cual sea la generación que hable el servidor; `client.server_capabilities` y `client.protocol_version` simplemente están ahí después, y `client.server_info` también cuando el servidor se identifica (ahora es `Implementation | None`, porque la identidad en la generación 2026 es opcional). Los callbacks de muestreo y elicitación que registraste en la v1 siguen funcionando (sus cuerpos ven el mismo renombramiento de atributos a snake_case que todo lo demás en esta página), ahora también responden a las solicitudes dentro de resultados al estilo 2026 (más abajo) y se ejecutan concurrentemente en lugar de una a una. `ClientSession` sigue debajo para quien quiera la superficie de bajo nivel, y `client.session` te la entrega; también cambió (se ejecuta sobre el nuevo motor de despacho, y algunas de sus propias firmas cambiaron), así que lee la **[Guía de migración](migration.md#clientsession-now-runs-on-jsonrpcdispatcher-basesession-removed)** antes de bajar a ese nivel. **[El Client](client/index.md)** lo presenta, **[Transportes del cliente](client/transports.md)** cubre las cuatro formas de conexión, **[Callbacks del cliente](client/callbacks.md)** cubre los callbacks en sí y **[Pruebas](get-started/testing.md)** muestra el patrón en memoria que sustituye al helper `create_connected_server_and_client_session()` de la v1. @@ -172,11 +172,15 @@ Toda solicitud iniciada por el servidor desaparece en 2026-07-28: elicitación p El reemplazo le da la vuelta a la llamada. Una herramienta que necesita algo del usuario *devuelve* la pregunta (`InputRequiredResult`), el cliente la responde con los mismos callbacks que siempre tuvo, y la llamada se reintenta con las respuestas adjuntas. `Client` dirige ese bucle por ti. En el servidor rara vez construyes tú el resultado, porque lo hace una **[dependencia](handlers/dependencies.md)**: anota un parámetro con `Resolve(ask_quantity)`, donde `ask_quantity` es una función ordinaria que escribes tú, y el SDK pregunta por el mecanismo que la conexión admita, una solicitud de elicitación en vivo en una sesión heredada o una solicitud de varias idas y vueltas en 2026. Un solo cuerpo de herramienta, ambas generaciones: -```python title="dual_era.py" hl_lines="24 37-38" +```python title="server.py" hl_lines="21" --8<-- "docs_src/legacy_clients/tutorial001.py" ``` -Ese archivo es la propuesta en un solo lugar: un servidor, una herramienta respaldada por `Resolve`, y un cliente heredado más un cliente moderno recibiendo ambos su respuesta, en memoria. **[Solicitudes de varias idas y vueltas](handlers/multi-round-trip.md)** explica el mecanismo (incluido `request_state`, que el SDK sella y verifica por ti); **[Elicitación](handlers/elicitation.md)** cubre cómo preguntar. +```python title="client.py" hl_lines="14-15" +--8<-- "docs_src/legacy_clients/tutorial001_client.py" +``` + +Esos dos archivos son toda la propuesta: un servidor, una herramienta respaldada por `Resolve`, y un cliente heredado más un cliente moderno recibiendo ambos su respuesta del mismo servidor en ejecución (**[Atender clientes heredados](run/legacy-clients.md)** los recorre). **[Solicitudes de varias idas y vueltas](handlers/multi-round-trip.md)** explica el mecanismo (incluido `request_state`, que el SDK sella y verifica por ti); **[Elicitación](handlers/elicitation.md)** cubre cómo preguntar. !!! warning "Este es el único lugar donde un servidor v1 portado cambia de comportamiento" Tus propias pruebas se lo encuentran primero: `Client(mcp)` negocia 2026-07-28 contra tu diff --git a/i18n/fr/pages/advanced/apps.md b/i18n/fr/pages/advanced/apps.md index 8e19e4db5e..414eee7e62 100644 --- a/i18n/fr/pages/advanced/apps.md +++ b/i18n/fr/pages/advanced/apps.md @@ -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 @@ Le SDK fournit cela sous la forme de l’extension intégrée `Apps` (`io.modelc ## Une horloge avec un cadran {#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" ``` @@ -31,7 +31,7 @@ Quatre étapes : Le HTML lui-même écoute le `postMessage` de l’hôte et affiche le résultat. Pour de vraies applications, utilisez dans votre HTML le SDK navigateur officiel [`@modelcontextprotocol/ext-apps`](https://github.com/modelcontextprotocol/ext-apps). Il vous donne `ontoolresult`, `callServerTool`, `getHostContext` et `onhostcontextchanged` au lieu d’événements de message bruts. -## Dégradation gracieuse {#graceful-degradation} +## Dégradation élégante {#graceful-degradation} Tous les clients n’affichent pas les apps. La spécification dit sans détour ce que cela implique pour vous : @@ -39,11 +39,31 @@ Tous les clients n’affichent pas les apps. La spécification dit sans détour Le modèle lit `content` ; l’iframe est pour les humains. Un hôte capable d’afficher une interface transmet quand même le résultat textuel au modèle, et un client purement textuel ne reçoit *que* cela. Le schéma canonique est donc : un outil, deux réponses. Regardez à nouveau `get_time` : -```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)` ne vaut `True` que lorsque le client a déclaré l’extension `io.modelcontextprotocol/ui` **et** listé `text/html;profile=mcp-app` dans ses paramètres `mimeTypes`. Le champ est obligatoire, donc un client qui l’omet ne compte pas. C’est exactement ce que déclare `main()` dans le même fichier : la moitié client de la négociation, et la réponse riche revient. +`client_supports_apps(ctx)` ne vaut `True` que lorsque le client a déclaré l’extension `io.modelcontextprotocol/ui` **et** listé `text/html;profile=mcp-app` dans ses paramètres `mimeTypes`. Le champ est obligatoire, donc un client qui l’omet ne compte pas. Voici la moitié client de la négociation : + +```python title="client.py" hl_lines="8 12" +--8<-- "docs_src/apps/tutorial001_client.py" +``` + +Servez `server.py` en HTTP, puis lancez le client depuis un second terminal : + +```console +uv run mcp run server.py --transport streamable-http +``` + +```console +python client.py +``` + +```text +2026-06-26T12:00:00Z +``` + +La réponse riche est revenue. Retirez `extensions=[APPS_SUPPORT]` de l’appel à `Client` et le même programme affiche `The time is 2026-06-26T12:00:00Z.` à la place, c’est-à-dire tout ce qu’un client purement textuel voit jamais. !!! warning Ne renvoyez jamais un texte de substitution comme `"[Rendered UI]"` pour seul contenu. Si le texte de repli est inutile, l’outil est inutile pour tout client purement textuel et pour le modèle lui-même. Écrivez la phrase. diff --git a/i18n/fr/pages/advanced/extensions.md b/i18n/fr/pages/advanced/extensions.md index 2ecdc7b295..bea862f34f 100644 --- a/i18n/fr/pages/advanced/extensions.md +++ b/i18n/fr/pages/advanced/extensions.md @@ -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 @@ Utilisez comme préfixe un domaine que vous contrôlez. `io.modelcontextprotocol La plus petite extension utile, c’est un outil et une table de paramètres : -```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 @@ La plus petite extension utile, c’est un outil et une table de paramètres : * `settings()` est la valeur annoncée sous `capabilities.extensions["com.example/stamps"]`. Renvoyez `{}` (la valeur par défaut) pour annoncer l’extension sans paramètres. * L’extension ne reçoit jamais le serveur. Elle déclare ses contributions sous forme de données ; `MCPServer` les consomme. Il n’y a pas de `self.server` à modifier. -Et `main()` en est la preuve, un client en mémoire branché directement sur `mcp` : +Servez-la en HTTP, et un client en est la preuve : -```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" ``` +Chaque `server.py` de cette page est servi avec cette commande, et chaque `client.py` tourne à côté, lancé avec `python client.py` depuis un second terminal. + ### Servir vos propres méthodes {#serving-your-own-methods} Une extension peut enregistrer de **nouvelles méthodes de requête** : ses propres verbes, servis à côté de ceux de la spécification : -```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 @@ Les méthodes sont **strictement additives**. Le SDK le fait respecter à la con ### Le côté client {#the-client-side} -Le `main()` du même fichier raconte toute l’histoire côté client, ses deux moitiés : +Le client est un programme à part entière, et il porte les deux moitiés de l’histoire côté client : -```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)])` déclare l’extension. Les déclarations deviennent `ClientCapabilities.extensions` : sur une connexion 2026-07-28, la table voyage dans l’enveloppe `_meta` de chaque requête, donc le serveur la voit sur **chaque** requête ; sur une connexion historique, elle transite par la poignée de main `initialize`. Le code serveur ne s’en soucie pas : `require_client_extension(ctx, ...)` et `ctx.session.check_client_capability(...)` lisent la bonne source dans les deux cas. * Les méthodes propres à un éditeur descendent d’un niveau, vers `client.session.send_request(...)` ; `Client` n’acquiert de méthodes de premier rang que pour les verbes de la spécification. `send_request` accepte n’importe quelle sous-classe de `Request`, donc la requête de l’éditeur passe telle quelle. +* `SearchRequest` et les deux modèles qu’elle transporte constituent le contrat de liaison de l’extension, donc le client les déclare de son côté. Une extension publiée les fournirait dans un paquet que les deux côtés importent. ### Intercepter `tools/call` {#intercepting-toolscall} @@ -109,12 +116,18 @@ Le hook enveloppe `tools/call` et rien d’autre. Pour ce qui concerne chaque me ## Utiliser une extension client {#using-a-client-extension} -Une **extension client**, c’est le même contrat vu du côté consommateur : un ensemble de comportements côté client derrière un seul identifiant. Passez des instances à `Client(extensions=[...])` et appelez les outils normalement : +Une **extension client**, c’est le même contrat vu du côté consommateur : un ensemble de comportements côté client derrière un seul identifiant. Ici, le serveur répond à `buy` par un reçu à échanger au lieu de la marchandise, et seulement pour un client qui a déclaré l’extension : -```python title="client.py" hl_lines="66-68" +```python title="server.py" hl_lines="22-25" --8<-- "docs_src/extensions/tutorial006.py" ``` +Côté client, passez des instances à `Client(extensions=[...])` et appelez les outils normalement : + +```python title="client.py" hl_lines="33-35" +--8<-- "docs_src/extensions/tutorial006_client.py" +``` + `call_tool("buy", ...)` renvoie un simple `CallToolResult`, comme tout autre appel. Ce que l’extension a changé : le serveur peut désormais répondre à `buy` par une **forme de résultat** `receipt` au lieu d’un résultat final, et `Receipts` la termine (ici en échangeant le reçu via un appel de suivi) avant que `call_tool` ne renvoie. Rien ne bouge au point d’appel. Retirez l’extension et rien de tout cela n’existe : le garde-fou du serveur refuse un client qui ne l’a pas déclarée (erreur -32021), et une forme revendiquée venant d’un serveur qui saute le garde-fou échoue à la validation, exactement comme la spécification l’exige pour un `resultType` non reconnu. Désactivé par défaut, aux deux bouts de la liaison. @@ -124,15 +137,15 @@ Pour annoncer un identifiant **sans aucun** comportement côté client (le serve ```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")]) ``` ## Écrire une extension client {#writing-a-client-extension} Dérivez `ClientExtension` et ne redéfinissez que ce dont vous avez besoin. Trois types de contributions, chacun avec une valeur par défaut : `settings()`, `claims()` et `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" ``` * L’identifiant suit la même grammaire que celui du serveur, validé au moment où la classe est définie. @@ -153,12 +166,18 @@ Deux règles discrètes. Les claims ne sont actifs que sur les connexions 2026-0 ### Verbes d’extension {#extension-verbs} -Les méthodes de requête propres à une extension n’ont besoin d’aucun enregistrement côté client. Un type de requête d’éditeur dérive de `mcp.types.Request` et passe par `client.session.send_request`, comme dans [Servir vos propres méthodes](#serving-your-own-methods). Un ajout : lorsqu’une clé des paramètres doit transiter par l’en-tête `Mcp-Name` (des spécifications d’extension comme tasks l’exigent pour leurs verbes), le type de requête déclare `name_param` : +Les méthodes de requête propres à une extension n’ont besoin d’aucun enregistrement côté client. Un type de requête d’éditeur dérive de `mcp.types.Request` et passe par `client.session.send_request`, comme dans [Servir vos propres méthodes](#serving-your-own-methods). Prenez un serveur dont l’extension sert un seul verbe portant sur un job nommé : -```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" ``` +Un ajout côté client : lorsqu’une clé des paramètres doit transiter par l’en-tête `Mcp-Name` (des spécifications d’extension comme tasks l’exigent pour leurs verbes), le type de requête déclare `name_param` : + +```python title="client.py" hl_lines="20-23 28-29" +--8<-- "docs_src/extensions/tutorial007_client.py" +``` + La session reflète `params["jobId"]` dans `Mcp-Name` sur chaque chemin d’envoi, et une valeur manquante échoue bruyamment au lieu d’omettre silencieusement un en-tête obligatoire. ## Ce qu’une extension ne peut pas faire {#what-an-extension-cannot-do} diff --git a/i18n/fr/pages/advanced/low-level-server.md b/i18n/fr/pages/advanced/low-level-server.md index 4b2d150c97..3fc1758b65 100644 --- a/i18n/fr/pages/advanced/low-level-server.md +++ b/i18n/fr/pages/advanced/low-level-server.md @@ -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 --- # Le Server de bas niveau {#the-low-level-server} @@ -36,18 +36,22 @@ Trois choses ont changé, et elles constituent toute l’API de bas niveau : ### Essayer {#try-it} -Pas d’Inspector pour celui-ci : `mcp dev` et `mcp run` n’acceptent qu’un `MCPServer`. Le `Client` en mémoire s’en moque ; il accepte un `Server` de bas niveau exactement comme il accepte un `MCPServer` : +`mcp dev` et `mcp run` n’acceptent qu’un `MCPServer`, vous servez donc celui-ci vous-même. La dernière ligne de `server.py` construit à partir de lui une application ASGI ordinaire, et uvicorn l’exécute : -```python title="main.py" +```console +uvicorn server:app --port 8000 +``` + +Pointez l’Inspector, ou n’importe quel client, vers `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 @@ Le même texte que celui produit par la version `@mcp.tool()`. Deux différences * `result.structured_content` vaut `None`. Le serveur de haut niveau enveloppe pour vous un `-> str` dans `{"result": ...}` ; ici, personne ne construit ce que vous n’avez pas construit. * `list_tools` renvoie le schéma que **vous** avez saisi, caractère pour caractère. La version de haut niveau avait `"title": "Query"` sur chaque propriété et un `"title": "search_booksArguments"` à la racine : des artefacts de Pydantic. À ce niveau, si quelque chose est sur la liaison, c’est vous qui l’y avez mis. +Dans un test, vous vous passez d’uvicorn et du port : `Client(server)` accepte un `Server` de bas niveau dans le même processus exactement comme il accepte un `MCPServer`, et **[Tests](../get-started/testing.md)** décrit précisément ce schéma. + ## Rien n’est vérifié pour vous {#nothing-is-checked-for-you} `MCPServer` rejette un mauvais argument avant même que votre fonction s’exécute, en validant l’appel par rapport au schéma qu’il a généré (**[Outils](../servers/tools.md)**). @@ -215,4 +221,4 @@ Chacun d’eux correspond à une idée pour laquelle vous avez désormais le voc * `add_request_handler(method, params_type, handler)` sert n’importe quelle méthode. `initialize` est réservée. * Les capacités qu’annonce un `Server` découlent des gestionnaires que vous avez enregistrés. -`Client(server)` a traité les deux serveurs de façon identique parce qu’ils *sont* le même protocole, et c’est tout l’intérêt. La couche suivante vers le bas n’est pas une classe du tout : c’est le **[Middleware](middleware.md)**. +Le client a traité les deux serveurs de façon identique parce qu’ils *sont* le même protocole, et c’est tout l’intérêt. La couche suivante vers le bas n’est pas une classe du tout : c’est le **[Middleware](middleware.md)**. diff --git a/i18n/fr/pages/advanced/pagination.md b/i18n/fr/pages/advanced/pagination.md index 4e0a9eff1f..2e6c50da06 100644 --- a/i18n/fr/pages/advanced/pagination.md +++ b/i18n/fr/pages/advanced/pagination.md @@ -1,6 +1,6 @@ --- translation: - sections: [a9aba7a026c7bd85, ed32bda7ba9ae33a, 7e64cc5646abb91f, 22a0129ee78b3c63, d875373c06d8d2f9] + sections: [a9aba7a026c7bd85, 83e2a08b9d46a398, 9fd8a0aa384b3257, 22a0129ee78b3c63, d875373c06d8d2f9] tool: 1 --- # Pagination {#pagination} @@ -31,9 +31,13 @@ La pagination sert au serveur dont la liste de ressources est en réalité une b ### Essayer {#try-it} -`Client(server)` se connecte à un `Server` de bas niveau en mémoire exactement comme il se connecte à un `MCPServer`. +`mcp run` n’accepte qu’un `MCPServer`, vous servez donc celui-ci vous-même. La dernière ligne de `server.py` construit une application ASGI ordinaire à partir du `Server`, et uvicorn l’exécute : -Appelez `list_resources()` sans argument. Vous obtenez dix ressources, de `book-1` à `book-10`, et `next_cursor` vaut la chaîne `"10"`. +```console +uvicorn server:app --port 8000 +``` + +Pointez n’importe quel client (**[Le client](../client/index.md)**, ou l’Inspector) vers `http://localhost:8000/mcp` et appelez `list_resources()` sans argument. Vous obtenez dix ressources, de `book-1` à `book-10`, et `next_cursor` vaut la chaîne `"10"`. Renvoyez-la avec `list_resources(cursor="10")` : la première ressource est `book-11`, le nouveau `next_cursor` vaut `"20"`. @@ -43,7 +47,7 @@ La dixième page revient avec `next_cursor` à `None`. Terminé. Chaque méthode `list_*` de `Client` (`list_tools`, `list_resources`, `list_resource_templates`, `list_prompts`) accepte un argument nommé `cursor=`. Vider une liste paginée tient en un `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 @@ Chaque méthode `list_*` de `Client` (`list_tools`, `list_resources`, `list_reso * Étendez la liste **avant** de regarder `next_cursor` : la dernière page contient elle aussi des ressources. * `next_cursor is None` est la sortie. Toute autre valeur repart directement dans `cursor=`, telle quelle. -Lancez son `main()` et il affiche `100 resources` : dix pages de dix, assemblées par une boucle qui n’a jamais su qu’il y avait dix pages. +Pendant qu’uvicorn sert toujours `server.py`, lancez `python client.py` dans un second terminal. Il affiche `100 resources` : dix pages de dix, assemblées par une boucle qui n’a jamais su qu’il y avait dix pages. C’est la même boucle que montre **[Le client](../client/index.md)** pour chaque verbe `list_*`, et elle ne coûte rien face à un serveur qui ne pagine pas : `next_cursor` vaut `None` dès la première réponse et la boucle s’exécute une seule fois. diff --git a/i18n/fr/pages/client/caching.md b/i18n/fr/pages/client/caching.md index a319b78471..6ccb5aab20 100644 --- a/i18n/fr/pages/client/caching.md +++ b/i18n/fr/pages/client/caching.md @@ -1,6 +1,6 @@ --- translation: - sections: [9e7b9a1710e5aeba, b74ca4c1d2ddddee, fa8714e61bf90c5a, 04db67a886b7271c, 857690fb8f876800] + sections: [9e7b9a1710e5aeba, 66a2e9acc9101d54, d3d25aa802f5145a, 04db67a886b7271c, 857690fb8f876800] tool: 1 --- # Indications de mise en cache {#caching-hints} @@ -30,7 +30,7 @@ Par défaut, chaque résultat indique `ttlMs: 0, cacheScope: "private"` : périm Sur le `Server` bas niveau, les gestionnaires (handlers) construisent leurs résultats à la main, et `ttl_ms` / `cache_scope` sont de simples champs des modèles de résultat. Un gestionnaire qui les définit explicitement l’emporte toujours sur le dictionnaire du constructeur, champ par champ : -```python title="server.py" hl_lines="10 16" +```python title="server.py" hl_lines="11 17" --8<-- "docs_src/caching/tutorial002.py" ``` @@ -44,10 +44,24 @@ Une réserve sur les listes paginées : le protocole exige **le même `cacheScop Sur une session 2026-07-28, `Client` respecte les indications pour vous : il embarque un cache de réponses, activé par défaut. Un résultat qui arrive avec un `ttlMs` est stocké, et un appel identique effectué dans ce TTL est servi depuis le cache, sans aller-retour. Un résultat qui ne porte *aucune* indication n’est pas mis en cache : les résultats sans indication reçoivent `CacheConfig.default_ttl_ms`, dont la valeur par défaut est `0` (périmé immédiatement), si bien qu’un serveur qui ne déclare rien voit exactement le même trafic, appel pour appel, qu’auparavant. -```python title="client.py" hl_lines="33 35 38" +Pour le voir à l’œuvre, servez avec uvicorn le `server.py` de la section précédente (sa dernière ligne construit l’application ASGI). Le gestionnaire affiche une ligne chaque fois qu’il s’exécute réellement : + +```console +uvicorn server:app --port 8000 +``` + +```python title="client.py" hl_lines="20 23 28" --8<-- "docs_src/caching/tutorial003.py" ``` +Lancez `python client.py` depuis un second terminal. Il affiche les indications que portait le premier résultat, le `ttlMs` du gestionnaire à côté du `cacheScope` du dictionnaire : + +```text +1000 public +``` + +Le terminal du serveur raconte la suite : entre les journaux de requêtes d’uvicorn, `tools/list served` apparaît trois fois. + Quatre appels, trois récupérations. Le deuxième appel a trouvé une entrée fraîche et n’a jamais atteint le serveur ; avancer l’horloge (injectée) au-delà du TTL a fait que le troisième récupère à nouveau ; le quatrième a indiqué `cache_mode="refresh"`. Cet argument nommé existe sur les cinq verbes avec cache (`list_tools`, `list_prompts`, `list_resources`, `list_resource_templates`, `read_resource`) : * `"use"` (la valeur par défaut) sert une entrée fraîche s’il y en a une, et stocke le résultat récupéré sinon. @@ -56,7 +70,7 @@ Quatre appels, trois récupérations. Le deuxième appel a trouvé une entrée f Une règle prime sur `"use"` : **les appels portant `meta` atteignent toujours le serveur.** Une requête avec `meta` défini (un jeton de progression, des champs de traçage) attend une requête sur la liaison ; sous `cache_mode="use"`, elle est donc traitée comme `"refresh"` : la lecture du cache est sautée, et le résultat récupéré remplace quand même l’entrée en cache. `"bypass"` et un `"refresh"` explicite se comportent comme d’habitude. -Pour désactiver entièrement la mise en cache, construisez avec `Client(server, cache=None)` : chaque appel redevient un aller-retour, et `cache_mode`, bien que toujours accepté, n’a aucun effet. +Pour désactiver entièrement la mise en cache, passez `cache=None` à la construction du `Client` : chaque appel redevient un aller-retour, et `cache_mode`, bien que toujours accepté, n’a aucun effet. La portée est elle aussi respectée automatiquement : les entrées `"private"` sont indexées sur la *partition* du cache (ci-dessous), tandis que les entrées `"public"` peuvent opter pour un partage plus large. Et **les notifications priment sur le TTL** pour les entrées exactes qu’elles désignent : une notification `list_changed` évince la liste correspondante en cache, et `resources/updated` évince la lecture en cache stockée exactement sous son URI, aussi fraîches soient-elles. Sur une connexion 2026-07-28, ces notifications arrivent sur un flux `subscriptions/listen` que vous ouvrez avec `client.listen(...)`, et l’éviction se termine avant que votre observateur ne voie l’événement ; tous les détails sont dans **[Abonnements](subscriptions.md)**. diff --git a/i18n/fr/pages/client/callbacks.md b/i18n/fr/pages/client/callbacks.md index 551453f2fa..2235e0f3a2 100644 --- a/i18n/fr/pages/client/callbacks.md +++ b/i18n/fr/pages/client/callbacks.md @@ -1,6 +1,6 @@ --- translation: - sections: [adf3c545b5be46b6, 916cd3ab1c03f461, e9be7a8d0eb0a456, 565890a636288ecf, 6af7e49db9129ec3, 06b0238c174186af, 90c6043be435fcb0] + sections: [adf3c545b5be46b6, 916cd3ab1c03f461, 32ef568335dd95a7, 565890a636288ecf, 6af7e49db9129ec3, 06b0238c174186af, 0abc5ea5cb7ff6b3] tool: 1 --- # Fonctions de rappel du client {#client-callbacks} @@ -63,7 +63,7 @@ Un `tools/call` de votre part, un `elicitation/create` en retour du serveur, auq `mode="legacy"` dans l’appel `Client(...)` fait un vrai travail. Par défaut, `Client(...)` négocie le chemin moderne du protocole, et ce chemin n’a pas de canal de retour (back-channel) pour les requêtes du serveur vers le client : `ctx.elicit` échoue avant même que votre fonction de rappel ne s’exécute. Ce n’est pas le transport qui en décide ; c’est le - protocole négocié, en mémoire comme via une URL. Fixez `mode="legacy"` dès que votre client doit + protocole négocié. Fixez `mode="legacy"` dès que votre client doit répondre à l’une d’elles ; tous les tests derrière cette page le font. Tous les détails sont dans **[Versions du protocole](../protocol-versions.md)**. Sur une session 2026-07-28, la fonction de rappel n’est pas morte, elle est alimentée autrement : quand un outil renvoie un @@ -151,4 +151,4 @@ Deux de plus. Aucune ne déclare quoi que ce soit. * `sampling_callback` et `list_roots_callback` fonctionnent de la même manière mais servent des fonctionnalités obsolètes ; les serveurs modernes utilisent à la place les requêtes à plusieurs allers-retours. * `logging_callback` et `message_handler` reçoivent des notifications. Ils ne déclarent rien. -Le premier argument de `Client(...)` est un objet transport. **[Transports client](transports.md)** couvre tous les types. +Le premier argument de `Client(...)` choisit le transport. **[Transports client](transports.md)** couvre tous les types. diff --git a/i18n/fr/pages/client/index.md b/i18n/fr/pages/client/index.md index 3a85f88440..291933a44c 100644 --- a/i18n/fr/pages/client/index.md +++ b/i18n/fr/pages/client/index.md @@ -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 --- # Le client {#the-client} @@ -11,13 +11,23 @@ C’est un seul objet avec un seul cycle de vie : vous le construisez, vous entr ## Votre premier client {#your-first-client} -```python title="client.py" hl_lines="14-18" +Un client a besoin d’un serveur avec qui dialoguer. Ce serveur Bookshop est celui auquel se connecte chaque extrait de cette page. Enregistrez-le sous le nom `server.py` et laissez-le tourner en HTTP : + +```python title="server.py" --8<-- "docs_src/client/tutorial001.py" ``` -Le serveur en haut n’est là que pour vous donner quelque chose à quoi vous connecter. Le client, ce sont les cinq lignes surlignées. +```console +uv run mcp run server.py --transport streamable-http +``` + +Cela le sert à l’adresse `http://localhost:8000/mcp`. Le client est un programme à part. Enregistrez-le sous le nom `client.py` et lancez `python client.py` dans un second terminal : -* `Client(mcp)` reçoit **l’objet serveur lui-même**. C’est le transport en mémoire : pas de sous-processus, pas de port, pas de HTTP. C’est ainsi que se connectent tous les exemples de cette page, et tous les tests que vous écrivez. +```python title="client.py" hl_lines="7-11" +--8<-- "docs_src/client/tutorial001_client.py" +``` + +* `Client("http://localhost:8000/mcp")` reçoit une **URL**, il se connecte donc en Streamable HTTP au serveur que vous venez de démarrer. * `async with` est le **cycle de vie**. Y entrer connecte et négocie ; en sortir déconnecte. Il n’y a pas de paire `connect()` / `close()`, et un `Client` ne peut pas être réutilisé une fois le bloc terminé. * À l’intérieur du bloc, les informations de connexion sont déjà là, sous forme de simples propriétés. @@ -25,10 +35,10 @@ Le serveur en haut n’est là que pour vous donner quelque chose à quoi vous c `Client` prend un seul argument positionnel et déduit le transport de son type : -* Une instance de `MCPServer` (ou du `Server` bas niveau) : connexion **dans le processus**. -* Une chaîne d’URL (`Client("http://localhost:8000/mcp")`) : Streamable HTTP, la voie de production. -* Un `StdioServerParameters` : la commande à lancer en **sous-processus**, avec laquelle le client dialogue via son stdin et son stdout. +* Une chaîne d’URL (`Client("http://localhost:8000/mcp")`) : Streamable HTTP, le transport derrière lequel vous déployez. +* Un `StdioServerParameters` : la commande à lancer comme **sous-processus** local, avec laquelle le client dialogue via son stdin et son stdout. * Un **transport** : tout ce sur quoi vous pouvez faire `async with ... as (read, write)`, comme `streamable_http_client(url, http_client=...)` autour de votre propre client HTTP. +* Une instance de `MCPServer` (ou du `Server` bas niveau) : connexion **dans le processus**, sans sous-processus ni port. Celle-ci sert aux tests, et **[Tests](../get-started/testing.md)** s’appuie dessus. Tout le reste de cette page est identique pour les quatre. Les en-têtes, les sous-processus, les délais d’expiration et le protocole `Transport` ont leur propre page : **[Transports côté client](transports.md)**. @@ -49,11 +59,11 @@ Vous n’avez jamais choisi de version du protocole. Par défaut, le `Client` so ## Lister les outils {#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()` renvoie un `ListToolsResult` ; les outils sont dans `.tools`. Chacun est la définition complète qu’un hôte transmettrait à un modèle : +`list_tools()` renvoie un `ListToolsResult` ; les outils sont dans `.tools`. Chacun est la définition complète qu’un hôte transmettrait à un modèle. Voici le premier : ```python tool.name # 'search_books' @@ -77,6 +87,8 @@ et `tool.input_schema` est le JSON Schema que le serveur a dérivé des annotati Ce schéma est tout ce dont une interface a besoin pour afficher un formulaire d’arguments, et tout ce dont un modèle a besoin pour produire des arguments valides. +Le second outil, `lookup_book`, a été enregistré sans `title=`, donc son `tool.title` vaut `None`. + !!! tip `title` est facultatif, donc une interface qui présente des outils à un humain doit choisir : le `title` s’il existe, le `name` sinon. `from mcp.shared.metadata_utils import get_display_name` fait exactement cela, @@ -86,7 +98,7 @@ Ce schéma est tout ce dont une interface a besoin pour afficher un formulaire d `call_tool(name, arguments)` exécute l’outil et vous renvoie un `CallToolResult`. -```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 @@ Un outil qui lève une exception ne lève **rien** dans votre client. Il revient Les verbes des ressources vont par paires : deux façons de lister, une façon de lire. -```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 @@ Un client peut aussi être prévenu quand une ressource change. Sur les connexio ## 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 @@ Un hôte transmet ces messages tels quels au modèle. C’est toute la fonctionn Un serveur doté d’un gestionnaire (handler) de complétion peut compléter automatiquement les arguments des prompts et des modèles de ressource au fil de la saisie de l’utilisateur. -```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 @@ La réponse se trouve dans `result.completion.values`. Tapez `"p"` et le serveur Chaque méthode `list_*` accepte un argument nommé `cursor=` et chaque résultat porte un `next_cursor`. Quand `next_cursor` vaut `None`, vous avez tout. -```python title="client.py" hl_lines="22-30" +```python title="client.py" hl_lines="7-15" --8<-- "docs_src/client/tutorial007.py" ``` -Cette boucle est correcte face à n’importe quel serveur. `MCPServer` renvoie tout en une seule page, donc `next_cursor` vaut `None` et la boucle s’exécute une fois, ce qui explique que la plupart du code ne l’écrive jamais. Les serveurs qui paginent réellement, et les règles auxquelles obéissent les curseurs, sont dans **[Pagination](../advanced/pagination.md)**. +La fonction `list_all_tools` est correcte face à n’importe quel serveur. `MCPServer` renvoie tout en une seule page, donc `next_cursor` vaut `None` et la boucle s’exécute une fois, ce qui explique que la plupart du code ne l’écrive jamais. Les serveurs qui paginent réellement, et les règles auxquelles obéissent les curseurs, sont dans **[Pagination](../advanced/pagination.md)**. ## Dans les tests {#in-tests} -`Client(mcp)`, sans processus ni port, est déjà un banc de test pour votre serveur. +Chaque `client.py` de cette page a atteint `server.py` en HTTP. Dans un test, vous vous passez du réseau et donnez à `Client` l’objet serveur lui-même : `from server import mcp`, puis `Client(mcp)`. Pas de processus, pas de port, et chaque méthode ci-dessus fonctionne de la même façon. -Il existe un drapeau du constructeur conçu pour cela : `Client(mcp, raise_exceptions=True)`. Il n’a d’effet que sur les connexions en mémoire, et **[Tests](../get-started/testing.md)** est la page qui l’explique et construit tout le modèle autour de lui. +Il existe un drapeau du constructeur conçu pour cela : `Client(mcp, raise_exceptions=True)`. Il n’a d’effet que sur les connexions dans le processus, et **[Tests](../get-started/testing.md)** est la page qui l’explique et construit tout le modèle autour de lui. ## Récapitulatif {#recap} -* `Client(x)` se connecte en mémoire à un objet serveur, en Streamable HTTP à une chaîne d’URL, et à tout le reste via un transport. +* `Client(x)` se connecte en Streamable HTTP à une chaîne d’URL, lance un sous-processus pour un `StdioServerParameters`, entre directement dans un transport et, dans les tests, prend l’objet serveur lui-même. * `async with` est tout le cycle de vie. À l’intérieur, `server_capabilities` et `protocol_version` sont déjà renseignés ; `server_info` et `instructions` le sont aussi lorsque le serveur les fournit. * `list_tools()` vous donne le `name`, le `title`, la `description` et le `input_schema` de chaque outil. * `call_tool()` renvoie `content` pour le modèle, `structured_content` pour votre code, et `is_error`. Un outil qui lève une exception est un résultat, pas une exception. diff --git a/i18n/fr/pages/client/oauth-clients.md b/i18n/fr/pages/client/oauth-clients.md index 346d2f49da..e89a259efa 100644 --- a/i18n/fr/pages/client/oauth-clients.md +++ b/i18n/fr/pages/client/oauth-clients.md @@ -1,6 +1,6 @@ --- translation: - sections: [c6899d3892bd9fa0, 79372cff3cc48a88, 63878d29e87c3e73, 13175843d3588af4, e7e2b9fd516f77de, 758f06399b513c1f, a05d7278487d610b] + sections: [c6899d3892bd9fa0, 79372cff3cc48a88, c2dae1ebe2ebd543, 13175843d3588af4, df06056fb16b3846, 758f06399b513c1f, a05d7278487d610b] tool: 1 --- # Clients OAuth {#oauth-clients} @@ -81,18 +81,20 @@ Regardez `main()`. Le fournisseur va sur le **client httpx2**, le client httpx2 La première fois que `Client` envoie une requête, le serveur répond `401`. Le fournisseur prend le relais : -1. **Découverte.** Il lit l’en-tête `WWW-Authenticate`, récupère les Protected Resource Metadata du serveur depuis `/.well-known/oauth-protected-resource`, apprend quel serveur d’autorisation protège cette ressource, et récupère les métadonnées de *ce* serveur-là. +1. **Découverte.** Il lit l’en-tête `WWW-Authenticate`, récupère les Protected Resource Metadata du serveur depuis `/.well-known/oauth-protected-resource`, apprend quel serveur d’autorisation protège cette ressource, et récupère les métadonnées de *ce* serveur-là. (Un serveur plus ancien qui ne publie aucune métadonnée de ressource se voit demander, à la place, les métadonnées de serveur d’autorisation à sa propre origine.) Dans les deux cas, les métadonnées doivent désigner, comme `issuer`, le serveur pour lequel elles ont été récupérées ; tout autre cas est refusé. 2. **Enregistrement.** Rien dans le stockage ? Il vous enregistre dynamiquement avec votre `OAuthClientMetadata` et stocke le résultat. 3. **Autorisation.** Il génère la paire PKCE et un `state`, construit l’URL d’autorisation, attend votre `redirect_handler`, puis attend votre `callback_handler` pour obtenir le code. 4. **Échange.** Il échange le code contre un `OAuthToken`, le stocke, et rejoue votre requête d’origine avec `Authorization: Bearer ...`. Après cela, il se fait discret. Les jetons sortent du stockage, un jeton d’accès expiré est actualisé avec le jeton d’actualisation, et ce n’est que lorsque rien de tout cela ne fonctionne qu’il relance le flux. +Une règle de transport s’applique à toutes ces requêtes : comme la requête MCP à l’intérieur de laquelle elles s’exécutent, elles ne suivent une redirection que si celle-ci reste sur la même origine et conserve la méthode (un 307/308 pour une barre oblique finale, par exemple), et traitent toute autre redirection comme si cette URL ne répondait pas. + Vous n’avez rien écrit de tout cela. Il reste deux arguments nommés (`client_metadata_url` et `validate_resource_url`), et ce fichier n’a besoin d’aucun des deux. `client_metadata_url` est celui qui mérite d’être connu ; il a sa propre section plus bas. ### Essayer {#try-it} -La plupart des exemples de cette documentation se vérifient avec un `Client(server)` en mémoire. Pas celui-ci : tout l’intérêt du flux est un `401` HTTP, et il n’y a pas de HTTP entre un client en mémoire et son serveur. +Le `Client(server)` en mémoire qu’utilisent vos tests n’est d’aucune aide ici : tout l’intérêt du flux est un `401` HTTP, et il n’y a pas de HTTP entre un client en mémoire et son serveur. Le dépôt fournit la version réelle. `examples/servers/simple-auth/` exécute un serveur d’autorisation autonome et un serveur MCP protégé ; `examples/clients/simple-auth-client/` est le client de cette page devenu une petite CLI. Son README donne les deux commandes : démarrez les serveurs, lancez le client contre eux, et vous voyez défiler les quatre étapes. @@ -110,13 +112,14 @@ Une tâche nocturne, une étape de CI, un autre service. Il n’y a pas de navig `ClientCredentialsOAuthProvider` est le même `httpx2.Auth`, l’humain en moins : -```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" ``` Ce qui a changé : * Aucun `OAuthClientMetadata`, aucun gestionnaire. Vous passez `client_id` et `client_secret` ; le fournisseur construit autour d’eux un enregistrement `client_credentials` minimal et saute entièrement l’enregistrement dynamique. +* `issuer` désigne le serveur d’autorisation qui a émis ces identifiants ; utilisez la valeur `issuer` que renvoie son document `/.well-known/oauth-authorization-server`. La découverte se déroule toujours comme ci-dessus, mais les requêtes de jeton ne sont jamais construites qu’à partir des métadonnées de *cet* émetteur-là ; si le serveur MCP pointe ailleurs, le flux s’arrête alors avec une `OAuthFlowError`. L’omettre est obsolète et il devient obligatoire en 3.0 (voir **[Fonctionnalités obsolètes](../deprecated.md#deprecated-sdk-helpers)**) ; d’ici là, le fournisseur émet un avertissement et utilise le serveur d’autorisation que la découverte trouve, quel qu’il soit. * `scope` est une chaîne séparée par des espaces, le format qu’OAuth utilise sur la liaison. * Tout ce qui se trouve en aval est identique : le même `TokenStorage`, le même `httpx2.AsyncClient(auth=...)`, le même `streamable_http_client`. @@ -129,7 +132,7 @@ Par défaut, le secret voyage en authentification HTTP Basic sur la requête de Un fournisseur de plus se trouve dans `mcp.client.auth.extensions.client_credentials` : **`PrivateKeyJWTOAuthProvider`**, pour les clients qui s’authentifient avec un JWT plutôt qu’avec un secret partagé (`private_key_jwt`, la variante à paire de clés et identité de charge de travail). Il suit - le même schéma : construisez-en un, placez-le sur `auth=`. Le même module fournit + le même schéma : construisez-en un (il accepte le même `issuer` optionnel), placez-le sur `auth=`. Le même module fournit `SignedJWTParameters` et `static_assertion_provider`, deux utilitaires qui construisent son assertion. Il existe une autre situation sans humain : le client appartient à une entreprise dont le fournisseur d’identité, et non l’utilisateur, décide quels serveurs MCP il peut atteindre. C’est un type d’octroi différent, avec son propre modèle de confiance et sa propre page, **[Assertion d’identité](identity-assertion.md)**. diff --git a/i18n/fr/pages/client/session-groups.md b/i18n/fr/pages/client/session-groups.md index 6ab8e19f6b..0f518986ff 100644 --- a/i18n/fr/pages/client/session-groups.md +++ b/i18n/fr/pages/client/session-groups.md @@ -1,6 +1,6 @@ --- translation: - sections: [09c857a25a9dc37a, 43bc6a76a243a50e, 0a716022a88768df, 4b7f78042bfcfff7, c112662e61b03315, 58974ba1f489a8b4, d18adbdbb835ea73] + sections: [09c857a25a9dc37a, 43bc6a76a243a50e, 0a716022a88768df, 4b7f78042bfcfff7, c112662e61b03315, 58974ba1f489a8b4, ed4d17e894864056] tool: 1 --- # Groupes de sessions {#session-groups} @@ -78,7 +78,7 @@ Si vous détenez déjà une `ClientSession` connectée (`Client.session` en est ## Récapitulatif {#recap} * `ClientSessionGroup` détient de nombreuses connexions serveur et fusionne leurs outils, ressources et prompts en un `dict` chacun. -* `connect_to_server(params)` par serveur. Il prend des paramètres de transport, jamais l’objet serveur ni l’URL que prend un `Client`. +* `connect_to_server(params)` par serveur. Il prend des paramètres de transport, jamais l’URL ni le `Transport` que prend un `Client`. * `group.call_tool(name, arguments)` achemine l’appel vers le serveur propriétaire à votre place. * Les noms doivent être uniques dans tout le groupe ; deux serveurs dotés d’un outil `search` ne peuvent pas coexister tels quels. * `component_name_hook=` réécrit chaque nom enregistré. La clé du dict change, pas le nom sur la liaison. diff --git a/i18n/fr/pages/client/transports.md b/i18n/fr/pages/client/transports.md index dd644ff6fe..e7aea55405 100644 --- a/i18n/fr/pages/client/transports.md +++ b/i18n/fr/pages/client/transports.md @@ -1,6 +1,6 @@ --- translation: - sections: [9cac816674181eb0, 0700f337babcd4dd, 2bde0dd58cdf00f5, 40b4916d82eaf1d4, 3d0832f39b0d7059, dfa4446556badef0, 5bd93be2ab2ecb9c] + sections: [9cac816674181eb0, 7c157764133fea1f, 40b4916d82eaf1d4, 10d151f2cc75317f, 3d0832f39b0d7059, 92742ba36533633d, 0aeca6145e7bd302] tool: 1 --- # Transports côté client {#client-transports} @@ -11,30 +11,15 @@ Vous n’en configurez jamais un séparément. `Client` prend un seul argument p Le côté *serveur* de chacun (ce que fait `mcp.run()` et ce que vous déployez) est traité dans **[Exécuter votre serveur](../run/index.md)**. -## En mémoire {#in-memory} - -Passez l’objet serveur lui-même : - -```python title="client.py" hl_lines="14" ---8<-- "docs_src/client_transports/tutorial001.py" -``` - -Pas de sous-processus, pas de port, aucun octet sur une liaison. Le client et le serveur sont deux objets dans le même processus, et l’appel passe tout de même par la véritable couche protocolaire : `search_books` est listé, validé et invoqué exactement comme il le serait via HTTP. - -Cela en fait deux choses à la fois : - -* **Un banc de test.** Chaque exemple de cette documentation est exécuté de cette façon, et la page **[Tests](../get-started/testing.md)** construit tout son modèle autour de lui. -* **Une API d’intégration.** Une application qui construit le serveur n’a pas besoin d’un saut réseau pour appeler ses outils. - ## Streamable HTTP {#streamable-http} -Passez une URL sous forme de chaîne et vous obtenez **Streamable HTTP**, le transport derrière lequel vous déployez : +Passez une URL sous forme de chaîne et vous obtenez **Streamable HTTP**, le transport derrière lequel vous déployez et le premier vers lequel vous tourner : ```python title="client.py" hl_lines="5" --8<-- "docs_src/client_transports/tutorial002.py" ``` -C’est tout le client de production. `Client` enveloppe l’URL dans `streamable_http_client(...)` pour vous, par-dessus un `httpx2.AsyncClient` configuré comme MCP l’exige : `follow_redirects=True`, un délai d’expiration de 30 secondes pour connect/write/pool, et un délai de lecture de 300 secondes parce que le serveur peut garder un flux de réponse ouvert. +C’est tout le client de production. `Client` enveloppe l’URL dans `streamable_http_client(...)` pour vous, par-dessus un `httpx2.AsyncClient` configuré comme MCP l’exige : un délai d’expiration de 30 secondes pour connect/write/pool, et un délai de lecture de 300 secondes parce que le serveur peut garder un flux de réponse ouvert. !!! check Un `Client` que vous venez de construire n’est **pas** connecté. La construction ne fait que choisir le transport ; @@ -50,7 +35,7 @@ C’est tout le client de production. `Client` enveloppe l’URL dans `streamabl Dès que vous avez besoin d’un en-tête `Authorization`, d’un cookie, d’un proxy, de mTLS ou d’un délai d’expiration différent, construisez le `httpx2.AsyncClient` vous-même et passez-le à `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 @@ ou passez un `verify=ssl_context` explicite à votre `httpx2.AsyncClient` !!! info `httpx2` conserve l’API familière de `httpx` ; si vous connaissez `httpx`, vous savez déjà comment gérer ici l’authentification, les proxys, les hooks d’événements, les nouvelles tentatives et les limites de connexions. Le SDK n’ajoute rien par-dessus et ne retire - rien. C’est aussi là qu’OAuth se branche : + rien, à l’exception de la [gestion des redirections](#redirects). C’est aussi là qu’OAuth se branche : `httpx2.AsyncClient(auth=OAuthClientProvider(...))`. Tout ce flux est décrit dans **[Clients OAuth](oauth-clients.md)**. +### Redirections {#redirects} + +Le transport se connecte à l’URL que vous lui avez donnée, et à cette origine uniquement. + +* Une redirection `307`/`308` qui reste sur le même schéma, le même hôte et le même port est suivie, tout comme `http://` → `https://` sur le même hôte. Cela couvre la redirection habituelle de barre oblique finale `/mcp` → `/mcp/`. +* Une redirection vers n’importe quel autre endroit n’est **pas** suivie. L’appel échoue avec : + + ```text + MCPError: Redirect to https://other.example.com/mcp not followed; use that URL as the endpoint if it is the intended server + ``` + + Si cette URL est bien le serveur que vous visiez, mettez-la dans votre configuration. Sinon, le serveur ou un proxy placé devant lui est mal configuré. + +Cela vaut pour tout `httpx2.AsyncClient` que vous passez : son réglage `follow_redirects` n’est pas consulté pour les requêtes MCP, dans un sens comme dans l’autre. Les fournisseurs OAuth du SDK appliquent la même règle à leurs propres requêtes. + +!!! tip + `Redirect to http://… not followed: it would downgrade this HTTPS endpoint to plain HTTP` signifie que le + serveur se trouve derrière un proxy à terminaison TLS dont il ignore l’existence et qu’il émet des redirections en `http://`. + Cela se corrige côté serveur (**[Déployer et passer à l’échelle](../run/deploy.md#behind-a-tls-terminating-proxy)**), + ou en utilisant l’URL `https://…/` exacte que le message suggère. + ## stdio {#stdio} Un serveur **stdio** est un sous-processus. Le client le lance, écrit du JSON-RPC sur son stdin et lit du JSON-RPC depuis son stdout. C’est ainsi qu’un hôte de bureau exécute un serveur sur votre machine : un hôte *est* ce code plus une interface utilisateur, et **[Se connecter à un véritable hôte](../get-started/real-host.md)** montre la même relation vue du côté de l’hôte, sous forme de fichier de configuration. @@ -105,6 +111,18 @@ Le stderr du processus enfant va vers le vôtre. Pour l’envoyer ailleurs, cons Un serveur qui a besoin d’une clé d’API ne l’y trouvera pas. Passez-la explicitement avec `env=` ; ces variables sont fusionnées par-dessus la liste d’autorisation. C’est ce que fait `BOOKSHOP_API_KEY` ci-dessus. +## En mémoire {#in-memory} + +Dans un test, il n’y a rien à déployer ni rien à lancer. Passez l’objet serveur lui-même : + +```python hl_lines="14" +--8<-- "docs_src/client_transports/tutorial001.py" +``` + +Pas de sous-processus, pas de port, aucun octet sur une liaison. Le client et le serveur sont deux objets dans le même processus, et l’appel passe tout de même par la véritable couche protocolaire : `search_books` est listé, validé et invoqué exactement comme il le serait via HTTP. La page **[Tests](../get-started/testing.md)** construit tout son modèle autour de lui. + +La même forme sert aussi d’API d’intégration : une application qui construit elle-même le serveur peut appeler ses outils sans saut réseau. + ## SSE {#sse} `sse_client(url)`, du module `mcp.client.sse`, est le transport HTTP que Streamable HTTP a remplacé. Enveloppez-le de la même manière, `Client(sse_client("http://localhost:8000/sse"))`, pour dialoguer avec un serveur qui le parle encore, et ne construisez rien de nouveau dessus. @@ -113,15 +131,16 @@ Le stderr du processus enfant va vers le vôtre. Pour l’envoyer ailleurs, cons Pour `Client`, tout ce qui précède est une seule et même chose. -Un **transport** est n’importe quel gestionnaire de contexte asynchrone qui produit une paire `(read, write)` de flux de messages : formellement, le protocole `Transport` de `mcp.client`. `Client` résout son argument selon son type : un objet serveur se connecte dans le processus, une `str` devient `streamable_http_client(url)`, un `StdioServerParameters` devient `stdio_client(params)`, et tout le reste est ouvert directement comme transport. C’est cette dernière règle qui explique pourquoi `stdio_client(...)`, `streamable_http_client(...)` et `sse_client(...)` s’insèrent tous au même emplacement, et pourquoi vous pouvez écrire le vôtre. +Un **transport** est n’importe quel gestionnaire de contexte asynchrone qui produit une paire `(read, write)` de flux de messages : formellement, le protocole `Transport` de `mcp.client`. `Client` résout son argument selon son type : une `str` devient `streamable_http_client(url)`, un `StdioServerParameters` devient `stdio_client(params)`, un objet serveur se connecte dans le processus, et tout le reste est ouvert directement comme transport. C’est cette dernière règle qui explique pourquoi `stdio_client(...)`, `streamable_http_client(...)` et `sse_client(...)` s’insèrent tous au même emplacement, et pourquoi vous pouvez écrire le vôtre. ## Récapitulatif {#recap} -* `Client(mcp)` (l’objet serveur) se connecte en mémoire. Utilisez-le pour les tests et pour l’intégration. * `Client("http://.../mcp")` (une URL) se connecte via Streamable HTTP, le transport de production. * Les en-têtes, l’authentification, les proxys et les délais d’expiration vont sur un `httpx2.AsyncClient` que vous passez à `streamable_http_client(url, http_client=...)`. Il n’y a pas de mot-clé `headers=`. +* Les redirections ne sont suivies qu’à l’intérieur de l’origine de l’URL (une redirection `307`/`308` de barre oblique finale), plus `http`→`https` sur le même hôte. Tout le reste échoue avec `Redirect to … not followed` ; configurez l’URL finale. * stdio s’écrit `Client(StdioServerParameters(...))`. Ne l’enveloppez vous-même dans `stdio_client(...)` que pour rediriger le stderr du processus enfant. * Le sous-processus reçoit un environnement sous liste d’autorisation, pas le vôtre ; `env=` s’y ajoute. +* `Client(mcp)` (l’objet serveur) se connecte en mémoire. Utilisez-le dans les tests, ou pour intégrer un serveur dans l’application qui l’a construit. * Un transport est tout ce sur quoi vous pouvez faire `async with x as (read, write)`. `Client` transmet directement à ce protocole tout ce qui n’est ni un objet serveur, ni une URL, ni un `StdioServerParameters`. * Construire un `Client` choisit le transport. `async with` l’ouvre. diff --git a/i18n/fr/pages/deprecated.md b/i18n/fr/pages/deprecated.md index 9a25f9165f..58facd9873 100644 --- a/i18n/fr/pages/deprecated.md +++ b/i18n/fr/pages/deprecated.md @@ -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 --- # Fonctionnalités obsolètes {#deprecated-features} -La spécification 2026-07-28 retire cinq éléments. Le SDK les implémente toujours tous, et chacun d’eux porte désormais un **avertissement d’obsolescence**. Un utilitaire du SDK est obsolète pour des raisons qui lui sont propres ; il figure [à la fin](#deprecated-sdk-helpers). +La spécification 2026-07-28 retire cinq éléments. Le SDK les implémente toujours tous, et chacun d’eux porte désormais un **avertissement d’obsolescence**. Quelques obsolescences propres au SDK existent pour des raisons qui leur sont propres ; elles figurent [à la fin](#deprecated-sdk-helpers). Le tableau ci-dessous nomme chaque fonctionnalité obsolète, la raison de sa disparition et le remplacement sur lequel vous appuyer. @@ -138,11 +138,13 @@ C’est toute l’API. Il n’y a pas d’interrupteur par méthode, et vous n ## Utilitaires du SDK obsolètes {#deprecated-sdk-helpers} -Il ne s’agit pas de changements de la spécification, seulement de rouages internes du SDK qui ont un meilleur remplacement. Ils avertissent avec le même `MCPDeprecationWarning` et seront supprimés dans la version 3.0. +Il ne s’agit pas de changements de la spécification, seulement d’usages du SDK qui ont un meilleur remplacement. Ils avertissent avec le même `MCPDeprecationWarning`, et la version 3.0 supprime l’ancienne forme. | Obsolète | Ce que vous faites à la place | |---|---| | `FuncMetadata.call_fn_with_arg_validation()` | `FuncMetadata.validate_arguments()` puis `FuncMetadata.call_fn()`. Seul du code qui pilote directement `FuncMetadata` (une sous-classe personnalisée de `Tool`, par exemple) l’a jamais appelée. | +| `AuthSettings(resource_server_url=...)` sans `validate_token_resource=` | Définissez-le : `True` fait refuser au serveur les jetons porteurs que votre vérificateur ne signale pas comme émis pour `resource_server_url`, `False` indique que votre vérificateur contrôle lui-même l’audience du jeton (voir **[Autorisation](run/authorization.md#a-token-verifier)**). Non défini, il se comporte comme `False` ; la version 3.0 fait de `True` la valeur par défaut dès que `resource_server_url` est défini. | +| `ClientCredentialsOAuthProvider(...)` ou `PrivateKeyJWTOAuthProvider(...)` sans `issuer=` | Passez `issuer=` pour nommer le serveur d’autorisation qui a émis les identifiants (voir **[Écrire des clients OAuth](client/oauth-clients.md#machine-to-machine)**). Sans lui, c’est le serveur MCP qui décide quel serveur d’autorisation les reçoit ; la version 3.0 rend ce paramètre nommé obligatoire. | ## Récapitulatif {#recap} @@ -151,7 +153,7 @@ Il ne s’agit pas de changements de la spécification, seulement de rouages int * L’obsolescence est indicative : aucun changement sur la liaison, tout continue de fonctionner sur les sessions d’avant 2026, et vous obtenez un `MCPDeprecationWarning` visible (un `UserWarning`, donc actif par défaut). * L’échantillonnage et les racines ont en plus besoin d’un canal de retour qu’une session 2026-07-28 n’a pas. Sur une connexion moderne, ils avertissent puis lèvent une exception. * `warnings.filterwarnings("ignore", category=MCPDeprecationWarning)` fait taire toute la catégorie ; `"error::mcp.MCPDeprecationWarning"` dans pytest la transforme en échec de test. -* Un utilitaire du SDK, `FuncMetadata.call_fn_with_arg_validation()`, est obsolète séparément, pour suppression dans la version 3.0. +* Les [obsolescences propres au SDK](#deprecated-sdk-helpers) suivent la même règle : elles avertissent dès maintenant, et la version 3.0 abandonne l’ancienne forme. * Aucun nouveau code ne devrait s’appuyer sur l’une de ces fonctionnalités. Toutes les autres pages de cette documentation enseignent l’API actuelle. diff --git a/i18n/fr/pages/get-started/first-steps.md b/i18n/fr/pages/get-started/first-steps.md index ac30ba9ad4..c5873f0c4e 100644 --- a/i18n/fr/pages/get-started/first-steps.md +++ b/i18n/fr/pages/get-started/first-steps.md @@ -1,6 +1,6 @@ --- translation: - sections: [0d6c05bcbf836bf3, 59a7b14eeefc68c1, 7114d8d6daba203f, e8bbb56a98ba7bc9, 5138010f6159901c, f78da7c7c363d4c6, 220a939cab348686] + sections: [0d6c05bcbf836bf3, 9a78b5f6b44b18ab, 7114d8d6daba203f, e8bbb56a98ba7bc9, bfd2fd1153e71dac, 1615a994ef071fdd, 65c599fae991f245] tool: 1 --- # Premiers pas {#first-steps} @@ -17,7 +17,7 @@ Trois mots que vous verrez sur chaque page à partir d’ici : * Un **client** vit à l’intérieur de l’hôte et parle MCP. L’hôte exécute un client par serveur auquel il est connecté. * Un **serveur** est ce que vous construisez avec ce SDK. Il expose des choses aux clients. Il ne parle jamais directement au modèle. -Vous écrivez le serveur. Les hôtes sont le produit de quelqu’un d’autre. Le SDK vous fournit aussi un `Client`. Vous l’utiliserez pour tester vos serveurs, et il apparaît plus loin sur cette page. +Vous écrivez le serveur. Les hôtes sont le produit de quelqu’un d’autre. Le SDK vous fournit aussi un `Client`, la même classe qu’un hôte utiliserait pour joindre un serveur par son URL ou le lancer comme sous-processus. Il apparaît plus loin sur cette page, et c’est aussi avec lui que vous testerez vos serveurs. ## Les trois primitives {#the-three-primitives} @@ -83,22 +83,20 @@ Vous avez vu trois onglets dans l’Inspector. Comment savait-il qu’il y en av Lorsqu’un client se connecte, le serveur déclare ses **capacités** (capabilities) : les familles de requêtes auxquelles il répondra. Le client utilise cette déclaration pour décider de ce qu’il peut même demander. Vous ne l’avez jamais écrite ; `MCPServer` la déclare pour vous. -Regardez par vous-même. Le `Client` du SDK accepte directement l’objet serveur et s’y connecte **en mémoire** (ni sous-processus, ni port) : - -```python -import asyncio - -from mcp import Client - -from server import mcp +Regardez par vous-même. Laissez `server.py` tourner en HTTP dans un terminal : +```console +uv run mcp run server.py --transport streamable-http +``` -async def main() -> None: - async with Client(mcp) as client: - print(client.server_capabilities.model_dump(exclude_none=True)) +et pointez un client dessus depuis un autre : +```python title="client.py" hl_lines="7-8" +--8<-- "docs_src/first_steps/tutorial001_client.py" +``` -asyncio.run(main()) +```console +python client.py ``` ```text @@ -118,8 +116,9 @@ Ce dictionnaire, ce sont les **capacités** déclarées de votre serveur. C’es Remarquez ce qui n’y figure pas. `completions` (la complétion automatique des arguments pour les modèles de ressources et les prompts) nécessite un gestionnaire que vous écrivez ; ce serveur n’en a pas, donc la capacité est absente et un client bien élevé ne demandera rien. C’est la règle pour tout ce qui est facultatif : enregistrez la chose et la capacité apparaît ; **[Complétions](../servers/completions.md)** le prouve. !!! info - `Client(mcp)` est le même client en mémoire avec lequel chaque exemple de cette documentation est testé, et - c’est ainsi que vous testerez les vôtres. Il a droit à une page entière : **[Tester](testing.md)**. + Ce `client.py` est un client MCP complet, et **[Le client](../client/index.md)** est sa page. + Dans un test, vous vous passez du terminal et du port et vous donnez à `Client` l’objet serveur lui-même, + `Client(mcp)`. Cela a aussi droit à une page entière : **[Tester](testing.md)**. ## Ce que vous n’avez pas écrit {#what-you-did-not-write} @@ -128,7 +127,7 @@ Reprenez cette page depuis le début. Vous avez écrit trois petites fonctions P * De JSON Schema. `a: int, b: int` *est* le schéma de `add`. * De gestionnaire de requêtes. `tools/list`, `resources/read`, `prompts/get` : tous servis pour vous. * De déclaration de capacités. `MCPServer` l’a faite pour vous. -* Une seule ligne de protocole. La négociation de version, l’encapsulation JSON-RPC, l’échange de capacités : tout cela s’est passé à l’intérieur de `mcp dev` et de `Client(mcp)`, et vous n’en avez rien vu. +* Une seule ligne de protocole. La négociation de version, l’encapsulation JSON-RPC, l’échange de capacités : tout cela s’est passé à l’intérieur de `mcp dev` et de `client.py`, et vous n’en avez rien vu. Ce rapport est tout l’intérêt du SDK. @@ -139,6 +138,6 @@ Ce rapport est tout l’intérêt du SDK. * Un décorateur par primitive : `@mcp.tool()`, `@mcp.resource(uri)`, `@mcp.prompt()`. Le nom, la description et le schéma viennent de la fonction. * Un URI avec un `{param}` crée un **modèle** de ressource, listé séparément des ressources concrètes. * Les **capacités** du serveur sont déclarées pour vous, et un client ne demande que ce qu’un serveur déclare. -* `Client(mcp)` se connecte à l’objet serveur en mémoire : votre banc d’essai dès le premier jour. +* `Client("http://localhost:8000/mcp")` parle à votre serveur en cours d’exécution. Donnez-lui plutôt l’objet serveur, `Client(mcp)`, et c’est votre banc d’essai dès le premier jour. La suite, c’est **[Se connecter à un vrai hôte](real-host.md)** : ce serveur dans Claude Desktop ou un IDE, pour de vrai. Puis **[Tester](testing.md)** : une page, un client en mémoire, et vous n’aurez plus jamais à deviner si cela fonctionne. Ensuite, chaque primitive a droit à sa propre page, en commençant par celle que pilote le modèle : **[Outils](../servers/tools.md)**. diff --git a/i18n/fr/pages/get-started/testing.md b/i18n/fr/pages/get-started/testing.md index c6464addb5..ab0e7d6ae2 100644 --- a/i18n/fr/pages/get-started/testing.md +++ b/i18n/fr/pages/get-started/testing.md @@ -1,13 +1,13 @@ --- translation: - sections: ['4926721070127497', c52a1de2b6b32f40, 8e792bf8c7489ec6, 627195f7159e24ef] + sections: [5d13c2f0ba42c0d2, c52a1de2b6b32f40, 8e792bf8c7489ec6, 38552ea228b0a04f] tool: 1 --- # Tests {#testing} -Le SDK Python fournit une classe `Client` dotée d’un **transport en mémoire** : passez-lui votre objet serveur et il s’y connecte directement. +La classe `Client` du SDK, celle-là même qui se connecte à une URL ou lance un sous-processus, se connecte aussi **en mémoire** : passez-lui votre objet serveur et elle lui parle directement. -Pas de sous-processus. Pas de port. Pas de transport du tout. C’est la même idée que le `TestClient` de FastAPI. +Pas de sous-processus. Pas de port. Rien sur la liaison. C’est la même idée que le `TestClient` de FastAPI. ## Utilisation de base {#basic-usage} @@ -98,15 +98,15 @@ change `raise_exceptions=True` : votre test voit le vrai message au lieu de la v Laissez-le activé dans les tests. Il n’a aucun sens dans du code de production. -## Dans le processus par défaut {#in-process-by-default} +## Neutre par défaut vis-à-vis de la génération du protocole {#era-neutral-by-default} !!! note `Client(mcp)` se connecte dans le processus et est **neutre vis-à-vis de la génération du protocole** par défaut : il sonde le serveur et choisit le chemin de protocole approprié. Fixez `mode="legacy"` si votre test exerce une sémantique propre aux connexions historiques (push d’échantillonnage (sampling) ou d’élicitation (elicitation), `message_handler`), et retirez alors - `raise_exceptions=True` : une connexion historique ne neutralise jamais rien, et l’indicateur - relève l’échec dans la tâche du serveur plutôt que dans votre test. + `raise_exceptions=True` : une connexion historique ne neutralise de toute façon jamais rien, et + l’indicateur relance l’échec à l’intérieur de la tâche du serveur plutôt que dans votre test. Cette unique ligne est aussi la raison pour laquelle cette documentation peut vous promettre que ses exemples fonctionnent : chaque fichier d’exemple est exercé par la propre suite de tests du diff --git a/i18n/fr/pages/handlers/multi-round-trip.md b/i18n/fr/pages/handlers/multi-round-trip.md index 661c27856a..7d8e8d3599 100644 --- a/i18n/fr/pages/handlers/multi-round-trip.md +++ b/i18n/fr/pages/handlers/multi-round-trip.md @@ -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 --- # Requêtes à plusieurs allers-retours (multi-round-trip) {#multi-round-trip-requests} @@ -164,7 +164,7 @@ Le `Server` bas niveau est le niveau sans rien de fourni d’office : contrairem ## Un résultat de la version 2026-07-28 {#a-2026-07-28-result} -`InputRequiredResult` n’existe qu’en version de protocole **2026-07-28**. Le `Client(server)` en mémoire la négocie pour vous ; sur la liaison, `mode="auto"` la découvre. Une fois connecté, `client.protocol_version` vous dit ce que vous avez obtenu. +`InputRequiredResult` n’existe qu’en version de protocole **2026-07-28**. Le `mode="auto"` par défaut de `Client` la découvre sur n’importe quelle connexion. Une fois connecté, `client.protocol_version` vous dit ce que vous avez obtenu. !!! warning Une session antérieure à 2026 n’a nulle part où mettre un `InputRequiredResult`. Renvoyez-en diff --git a/i18n/fr/pages/handlers/progress.md b/i18n/fr/pages/handlers/progress.md index 29182ac1a8..64b08fcf7b 100644 --- a/i18n/fr/pages/handlers/progress.md +++ b/i18n/fr/pages/handlers/progress.md @@ -1,6 +1,6 @@ --- translation: - sections: [5315262fe26b33e1, 9d8e98840f1b78f0, 0284b215e85366c4, 8534d8dbb4053a70, 2966fac6fe697007] + sections: [5315262fe26b33e1, 9d8e98840f1b78f0, 52d6009a07e770ea, 8534d8dbb4053a70, 2966fac6fe697007] tool: 1 --- # Progression {#progress} @@ -29,19 +29,17 @@ Trois arguments, et c’est vous qui décidez de leur sens : Le client active la fonctionnalité **appel par appel**, en passant `progress_callback=` à `call_tool` : -```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,23 +54,27 @@ anyio.run(main) La fonction de rappel (callback) est une fonction `async` qui prend exactement ce que le serveur a signalé : `progress`, `total`, `message`. !!! info - `Client(mcp)` se connecte directement à l’objet serveur, en mémoire : c’est le même client que celui sur lequel repose la page **[Tests](../get-started/testing.md)**. `progress_callback` est le même paramètre quel que soit le transport qu’utilise le `Client` ; le *timing* que vous allez observer est celui de la connexion en mémoire. Elle exécute votre fonction de rappel de façon synchrone, si bien que chaque signalement arrive avant que `call_tool` ne renvoie. Sur un vrai transport, les notifications font la course avec le résultat, et une fonction de rappel lente peut encore être en cours d’exécution après le retour de `call_tool`. + `progress_callback` est le même paramètre quoi que vous ayez passé à `Client` : une URL comme ici, un `StdioServerParameters`, ou l’objet serveur dans un test. Attention toutefois au timing sur un vrai transport. Chaque notification est acheminée seule, à côté de la réponse, si bien qu’une fonction de rappel lente peut encore être en cours d’exécution après le retour de `call_tool`. Seule la connexion de test en mémoire exécute la fonction de rappel de façon synchrone et garantit que chaque signalement arrive d’abord. ### Essayer {#try-it} -Placez `client.py` à côté de `server.py` et lancez-le : +Servez `server.py` en HTTP, puis lancez le client depuis un second 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.'} ``` -Chaque `await ctx.report_progress(...)` côté serveur est devenu un appel à `show` côté client, dans l’ordre, et les deux lignes se sont affichées **avant** que `call_tool` ne renvoie. La progression n’est pas empaquetée dans le résultat ; elle est diffusée pendant que l’outil travaille encore. +Chaque `await ctx.report_progress(...)` côté serveur est devenu un appel à `show` côté client, dans l’ordre. La progression n’est pas empaquetée dans le résultat. Elle est diffusée pendant que l’outil travaille encore. !!! warning `progress_callback` appartient à l’**appel**, pas au `Client`. Il n’existe aucun argument de constructeur pour cela, parce que des appels différents veulent des fonctions de rappel différentes : l’un pilote une barre de téléchargement, le suivant une ligne de journal. diff --git a/i18n/fr/pages/protocol-versions.md b/i18n/fr/pages/protocol-versions.md index a655ec8d77..b9e120cee0 100644 --- a/i18n/fr/pages/protocol-versions.md +++ b/i18n/fr/pages/protocol-versions.md @@ -1,6 +1,6 @@ --- translation: - sections: [478fd619e5f90ef8, aef094a00e44e248, bab8cbf3449fa7e9, df1809b15a58335b, 5f9d8c2336ed0239, f54974398e43ddef, b24443dd78584870] + sections: [d98e337bf28845e0, dd642acbba36f615, f547b3e76da19c94, 149513378588da5c, 4e149ed992046709, f54974398e43ddef, b24443dd78584870] tool: 1 --- # Versions du protocole {#protocol-versions} @@ -11,9 +11,17 @@ Les serveurs publiés avant la version 2026-07-28 ouvrent chaque connexion par l Vous n’avez presque jamais à vous en soucier, car `Client` négocie pour vous. Cette page porte sur le seul argument du constructeur qui contrôle cela, `mode=`, et sur les trois cas où vous le changez. +Chaque extrait de cette page est un `client.py` qui dialogue avec le `server.py` Bookshop de la page **[Le client](client/index.md)**. Lancez ce serveur dans un premier terminal : + +```console +uv run mcp run server.py --transport streamable-http +``` + +Puis exécutez chaque extrait dans un second terminal avec `python client.py`. + ## `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 @@ Dans les deux cas, vous ressortez connecté, et `client.protocol_version` vous i C’est toute la fonctionnalité. Un seul `Client`, un serveur de n’importe quelle génération, aucun branchement dans votre code. !!! info - `MCPServer` répond à `server/discover` sur tous les transports — en mémoire, stdio, Streamable - HTTP — donc face à votre propre serveur, `auto` aboutit toujours à `2026-07-28`. Le repli ne - se déclenche que face à un vrai serveur antérieur à 2026, c’est-à-dire exactement quand vous le souhaitez. + `MCPServer` répond à `server/discover` sur tous les transports — Streamable HTTP, stdio et la + connexion intra-processus qu’utilisent vos tests — donc face à votre propre serveur, `auto` aboutit + toujours à `2026-07-28`. Le repli ne se déclenche que face à un vrai serveur antérieur à 2026, + c’est-à-dire exactement quand vous le souhaitez. ## `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 @@ En version 2026-07-28, il a disparu. Le serveur *renvoie* ses questions et vous `mode` accepte aussi une chaîne de version moderne du protocole. Aujourd’hui, cet ensemble est exactement `["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 @@ La sonde est peu coûteuse, mais cela reste un aller-retour que vous payez à ch Alors conservez-la. Après une connexion `auto`, `client.session.discover_result` contient le `DiscoverResult` exact que le serveur a envoyé : ses `supported_versions`, ses `capabilities`, ses `instructions` et l’identité que le serveur a inscrite dans le `_meta` du résultat. Repassez-le via `prior_discover=` la fois suivante : -```python title="client.py" hl_lines="15 17" +```python title="client.py" hl_lines="8 10" --8<-- "docs_src/protocol_versions/tutorial004.py" ``` diff --git a/i18n/fr/pages/run/asgi.md b/i18n/fr/pages/run/asgi.md index 288262c6ed..d2821f71d6 100644 --- a/i18n/fr/pages/run/asgi.md +++ b/i18n/fr/pages/run/asgi.md @@ -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 --- # Ajouter à une application existante {#add-to-an-existing-app} @@ -99,7 +99,7 @@ Ce `/mcp` final, c’est `streamable_http_path`. Définissez-le à `"/"` et le p --8<-- "docs_src/asgi/tutorial004.py" ``` -Les clients se connectent désormais à `/notes`, et non à `/notes/mcp`. +Les clients se connectent désormais à `/notes/`, et non à `/notes/mcp`. ## CORS pour les clients navigateur {#cors-for-browser-clients} @@ -142,4 +142,4 @@ Un client qui s’exécute dans un navigateur a besoin de deux permissions de vo * Les clients navigateur ont besoin de CORS : `allow_headers` pour les en-têtes de requête `Mcp-*`, `expose_headers=["Mcp-Session-Id"]` pour la réponse. * `@mcp.custom_route()` ajoute des points de terminaison HTTP ordinaires, non authentifiés, à côté de `/mcp`. -Une fois le serveur joignable à une vraie URL, **[Le client](../client/index.md)** s’y connecte avec cette URL plutôt qu’avec un objet serveur. +Une fois le serveur joignable à une vraie URL, **[Le client](../client/index.md)** s’y connecte avec cette URL. diff --git a/i18n/fr/pages/run/authorization.md b/i18n/fr/pages/run/authorization.md index 57e515a54b..5726f506ac 100644 --- a/i18n/fr/pages/run/authorization.md +++ b/i18n/fr/pages/run/authorization.md @@ -1,6 +1,6 @@ --- translation: - sections: [d62c13457fc4a534, 80e73abaca6e0652, d1dc4c54cd00ec9c, 14ad3bc7904036bb, 5225f127bc1b9c77, fe1626fdd5aad1da, 4556cb7ea1a04a31] + sections: [d62c13457fc4a534, 80e73abaca6e0652, 128a492d18295f64, 14ad3bc7904036bb, 54a6697833fcc1d9, fe1626fdd5aad1da, 811d083c1da8bcf6] tool: 1 --- # Autorisation {#authorization} @@ -23,12 +23,12 @@ C’est tout le triangle. Toute cette page porte sur le point du milieu. Le SDK n’a aucun avis sur ce à quoi ressemble un jeton valide. C’est vous qui le lui dites, en implémentant **`TokenVerifier`** : -```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` est un protocole avec une seule méthode asynchrone. `verify_token` reçoit le jeton brut de l’en-tête `Authorization` et renvoie un **`AccessToken`** s’il est valide, `None` sinon. Il n’y a rien d’autre à implémenter. -* Celui-ci cherche le jeton dans une table. Un vérificateur réel vérifie la signature d’un JWT ou appelle le point de terminaison d’introspection de jetons du serveur d’autorisation. Ce code est le vôtre ; le SDK ne fait que l’appeler. +* Celui-ci cherche le jeton dans une table ; chaque entrée consigne la ressource pour laquelle le jeton a été émis. Un vérificateur réel vérifie la signature d’un JWT ou appelle le point de terminaison d’introspection de jetons du serveur d’autorisation, et indique pour qui le jeton a été émis (son `aud`) dans `AccessToken.resource`. Ce code est le vôtre ; le SDK ne fait que l’appeler. * `token_verifier=` et `auth=` vont toujours de pair. Passez l’un sans l’autre et `MCPServer(...)` lève une `ValueError` avant même de servir la moindre requête. `AuthSettings` est la face publique de votre serveur de ressources : @@ -36,6 +36,10 @@ Le SDK n’a aucun avis sur ce à quoi ressemble un jeton valide. C’est vous q * `issuer_url` : le serveur d’autorisation qui émet vos jetons. * `resource_server_url` : l’URL publique de ce point de terminaison MCP. Elle désigne *quelle* ressource un jeton vise, et c’est là que réside le document de découverte. * `required_scopes` : chaque jeton doit tous les porter. +* `validate_token_resource` : refuser tout jeton dont `AccessToken.resource` n’est pas `resource_server_url`. Le laisser non défini alors que `resource_server_url` est défini produit un avertissement (`MCPDeprecationWarning`) et se comporte comme `False` ; la version 3.0 fait de `True` la valeur par défaut pour les serveurs de ressources. + * Activez-le lorsque votre serveur d’autorisation lie les jetons à la `resource` demandée par le client, que les clients MCP envoient toujours. Gardez dans `resource_server_url` l’URL exacte à laquelle les clients se connectent. + * Laissez-le désactivé lorsque votre serveur d’autorisation utilise ses propres identifiants d’audience (un identifiant d’API Auth0, un ID d’application Entra) et vérifiez plutôt `aud` dans votre vérificateur, en renvoyant `None` pour un jeton qui n’est pas destiné à ce serveur. + * Si `aud` est une liste, placez dans `resource` l’entrée égale à `resource_server_url`. !!! tip `examples/servers/simple-auth/` dans le dépôt du SDK contient un `IntrospectionTokenVerifier` qui appelle @@ -91,7 +95,7 @@ C’est grâce à ce document qu’un client qui n’a jamais entendu parler de Dans n’importe quel gestionnaire (handler), **`get_access_token()`** est l’objet `AccessToken` que votre vérificateur a renvoyé pour la requête en cours : -```python title="server.py" hl_lines="4 32-35" +```python title="server.py" hl_lines="4 35-38" --8<-- "docs_src/authorization/tutorial002.py" ``` @@ -125,6 +129,6 @@ Un serveur d’autorisation peut aussi accepter l’assertion signée d’un fou * `token_verifier=` et `auth=AuthSettings(issuer_url=..., resource_server_url=..., required_scopes=[...])` vont toujours de pair. * Le SDK publie les métadonnées de ressource protégée (Protected Resource Metadata) de la [RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728) sur `/.well-known/oauth-protected-resource/...` et répond aux requêtes non authentifiées par un 401 dont l’en-tête `WWW-Authenticate` pointe vers elles. C’est tout le mécanisme de découverte. * `get_access_token()` dans n’importe quel gestionnaire indique qui appelle. -* L’autorisation est une affaire de HTTP. `stdio` et le client en mémoire ne la voient jamais. +* L’autorisation est une affaire de HTTP. `stdio` et le client de test en mémoire ne la voient jamais. La moitié client (découvrir votre serveur d’autorisation et récupérer le jeton pour vous), c’est **[Clients OAuth](../client/oauth-clients.md)**. Et un client qui *affirme* une identité au lieu d’en demander une à un utilisateur, c’est **[Assertion d’identité](../client/identity-assertion.md)**. diff --git a/i18n/fr/pages/run/deploy.md b/i18n/fr/pages/run/deploy.md index 14df623049..91ba48e4db 100644 --- a/i18n/fr/pages/run/deploy.md +++ b/i18n/fr/pages/run/deploy.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 --- # Déployer et passer à l’échelle {#deploy-scale} @@ -48,6 +48,22 @@ Déployée derrière un vrai nom d’hôte, cette même valeur par défaut rejet de liste des hôtes autorisés jusqu’à preuve du contraire. **[Dépannage](../troubleshooting.md)** commence aussi par là. +## Derrière un proxy de terminaison TLS {#behind-a-tls-terminating-proxy} + +Si TLS se termine sur un proxy (un ingress, un répartiteur de charge, Caddy, nginx) et qu’uvicorn sert du HTTP en clair derrière lui, dites à uvicorn de faire confiance aux en-têtes `X-Forwarded-*` du proxy : + +```console +uvicorn server:app --proxy-headers --forwarded-allow-ips='' +``` + +Sans cela, l’application croit être servie en `http://`, et toute redirection qu’elle émet (la plus courante est `/mcp` → `/mcp/`) pointe vers `http://…`. Le client Python refuse de suivre une redirection d’un point de terminaison HTTPS vers du HTTP en clair, et le dit : + +```text +MCPError: Redirect to http://mcp.example.com/mcp/ not followed: it would downgrade this HTTPS endpoint to plain HTTP. +``` + +Le palliatif côté client consiste à configurer l’URL exacte que sert le serveur (`https://mcp.example.com/mcp/`, barre oblique finale comprise) pour qu’aucune redirection n’ait lieu. Le correctif, c’est l’option ci-dessus. `FORWARDED_ALLOW_IPS` en est la forme variable d’environnement ; `*` fait confiance à chaque saut, ce qui n’est correct que si rien d’autre que le proxy ne peut atteindre uvicorn. + ## Les workers, et qui a besoin d’affinité {#workers-and-who-has-to-be-sticky} Une fois que le nom d’hôte répond, placez plus d’un worker derrière lui. Le SDK n’a aucun réglage pour cela ; vous passez une application Starlette à l’échelle comme n’importe quelle application ASGI, en confiant l’objet à quelque chose qui sait créer des processus (fork) : @@ -173,6 +189,7 @@ Un `MCPServer` est une implémentation du protocole, pas un serveur d’applicat ## Récapitulatif {#recap} * Par défaut, l’application ne répond qu’aux requêtes adressées à localhost. `transport_security=TransportSecuritySettings(allowed_hosts=[...], allowed_origins=[...])` est le passage obligé avant la mise en production : tant que vous ne le passez pas, chaque requête derrière un vrai nom d’hôte est un `421` et la raison n’est que dans le journal du serveur. +* Derrière un proxy de terminaison TLS, lancez uvicorn avec `--proxy-headers --forwarded-allow-ips=...`, sans quoi ses redirections pointent vers `http://` et le client les refuse. * En version 2026-07-28, il n’y a pas de session et rien sur quoi un répartiteur de charge pourrait établir une affinité. `stateless_http=True` est un réglage réservé à la branche historique, parce qu’une requête moderne est routée et traitée avant même que cet indicateur soit lu. * La clé `requestState` par défaut est `os.urandom(32)`, générée par processus. Une nouvelle tentative à plusieurs allers-retours qui atteint un autre worker échoue avec `-32602` *« Invalid or expired requestState »*. * Le correctif est `RequestStateSecurity(keys=[...])` **et** le même nom de serveur sur chaque instance. Le nom est la revendication d’audience par défaut du jeton. Mêmes clés, même nom. diff --git a/i18n/fr/pages/run/index.md b/i18n/fr/pages/run/index.md index 27adf13521..d528c37529 100644 --- a/i18n/fr/pages/run/index.md +++ b/i18n/fr/pages/run/index.md @@ -1,6 +1,6 @@ --- translation: - sections: [fea8d769ff9edeba, ce8e2ad42f29ef71, 0d705efb19cf99c2, 2fd7cf825e6d2b2c, 9adc400e8c88e854, 318893ad8e2e9924, 6b63ab96b34476c0] + sections: [fea8d769ff9edeba, ce8e2ad42f29ef71, 0d705efb19cf99c2, 9fd2357154a5b7e7, 9adc400e8c88e854, 318893ad8e2e9924, 6b63ab96b34476c0] tool: 1 --- # Exécuter votre serveur {#running-your-server} @@ -75,6 +75,11 @@ Chaque transport a ses propres arguments nommés, tous sur `run()` : * `max_request_body_size` : taille maximale acceptée pour le corps d’une requête, en octets. Vaut 4 Mio par défaut ; les requêtes plus grandes reçoivent un HTTP 413 avant toute analyse ou création de session. Ne l’augmentez que lorsque des messages MCP légitimes dépassent cette taille. +* `session_idle_timeout` : nombre de secondes pendant lesquelles une session historique peut rester sans rien en cours avant que le + serveur ne la ferme. Valeur par défaut 1800. `None` le désactive. Voir + [Durée de vie des sessions et limites](legacy-clients.md#session-lifetime-and-limits). +* `max_sessions` : nombre de sessions historiques qu’un même processus conserve simultanément. Valeur par défaut 10 000. `None` + supprime la limite. Traité dans la même section. * `event_store`, `retry_interval`, `transport_security` : reprise après coupure et protection contre le DNS rebinding. Ils peuvent attendre, jusqu’à ce que vous déployiez ailleurs que sur localhost ; **[Déployer et passer à l’échelle](deploy.md)** couvre `transport_security`. !!! warning diff --git a/i18n/fr/pages/run/legacy-clients.md b/i18n/fr/pages/run/legacy-clients.md index 80b1f796e3..c50f3763bd 100644 --- a/i18n/fr/pages/run/legacy-clients.md +++ b/i18n/fr/pages/run/legacy-clients.md @@ -1,6 +1,6 @@ --- translation: - sections: [3d1663c18edc824c, d4fd37009a13f03d, af9f398a5a8b679a, 470c2dd144294d69, 8e45827e6d24e8c8, 91dfd0ce98ebb03c] + sections: [3d1663c18edc824c, 90956965ae6a1ca1, af9f398a5a8b679a, 5ce83b1f9d88da62, 0d9b5d13fffc94e5, 8e45827e6d24e8c8, 91dfd0ce98ebb03c] tool: 1 --- # Prendre en charge les clients historiques {#serving-legacy-clients} @@ -22,15 +22,25 @@ Un client historique n’est donc pas quelque chose *pour* lequel vous construis ## Un gestionnaire, deux générations {#one-handler-both-eras} -Voici un outil (tool) qui doit demander quelque chose à l’utilisateur, et des clients des deux générations qui l’appellent : +Voici un outil (tool) qui doit demander quelque chose à l’utilisateur : -```python title="server.py" hl_lines="24 37-38" +```python title="server.py" hl_lines="21" --8<-- "docs_src/legacy_clients/tutorial001.py" ``` `reserve` a besoin d’une chose que le modèle n’a pas fournie : le nombre d’exemplaires. `Annotated[..., Resolve(ask_quantity)]` est la façon dont un outil le déclare (tous les détails sont dans **[Dépendances](../handlers/dependencies.md)**). Rien dans `reserve` ne nomme une version, ne vérifie une capacité ni ne bifurque. -Les deux clients sont ouverts **en même temps**, sur le même objet `mcp`. `mode="legacy"` exécute la poignée de main `initialize` : exactement la connexion qu’ouvre un client antérieur à 2026. L’autre prend la valeur par défaut et arrive en version `2026-07-28`. +Servez-le en HTTP, et voici des clients des deux générations qui l’appellent : + +```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" +``` + +Les deux clients sont ouverts **en même temps**, sur le même serveur en cours d’exécution. `mode="legacy"` exécute la poignée de main `initialize` : exactement la connexion qu’ouvre un client antérieur à 2026. L’autre prend la valeur par défaut et arrive en version `2026-07-28`. Lancez `python client.py` depuis un second terminal : ```text 2025-11-25 {'result': "Reserved 2 of 'Dune'."} @@ -63,6 +73,41 @@ Sur un seul worker, c’est invisible. Sur deux, c’est tout le problème : une événements SSE manqués pour un client qui se reconnecte à la *même* session), pas un magasin de sessions. Il ne rend jamais une session accessible depuis un autre processus. +## Durée de vie et limites des sessions {#session-lifetime-and-limits} + +Une session historique ne vit pas éternellement, et un processus n’en conserve pas un nombre +illimité. Deux réglages contrôlent cela. Tous deux sont des arguments nommés de `run()`, +`streamable_http_app()` et `Server.streamable_http_app()`. Les connexions modernes (`2026-07-28`) et +`stateless_http=True` n’ont pas de sessions, si bien qu’aucun des deux réglages ne s’y applique. + +| Réglage | Valeur par défaut | Ce qu’il fait | Ce que voit le client | Pour le désactiver | +|---|---|---|---|---| +| `session_idle_timeout` | `1800` (30 min) | Ferme une session qui n’a rien eu en cours pendant cette durée. | `404 Session not found`. Il doit refaire `initialize`. | `None` | +| `max_sessions` | `10_000` | Refuse d’ouvrir une session au-delà de ce nombre. Les sessions existantes ne sont pas touchées et rien n’est évincé. | `503 Too many open sessions` avec le code JSON-RPC `-32603`. | `None` | + +Ce qui compte comme « en cours » : + +* Un flux `GET` ouvert. Les clients du SDK en gardent un ouvert, si bien que la session d’un client + connecté n’expire jamais. +* Une requête à laquelle on est encore en train de répondre. Un appel d’outil qui dure plus longtemps + que le délai n’est pas interrompu, et le compte à rebours ne démarre qu’une fois qu’il a terminé. +* Rien d’autre. Entre les requêtes, l’horloge tourne. N’importe quelle requête sur la session la + réinitialise, `ping` compris. Une fois qu’une session a expiré, rien ne la ranime. + +Un client qui met fin à sa session par un `DELETE` la libère immédiatement. Même chose pour un client +dont la requête d’ouverture a été refusée. + +```python +mcp.run(transport="streamable-http", session_idle_timeout=None, max_sessions=50_000) +``` + +Les deux événements apparaissent dans le journal du serveur. Une expiration donne `Session idle timeout` +au niveau `INFO`. Une ouverture refusée donne `Refusing to open a new session: sessions are already open` +au niveau `WARNING`. + +Les limites s’entendent par processus. Avec quatre workers, le plafond est quatre fois `max_sessions`, et +chaque worker fait expirer ses propres sessions. + ## Le seul réglage : `stateless_http` {#the-one-knob-stateless_http} Si l’affinité est un coût que vous refusez de payer, il y a exactement une chose que vous pouvez changer. @@ -88,8 +133,8 @@ Deux choses à son sujet comptent plus que ce qu’il fait. !!! check Faites la mauvaise chose. `reserve` est exactement l’outil qui vient de servir les deux clients. - Déployez-le avec `stateless_http=True`, connectez les deux mêmes clients en HTTP et appelez-le - depuis chacun. + Déployez-le avec `stateless_http=True`, connectez les deux mêmes clients et appelez-le depuis + chacun. Le client moderne obtient toujours `Reserved 2 of 'Dune'.` La voie moderne n’a pas changé. diff --git a/i18n/fr/pages/troubleshooting.md b/i18n/fr/pages/troubleshooting.md index c928c6b74d..1cfa29d8fa 100644 --- a/i18n/fr/pages/troubleshooting.md +++ b/i18n/fr/pages/troubleshooting.md @@ -1,6 +1,6 @@ --- translation: - sections: [2efaecdef109a5c5, fcacd3e66b8635a4, 25323d737dcf0261, 8a6e351ec756904d, 137454d469c867f5, 6392596bd6df54f0, 41126fa9c4fe432f, 480b6d7897e30ab4, d83bb682e708dde0, ebbed3449c499db4, 323ef84f6b4bebde, 30fd31be74169d9a, 656943c6cb567218, c2dc3b1007d2e987, 7cf5386b997d04e9, 0b59feed8384456e, 0cba47bae78d04eb, e4355f4c7cf4fb2e] + sections: [3d58228e81b99543, 170514ce901c4139, 17d61fad0a50d62b, 8a6e351ec756904d, 137454d469c867f5, 6392596bd6df54f0, 41126fa9c4fe432f, 480b6d7897e30ab4, d83bb682e708dde0, ebbed3449c499db4, 525cdf1755e29d4c, 30fd31be74169d9a, d2e88333d4f7841f, c2dc3b1007d2e987, d6eabf60cc366341, f798e815252852c2, 0cba47bae78d04eb, 2c218ba829abf74e] tool: 1 --- # Dépannage {#troubleshooting} @@ -13,6 +13,12 @@ Plusieurs entrées s’appuient sur ce même serveur. Un outil (tool) et une res --8<-- "docs_src/troubleshooting/tutorial001.py" ``` +Ces entrées le joignent à l’adresse `http://localhost:8000/mcp`, laissez-le donc tourner en HTTP : + +```console +uv run mcp run server.py --transport streamable-http +``` + Les erreurs citées sur cette page sont réelles : la suite de tests du SDK reproduit chacune d’entre elles. ## `ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception)` {#exceptiongroup-unhandled-errors-in-a-taskgroup-1-sub-exception} @@ -23,7 +29,7 @@ Ce n’est pas une erreur MCP. C’est du bruit produit par anyio, et votre vrai ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: await client.read_resource("weather://Atlantis") ``` @@ -49,7 +55,7 @@ Deux choses à faire avec cela : ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: try: await client.read_resource("weather://Atlantis") except MCPError as e: @@ -67,7 +73,7 @@ async def main() -> None: ```python async def main() -> None: - client = Client(mcp) + client = Client("http://localhost:8000/mcp") tools = await client.list_tools() # RuntimeError ``` @@ -75,7 +81,7 @@ Entrez-y. `__aenter__` est la connexion : ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: tools = await client.list_tools() ``` @@ -252,7 +258,7 @@ app = Starlette(routes=[Mount("/", app=mcp.streamable_http_app())], lifespan=lif ## `MCPError: Session not found` {#mcperror-session-not-found} -Le serveur ne reconnaît pas le `Mcp-Session-Id` que votre client a envoyé, presque toujours parce que le serveur a **redémarré** (ou que vous avez été routé vers une autre instance). Les sessions vivent dans la mémoire de ce seul processus. +Le serveur ne reconnaît pas le `Mcp-Session-Id` que votre client a envoyé. Soit le serveur a **redémarré** (ou vous avez été routé vers une autre instance), soit la session a **expiré** parce que rien n’était en cours pendant `session_idle_timeout`, qui vaut 30 minutes par défaut. Voir [Durée de vie et limites des sessions](run/legacy-clients.md#session-lifetime-and-limits). Les sessions vivent dans la mémoire de ce seul processus. Il n’y a pas de bogue serveur à trouver. La réponse HTTP est un `404` dont le corps *est* du JSON-RPC, donc, contrairement au `421` ci-dessus, le `Client` python vous montre celui-ci mot pour mot : @@ -262,9 +268,9 @@ Il n’y a pas de bogue serveur à trouver. La réponse HTTP est un `404` dont l Le correctif est de vous reconnecter : quittez le bloc `async with Client(...)` et entrez dans un nouveau, qui négocie une session neuve. Pour un client de longue durée, cela signifie intercepter `MCPError` autour de vos appels et vous reconnecter sur ce message plutôt que de réessayer dans une session morte. -Si cela arrive *sans* redémarrage, vous exécutez plus d’un worker sans sessions persistantes (sticky sessions) : chaque worker détient sa propre table de sessions, donc une requête routée vers le mauvais atterrit ici. **[Déployer et passer à l’échelle](run/deploy.md)** et **[Prendre en charge les clients historiques](run/legacy-clients.md)** traitent ce sujet et ses deux correctifs (routage persistant, ou `stateless_http=True`). +Si cela arrive *sans* redémarrage et sans que le client soit resté silencieux aussi longtemps, vous exécutez plus d’un worker sans sessions persistantes (sticky sessions) : chaque worker détient sa propre table de sessions, donc une requête routée vers le mauvais atterrit ici. **[Déployer et passer à l’échelle](run/deploy.md)** et **[Prendre en charge les clients historiques](run/legacy-clients.md)** traitent ce sujet et ses deux correctifs (routage persistant, ou `stateless_http=True`). -Pour l’opérateur du serveur, la ligne de journal correspondante est `Rejected request with unknown or expired session ID: `. Elle est journalisée au niveau `INFO`, elle est donc invisible au seuil habituel `WARNING`. La voir par rafales juste après un déploiement est normal ; chaque client connecté se reconnecte. +Pour l’opérateur du serveur, la ligne de journal correspondante est `Rejected request with unknown or expired session ID: `. Elle est journalisée au niveau `INFO`, elle est donc invisible au seuil habituel `WARNING`. La voir par rafales juste après un déploiement est normal ; chaque client connecté se reconnecte. Lorsque la session a plutôt expiré, cette ligne est précédée de `Session idle timeout`, également au niveau `INFO`. ## `MCPError: Method not found` {#mcperror-method-not-found} @@ -276,7 +282,13 @@ Une chose ne produit **pas** cette erreur, bien qu’il s’agisse d’une requ Votre serveur veut demander quelque chose à l’utilisateur, et ce client n’a jamais dit qu’on pouvait l’interroger. -Un résolveur d’élicitation (elicitation) refuse d’emblée lorsque le client connecté n’a pas déclaré l’élicitation par formulaire, et `e.error.data` nomme exactement ce qui manque : +Ce Bistro pose la question avant de réserver, via un résolveur : + +```python title="server.py" hl_lines="15-17 21" +--8<-- "docs_src/troubleshooting/tutorial007.py" +``` + +Servez-le à la place du serveur Weather et appelez `book_table` depuis un client qui n’a passé aucun `elicitation_callback`. Le résolveur refuse d’emblée, parce que le client connecté n’a jamais déclaré l’élicitation (elicitation) par formulaire, et `e.error.data` nomme exactement ce qui manque : ```json { @@ -290,7 +302,7 @@ Passez `elicitation_callback=` à `Client(...)`. Enregistrer la fonction de rapp ```python async def main() -> None: - async with Client(mcp, elicitation_callback=handle_elicitation) as client: + async with Client("http://localhost:8000/mcp", elicitation_callback=handle_elicitation) as client: result = await client.call_tool("book_table", {"date": "Friday"}) ``` @@ -316,14 +328,14 @@ Vous voyez celui-ci depuis `ctx.elicit()` sur une connexion historique, et sur n Votre gestionnaire a tenté de joindre le client en cours de requête, sur une connexion dont l’appel n’a aucun canal capable de transporter une requête venant du serveur. Trois configurations de serveur placent un appel dans cette situation. -**Une connexion `2026-07-28` : n’importe quel transport, toujours.** Le protocole moderne n’a aucune requête à l’initiative du serveur, si bien que le serveur refuse avant que quoi que ce soit ne soit envoyé. `ctx.elicit()` dans un outil est la façon classique de la rencontrer (dès le tout premier test en mémoire, puisque `Client(server)` négocie `2026-07-28` sans qu’on le lui demande), et passer `elicitation_callback=` ne change rien, parce qu’aucune requête n’atteint jamais le client pour qu’il y réponde : +**Une connexion `2026-07-28` : n’importe quel transport, toujours.** Le protocole moderne n’a aucune requête à l’initiative du serveur, si bien que le serveur refuse avant que quoi que ce soit ne soit envoyé. `ctx.elicit()` dans un outil est la façon classique de la rencontrer, le plus souvent dès le tout premier **[test](get-started/testing.md)** en mémoire de cet outil, puisque `Client(mcp)` négocie `2026-07-28` sans qu’on le lui demande. Passer `elicitation_callback=` ne change rien, parce qu’aucune requête n’atteint jamais le client pour qu’il y réponde : ```python title="server.py" hl_lines="16" --8<-- "docs_src/troubleshooting/tutorial006.py" ``` ```python -async def main() -> None: +async def test_book_table() -> None: async with Client(mcp) as client: await client.call_tool("book_table", {"date": "Friday"}) ``` @@ -366,7 +378,7 @@ Le serveur n’a pas pu vérifier le jeton `requestState` que votre client a ren ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: await client.call_tool("forecast", {"city": "London"}, request_state="round-1-from-worker-a") ``` @@ -419,7 +431,7 @@ mcp = MCPServer("Weather", request_state_security=RequestStateSecurity(keys=[key * `Tool already exists:` dans le journal du serveur est le seul signe que deux outils de même nom se sont fondus en un seul. * Un 421, trois formulations : `Server returned an error response` (le `Client` python), `421 Misdirected Request` / `Invalid Host header` (tout le reste), `Invalid Host header: ` (le journal du serveur). Correctif : `transport_security=TransportSecuritySettings(allowed_hosts=[...])`. * `Task group is not initialized` -> une application montée dont le cycle de vie de l’hôte n’est jamais entré dans `mcp.session_manager.run()`. -* `Session not found` -> le serveur a redémarré ; reconnectez-vous. +* `Session not found` -> le serveur a redémarré ou la session a expiré (`session_idle_timeout`) ; reconnectez-vous. * `Cannot send 'elicitation/create': ... no back-channel ...` -> `ctx.elicit()` a besoin d’un canal serveur-vers-client : une connexion `2026-07-28` n’en a jamais, `stateless_http=True` retire celui des connexions historiques, et `json_response=True` retire celui attaché à la requête. Utilisez un résolveur (un client historique a aussi besoin d’un serveur qui conserve le canal). Son voisin `Method not found` est une requête pour une méthode que la révision du protocole de l’autre côté ne possède pas. * `Client did not declare the form elicitation capability ...` et `Elicitation not supported` -> il manque `elicitation_callback=` au client. * `Invalid or expired requestState` ne dit jamais pourquoi sur la liaison. Le journal du serveur, si ; `unknown key` signifie qu’il faut partager `RequestStateSecurity(keys=[...])` entre les workers. diff --git a/i18n/fr/pages/whats-new.md b/i18n/fr/pages/whats-new.md index e8a36aacb1..28a15ebbf1 100644 --- a/i18n/fr/pages/whats-new.md +++ b/i18n/fr/pages/whats-new.md @@ -1,6 +1,6 @@ --- translation: - sections: [cfe01c0c5863dfa2, 1c58c5cfcc37d455, a7392996acf1ad8f, 875eb2889263424e] + sections: [cfe01c0c5863dfa2, 0dbb68d8b210177b, 80cf193023af3ed4, 875eb2889263424e] tool: 1 --- # Nouveautés de la v2 {#whats-new-in-v2} @@ -43,11 +43,11 @@ Tout ce dont un outil a besoin ne devrait pas venir du modèle. Nouveauté de la La v1 vous donnait trois couches imbriquées : un gestionnaire de contexte de transport produisant des flux bruts, une `ClientSession` qui les enveloppait et un `await session.initialize()` appelé à la main. La v2 a un seul objet : -```python title="client.py" hl_lines="14-18" ---8<-- "docs_src/client/tutorial001.py" +```python title="client.py" hl_lines="7-11" +--8<-- "docs_src/client/tutorial001_client.py" ``` -`Client` accepte un objet serveur (en mémoire, sans transport : c’est la solution pour les tests), une URL (Streamable HTTP), un `StdioServerParameters` (un sous-processus stdio) ou n’importe quel autre gestionnaire de contexte de transport comme `sse_client(...)`. Entrer dans `async with` établit la connexion et négocie la version du protocole, quelle que soit la génération que parle le serveur ; `client.server_capabilities` et `client.protocol_version` sont simplement disponibles ensuite, et `client.server_info` aussi lorsque le serveur s’identifie (c’est désormais `Implementation | None`, puisque l’identité est optionnelle dans la génération 2026). Les fonctions de rappel (callbacks) d’échantillonnage et d’élicitation que vous aviez enregistrées en v1 fonctionnent toujours (leur corps voit le même renommage d’attributs en snake_case que tout le reste de cette page), elles répondent désormais aussi aux requêtes-dans-les-résultats de style 2026 (ci-dessous), et elles s’exécutent de façon concurrente plutôt qu’une à la fois. `ClientSession` reste en dessous pour qui veut la surface bas niveau, et `client.session` vous la donne ; elle a bougé elle aussi (elle tourne sur le nouveau moteur de répartition, et certaines de ses propres signatures ont changé), alors lisez le **[Guide de migration](migration.md#clientsession-now-runs-on-jsonrpcdispatcher-basesession-removed)** avant de descendre à ce niveau. +`Client` accepte une URL (Streamable HTTP), un `StdioServerParameters` (un sous-processus stdio), n’importe quel autre gestionnaire de contexte de transport comme `sse_client(...)` ou, dans les tests, l’objet serveur lui-même (en mémoire, sans transport). Entrer dans `async with` établit la connexion et négocie la version du protocole, quelle que soit la génération que parle le serveur ; `client.server_capabilities` et `client.protocol_version` sont simplement disponibles ensuite, et `client.server_info` aussi lorsque le serveur s’identifie (c’est désormais `Implementation | None`, puisque l’identité est optionnelle dans la génération 2026). Les fonctions de rappel (callbacks) d’échantillonnage et d’élicitation que vous aviez enregistrées en v1 fonctionnent toujours (leur corps voit le même renommage d’attributs en snake_case que tout le reste de cette page), elles répondent désormais aussi aux requêtes-dans-les-résultats de style 2026 (ci-dessous), et elles s’exécutent de façon concurrente plutôt qu’une à la fois. `ClientSession` reste en dessous pour qui veut la surface bas niveau, et `client.session` vous la donne ; elle a bougé elle aussi (elle tourne sur le nouveau moteur de répartition, et certaines de ses propres signatures ont changé), alors lisez le **[Guide de migration](migration.md#clientsession-now-runs-on-jsonrpcdispatcher-basesession-removed)** avant de descendre à ce niveau. **[Le Client](client/index.md)** le présente, **[Transports du client](client/transports.md)** couvre les quatre formes de connexion, **[Fonctions de rappel du client](client/callbacks.md)** couvre les fonctions de rappel elles-mêmes, et **[Tests](get-started/testing.md)** montre le modèle en mémoire qui remplace l’utilitaire `create_connected_server_and_client_session()` de la v1. @@ -172,11 +172,15 @@ Toutes les requêtes initiées par le serveur disparaissent en version 2026-07-2 Le remplacement inverse l’appel. Un outil qui a besoin de quelque chose de la part de l’utilisateur *renvoie* la question (`InputRequiredResult`), le client y répond avec les mêmes fonctions de rappel qu’il a toujours eues, et l’appel est relancé avec les réponses jointes. `Client` pilote cette boucle pour vous. Côté serveur, vous construisez rarement le résultat vous-même, car une **[dépendance](handlers/dependencies.md)** le fait : annotez un paramètre avec `Resolve(ask_quantity)`, où `ask_quantity` est une fonction ordinaire que vous écrivez, et le SDK pose la question par le mécanisme que la connexion prend en charge, une requête d’élicitation en direct sur une session historique ou une requête à plusieurs allers-retours en 2026. Un seul corps d’outil, les deux générations : -```python title="dual_era.py" hl_lines="24 37-38" +```python title="server.py" hl_lines="21" --8<-- "docs_src/legacy_clients/tutorial001.py" ``` -Ce fichier résume tout l’argument en un seul endroit : un serveur, un outil adossé à `Resolve`, et un client historique plus un client moderne qui obtiennent tous deux leur réponse, en mémoire. **[Requêtes à plusieurs allers-retours](handlers/multi-round-trip.md)** explique le mécanisme (y compris `request_state`, que le SDK scelle et vérifie pour vous) ; **[Élicitation](handlers/elicitation.md)** couvre la façon de poser la question. +```python title="client.py" hl_lines="14-15" +--8<-- "docs_src/legacy_clients/tutorial001_client.py" +``` + +Ces deux fichiers résument tout l’argument : un serveur, un outil adossé à `Resolve`, et un client historique plus un client moderne qui obtiennent tous deux leur réponse du même serveur en cours d’exécution (**[Prendre en charge les clients historiques](run/legacy-clients.md)** les passe en revue). **[Requêtes à plusieurs allers-retours](handlers/multi-round-trip.md)** explique le mécanisme (y compris `request_state`, que le SDK scelle et vérifie pour vous) ; **[Élicitation](handlers/elicitation.md)** couvre la façon de poser la question. !!! warning "C’est le seul endroit où un serveur v1 porté change de comportement" Vos propres tests y butent en premier : `Client(mcp)` négocie par défaut 2026-07-28 avec votre diff --git a/i18n/hi/pages/advanced/apps.md b/i18n/hi/pages/advanced/apps.md index 13361d90a5..efaea6c3a6 100644 --- a/i18n/hi/pages/advanced/apps.md +++ b/i18n/hi/pages/advanced/apps.md @@ -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 @@ SDK इसे built-in `Apps` extension (`io.modelcontextprotocol/ui`) के ## चेहरे वाली घड़ी {#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" ``` @@ -49,15 +49,37 @@ model `content` पढ़ता है; iframe इंसानों के ल model को देता है, और text-only client को **सिर्फ़** वही मिलता है। इसलिए मानक pattern है: एक tool, दो जवाब। `get_time` को फिर से देखें: -```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)` तभी `True` होता है जब client ने `io.modelcontextprotocol/ui` extension declare किया हो **और** अपनी `mimeTypes` settings में `text/html;profile=mcp-app` सूचीबद्ध किया हो। यह field ज़रूरी है, -इसलिए जो client इसे छोड़ देता है वह गिना नहीं जाता। इसी file में `main()` ठीक यही -declare करता है: negotiation का client वाला आधा हिस्सा, और rich जवाब वापस आता है। +इसलिए जो client इसे छोड़ देता है वह गिना नहीं जाता। negotiation का client वाला +आधा हिस्सा यह रहा: + +```python title="client.py" hl_lines="8 12" +--8<-- "docs_src/apps/tutorial001_client.py" +``` + +`server.py` को HTTP पर serve करें, फिर दूसरे terminal से client चलाएँ: + +```console +uv run mcp run server.py --transport streamable-http +``` + +```console +python client.py +``` + +```text +2026-06-26T12:00:00Z +``` + +rich जवाब वापस आया। `Client` call से `extensions=[APPS_SUPPORT]` हटा दें, तो यही +program इसकी जगह `The time is 2026-06-26T12:00:00Z.` print करता है, और text-only +client को हमेशा बस इतना ही दिखता है। !!! warning कभी भी `"[Rendered UI]"` जैसा placeholder अकेले content के रूप में न लौटाएँ। diff --git a/i18n/hi/pages/advanced/extensions.md b/i18n/hi/pages/advanced/extensions.md index 3617649c98..3b4366f06e 100644 --- a/i18n/hi/pages/advanced/extensions.md +++ b/i18n/hi/pages/advanced/extensions.md @@ -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 @@ prefix के रूप में ऐसा domain इस्तेमाल क सबसे छोटा काम का extension एक tool और एक 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 @@ prefix के रूप में ऐसा domain इस्तेमाल क * `settings()` वह value है जो `capabilities.extensions["com.example/stamps"]` पर advertise होती है। बिना settings के extension advertise करने के लिए `{}` (default) लौटाएँ। * extension को server कभी नहीं मिलता। यह अपने योगदान data के रूप में declare करता है; `MCPServer` उन्हें consume करता है। mutate करने के लिए कोई `self.server` नहीं है। -और `main()` इसका सबूत है, सीधे `mcp` से जुड़ा एक in-memory client: +इसे HTTP पर serve करें, और एक client इसका सबूत है: -```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" ``` +इस page की हर `server.py` इसी command से serve होती है, और हर `client.py` उसके साथ-साथ दूसरे terminal से `python client.py` से चलती है। + ### अपने methods serve करना {#serving-your-own-methods} extension **नए request methods** register कर सकता है: उसके अपने verbs, जो spec के verbs के साथ-साथ serve होते हैं: -```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 @@ methods **सख़्ती से additive** हैं। SDK इसे constr ### Client side {#the-client-side} -उसी file का `main()` ही client की पूरी कहानी है, उसके दोनों हिस्से: +client अपना अलग program है, और client की कहानी के दोनों हिस्से इसी में हैं: -```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)])` extension declare करता है। ये declarations `ClientCapabilities.extensions` बन जाती हैं: 2026-07-28 connection पर यह map हर request के `_meta` envelope में जाता है, इसलिए server इसे **हर** request पर देखता है; legacy connection पर यह `initialize` handshake के साथ जाता है। server code को फ़र्क नहीं पड़ता कि कौन सा: `require_client_extension(ctx, ...)` और `ctx.session.check_client_capability(...)` दोनों रास्तों पर सही स्रोत पढ़ते हैं। * vendor methods एक परत नीचे `client.session.send_request(...)` पर उतरते हैं; `Client` सिर्फ़ spec verbs के लिए first-class methods जोड़ता है। `send_request` कोई भी `Request` subclass स्वीकार करता है, इसलिए vendor request जैसी है वैसी ही चली जाती है। +* `SearchRequest` और उसके साथ जाने वाले दो models extension का wire contract हैं, इसलिए client उन्हें अपने लिए खुद declare करता है। published extension इन्हें ऐसे package में देगा जिसे दोनों पक्ष import करें। ### `tools/call` को intercept करना {#intercepting-toolscall} @@ -109,12 +116,18 @@ hook `tools/call` को wrap करता है, और कुछ नहीं ## client extension इस्तेमाल करना {#using-a-client-extension} -**client extension** वही contract है, इस्तेमाल करने वाले पक्ष से: एक identifier के पीछे client-side behaviour का bundle। instances को `Client(extensions=[...])` में पास करें और tools सामान्य तरीके से call करें: +**client extension** वही contract है, इस्तेमाल करने वाले पक्ष से: एक identifier के पीछे client-side behaviour का bundle। यहाँ server `buy` का जवाब सामान के बजाय redeem करने लायक receipt से देता है, और सिर्फ़ उसी client को जिसने extension declare किया हो: -```python title="client.py" hl_lines="66-68" +```python title="server.py" hl_lines="22-25" --8<-- "docs_src/extensions/tutorial006.py" ``` +client पर, instances को `Client(extensions=[...])` में पास करें और tools सामान्य तरीके से call करें: + +```python title="client.py" hl_lines="33-35" +--8<-- "docs_src/extensions/tutorial006_client.py" +``` + `call_tool("buy", ...)` हर दूसरे call की तरह सादा `CallToolResult` लौटाता है। extension ने जो बदला: server अब `buy` का जवाब final result के बजाय `receipt` **result shape** से दे सकता है, और `call_tool` के लौटने से पहले `Receipts` उसे पूरा कर देता है (यहाँ follow-up call से receipt redeem करके)। call site में कुछ नहीं हिलता। extension हटा दें तो इनमें से कुछ भी मौजूद नहीं: server का gate उस client को मना कर देता है जिसने इसे declare नहीं किया (error -32021), और gate छोड़ने वाले server से आया claimed shape validation में fail होता है, ठीक वैसे जैसे spec अनजान `resultType` के लिए माँगता है। default रूप से बंद, wire के दोनों सिरों पर। @@ -124,15 +137,15 @@ extension हटा दें तो इनमें से कुछ भी म ```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")]) ``` ## client extension लिखना {#writing-a-client-extension} `ClientExtension` को subclass करें और सिर्फ़ वही override करें जिसकी ज़रूरत हो। योगदान के तीन प्रकार, हर एक का default: `settings()`, `claims()` और `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" ``` * identifier वही grammar मानता है जो server का, और class define होते ही validate होता है। @@ -153,12 +166,18 @@ handler को validated params एक-एक करके, dispatch के क ### Extension verbs {#extension-verbs} -extension के अपने request methods को client-side registration की ज़रूरत नहीं। vendor request type `mcp.types.Request` को subclass करता है और `client.session.send_request` से जाता है, जैसा [अपने methods serve करना](#serving-your-own-methods) में है। एक बात और: जब किसी params key का `Mcp-Name` header में जाना ज़रूरी हो (tasks जैसे extension specs अपने verbs के लिए यह माँगते हैं), तो request type `name_param` declare करता है: +extension के अपने request methods को client-side registration की ज़रूरत नहीं। vendor request type `mcp.types.Request` को subclass करता है और `client.session.send_request` से जाता है, जैसा [अपने methods serve करना](#serving-your-own-methods) में है। ऐसा server लें जिसका extension एक named job के बारे में एक verb serve करता है: -```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" ``` +client पर एक बात और जुड़ती है: जब किसी params key का `Mcp-Name` header में जाना ज़रूरी हो (tasks जैसे extension specs अपने verbs के लिए यह माँगते हैं), तो request type `name_param` declare करता है: + +```python title="client.py" hl_lines="20-23 28-29" +--8<-- "docs_src/extensions/tutorial007_client.py" +``` + session हर send path पर `params["jobId"]` को `Mcp-Name` में mirror करता है, और value न होने पर ज़रूरी header चुपचाप छोड़ने के बजाय साफ़ तौर पर fail होता है। ## extension क्या नहीं कर सकता {#what-an-extension-cannot-do} diff --git a/i18n/hi/pages/advanced/low-level-server.md b/i18n/hi/pages/advanced/low-level-server.md index d2032b78a4..90f0b3a105 100644 --- a/i18n/hi/pages/advanced/low-level-server.md +++ b/i18n/hi/pages/advanced/low-level-server.md @@ -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 --- # Low-level Server {#the-low-level-server} @@ -25,9 +25,9 @@ translation: तीन चीज़ें बदलीं, और पूरा low-level API बस यही है: -* **Handlers constructor parameters हैं।** `on_list_tools=` और `on_call_tool=` `Server(...)` में जाते हैं। यहाँ नीचे कोई decorator नहीं है, और हर handler का आकार एक ही है: `async (ctx, params) -> result`। -* **Input schema आप लिखते हैं।** `Tool.input_schema` एक सादा JSON Schema `dict` है। कोई इसे type hints से नहीं निकालता, क्योंकि निकालने के लिए type hints हैं ही नहीं। -* **Result आप बनाते हैं।** `CallToolResult(content=[TextContent(...)])`, हाथ से। न कुछ wrap होता है, न convert, न return annotation से अनुमान लगाया जाता है। +* **handlers constructor parameters हैं।** `on_list_tools=` और `on_call_tool=` `Server(...)` में जाते हैं। यहाँ नीचे कोई decorator नहीं है, और हर handler का आकार एक ही है: `async (ctx, params) -> result`। +* **input schema आप लिखते हैं।** `Tool.input_schema` एक सादा JSON Schema `dict` है। कोई इसे type hints से नहीं निकालता, क्योंकि निकालने के लिए type hints हैं ही नहीं। +* **result आप बनाते हैं।** `CallToolResult(content=[TextContent(...)])`, हाथ से। न कुछ wrap होता है, न convert, न return annotation से अनुमान लगाया जाता है। `params` parse की हुई request है: `CallToolRequestParams` आपको `.name` और `.arguments` देता है। `ctx` एक `ServerRequestContext` है: client से वापस बात करने के लिए `ctx.session`, `ctx.lifespan_context`, `ctx.request_id`, और `ctx.meta`, यानी request का आने वाला `_meta`। @@ -36,18 +36,22 @@ translation: ### इसे आज़माएँ {#try-it} -इसके लिए कोई Inspector नहीं है: `mcp dev` और `mcp run` सिर्फ़ `MCPServer` स्वीकार करते हैं। In-memory `Client` को कोई फ़र्क नहीं पड़ता; वह low-level `Server` को ठीक वैसे ही लेता है जैसे `MCPServer` को: +`mcp dev` और `mcp run` सिर्फ़ `MCPServer` स्वीकार करते हैं, इसलिए इसे serve आप खुद करते हैं। `server.py` की आख़िरी line इससे एक साधारण ASGI app बनाती है, और uvicorn उसे चलाता है: -```python title="main.py" +```console +uvicorn server:app --port 8000 +``` + +Inspector को, या किसी भी client को, `http://localhost:8000/mcp` पर point करें: + +```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) @@ -61,8 +65,10 @@ asyncio.run(main()) वही text जो `@mcp.tool()` वाले version ने दिया था। दो असली अंतर: -* `result.structured_content` `None` है। High-level server आपके लिए `-> str` को `{"result": ...}` में wrap कर देता है; यहाँ जो आपने नहीं बनाया, उसे कोई नहीं बनाता। -* `list_tools` वही schema लौटाता है जो **आपने** type किया, अक्षर-दर-अक्षर। High-level version में हर property पर `"title": "Query"` था और root पर `"title": "search_booksArguments"`: Pydantic की देन। यहाँ नीचे, अगर कुछ wire पर है, तो उसे वहाँ आपने रखा है। +* `result.structured_content` `None` है। high-level server आपके लिए `-> str` को `{"result": ...}` में wrap कर देता है; यहाँ जो आपने नहीं बनाया, उसे कोई नहीं बनाता। +* `list_tools` वही schema लौटाता है जो **आपने** type किया, अक्षर-दर-अक्षर। high-level version में हर property पर `"title": "Query"` था और root पर `"title": "search_booksArguments"`: Pydantic की देन। यहाँ नीचे, अगर कुछ wire पर है, तो उसे वहाँ आपने रखा है। + +test में आप uvicorn और port दोनों छोड़ देते हैं: `Client(server)` low-level `Server` को in-process ठीक वैसे ही लेता है जैसे `MCPServer` को, और **[Testing](../get-started/testing.md)** यही pattern है। ## आपके लिए कुछ जाँचा नहीं जाता {#nothing-is-checked-for-you} @@ -207,12 +213,12 @@ Handshake runner का है। `server/discover`, `ping`, और बाकी ## सारांश {#recap} -* Low-level `Server` अपने handlers `on_*` **constructor parameters** के रूप में लेता है; हर handler `async (ctx, params) -> result` है। +* low-level `Server` अपने handlers `on_*` **constructor parameters** के रूप में लेता है; हर handler `async (ctx, params) -> result` है। * `input_schema` dict आप लिखते हैं और `CallToolResult` आप बनाते हैं। आपके लिए न कुछ derive होता है, न wrap, न validate। -* Handler में exception `-32603` protocol error है। जिस tool error को model पढ़ सके, वह `is_error=True` वाला `CallToolResult` है जिसे **आप** लौटाते हैं। -* Result पर `_meta` client application के नाम है, model के नहीं। +* handler में exception `-32603` protocol error है। जिस tool error को model पढ़ सके, वह `is_error=True` वाला `CallToolResult` है जिसे **आप** लौटाते हैं। +* result पर `_meta` client application के नाम है, model के नहीं। * `Server[T]` उस चीज़ में generic है जो उसका lifespan yield करता है; `ctx.lifespan_context` एक typed `T` है। * `add_request_handler(method, params_type, handler)` कोई भी method serve करता है। `initialize` reserved है। * `Server` जो capabilities advertise करता है, वे इससे निकलती हैं कि आपने कौन से handlers register किए। -`Client(server)` ने दोनों servers के साथ एक जैसा बर्ताव किया क्योंकि वे एक ही protocol **हैं**, और यही असली बात है। इससे नीचे की अगली layer कोई class है ही नहीं: वह **[Middleware](middleware.md)** है। +client ने दोनों servers के साथ एक जैसा बर्ताव किया क्योंकि वे एक ही protocol **हैं**, और यही असली बात है। इससे नीचे की अगली layer कोई class है ही नहीं: वह **[Middleware](middleware.md)** है। diff --git a/i18n/hi/pages/advanced/pagination.md b/i18n/hi/pages/advanced/pagination.md index 310964061f..6bd3fa88c1 100644 --- a/i18n/hi/pages/advanced/pagination.md +++ b/i18n/hi/pages/advanced/pagination.md @@ -1,6 +1,6 @@ --- translation: - sections: [a9aba7a026c7bd85, ed32bda7ba9ae33a, 7e64cc5646abb91f, 22a0129ee78b3c63, d875373c06d8d2f9] + sections: [a9aba7a026c7bd85, 83e2a08b9d46a398, 9fd8a0aa384b3257, 22a0129ee78b3c63, d875373c06d8d2f9] tool: 1 --- # Pagination {#pagination} @@ -31,9 +31,13 @@ Pagination उस server के लिए है जिसकी resource list ### इसे आज़माएँ {#try-it} -`Client(server)` memory में low-level `Server` से ठीक वैसे ही जुड़ता है जैसे `MCPServer` से। +`mcp run` सिर्फ़ `MCPServer` स्वीकार करता है, इसलिए इसे आप ख़ुद serve करते हैं। `server.py` की आख़िरी line `Server` से एक साधारण ASGI app बनाती है, और uvicorn उसी को चलाता है: -बिना arguments के `list_resources()` call करें। आपको दस resources मिलते हैं, `book-1` से `book-10` तक, और `next_cursor` string `"10"` है। +```console +uvicorn server:app --port 8000 +``` + +किसी भी client (**[The Client](../client/index.md)**, या Inspector) को `http://localhost:8000/mcp` की ओर point करें और बिना arguments के `list_resources()` call करें। आपको दस resources मिलते हैं, `book-1` से `book-10` तक, और `next_cursor` string `"10"` है। इसे `list_resources(cursor="10")` से वापस दें, तो पहला resource `book-11` है और नया `next_cursor` `"20"` है। @@ -43,7 +47,7 @@ Pagination उस server के लिए है जिसकी resource list `Client` का हर `list_*` method (`list_tools`, `list_resources`, `list_resource_templates`, `list_prompts`) `cursor=` keyword लेता है। Paged list को पूरा खींचना एक `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 @@ Pagination उस server के लिए है जिसकी resource list * `next_cursor` देखने से **पहले** extend करें: आख़िरी page में भी resources होते हैं। * `next_cursor is None` ही बाहर निकलने का रास्ता है। बाकी कुछ भी सीधे `cursor=` में वापस जाता है, बिना छेड़े। -इसका `main()` चलाएँ और यह `100 resources` print करता है: दस-दस के दस pages, एक ऐसे loop से जुड़े हुए जिसे कभी पता ही नहीं था कि दस pages थे। +uvicorn अब भी `server.py` serve कर रहा हो, तब दूसरे terminal में `python client.py` चलाएँ। यह `100 resources` print करता है: दस-दस के दस pages, एक ऐसे loop से जुड़े हुए जिसे कभी पता ही नहीं था कि दस pages थे। यह वही loop है जो **[The Client](../client/index.md)** हर `list_*` verb के लिए दिखाता है, और paging न करने वाले server पर इसकी कोई क़ीमत नहीं: पहले ही response में `next_cursor` `None` होता है और loop एक बार चलता है। diff --git a/i18n/hi/pages/client/caching.md b/i18n/hi/pages/client/caching.md index dd239cb2c5..e22005c443 100644 --- a/i18n/hi/pages/client/caching.md +++ b/i18n/hi/pages/client/caching.md @@ -1,6 +1,6 @@ --- translation: - sections: [9e7b9a1710e5aeba, b74ca4c1d2ddddee, fa8714e61bf90c5a, 04db67a886b7271c, 857690fb8f876800] + sections: [9e7b9a1710e5aeba, 66a2e9acc9101d54, d3d25aa802f5145a, 04db67a886b7271c, 857690fb8f876800] tool: 1 --- # Caching hints {#caching-hints} @@ -30,7 +30,7 @@ server खुद कुछ भी cache नहीं करता। ये fiel low-level `Server` पर handlers अपने results खुद बनाते हैं, और `ttl_ms` / `cache_scope` result models पर बस fields हैं। जो handler इन्हें explicitly set करता है, वह constructor map पर हमेशा भारी पड़ता है, field दर field: -```python title="server.py" hl_lines="10 16" +```python title="server.py" hl_lines="11 17" --8<-- "docs_src/caching/tutorial002.py" ``` @@ -44,10 +44,24 @@ paginated lists पर एक सावधानी: protocol की माँ 2026-07-28 session पर `Client` आपके लिए hints का पालन करता है: इसमें built-in response cache है, जो default रूप से चालू रहता है। जो result `ttlMs` के साथ आता है, वह store हो जाता है, और उस TTL के भीतर वैसा ही call cache से serve होता है, बिना round trip के। जिस result में **कोई** hint नहीं होता, वह cache नहीं होता: बिना hint वाले results को `CacheConfig.default_ttl_ms` मिलता है, जिसका default `0` है (तुरंत stale), इसलिए जो server कुछ भी declare नहीं करता, उसे ठीक वैसा ही call-दर-call traffic दिखता है जैसा हमेशा दिखता था। -```python title="client.py" hl_lines="33 35 38" +इसे होते हुए देखने के लिए, पिछले section की `server.py` को uvicorn से serve करें (उसकी आखिरी line ASGI app बनाती है)। handler जब भी सच में चलता है, एक line print करता है: + +```console +uvicorn server:app --port 8000 +``` + +```python title="client.py" hl_lines="20 23 28" --8<-- "docs_src/caching/tutorial003.py" ``` +दूसरे terminal से `python client.py` चलाएँ। यह वे hints print करता है जो पहले result में आए थे, handler का `ttlMs` और उसके बगल में map का `cacheScope`: + +```text +1000 public +``` + +बाकी कहानी server का terminal बताता है: uvicorn के request logs के बीच `tools/list served` तीन बार दिखता है। + चार calls, तीन fetches। दूसरे call को fresh entry मिली और वह server तक पहुँचा ही नहीं; (inject की गई) clock को TTL से आगे बढ़ाने पर तीसरे ने फिर से fetch किया; चौथे ने `cache_mode="refresh"` कहा। यह kwarg पाँचों caching verbs पर मौजूद है (`list_tools`, `list_prompts`, `list_resources`, `list_resource_templates`, `read_resource`): * `"use"` (default) fresh entry हो तो उसे serve करता है, और न हो तो fetch करके store करता है। @@ -56,7 +70,7 @@ paginated lists पर एक सावधानी: protocol की माँ एक नियम `"use"` से ऊपर है: **`meta` वाले calls हमेशा server तक पहुँचते हैं।** जिस request में `meta` set हो (progress token, tracing fields), उसे wire request की उम्मीद होती है, इसलिए `cache_mode="use"` में उसे `"refresh"` माना जाता है: cache read छोड़ दिया जाता है, और fetch किया गया result फिर भी cache की entry की जगह ले लेता है। `"bypass"` और explicit `"refresh"` हमेशा की तरह ही बर्ताव करते हैं। -caching पूरी तरह बंद करने के लिए `Client(server, cache=None)` से construct करें: हर call फिर से round trip है, और `cache_mode`, भले ही अब भी स्वीकार होता है, कुछ नहीं करता। +caching पूरी तरह बंद करने के लिए `Client` construct करते समय `cache=None` pass करें: हर call फिर से round trip है, और `cache_mode`, भले ही अब भी स्वीकार होता है, कुछ नहीं करता। scope का पालन भी अपने आप होता है: `"private"` entries cache के *partition* (नीचे देखें) से बँधी होती हैं, जबकि `"public"` वाली चाहें तो ज़्यादा व्यापक sharing चुन सकती हैं। और जिन entries का नाम notifications लेते हैं, ठीक उनके लिए **notifications TTL पर भारी पड़ते हैं**: `list_changed` notification मेल खाती cached listing को evict कर देता है, और `resources/updated` ठीक उसी URI के तहत store किए गए cached read को evict करता है, चाहे वे कितने भी fresh रहे हों। 2026-07-28 connection पर ये notifications `subscriptions/listen` stream पर आते हैं जिसे आप `client.listen(...)` से खोलते हैं, और eviction आपके watcher को event दिखने से पहले पूरा हो जाता है; **[Subscriptions](subscriptions.md)** वही page है। diff --git a/i18n/hi/pages/client/callbacks.md b/i18n/hi/pages/client/callbacks.md index c7f70ccf7b..6d3ff28287 100644 --- a/i18n/hi/pages/client/callbacks.md +++ b/i18n/hi/pages/client/callbacks.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 @@ result.content # [TextContent(type='text', text='Card issued to Ada Lovelace.') `Client(...)` call पर `mode="legacy"` असल में काम कर रहा है। default रूप से `Client(...)` modern protocol path negotiate करता है, और उस path में server-से-client requests के लिए कोई back-channel नहीं है: आपका callback चलने से पहले ही `ctx.elicit` fail हो जाता है। यह transport तय नहीं करता; negotiated - protocol तय करता है, in-memory में भी और URL पर भी। जब भी आपके client को ऐसी किसी request का जवाब देना हो, + protocol तय करता है। जब भी आपके client को ऐसी किसी request का जवाब देना हो, `mode="legacy"` तय करें; इस page के पीछे का हर test यही करता है। पूरी जानकारी **[Protocol versions](../protocol-versions.md)** में है। 2026-07-28 session पर callback बेकार नहीं होता, उसे input अलग तरीके से मिलता है: जब कोई tool @@ -149,6 +149,6 @@ result.structured_content # {'result': ['elicitation']} * **callback register करना ही capability घोषित करना है।** इसके बिना SDK आपकी ओर से server की request ठुकरा देता है और पूरा call `MCPError` के साथ fail हो जाता है। * server पूछने से पहले `ctx.session.check_client_capability(...)` से पता कर लेता है। * `sampling_callback` और `list_roots_callback` इसी तरह काम करते हैं लेकिन deprecated features को serve करते हैं; modern servers इनकी जगह multi-round-trip requests इस्तेमाल करते हैं। -* `logging_callback` और `message_handler` को notifications मिलती हैं। वे कुछ घोषित नहीं करते। +* `logging_callback` और `message_handler` को notifications मिलते हैं। वे कुछ घोषित नहीं करते। -`Client(...)` का पहला argument transport object है। हर प्रकार की जानकारी **[Client transports](transports.md)** में है। +`Client(...)` का पहला argument transport चुनता है। हर प्रकार की जानकारी **[Client transports](transports.md)** में है। diff --git a/i18n/hi/pages/client/index.md b/i18n/hi/pages/client/index.md index bc2a393fe7..eb85cb863c 100644 --- a/i18n/hi/pages/client/index.md +++ b/i18n/hi/pages/client/index.md @@ -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 --- # Client {#the-client} @@ -11,13 +11,23 @@ translation: ## आपका पहला client {#your-first-client} -```python title="client.py" hl_lines="14-18" +client को बात करने के लिए server चाहिए। इस page का हर उदाहरण इसी Bookshop से connect करता है। इसे `server.py` के नाम से save करें और HTTP पर चलता छोड़ दें: + +```python title="server.py" --8<-- "docs_src/client/tutorial001.py" ``` -ऊपर वाला server सिर्फ़ इसलिए है ताकि connect करने के लिए कुछ हो। client वे पाँच highlighted lines हैं। +```console +uv run mcp run server.py --transport streamable-http +``` + +इससे server `http://localhost:8000/mcp` पर serve होता है। client अपना अलग program है। इसे `client.py` के नाम से save करें और दूसरे terminal में `python client.py` चलाएँ: -* `Client(mcp)` को **server object ही** दिया गया है। यही in-memory transport है: न subprocess, न port, न HTTP। इस page का हर उदाहरण, और आपका लिखा हर test, इसी तरह connect करता है। +```python title="client.py" hl_lines="7-11" +--8<-- "docs_src/client/tutorial001_client.py" +``` + +* `Client("http://localhost:8000/mcp")` को **URL** दिया गया है, इसलिए यह अभी शुरू किए गए server से Streamable HTTP पर connect करता है। * `async with` ही **lifecycle** है। इसमें enter करते ही connect और negotiate होता है; बाहर निकलते ही disconnect। कोई `connect()` / `close()` जोड़ी नहीं है, और block खत्म होने के बाद `Client` दोबारा इस्तेमाल नहीं हो सकता। * block के अंदर connection की जानकारी पहले से सादी properties के रूप में मौजूद है। @@ -25,10 +35,10 @@ translation: `Client` एक positional argument लेता है और उसके type से transport तय करता है: -* `MCPServer` (या low-level `Server`) instance: **in-process** connect होता है। -* URL string (`Client("http://localhost:8000/mcp")`): Streamable HTTP, production वाला रास्ता। -* `StdioServerParameters`: वह command जो **subprocess** के रूप में launch होता है, और जिससे उसके stdin और stdout के ज़रिए बात होती है। +* URL string (`Client("http://localhost:8000/mcp")`): Streamable HTTP, वह transport जिसके पीछे आप deploy करते हैं। +* `StdioServerParameters`: वह command जो local **subprocess** के रूप में launch होता है, और जिससे उसके stdin और stdout के ज़रिए बात होती है। * **transport**: कोई भी चीज़ जिसे आप `async with ... as (read, write)` कर सकें, जैसे आपके अपने HTTP client के ऊपर `streamable_http_client(url, http_client=...)`। +* `MCPServer` (या low-level `Server`) instance: **in-process** connect होता है, न subprocess, न port। यह tests के लिए है, और **[Testing](../get-started/testing.md)** इसी पर आगे बढ़ता है। इस page की बाकी हर चीज़ चारों में एक जैसी है। Headers, subprocesses, timeouts और `Transport` protocol का अपना अलग page है: **[Client transports](transports.md)**। @@ -49,11 +59,11 @@ translation: ## tools की सूची लेना {#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()` एक `ListToolsResult` लौटाता है; tools `.tools` में हैं। हर एक वह पूरी definition है जो host किसी model को देगा: +`list_tools()` एक `ListToolsResult` लौटाता है; tools `.tools` में हैं। हर एक वह पूरी definition है जो host किसी model को देगा। यह पहला है: ```python tool.name # 'search_books' @@ -77,6 +87,8 @@ tool.description # 'Search the catalog by title or author.' UI को argument form दिखाने के लिए, और model को valid arguments बनाने के लिए, जो कुछ चाहिए वह सब इसी schema में है। +दूसरा tool, `lookup_book`, बिना `title=` के register हुआ था, इसलिए उसका `tool.title` `None` है। + !!! tip `title` optional है, इसलिए किसी इंसान को tools दिखाने वाले UI को चुनना पड़ता है: `title` हो तो वही, नहीं तो `name`। `from mcp.shared.metadata_utils import get_display_name` ठीक यही करता है, @@ -86,7 +98,7 @@ UI को argument form दिखाने के लिए, और model को `call_tool(name, arguments)` tool चलाता है और आपको `CallToolResult` वापस देता है। -```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 @@ result.is_error # False resource verbs जोड़ियों में आते हैं: सूची लेने के दो तरीके, पढ़ने का एक। -```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 @@ client को यह भी बताया जा सकता है कि ## 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 @@ host ये messages सीधे model को दे देता है। प जिस server में completion handler हो वह user के type करते-करते prompt और resource-template arguments autocomplete कर सकता है। -```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 @@ host ये messages सीधे model को दे देता है। प हर `list_*` method एक `cursor=` keyword लेता है और हर result में `next_cursor` होता है। जब `next_cursor` `None` हो, आपके पास सब कुछ है। -```python title="client.py" hl_lines="22-30" +```python title="client.py" hl_lines="7-15" --8<-- "docs_src/client/tutorial007.py" ``` -यह loop हर server के साथ सही है। `MCPServer` सब कुछ एक ही page में लौटाता है, इसलिए `next_cursor` `None` होता है और loop एक बार चलता है, यही वजह है कि ज़्यादातर code इसे कभी लिखता ही नहीं। जो servers सच में page करते हैं, और cursors जिन नियमों का पालन करते हैं, वे **[Pagination](../advanced/pagination.md)** में हैं। +`list_all_tools` हर server के साथ सही है। `MCPServer` सब कुछ एक ही page में लौटाता है, इसलिए `next_cursor` `None` होता है और loop एक बार चलता है, यही वजह है कि ज़्यादातर code इसे कभी लिखता ही नहीं। जो servers सच में page करते हैं, और cursors जिन नियमों का पालन करते हैं, वे **[Pagination](../advanced/pagination.md)** में हैं। ## tests में {#in-tests} -बिना process और बिना port वाला `Client(mcp)` अपने आप में server के लिए test harness है। +इस page की हर `client.py` HTTP के ज़रिए `server.py` तक पहुँची। test में आप network छोड़ देते हैं और `Client` को server object ही दे देते हैं: `from server import mcp`, फिर `Client(mcp)`। न process, न port, और ऊपर का हर method वैसे ही काम करता है। -इसी के लिए एक constructor flag बना है: `Client(mcp, raise_exceptions=True)`। इसका असर सिर्फ़ in-memory connections पर होता है, और **[Testing](../get-started/testing.md)** वह page है जो इसे समझाता है और इसके चारों ओर पूरा pattern बनाता है। +इसी के लिए एक constructor flag बना है: `Client(mcp, raise_exceptions=True)`। इसका असर सिर्फ़ in-process connections पर होता है, और **[Testing](../get-started/testing.md)** वह page है जो इसे समझाता है और इसके चारों ओर पूरा pattern बनाता है। ## सारांश {#recap} -* `Client(x)` server object से in-memory connect होता है, URL string से Streamable HTTP पर, और बाकी किसी भी चीज़ से transport के ज़रिए। +* `Client(x)` URL string से Streamable HTTP पर connect होता है, `StdioServerParameters` के लिए subprocess launch करता है, transport में सीधे enter करता है, और tests में server object ही ले लेता है। * `async with` ही पूरा lifecycle है। इसके अंदर `server_capabilities` और `protocol_version` पहले से भरे होते हैं; server दे तो `server_info` और `instructions` भी। * `list_tools()` आपको हर tool का `name`, `title`, `description` और `input_schema` देता है। * `call_tool()` model के लिए `content`, आपके code के लिए `structured_content`, और `is_error` लौटाता है। raise करने वाला tool एक result है, exception नहीं। diff --git a/i18n/hi/pages/client/oauth-clients.md b/i18n/hi/pages/client/oauth-clients.md index 51526eb208..bc31d4ceeb 100644 --- a/i18n/hi/pages/client/oauth-clients.md +++ b/i18n/hi/pages/client/oauth-clients.md @@ -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 @@ authorization code flow को इंसान की ज़रूरत ठी जब `Client` पहली बार request भेजता है, server `401` लौटाता है। provider कमान संभाल लेता है: -1. **Discovery.** यह `WWW-Authenticate` header पढ़ता है, `/.well-known/oauth-protected-resource` से server का Protected Resource Metadata लाता है, पता करता है कि कौन सा authorization server इस resource की रक्षा करता है, और **उस** server का metadata लाता है। +1. **Discovery.** यह `WWW-Authenticate` header पढ़ता है, `/.well-known/oauth-protected-resource` से server का Protected Resource Metadata लाता है, पता करता है कि कौन सा authorization server इस resource की रक्षा करता है, और **उस** server का metadata लाता है। (कोई पुराना server जो resource metadata publish नहीं करता, उससे इसके बजाय उसके अपने origin पर authorization server metadata माँगा जाता है।) दोनों ही सूरतों में metadata को अपने `issuer` के रूप में उसी server का नाम देना होगा जिसके लिए उसे लाया गया था; इसके अलावा कुछ भी हो तो मना कर दिया जाता है। 2. **Registration.** storage में कुछ नहीं है? यह आपके `OAuthClientMetadata` के साथ आपको dynamically register करता है और नतीजा store कर लेता है। 3. **Authorization.** यह PKCE pair और `state` बनाता है, authorization URL तैयार करता है, आपके `redirect_handler` को await करता है, फिर code के लिए आपके `callback_handler` को await करता है। 4. **Exchange.** यह code के बदले `OAuthToken` लेता है, उसे store करता है, और आपकी मूल request को `Authorization: Bearer ...` के साथ दोबारा भेजता है। उसके बाद यह शांत रहता है। tokens storage से आते हैं, expire हुआ access token refresh token से refresh हो जाता है, और सिर्फ़ तब जब इनमें से कुछ काम नहीं करता, यह flow फिर से चलाता है। +इन सभी requests पर transport का एक नियम लागू होता है: जिस MCP request के अंदर ये चलती हैं उसी की तरह, ये किसी redirect को सिर्फ़ तभी follow करती हैं जब वह उसी origin पर रहे और method वही रखे (जैसे trailing-slash वाला 307/308), और किसी भी दूसरे redirect को ऐसे मानती हैं जैसे उस URL ने जवाब ही नहीं दिया। + आपने इसमें से कुछ नहीं लिखा। दो keyword arguments बचते हैं (`client_metadata_url` और `validate_resource_url`), और इस file को दोनों में से किसी की ज़रूरत नहीं। `client_metadata_url` जानने लायक है; इसका अपना section नीचे है। ### इसे आज़माएँ {#try-it} -इन docs के ज़्यादातर उदाहरण आप in-memory `Client(server)` से जाँच सकते हैं। यह नहीं: इस flow का पूरा मतलब ही HTTP `401` है, और in-memory client व उसके server के बीच कोई HTTP होता ही नहीं। +in-memory `Client(server)`, जिसे आपके tests इस्तेमाल करते हैं, यहाँ किसी काम का नहीं: इस flow का पूरा मतलब ही HTTP `401` है, और in-memory client व उसके server के बीच कोई HTTP होता ही नहीं। repository में live version मौजूद है। `examples/servers/simple-auth/` एक standalone authorization server और एक protected MCP server चलाता है; `examples/clients/simple-auth-client/` इसी page का client है, छोटी CLI में बढ़ा हुआ। उसकी README में दो commands हैं: servers शुरू करें, client को उनके सामने चलाएँ, और चारों चरण अपनी आँखों के सामने होते देखें। @@ -110,13 +112,14 @@ URL HTTPS होना चाहिए और उसका path root न हो; `ClientCredentialsOAuthProvider` वही `httpx2.Auth` है, बस इंसान के बिना: -```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" ``` क्या बदला: * न `OAuthClientMetadata`, न handlers। आप `client_id` और `client_secret` देते हैं; provider उनके इर्द-गिर्द एक न्यूनतम `client_credentials` registration बनाता है और dynamic registration पूरी तरह छोड़ देता है। +* `issuer` उस authorization server का नाम बताता है जिसने वे credentials जारी किए; वही `issuer` value इस्तेमाल करें जो उसका `/.well-known/oauth-authorization-server` document लौटाता है। discovery अब भी ऊपर बताए तरीके से चलती है, लेकिन token requests हमेशा सिर्फ़ **उसी** issuer के metadata से बनती हैं; अगर MCP server कहीं और इशारा करता है, तो flow इसके बजाय `OAuthFlowError` के साथ रुक जाता है। इसे छोड़ देना deprecated है और 3.0 में यह ज़रूरी हो जाएगा (**[Deprecated features](../deprecated.md#deprecated-sdk-helpers)** देखें); तब तक provider warning देता है और discovery को जो भी authorization server मिलता है, उसे इस्तेमाल करता है। * `scope` space से अलग की गई string है, OAuth का wire format। * आगे का सब कुछ बिल्कुल वही है: वही `TokenStorage`, वही `httpx2.AsyncClient(auth=...)`, वही `streamable_http_client`। @@ -129,7 +132,7 @@ default रूप से secret token request पर HTTP Basic auth के र एक और provider `mcp.client.auth.extensions.client_credentials` में रहता है: **`PrivateKeyJWTOAuthProvider`**, उन clients के लिए जो shared secret के बजाय JWT से authenticate करते हैं (`private_key_jwt`, key-pair और workload-identity वाला रूप)। यह उसी - pattern पर चलता है: एक बनाएँ, `auth=` पर लगाएँ। उसी module में + pattern पर चलता है: एक बनाएँ (यह भी वही optional `issuer` लेता है), `auth=` पर लगाएँ। उसी module में `SignedJWTParameters` और `static_assertion_provider` भी हैं, दो helpers जो इसका assertion बनाते हैं। बिना इंसान वाली एक और स्थिति है: client किसी enterprise का है जिसका identity provider, न कि user, तय करता है कि वह किन MCP servers तक पहुँच सकता है। वह अलग grant है, अपने trust model और अपने page के साथ, **[Identity assertion](identity-assertion.md)**। diff --git a/i18n/hi/pages/client/session-groups.md b/i18n/hi/pages/client/session-groups.md index f06c315b01..f3e7eb5a62 100644 --- a/i18n/hi/pages/client/session-groups.md +++ b/i18n/hi/pages/client/session-groups.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 groups {#session-groups} @@ -78,7 +78,7 @@ translation: ## सारांश {#recap} * `ClientSessionGroup` कई server connections रखता है और उनके tools, resources और prompts को एक-एक `dict` में मिला देता है। -* हर server के लिए `connect_to_server(params)`। यह transport parameters लेता है, कभी वह server object या URL नहीं जो `Client` लेता है। +* हर server के लिए `connect_to_server(params)`। यह transport parameters लेता है, कभी वह URL या `Transport` नहीं जो `Client` लेता है। * `group.call_tool(name, arguments)` आपके लिए call को उस server तक पहुँचाता है जिसका वह tool है। * नाम पूरे group में unique होने ज़रूरी हैं; `search` tool वाले दो servers अपने आप साथ नहीं रह सकते। * `component_name_hook=` हर register किए गए नाम को फिर से लिखता है। dict key बदलती है, wire पर जाने वाला नाम नहीं। diff --git a/i18n/hi/pages/client/transports.md b/i18n/hi/pages/client/transports.md index e6cb756bd6..d049588dfe 100644 --- a/i18n/hi/pages/client/transports.md +++ b/i18n/hi/pages/client/transports.md @@ -1,6 +1,6 @@ --- translation: - sections: [9cac816674181eb0, 0700f337babcd4dd, 2bde0dd58cdf00f5, 40b4916d82eaf1d4, 3d0832f39b0d7059, dfa4446556badef0, 5bd93be2ab2ecb9c] + sections: [9cac816674181eb0, 7c157764133fea1f, 40b4916d82eaf1d4, 10d151f2cc75317f, 3d0832f39b0d7059, 92742ba36533633d, 0aeca6145e7bd302] tool: 1 --- # Client transports {#client-transports} @@ -11,30 +11,15 @@ translation: हर transport का **server** वाला पक्ष (`mcp.run()` क्या करता है और आप क्या deploy करते हैं) **[अपना server चलाना](../run/index.md)** में है। -## Memory में {#in-memory} - -server object ही पास करें: - -```python title="client.py" hl_lines="14" ---8<-- "docs_src/client_transports/tutorial001.py" -``` - -न कोई subprocess, न कोई port, न wire पर कोई bytes। client और server एक ही process में दो objects हैं, और call फिर भी असली protocol layer से होकर जाती है: `search_books` ठीक वैसे ही list, validate और invoke होता है जैसे HTTP पर होता। - -इससे यह एक साथ दो काम करता है: - -* **Test harness।** इस documentation का हर उदाहरण इसी तरीके से चलाया जाता है, और **[Testing](../get-started/testing.md)** page पूरा pattern इसी के इर्द-गिर्द बनाता है। -* **Embedding API।** जो application खुद server बनाता है, उसे उसके tools call करने के लिए network hop की ज़रूरत नहीं। - ## Streamable HTTP {#streamable-http} -URL string पास करें और आपको **Streamable HTTP** मिलता है, वह transport जिसके पीछे आप deploy करते हैं: +URL string पास करें और आपको **Streamable HTTP** मिलता है, वह transport जिसके पीछे आप deploy करते हैं और जिसे सबसे पहले चुनना चाहिए: ```python title="client.py" hl_lines="5" --8<-- "docs_src/client_transports/tutorial002.py" ``` -पूरा production client बस इतना ही है। `Client` आपके लिए URL को `streamable_http_client(...)` में लपेट देता है, एक `httpx2.AsyncClient` के ऊपर जो MCP की ज़रूरत के हिसाब से configure किया गया है: `follow_redirects=True`, connect/write/pool के लिए 30 सेकंड का timeout, और 300 सेकंड का read timeout, क्योंकि server response stream को खुला रख सकता है। +पूरा production client बस इतना ही है। `Client` आपके लिए URL को `streamable_http_client(...)` में लपेट देता है, एक `httpx2.AsyncClient` के ऊपर जो MCP की ज़रूरत के हिसाब से configure किया गया है: connect/write/pool के लिए 30 सेकंड का timeout, और 300 सेकंड का read timeout, क्योंकि server response stream को खुला रख सकता है। !!! check जो `Client` आपने बनाया है वह connected **नहीं** है। बनाने से सिर्फ़ transport चुना जाता है; @@ -50,7 +35,7 @@ URL string पास करें और आपको **Streamable HTTP** मि जैसे ही आपको `Authorization` header, cookie, proxy, mTLS या कोई अलग timeout चाहिए, `httpx2.AsyncClient` खुद बनाएँ और उसे `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 @@ environment variables set करें या अपने `httpx2.AsyncClient` !!! info `httpx2` जाना-पहचाना `httpx` API ही रखता है, इसलिए अगर आप `httpx` जानते हैं तो यहाँ auth, proxies, event hooks, retries और connection limits कैसे करने हैं, यह आप पहले से जानते हैं। SDK न ऊपर से कुछ जोड़ता है, न कुछ - हटाता है। OAuth भी यहीं जुड़ता है: + हटाता है, सिवाय [redirect handling](#redirects) के। OAuth भी यहीं जुड़ता है: `httpx2.AsyncClient(auth=OAuthClientProvider(...))`। वह पूरा flow **[OAuth clients](oauth-clients.md)** में है। +### Redirects {#redirects} + +transport उसी URL से जुड़ता है जो आपने दिया, और सिर्फ़ उसी origin से। + +* जो `307`/`308` redirect उसी scheme, host और port पर रहता है, उसे follow किया जाता है, और उसी host पर `http://` → `https://` को भी। आम `/mcp` → `/mcp/` वाला trailing-slash redirect इसी में आ जाता है। +* कहीं और जाने वाला redirect follow **नहीं** किया जाता। call इस error के साथ fail होती है: + + ```text + MCPError: Redirect to https://other.example.com/mcp not followed; use that URL as the endpoint if it is the intended server + ``` + + अगर वह URL वही server है जो आप चाहते थे, तो उसे अपने config में डालें। अगर नहीं, तो server या उसके आगे लगा कोई proxy गलत configure है। + +यह आपके पास किए गए किसी भी `httpx2.AsyncClient` पर लागू होता है: MCP requests के लिए उसकी `follow_redirects` setting नहीं देखी जाती, किसी भी दिशा में। SDK के OAuth providers अपनी requests पर यही नियम लागू करते हैं। + +!!! tip + `Redirect to http://… not followed: it would downgrade this HTTPS endpoint to plain HTTP` का मतलब है कि + server किसी ऐसे TLS-terminating proxy के पीछे है जिसके बारे में उसे पता नहीं, और वह `http://` redirects जारी कर रहा है। + इसे server पर ठीक किया जाता है (**[Deploy & scale](../run/deploy.md#behind-a-tls-terminating-proxy)**), + या ठीक वही `https://…/` URL इस्तेमाल करके जो message सुझाता है। + ## stdio {#stdio} **stdio** server एक subprocess है। client उसे launch करता है, उसके stdin पर JSON-RPC लिखता है और उसके stdout से JSON-RPC पढ़ता है। desktop host आपकी machine पर server इसी तरह चलाता है: host यही code **है**, बस ऊपर एक UI के साथ, और **[असली host से जुड़ें](../get-started/real-host.md)** यही रिश्ता host की तरफ़ से, एक config file के रूप में दिखाता है। @@ -105,6 +111,18 @@ child का stderr आपके stderr पर जाता है। उसे जिस server को API key चाहिए, उसे वह वहाँ नहीं मिलेगी। उसे `env=` से explicitly पास करें; वे variables allow-list के ऊपर merge हो जाते हैं। ऊपर `BOOKSHOP_API_KEY` यही कर रहा है। +## Memory में {#in-memory} + +test में न कुछ deploy करना है, न कुछ launch करना। server object ही पास करें: + +```python hl_lines="14" +--8<-- "docs_src/client_transports/tutorial001.py" +``` + +न कोई subprocess, न कोई port, न wire पर कोई bytes। client और server एक ही process में दो objects हैं, और call फिर भी असली protocol layer से होकर जाती है: `search_books` ठीक वैसे ही list, validate और invoke होता है जैसे HTTP पर होता। **[Testing](../get-started/testing.md)** पूरा pattern इसी के इर्द-गिर्द बनाता है। + +यही रूप embedding API का काम भी करता है: जो application खुद server बनाता है, वह बिना network hop के उसके tools call कर सकता है। + ## SSE {#sse} `mcp.client.sse` का `sse_client(url)` वह HTTP transport है जिसकी जगह Streamable HTTP ने ली। जो server अब भी इसे बोलता है, उससे बात करने के लिए इसे उसी तरह wrap करें, `Client(sse_client("http://localhost:8000/sse"))`, और इस पर कुछ नया न बनाएँ। @@ -113,15 +131,16 @@ child का stderr आपके stderr पर जाता है। उसे `Client` के लिए ऊपर की सभी चीज़ें एक ही हैं। -**transport** कोई भी async context manager है जो message streams का `(read, write)` जोड़ा yield करता है: औपचारिक रूप से, `mcp.client` का `Transport` protocol। `Client` अपने argument को type से resolve करता है: server object in-process जुड़ता है, `str` `streamable_http_client(url)` बन जाता है, `StdioServerParameters` `stdio_client(params)` बन जाता है, और बाकी सब कुछ सीधे transport के रूप में enter किया जाता है। यही आख़िरी नियम वजह है कि `stdio_client(...)`, `streamable_http_client(...)` और `sse_client(...)` सब उसी एक slot में बैठते हैं, और यही वजह है कि आप अपना खुद का भी लिख सकते हैं। +**transport** कोई भी async context manager है जो message streams का `(read, write)` जोड़ा yield करता है: औपचारिक रूप से, `mcp.client` का `Transport` protocol। `Client` अपने argument को type से resolve करता है: `str` `streamable_http_client(url)` बन जाता है, `StdioServerParameters` `stdio_client(params)` बन जाता है, server object in-process जुड़ता है, और बाकी सब कुछ सीधे transport के रूप में enter किया जाता है। यही आख़िरी नियम वजह है कि `stdio_client(...)`, `streamable_http_client(...)` और `sse_client(...)` सब उसी एक slot में बैठते हैं, और यही वजह है कि आप अपना खुद का भी लिख सकते हैं। ## सारांश {#recap} -* `Client(mcp)` (server object) memory में जुड़ता है। इसे tests और embedding के लिए इस्तेमाल करें। * `Client("http://.../mcp")` (URL) Streamable HTTP पर जुड़ता है, जो production transport है। * Headers, auth, proxies और timeouts उस `httpx2.AsyncClient` पर होने चाहिए जो आप `streamable_http_client(url, http_client=...)` को पास करते हैं। कोई `headers=` keyword नहीं है। +* Redirects सिर्फ़ URL के अपने origin के भीतर follow होते हैं (trailing-slash वाला `307`/`308`), और उसी host पर `http`→`https`। बाकी सब `Redirect to … not followed` के साथ fail होता है; final URL configure करें। * stdio है `Client(StdioServerParameters(...))`। इसे खुद `stdio_client(...)` में सिर्फ़ तब wrap करें जब child का stderr कहीं और भेजना हो। * subprocess को allow-list वाला environment मिलता है, आपका नहीं; `env=` उसमें जोड़ता है। +* `Client(mcp)` (server object) memory में जुड़ता है। इसे tests में इस्तेमाल करें, या server को उसी application में embed करने के लिए जिसने उसे बनाया। * transport वह हर चीज़ है जिस पर आप `async with x as (read, write)` कर सकें। जो कुछ server object, URL या `StdioServerParameters` नहीं है, `Client` उसे सीधे उसी protocol को सौंप देता है। * `Client` बनाने से transport चुना जाता है। `async with` उसे खोलता है। diff --git a/i18n/hi/pages/deprecated.md b/i18n/hi/pages/deprecated.md index d986c75aec..db2b439ed4 100644 --- a/i18n/hi/pages/deprecated.md +++ b/i18n/hi/pages/deprecated.md @@ -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 --- # Deprecated features {#deprecated-features} -2026-07-28 spec पाँच चीज़ों को retire करता है। SDK अब भी इनमें से हर एक को implement करता है, और अब हर एक पर **deprecation warning** लगी है। एक SDK helper अपनी अलग वजह से deprecated है और [आख़िर में](#deprecated-sdk-helpers) दिया गया है। +2026-07-28 spec पाँच चीज़ों को retire करता है। SDK अब भी इनमें से हर एक को implement करता है, और अब हर एक पर **deprecation warning** लगी है। कुछ SDK-level deprecations अपनी अलग वजह से हैं और [आख़िर में](#deprecated-sdk-helpers) दी गई हैं। नीचे दी गई table हर deprecated feature का नाम, उसके हटने की वजह, और उसकी जगह किस replacement पर build करना है, यह बताती है। @@ -137,11 +137,13 @@ warnings.filterwarnings("ignore", category=MCPDeprecationWarning) ## Deprecated SDK helpers {#deprecated-sdk-helpers} -ये spec के बदलाव नहीं हैं, सिर्फ़ SDK के अंदरूनी हिस्से हैं जिनका बेहतर replacement मौजूद है। ये उसी `MCPDeprecationWarning` के साथ warn करते हैं और 3.0 में हटा दिए जाएँगे। +ये spec के बदलाव नहीं हैं, सिर्फ़ SDK के इस्तेमाल के वे तरीके हैं जिनका बेहतर replacement मौजूद है। ये उसी `MCPDeprecationWarning` के साथ warn करते हैं, और 3.0 पुराना रूप हटा देता है। | Deprecated | इसके बजाय क्या करें | |---|---| | `FuncMetadata.call_fn_with_arg_validation()` | `FuncMetadata.validate_arguments()` और फिर `FuncMetadata.call_fn()`। इसे सिर्फ़ वही code call करता था जो `FuncMetadata` को सीधे चलाता है (जैसे कोई custom `Tool` subclass)। | +| `validate_token_resource=` के बिना `AuthSettings(resource_server_url=...)` | इसे set करें: `True` पर server वे bearer tokens ठुकरा देता है जिन्हें आपका verifier `resource_server_url` के लिए जारी हुआ नहीं बताता, `False` का मतलब है कि आपका verifier token का audience ख़ुद जाँचता है (**[Authorization](run/authorization.md#a-token-verifier)** देखें)। set न होने पर व्यवहार `False` जैसा है; 3.0 में जब भी `resource_server_url` set हो, default `True` होगा। | +| `issuer=` के बिना `ClientCredentialsOAuthProvider(...)` या `PrivateKeyJWTOAuthProvider(...)` | `issuer=` दें, जिसमें credentials जारी करने वाले authorization server का नाम हो (**[OAuth clients लिखना](client/oauth-clients.md#machine-to-machine)** देखें)। इसके बिना MCP server तय करता है कि credentials किस authorization server को मिलें; 3.0 में यह keyword ज़रूरी हो जाएगा। | ## सारांश {#recap} @@ -150,7 +152,7 @@ warnings.filterwarnings("ignore", category=MCPDeprecationWarning) * Deprecated होना बस सलाह भर है: wire में कोई बदलाव नहीं, 2026 से पहले के sessions पर सब कुछ काम करता रहता है, और आपको साफ़ दिखने वाली `MCPDeprecationWarning` मिलती है (यह `UserWarning` है, इसलिए default रूप से चालू है)। * sampling और roots को इसके अलावा back-channel चाहिए जो 2026-07-28 session के पास नहीं है। modern connection पर ये warn करते हैं और फिर raise करते हैं। * `warnings.filterwarnings("ignore", category=MCPDeprecationWarning)` पूरी category को चुप कराता है; pytest में `"error::mcp.MCPDeprecationWarning"` इसे test failure में बदल देता है। -* एक SDK helper, `FuncMetadata.call_fn_with_arg_validation()`, अलग से deprecated है और 3.0 में हटाया जाएगा। +* [SDK-level deprecations](#deprecated-sdk-helpers) पर भी यही नियम लागू है: अभी ये warn करते हैं, और 3.0 पुराना रूप हटा देता है। * नया code इनमें से किसी पर भी नहीं बनना चाहिए। इन docs का बाकी हर page मौजूदा API सिखाता है। diff --git a/i18n/hi/pages/get-started/first-steps.md b/i18n/hi/pages/get-started/first-steps.md index 89dffcdcc0..3d00d05f8b 100644 --- a/i18n/hi/pages/get-started/first-steps.md +++ b/i18n/hi/pages/get-started/first-steps.md @@ -1,6 +1,6 @@ --- translation: - sections: [0d6c05bcbf836bf3, 59a7b14eeefc68c1, 7114d8d6daba203f, e8bbb56a98ba7bc9, 5138010f6159901c, f78da7c7c363d4c6, 220a939cab348686] + sections: [0d6c05bcbf836bf3, 9a78b5f6b44b18ab, 7114d8d6daba203f, e8bbb56a98ba7bc9, bfd2fd1153e71dac, 1615a994ef071fdd, 65c599fae991f245] tool: 1 --- # पहले कदम {#first-steps} @@ -17,7 +17,7 @@ translation: * **client** host के अंदर रहता है और MCP बोलता है। host जितने servers से जुड़ा है, हर एक के लिए एक client चलाता है। * **server** वह है जो आप इस SDK से बनाते हैं। यह clients को चीज़ें expose करता है। यह model से सीधे कभी बात नहीं करता। -server आप लिखते हैं। hosts किसी और का product हैं। SDK आपको एक `Client` भी देता है। इससे आप अपने servers test करेंगे, और यह इसी page पर आगे दिखता है। +server आप लिखते हैं। hosts किसी और का product हैं। SDK आपको `Client` भी देता है, वही class जिससे कोई host URL से server तक पहुँचता है या उसे subprocess के रूप में launch करता है। यह इसी page पर आगे दिखता है, और इसी से आप अपने servers test भी करेंगे। ## तीन primitives {#the-three-primitives} @@ -83,22 +83,20 @@ Inspector में आपने तीन tabs देखे। उसे कै जब client जुड़ता है, server अपनी **capabilities** declare करता है: requests के कौन-से परिवारों का वह जवाब देगा। client उसी declaration से तय करता है कि माँगे भी तो क्या। आपने यह कभी नहीं लिखा; `MCPServer` आपके लिए इसे declare करता है। -खुद देखें। SDK का `Client` सीधे server object लेता है और उससे **in memory** जुड़ता है (न subprocess, न port): - -```python -import asyncio - -from mcp import Client - -from server import mcp +खुद देखें। एक terminal में `server.py` को HTTP पर चलता छोड़ दें: +```console +uv run mcp run server.py --transport streamable-http +``` -async def main() -> None: - async with Client(mcp) as client: - print(client.server_capabilities.model_dump(exclude_none=True)) +और दूसरे terminal से client को उससे जोड़ें: +```python title="client.py" hl_lines="7-8" +--8<-- "docs_src/first_steps/tutorial001_client.py" +``` -asyncio.run(main()) +```console +python client.py ``` ```text @@ -118,8 +116,9 @@ asyncio.run(main()) ध्यान दें कि क्या नहीं है। `completions` (resource templates और prompts के लिए argument autocomplete) को आपका लिखा handler चाहिए, इस server में वह नहीं है, इसलिए capability मौजूद नहीं है और सलीके वाला client पूछेगा ही नहीं। हर optional चीज़ का यही नियम है: चीज़ register करें और capability आ जाती है; **[Completions](../servers/completions.md)** इसे साबित करता है। !!! info - `Client(mcp)` वही in-memory client है जिससे इन docs का हर उदाहरण test होता है, और - इसी से आप अपने servers test करेंगे। इसे पूरा एक page मिलता है: **[Testing](testing.md)**। + वह `client.py` पूरा MCP client है, और **[Client](../client/index.md)** उसका page है। + test में आप terminal और port छोड़ देते हैं और `Client` को सीधे server object ही थमा देते हैं, + `Client(mcp)`। उसे भी पूरा एक page मिलता है: **[Testing](testing.md)**। ## जो आपने नहीं लिखा {#what-you-did-not-write} @@ -128,7 +127,7 @@ asyncio.run(main()) * JSON Schema। `a: int, b: int` **ही** `add` का schema है। * request handler। `tools/list`, `resources/read`, `prompts/get`: सब आपके लिए serve होते हैं। * capability declaration। `MCPServer` ने आपके लिए बना दिया। -* protocol की एक भी line। version negotiation, JSON-RPC framing, capability exchange: यह सब `mcp dev` और `Client(mcp)` के अंदर हुआ, और आपने कभी देखा ही नहीं। +* protocol की एक भी line। version negotiation, JSON-RPC framing, capability exchange: यह सब `mcp dev` और `client.py` के अंदर हुआ, और आपने कभी देखा ही नहीं। यही अनुपात SDK का पूरा मतलब है। @@ -139,6 +138,6 @@ asyncio.run(main()) * हर primitive के लिए एक decorator: `@mcp.tool()`, `@mcp.resource(uri)`, `@mcp.prompt()`। नाम, description और schema function से आते हैं। * `{param}` वाला URI resource **template** बनाता है, जो concrete resources से अलग सूची में दिखता है। * server की **capabilities** आपके लिए declare हो जाती हैं, और client वही माँगता है जो server declare करता है। -* `Client(mcp)` server object से in memory जुड़ता है: पहले दिन से आपका test harness। +* `Client("http://localhost:8000/mcp")` आपके चलते हुए server से बात करता है। उसकी जगह इसे server object थमा दें, `Client(mcp)`, और पहले दिन से यही आपका test harness है। आगे है **[असली host से जुड़ें](real-host.md)**: यही server Claude Desktop या किसी IDE के अंदर, सच में। फिर **[Testing](testing.md)**: एक page, एक in-memory client, और आपको कभी अंदाज़ा नहीं लगाना पड़ेगा कि यह काम करता है या नहीं। उसके बाद हर primitive को अपना page मिलता है, शुरुआत उससे जिसे model चलाता है: **[Tools](../servers/tools.md)**। diff --git a/i18n/hi/pages/get-started/testing.md b/i18n/hi/pages/get-started/testing.md index 2e72aa19c7..c7cdf9b821 100644 --- a/i18n/hi/pages/get-started/testing.md +++ b/i18n/hi/pages/get-started/testing.md @@ -1,13 +1,13 @@ --- translation: - sections: ['4926721070127497', c52a1de2b6b32f40, 8e792bf8c7489ec6, 627195f7159e24ef] + sections: [5d13c2f0ba42c0d2, c52a1de2b6b32f40, 8e792bf8c7489ec6, 38552ea228b0a04f] tool: 1 --- # Testing {#testing} -Python SDK में `Client` class आती है जिसके साथ **in-memory transport** मिलता है: इसे अपना server object दें और यह उससे सीधे जुड़ जाता है। +SDK की `Client` class, वही जो किसी URL से जुड़ती है या subprocess शुरू करती है, **in memory** भी जुड़ती है: इसे अपना server object दें और यह उससे सीधे बात करती है। -कोई subprocess नहीं। कोई port नहीं। कोई transport ही नहीं। यह वही विचार है जो FastAPI के `TestClient` का है। +कोई subprocess नहीं। कोई port नहीं। wire पर कुछ नहीं। यह वही विचार है जो FastAPI के `TestClient` का है। ## Basic usage {#basic-usage} @@ -91,7 +91,7 @@ tool body के **बाहर** का failure अलग है। `Client(mcp tests में इसे चालू रहने दें। production code में इसका कोई मतलब नहीं है। -## Default रूप से in-process {#in-process-by-default} +## Default रूप से पीढ़ी-निरपेक्ष {#era-neutral-by-default} !!! note `Client(mcp)` in-process जुड़ता है और default रूप से **पीढ़ी-निरपेक्ष** है: यह server को probe करता है और diff --git a/i18n/hi/pages/handlers/multi-round-trip.md b/i18n/hi/pages/handlers/multi-round-trip.md index 251145ad9d..4928269c79 100644 --- a/i18n/hi/pages/handlers/multi-round-trip.md +++ b/i18n/hi/pages/handlers/multi-round-trip.md @@ -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-round-trip requests {#multi-round-trip-requests} @@ -164,7 +164,7 @@ low-level `Server` बिना-batteries वाला स्तर है: `MCP ## एक 2026-07-28 result {#a-2026-07-28-result} -`InputRequiredResult` सिर्फ़ protocol version **2026-07-28** पर मौजूद है। in-memory `Client(server)` इसे आपके लिए negotiate करता है; wire पर, `mode="auto"` इसे खोज लेता है। connect करने के बाद `client.protocol_version` बताता है कि आपको क्या मिला। +`InputRequiredResult` सिर्फ़ protocol version **2026-07-28** पर मौजूद है। `Client` का default `mode="auto"` इसे किसी भी connection पर खोज लेता है। connect करने के बाद `client.protocol_version` बताता है कि आपको क्या मिला। !!! warning pre-2026 session के पास `InputRequiredResult` रखने की कोई जगह नहीं है। `mode="legacy"` connection पर diff --git a/i18n/hi/pages/handlers/progress.md b/i18n/hi/pages/handlers/progress.md index b7b3f16ca7..0ccf140b72 100644 --- a/i18n/hi/pages/handlers/progress.md +++ b/i18n/hi/pages/handlers/progress.md @@ -1,6 +1,6 @@ --- translation: - sections: [5315262fe26b33e1, 9d8e98840f1b78f0, 0284b215e85366c4, 8534d8dbb4053a70, 2966fac6fe697007] + sections: [5315262fe26b33e1, 9d8e98840f1b78f0, 52d6009a07e770ea, 8534d8dbb4053a70, 2966fac6fe697007] tool: 1 --- # Progress {#progress} @@ -27,21 +27,19 @@ translation: ## Client से सुनें {#listen-for-it-from-the-client} -Client **हर call पर** अलग से opt in करता है, `call_tool` को `progress_callback=` देकर: +client **हर call पर** अलग से opt in करता है, `call_tool` को `progress_callback=` देकर: -```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"]}, @@ -53,30 +51,34 @@ async def main() -> None: anyio.run(main) ``` -Callback एक `async` function है जो ठीक वही लेता है जो server ने report किया: `progress`, `total`, `message`। +callback एक `async` function है जो ठीक वही लेता है जो server ने report किया: `progress`, `total`, `message`। !!! info - `Client(mcp)` सीधे server object से जुड़ता है, memory में, वही client जिस पर **[Testing](../get-started/testing.md)** - page बना है। `Client` चाहे कोई भी transport इस्तेमाल करे, `progress_callback` parameter वही रहता है; - जो **timing** आप अभी देखने वाले हैं वह in-memory connection की है। वह आपका callback inline चलाता है, - इसलिए हर report `call_tool` के लौटने से पहले पहुँच जाती है। असली transport पर notifications और result - में होड़ लगती है, और एक धीमा callback `call_tool` के लौटने के बाद भी चल रहा हो सकता है। + `Client` को आपने चाहे जो भी दिया हो, `progress_callback` parameter वही रहता है: यहाँ की तरह + URL, कोई `StdioServerParameters`, या test में server object। हाँ, असली transport पर timing का + ध्यान रखें। हर notification अपने आप में अलग, response के साथ-साथ पहुँचता है, इसलिए एक धीमा + callback `call_tool` के लौटने के बाद भी चल रहा हो सकता है। सिर्फ़ in-process test connection + ही callback को inline चलाता है और यह पक्का करता है कि हर report पहले पहुँचे। ### इसे आज़माएँ {#try-it} -`client.py` को `server.py` के बगल में रखें और चलाएँ: +`server.py` को HTTP पर serve करें, फिर दूसरे terminal से client चलाएँ: + +```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.'} ``` -Server पर हर `await ctx.report_progress(...)` client पर `show` का एक call बना, उसी क्रम में, और दोनों lines `call_tool` के लौटने से **पहले** print हुईं। Progress result में बंडल होकर नहीं आता; tool के काम करते रहने के दौरान ही stream होता है। +server पर हर `await ctx.report_progress(...)` client पर `show` का एक call बना, उसी क्रम में। progress result में समेटकर नहीं भेजा जाता। वह tool के काम करते रहने के दौरान ही stream होता है। !!! warning `progress_callback` **call** का है, `Client` का नहीं। इसके लिए कोई constructor argument नहीं है, diff --git a/i18n/hi/pages/protocol-versions.md b/i18n/hi/pages/protocol-versions.md index 79eecb1c78..8280d042d2 100644 --- a/i18n/hi/pages/protocol-versions.md +++ b/i18n/hi/pages/protocol-versions.md @@ -1,6 +1,6 @@ --- translation: - sections: [478fd619e5f90ef8, aef094a00e44e248, bab8cbf3449fa7e9, df1809b15a58335b, 5f9d8c2336ed0239, f54974398e43ddef, b24443dd78584870] + sections: [d98e337bf28845e0, dd642acbba36f615, f547b3e76da19c94, 149513378588da5c, 4e149ed992046709, f54974398e43ddef, b24443dd78584870] tool: 1 --- # Protocol versions {#protocol-versions} @@ -11,9 +11,17 @@ MCP की दो पीढ़ियाँ हैं। आपको इसकी चिंता लगभग कभी नहीं करनी पड़ती, क्योंकि `Client` आपके लिए negotiate कर लेता है। यह page उस एक constructor argument के बारे में है जो इसे नियंत्रित करता है, `mode=`, और उन तीन मौकों के बारे में जब आप इसे बदलते हैं। +इस page का हर snippet एक `client.py` है जो **[Client](client/index.md)** वाले Bookshop `server.py` से बात करता है। उस server को एक terminal में शुरू करें: + +```console +uv run mcp run server.py --transport streamable-http +``` + +फिर हर snippet को दूसरे terminal में `python client.py` से चलाएँ। + ## `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 @@ MCP की दो पीढ़ियाँ हैं। पूरा feature बस इतना ही है। एक `Client`, किसी भी पीढ़ी का server, और code में कोई branching नहीं। !!! info - `MCPServer` हर transport पर `server/discover` का जवाब देता है — in-memory, stdio, streamable - HTTP — इसलिए आपके अपने server के साथ `auto` हमेशा `2026-07-28` पर पहुँचता है। fallback सिर्फ़ - असली pre-2026 server के सामने ही चलता है, और ठीक वहीं आप इसे चाहते भी हैं। + `MCPServer` हर transport पर `server/discover` का जवाब देता है — Streamable HTTP, stdio, और वह + in-process connection जो आपके tests इस्तेमाल करते हैं — इसलिए आपके अपने server के साथ `auto` हमेशा + `2026-07-28` पर पहुँचता है। fallback सिर्फ़ असली pre-2026 server के सामने ही चलता है, और ठीक वहीं + आप इसे चाहते भी हैं। ## `mode="legacy"` {#modelegacy} -```python title="client.py" hl_lines="14" +```python title="client.py" hl_lines="7" --8<-- "docs_src/protocol_versions/tutorial002.py" ``` @@ -57,11 +66,11 @@ server-initiated request का मतलब है server का **आपको `mode="auto"` handshake तभी देता है जब server इतना पुराना हो कि और कुछ चले ही नहीं। `mode="legacy"` इसकी गारंटी देता है। जब भी आप `Client(...)` को `sampling_callback`, request के रूप में चलाया जाने वाला `elicitation_callback`, या `message_handler` देते हैं, इसे चुनें। **[Client callbacks](client/callbacks.md)** में हर एक की बात विस्तार से है। -## Version pin करना {#pinning-a-version} +## version pin करना {#pinning-a-version} `mode` modern protocol version string भी स्वीकार करता है। आज यह set ठीक `["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 @@ probe सस्ता है, लेकिन फिर भी यह एक rou इसलिए इसे संभाल कर रखें। `auto` connection के बाद `client.session.discover_result` में ठीक वही `DiscoverResult` होता है जो server ने भेजा था: उसके `supported_versions`, उसकी `capabilities`, उसके `instructions`, और वह पहचान जो server ने result के `_meta` में दर्ज की थी। अगली बार इसे `prior_discover=` के रूप में वापस दें: -```python title="client.py" hl_lines="15 17" +```python title="client.py" hl_lines="8 10" --8<-- "docs_src/protocol_versions/tutorial004.py" ``` diff --git a/i18n/hi/pages/run/asgi.md b/i18n/hi/pages/run/asgi.md index aab873623b..c336e7eb24 100644 --- a/i18n/hi/pages/run/asgi.md +++ b/i18n/hi/pages/run/asgi.md @@ -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 --- # मौजूदा app में जोड़ना {#add-to-an-existing-app} @@ -99,7 +99,7 @@ Starlette का `Host` route इसी तरह काम करता है: --8<-- "docs_src/asgi/tutorial004.py" ``` -अब clients `/notes` से जुड़ते हैं, `/notes/mcp` से नहीं। +अब clients `/notes/` से जुड़ते हैं, `/notes/mcp` से नहीं। ## browser clients के लिए CORS {#cors-for-browser-clients} @@ -142,4 +142,4 @@ browser-based client को आपसे दो अनुमतियाँ च * browser clients को CORS चाहिए: `Mcp-*` request headers के लिए `allow_headers`, response के लिए `expose_headers=["Mcp-Session-Id"]`। * `@mcp.custom_route()` `/mcp` के बगल में सादे, बिना authentication वाले HTTP endpoints जोड़ता है। -जब server असली URL पर पहुँच में आ जाए, तो **[Client](../client/index.md)** server object के बजाय उसी URL से उससे जुड़ता है। +जब server असली URL पर पहुँच में आ जाए, तो **[Client](../client/index.md)** उसी URL से उससे जुड़ता है। diff --git a/i18n/hi/pages/run/authorization.md b/i18n/hi/pages/run/authorization.md index 40f932377c..e284739d09 100644 --- a/i18n/hi/pages/run/authorization.md +++ b/i18n/hi/pages/run/authorization.md @@ -1,6 +1,6 @@ --- translation: - sections: [d62c13457fc4a534, 80e73abaca6e0652, d1dc4c54cd00ec9c, 14ad3bc7904036bb, 5225f127bc1b9c77, fe1626fdd5aad1da, 4556cb7ea1a04a31] + sections: [d62c13457fc4a534, 80e73abaca6e0652, 128a492d18295f64, 14ad3bc7904036bb, 54a6697833fcc1d9, fe1626fdd5aad1da, 811d083c1da8bcf6] tool: 1 --- # Authorization {#authorization} @@ -23,12 +23,12 @@ OAuth की भाषा में, आपका server **resource server** ह valid token कैसा दिखता है, इस बारे में SDK की कोई राय नहीं है। यह आप बताते हैं, **`TokenVerifier`** implement करके: -```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` एक async method वाला protocol है। `verify_token` को `Authorization` header से raw token मिलता है, और token valid हो तो यह **`AccessToken`** लौटाता है, न हो तो `None`। इसके अलावा implement करने को कुछ नहीं है। -* यह वाला token को एक table में ढूँढता है। असली verifier JWT signature verify करता है या authorization server के token-introspection endpoint को call करता है। वह code आपका है; SDK उसे सिर्फ़ call करता है। +* यह वाला token को एक table में ढूँढता है; हर entry में दर्ज है कि token किस resource के लिए जारी हुआ था। असली verifier JWT signature verify करता है या authorization server के token-introspection endpoint को call करता है, और `AccessToken.resource` में बताता है कि token किसके लिए जारी हुआ था (उसका `aud`)। वह code आपका है; SDK उसे सिर्फ़ call करता है। * `token_verifier=` और `auth=` हमेशा साथ चलते हैं। एक को दूसरे के बिना pass करें तो `MCPServer(...)` कोई request serve करने से पहले ही `ValueError` raise कर देता है। `AuthSettings` आपके resource server का सार्वजनिक चेहरा है: @@ -36,6 +36,10 @@ valid token कैसा दिखता है, इस बारे में S * `issuer_url`: वह authorization server जो आपके tokens जारी करता है। * `resource_server_url`: इस MCP endpoint का public URL। यह बताता है कि token **किस** resource के लिए है, और discovery document भी यहीं रहता है। * `required_scopes`: हर token में ये सभी होने ही चाहिए। +* `validate_token_resource`: हर उस token को मना कर देता है जिसका `AccessToken.resource` `resource_server_url` नहीं है। `resource_server_url` set हो और इसे unset छोड़ दें तो warning (`MCPDeprecationWarning`) आती है और यह `False` की तरह बर्ताव करता है; 3.0 में resource servers के लिए default `True` हो जाएगा। + * इसे तब चालू करें जब आपका authorization server tokens को उस `resource` से बाँधता हो जो client ने माँगा था, और जिसे MCP clients हमेशा भेजते हैं। `resource_server_url` ठीक वही URL रखें जिससे clients जुड़ते हैं। + * इसे तब बंद रहने दें जब आपका authorization server अपने खुद के audience identifiers इस्तेमाल करता हो (Auth0 API identifier, Entra application ID), और इसकी जगह अपने verifier में `aud` जाँचें, और जो token इस server के लिए नहीं है उसके लिए `None` लौटाएँ। + * अगर `aud` एक list है, तो जो entry `resource_server_url` के बराबर है उसे `resource` में रखें। !!! tip SDK repository में `examples/servers/simple-auth/` के अंदर एक `IntrospectionTokenVerifier` है जो @@ -91,7 +95,7 @@ authorization HTTP headers में रहता है, इसलिए यह किसी भी handler के अंदर, **`get_access_token()`** वही `AccessToken` है जो आपके verifier ने मौजूदा request के लिए लौटाया था: -```python title="server.py" hl_lines="4 32-35" +```python title="server.py" hl_lines="4 35-38" --8<-- "docs_src/authorization/tutorial002.py" ``` @@ -125,6 +129,6 @@ authorization server, user के consent screen पर click करने क * `token_verifier=` और `auth=AuthSettings(issuer_url=..., resource_server_url=..., required_scopes=[...])` हमेशा साथ चलते हैं। * SDK `/.well-known/oauth-protected-resource/...` पर [RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728) Protected Resource Metadata publish करता है और unauthenticated requests का जवाब 401 से देता है, जिसका `WWW-Authenticate` header उसी की ओर इशारा करता है। discovery की पूरी कहानी बस इतनी ही है। * किसी भी handler में `get_access_token()` बताता है कि call कौन कर रहा है। -* authorization HTTP का मामला है। `stdio` और in-memory client इसे कभी नहीं देखते। +* authorization HTTP का मामला है। `stdio` और in-memory test client इसे कभी नहीं देखते। client वाला आधा हिस्सा (आपके authorization server को खोजना और आपके लिए token लाना) **[OAuth clients](../client/oauth-clients.md)** में है। और जो client user से पहचान पूछने के बजाय खुद कोई पहचान **assert** करता है, वह **[Identity assertion](../client/identity-assertion.md)** में है। diff --git a/i18n/hi/pages/run/deploy.md b/i18n/hi/pages/run/deploy.md index b27a3a9319..9afb39927f 100644 --- a/i18n/hi/pages/run/deploy.md +++ b/i18n/hi/pages/run/deploy.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 --- # Deploy और scale करना {#deploy-scale} @@ -47,6 +47,22 @@ translation: deploy हुआ server जो हर connection ठुकरा रहा हो, उसे Host allowlist की समस्या ही मानें जब तक कुछ और साबित न हो। **[Troubleshooting](../troubleshooting.md)** भी यहीं से शुरू होता है। +## TLS-terminating proxy के पीछे {#behind-a-tls-terminating-proxy} + +अगर TLS किसी proxy पर ख़त्म होता है (कोई ingress, load balancer, Caddy, nginx) और uvicorn उसके पीछे plain HTTP serve करता है, तो uvicorn को बताएँ कि वह proxy के `X-Forwarded-*` headers पर भरोसा करे: + +```console +uvicorn server:app --proxy-headers --forwarded-allow-ips='' +``` + +इसके बिना app यह मान लेता है कि उसे `http://` पर serve किया जा रहा है, और वह जो भी redirect भेजता है (आम तौर पर `/mcp` → `/mcp/`) वह `http://…` की ओर इशारा करता है। Python client किसी HTTPS endpoint से plain HTTP पर जाने से इनकार कर देता है और साफ़ बताता है: + +```text +MCPError: Redirect to http://mcp.example.com/mcp/ not followed: it would downgrade this HTTPS endpoint to plain HTTP. +``` + +client की तरफ़ का कामचलाऊ उपाय है ठीक वही URL configure करना जो server serve करता है (`https://mcp.example.com/mcp/`, slash सहित), ताकि कोई redirect हो ही नहीं। असली इलाज ऊपर वाला flag है। `FORWARDED_ALLOW_IPS` इसी का environment-variable रूप है; `*` हर hop पर भरोसा करता है, जो सिर्फ़ तभी सही है जब proxy के अलावा कोई uvicorn तक न पहुँच सके। + ## Workers, और sticky किसे होना है {#workers-and-who-has-to-be-sticky} जब hostname जवाब देने लगे, तो उसके पीछे एक से ज़्यादा worker लगाएँ। इसके लिए SDK में कोई knob नहीं है; Starlette app को वैसे ही scale किया जाता है जैसे किसी भी ASGI app को, object किसी ऐसी चीज़ को सौंपकर जो fork करना जानती है: @@ -170,6 +186,7 @@ fan-out को इससे कोई मतलब नहीं कि stream ## सारांश {#recap} * बिना कुछ configure किए app सिर्फ़ उन्हीं requests का जवाब देता है जो localhost को भेजी गई हों। `transport_security=TransportSecuritySettings(allowed_hosts=[...], allowed_origins=[...])` ही go-live gate है: जब तक आप इसे नहीं देते, असली hostname के पीछे हर request `421` है और कारण सिर्फ़ server के log में है। +* TLS-terminating proxy के पीछे uvicorn को `--proxy-headers --forwarded-allow-ips=...` के साथ चलाएँ, वरना उसके redirects `http://` की ओर इशारा करते हैं और client उन्हें ठुकरा देता है। * 2026-07-28 पर कोई session नहीं है और load balancer के sticky होने के लिए कुछ नहीं। `stateless_http=True` सिर्फ़ legacy का knob है क्योंकि modern request उस flag के पढ़े जाने से पहले ही route होकर जवाब पा लेती है। * default `requestState` key `os.urandom(32)` है, हर process में अलग बनी हुई। कोई multi-round-trip retry जो दूसरे worker तक पहुँचे, `-32602` *"Invalid or expired requestState"* के साथ fail होती है। * इलाज है `RequestStateSecurity(keys=[...])` **और** हर instance पर एक ही server नाम। नाम ही token का default audience claim है। वही keys, वही नाम। diff --git a/i18n/hi/pages/run/index.md b/i18n/hi/pages/run/index.md index 7eaaee37a2..3c99980f3b 100644 --- a/i18n/hi/pages/run/index.md +++ b/i18n/hi/pages/run/index.md @@ -1,6 +1,6 @@ --- translation: - sections: [fea8d769ff9edeba, ce8e2ad42f29ef71, 0d705efb19cf99c2, 2fd7cf825e6d2b2c, 9adc400e8c88e854, 318893ad8e2e9924, 6b63ab96b34476c0] + sections: [fea8d769ff9edeba, ce8e2ad42f29ef71, 0d705efb19cf99c2, 9fd2357154a5b7e7, 9adc400e8c88e854, 318893ad8e2e9924, 6b63ab96b34476c0] tool: 1 --- # अपना server चलाना {#running-your-server} @@ -64,21 +64,26 @@ Inspector ठीक वही करता है जो असली host क --8<-- "docs_src/run/tutorial002.py" ``` -वह एक line Starlette app बनाती है और उसे uvicorn से serve करती है। Clients `http://127.0.0.1:3001/mcp` से जुड़ते हैं। +वह एक line Starlette app बनाती है और उसे uvicorn से serve करती है। clients `http://127.0.0.1:3001/mcp` से जुड़ते हैं। हर transport के अपने keyword arguments हैं, सब `run()` पर: -* `host` / `port`: कहाँ सुनना है। Default `127.0.0.1` और `8000`। -* `streamable_http_path`: MCP endpoint कहाँ रहता है। Default `/mcp`। +* `host` / `port`: कहाँ सुनना है। default `127.0.0.1` और `8000`। +* `streamable_http_path`: MCP endpoint कहाँ रहता है। default `/mcp`। * `json_response=True`: हर POST का जवाब SSE stream के बजाय एक अकेली JSON body से देना। उस body में सिर्फ़ response की जगह है, और कुछ नहीं, इसलिए जो tool request के बीच में client को वापस call करता है (`ctx.elicit()`, sampling), वह इस leg पर `NoBackChannelError` raise करता है, और चल रही call से जुड़े notifications (`ctx.report_progress()` का progress, per-call log messages) छोड़ दिए जाते हैं; standalone `GET` stream असंबंधित notifications अब भी ले जाती है। * `stateless_http=True`: हर request के लिए नया transport, कोई session tracking नहीं। -* `max_request_body_size`: स्वीकार की जाने वाली सबसे बड़ी request body, bytes में। Default 4 MiB है; इससे बड़ी requests +* `max_request_body_size`: स्वीकार की जाने वाली सबसे बड़ी request body, bytes में। default 4 MiB है; इससे बड़ी requests को parsing या session बनने से पहले ही HTTP 413 मिलता है। इसे तभी बढ़ाएँ जब जायज़ MCP messages उस आकार से बड़े हों। +* `session_idle_timeout`: कोई legacy session कितने seconds तक बिना किसी चल रही request के खाली रह सकता है, + इससे पहले कि server उसे बंद कर दे। default 1800। `None` इसे बंद कर देता है। देखें + [session का जीवनकाल और सीमाएँ](legacy-clients.md#session-lifetime-and-limits)। +* `max_sessions`: एक process एक साथ कितने legacy sessions रखता है। default 10 000। `None` + सीमा हटा देता है। इसकी जानकारी भी उसी section में है। * `event_store`, `retry_interval`, `transport_security`: resumability और DNS-rebinding से सुरक्षा। ये इंतज़ार कर सकते हैं, जब तक आप localhost के अलावा कहीं deploy न करें; `transport_security` की जानकारी **[Deploy & scale](deploy.md)** में है। !!! warning - Transport options `run()` को जाते हैं, `MCPServer(...)` को **नहीं**। Constructor बताता है कि + transport options `run()` को जाते हैं, `MCPServer(...)` को **नहीं**। constructor बताता है कि आपका server **क्या है**: name, version, instructions। `run()` बताता है कि वह कैसे serve होता है। इसे उल्टा करेंगे तो MCP के शामिल होने से पहले ही Python जवाब दे देता है: diff --git a/i18n/hi/pages/run/legacy-clients.md b/i18n/hi/pages/run/legacy-clients.md index c97b54130e..b3477bf086 100644 --- a/i18n/hi/pages/run/legacy-clients.md +++ b/i18n/hi/pages/run/legacy-clients.md @@ -1,6 +1,6 @@ --- translation: - sections: [3d1663c18edc824c, d4fd37009a13f03d, af9f398a5a8b679a, 470c2dd144294d69, 8e45827e6d24e8c8, 91dfd0ce98ebb03c] + sections: [3d1663c18edc824c, 90956965ae6a1ca1, af9f398a5a8b679a, 5ce83b1f9d88da62, 0d9b5d13fffc94e5, 8e45827e6d24e8c8, 91dfd0ce98ebb03c] tool: 1 --- # legacy clients को serve करना {#serving-legacy-clients} @@ -21,15 +21,25 @@ SDK हर request को उसके `MCP-Protocol-Version` header के ह ## एक handler, दोनों पीढ़ियाँ {#one-handler-both-eras} -यह रहा एक tool जिसे user से कुछ पूछना है, और दोनों पीढ़ियों के client जो उसे call कर रहे हैं: +यह रहा एक tool जिसे user से कुछ पूछना है: -```python title="server.py" hl_lines="24 37-38" +```python title="server.py" hl_lines="21" --8<-- "docs_src/legacy_clients/tutorial001.py" ``` `reserve` को एक चीज़ चाहिए जो model ने नहीं दी: कितनी copies। tool यह बात `Annotated[..., Resolve(ask_quantity)]` से declare करता है (पूरी जानकारी **[Dependencies](../handlers/dependencies.md)** में है)। `reserve` में कहीं भी न किसी version का नाम है, न capability की जाँच, न कोई branch। -दोनों clients **एक ही समय पर** खुले हैं, उसी `mcp` object पर। `mode="legacy"` `initialize` handshake चलाता है: ठीक वही connection जो 2026 से पहले का client खोलता है। दूसरा client default लेता है और `2026-07-28` पर पहुँचता है। +इसे HTTP पर serve करें, और ये रहे दोनों पीढ़ियों के client जो इसे call कर रहे हैं: + +```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" +``` + +दोनों clients **एक ही समय पर** खुले हैं, उसी चल रहे server पर। `mode="legacy"` `initialize` handshake चलाता है: ठीक वही connection जो 2026 से पहले का client खोलता है। दूसरा client default लेता है और `2026-07-28` पर पहुँचता है। दूसरे terminal से `python client.py` चलाएँ: ```text 2025-11-25 {'result': "Reserved 2 of 'Dune'."} @@ -61,6 +71,40 @@ routing मुफ़्त है। session नहीं। client को छूटे हुए SSE events फिर से भेजना), session store नहीं। यह कभी किसी session को दूसरे process से पहुँच लायक नहीं बनाता। +## session की उम्र और सीमाएँ {#session-lifetime-and-limits} + +legacy session हमेशा के लिए नहीं जीता, और एक process अनगिनत sessions नहीं रखता। +इसे दो settings नियंत्रित करती हैं। दोनों `run()`, `streamable_http_app()` +और `Server.streamable_http_app()` पर keyword arguments हैं। modern (`2026-07-28`) connections और `stateless_http=True` +में sessions होते ही नहीं, इसलिए दोनों में से कोई setting उन पर लागू नहीं होती। + +| Setting | Default | यह क्या करती है | client को क्या दिखता है | बंद करने के लिए | +|---|---|---|---|---| +| `session_idle_timeout` | `1800` (30 मिनट) | ऐसे session को बंद कर देती है जिसमें इतनी देर से कुछ in flight न रहा हो। | `404 Session not found`। उसे फिर से `initialize` करना पड़ता है। | `None` | +| `max_sessions` | `10_000` | इतने से ज़्यादा होने पर नया session खोलने से मना कर देती है। मौजूदा sessions अछूते रहते हैं और कुछ भी evict नहीं होता। | `503 Too many open sessions`, JSON-RPC code `-32603` के साथ। | `None` | + +"in flight" किसे माना जाता है: + +* खुला `GET` stream। SDK के clients एक खुला रखते हैं, इसलिए जुड़े हुए client का session कभी + expire नहीं होता। +* ऐसी request जिसका जवाब अभी दिया जा रहा है। timeout से ज़्यादा देर चलने वाला tool call बीच में + नहीं रोका जाता, और उलटी गिनती उसके खत्म होने पर ही शुरू होती है। +* और कुछ नहीं। requests के बीच घड़ी चलती रहती है। session पर आई कोई भी request उसे फिर से शुरू कर देती है, + `ping` भी। एक बार session expire हो जाए, तो उसे कुछ भी वापस नहीं लाता। + +जो client अपना session `DELETE` से खत्म करता है, वह उसे तुरंत खाली कर देता है। जिस client की +पहली request मना कर दी गई हो, उसके साथ भी यही होता है। + +```python +mcp.run(transport="streamable-http", session_idle_timeout=None, max_sessions=50_000) +``` + +दोनों घटनाएँ server log में दिखती हैं। expiry `INFO` पर `Session idle timeout` के रूप में दिखती है। +मना किया गया open `WARNING` पर `Refusing to open a new session: sessions are already open` के रूप में। + +सीमाएँ हर process की अपनी हैं। चार workers के साथ ऊपरी सीमा `max_sessions` की चार गुना है, और हर +worker अपने sessions खुद expire करता है। + ## इकलौता switch: `stateless_http` {#the-one-knob-stateless_http} अगर stickiness ऐसी कीमत है जो आप चुकाना नहीं चाहते, तो ठीक एक चीज़ है जो आप बदल सकते हैं। @@ -86,7 +130,7 @@ routing मुफ़्त है। session नहीं। !!! check जानबूझकर गलत काम करें। `reserve` ठीक वही tool है जिसने अभी दोनों clients को serve किया। इसे - `stateless_http=True` के साथ deploy करें, वही दो clients HTTP पर जोड़ें, और हर एक से इसे call करें। + `stateless_http=True` के साथ deploy करें, वही दो clients जोड़ें, और हर एक से इसे call करें। modern client को अब भी `Reserved 2 of 'Dune'.` मिलता है। modern हिस्सा नहीं बदला। diff --git a/i18n/hi/pages/troubleshooting.md b/i18n/hi/pages/troubleshooting.md index bb0638ede1..a8088dd31d 100644 --- a/i18n/hi/pages/troubleshooting.md +++ b/i18n/hi/pages/troubleshooting.md @@ -1,6 +1,6 @@ --- translation: - sections: [2efaecdef109a5c5, fcacd3e66b8635a4, 25323d737dcf0261, 8a6e351ec756904d, 137454d469c867f5, 6392596bd6df54f0, 41126fa9c4fe432f, 480b6d7897e30ab4, d83bb682e708dde0, ebbed3449c499db4, 323ef84f6b4bebde, 30fd31be74169d9a, 656943c6cb567218, c2dc3b1007d2e987, 7cf5386b997d04e9, 0b59feed8384456e, 0cba47bae78d04eb, e4355f4c7cf4fb2e] + sections: [3d58228e81b99543, 170514ce901c4139, 17d61fad0a50d62b, 8a6e351ec756904d, 137454d469c867f5, 6392596bd6df54f0, 41126fa9c4fe432f, 480b6d7897e30ab4, d83bb682e708dde0, ebbed3449c499db4, 525cdf1755e29d4c, 30fd31be74169d9a, d2e88333d4f7841f, c2dc3b1007d2e987, d6eabf60cc366341, f798e815252852c2, 0cba47bae78d04eb, 2c218ba829abf74e] tool: 1 --- # समस्याएँ सुलझाना {#troubleshooting} @@ -13,6 +13,12 @@ translation: --8<-- "docs_src/troubleshooting/tutorial001.py" ``` +वे entries इस तक `http://localhost:8000/mcp` पर पहुँचती हैं, इसलिए इसे HTTP पर चलता छोड़ दें: + +```console +uv run mcp run server.py --transport streamable-http +``` + इस page पर quote किए गए errors असली हैं: SDK का अपना test suite इनमें से हर एक को reproduce करता है। ## `ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception)` {#exceptiongroup-unhandled-errors-in-a-taskgroup-1-sub-exception} @@ -23,7 +29,7 @@ translation: ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: await client.read_resource("weather://Atlantis") ``` @@ -49,7 +55,7 @@ async def main() -> None: ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: try: await client.read_resource("weather://Atlantis") except MCPError as e: @@ -67,7 +73,7 @@ async def main() -> None: ```python async def main() -> None: - client = Client(mcp) + client = Client("http://localhost:8000/mcp") tools = await client.list_tools() # RuntimeError ``` @@ -75,7 +81,7 @@ async def main() -> None: ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: tools = await client.list_tools() ``` @@ -251,7 +257,7 @@ app = Starlette(routes=[Mount("/", app=mcp.streamable_http_app())], lifespan=lif ## `MCPError: Session not found` {#mcperror-session-not-found} -client ने जो `Mcp-Session-Id` भेजा उसे server नहीं पहचानता, लगभग हमेशा इसलिए कि server **restart** हुआ (या आपको किसी दूसरे instance पर route कर दिया गया)। sessions उसी एक process की memory में रहते हैं। +client ने जो `Mcp-Session-Id` भेजा उसे server नहीं पहचानता। या तो server **restart** हुआ (या आपको किसी दूसरे instance पर route कर दिया गया), या session **expire** हो गया क्योंकि `session_idle_timeout` तक, जो default रूप से 30 मिनट है, कुछ भी in flight नहीं था। [Session lifetime और limits](run/legacy-clients.md#session-lifetime-and-limits) देखें। sessions उसी एक process की memory में रहते हैं। खोजने को कोई server bug नहीं है। HTTP response एक `404` है जिसकी body JSON-RPC **है**, इसलिए ऊपर वाले `421` के उलट, python `Client` इसे आपको ज्यों का त्यों दिखाता है: @@ -261,9 +267,9 @@ client ने जो `Mcp-Session-Id` भेजा उसे server नहीं सुधार है reconnect करना: `async with Client(...)` block से बाहर निकलें और नए में enter करें, जो नया session negotiate करता है। लंबे समय तक चलने वाले client के लिए इसका मतलब है अपने calls के चारों ओर `MCPError` catch करना और इस message पर reconnect करना, न कि मरे हुए session के अंदर retry करते रहना। -अगर यह restart के **बिना** होता है, तो आप sticky sessions के बिना एक से ज़्यादा worker चला रहे हैं: हर worker की अपनी session table होती है, इसलिए गलत worker पर route हुई request यहीं आ गिरती है। वह पूरी कहानी और उसके दो सुधार (sticky routing, या `stateless_http=True`) **[Deploy और scale](run/deploy.md)** और **[legacy clients को serve करना](run/legacy-clients.md)** में हैं। +अगर यह restart के **बिना** होता है और client इतनी देर चुप भी नहीं रहा था, तो आप sticky sessions के बिना एक से ज़्यादा worker चला रहे हैं: हर worker की अपनी session table होती है, इसलिए गलत worker पर route हुई request यहीं आ गिरती है। वह पूरी कहानी और उसके दो सुधार (sticky routing, या `stateless_http=True`) **[Deploy और scale](run/deploy.md)** और **[legacy clients को serve करना](run/legacy-clients.md)** में हैं। -server operator के लिए इससे मेल खाती log line है `Rejected request with unknown or expired session ID: `। यह `INFO` पर log होती है, इसलिए आम `WARNING` threshold पर नहीं दिखती। deploy के ठीक बाद इसे झुंड में देखना सामान्य है; हर जुड़ा हुआ client reconnect कर रहा है। +server operator के लिए इससे मेल खाती log line है `Rejected request with unknown or expired session ID: `। यह `INFO` पर log होती है, इसलिए आम `WARNING` threshold पर नहीं दिखती। deploy के ठीक बाद इसे झुंड में देखना सामान्य है; हर जुड़ा हुआ client reconnect कर रहा है। जब session expire हुआ हो, तो उस line से पहले `Session idle timeout` आती है, वह भी `INFO` पर। ## `MCPError: Method not found` {#mcperror-method-not-found} @@ -275,7 +281,13 @@ server operator के लिए इससे मेल खाती log line आपका server user से कुछ पूछना चाहता है, और इस client ने कभी कहा ही नहीं कि उससे पूछा जा सकता है। -जब जुड़े हुए client ने form elicitation declare नहीं किया हो, तो elicitation resolver शुरू में ही मना कर देता है, और `e.error.data` ठीक-ठीक बताता है कि क्या गायब है: +यह Bistro book करने से पहले पूछता है, एक resolver के ज़रिए: + +```python title="server.py" hl_lines="15-17 21" +--8<-- "docs_src/troubleshooting/tutorial007.py" +``` + +Weather server की जगह इसे serve करें और ऐसे client से `book_table` call करें जिसने कोई `elicitation_callback` नहीं दिया। resolver शुरू में ही मना कर देता है, क्योंकि जुड़े हुए client ने form elicitation कभी declare नहीं किया, और `e.error.data` ठीक-ठीक बताता है कि क्या गायब है: ```json { @@ -289,7 +301,7 @@ server operator के लिए इससे मेल खाती log line ```python async def main() -> None: - async with Client(mcp, elicitation_callback=handle_elicitation) as client: + async with Client("http://localhost:8000/mcp", elicitation_callback=handle_elicitation) as client: result = await client.call_tool("book_table", {"date": "Friday"}) ``` @@ -314,14 +326,14 @@ async def main() -> None: आपके handler ने request के बीच में client तक पहुँचने की कोशिश की, ऐसे connection पर जिसके call में server की ओर से request ले जाने वाला कोई channel नहीं है। तीन server configurations हैं जो किसी call को इस हालत में डालते हैं। -**`2026-07-28` connection: कोई भी transport, हमेशा।** आधुनिक protocol में server-initiated requests हैं ही नहीं, इसलिए server कुछ भेजे जाने से पहले ही मना कर देता है। tool के अंदर `ctx.elicit()` इससे टकराने का classic तरीका है (पहले ही in-memory test पर, क्योंकि `Client(server)` बिना कहे `2026-07-28` negotiate करता है), और `elicitation_callback=` देने से कुछ नहीं बदलता, क्योंकि client तक कभी कोई request पहुँचती ही नहीं जिसका वह जवाब दे: +**`2026-07-28` connection: कोई भी transport, हमेशा।** आधुनिक protocol में server-initiated requests हैं ही नहीं, इसलिए server कुछ भेजे जाने से पहले ही मना कर देता है। tool के अंदर `ctx.elicit()` इससे टकराने का classic तरीका है, आम तौर पर उस tool के पहले ही in-memory **[test](get-started/testing.md)** में, क्योंकि `Client(mcp)` बिना कहे `2026-07-28` negotiate करता है। `elicitation_callback=` देने से कुछ नहीं बदलता, क्योंकि client तक कभी कोई request पहुँचती ही नहीं जिसका वह जवाब दे: ```python title="server.py" hl_lines="16" --8<-- "docs_src/troubleshooting/tutorial006.py" ``` ```python -async def main() -> None: +async def test_book_table() -> None: async with Client(mcp) as client: await client.call_tool("book_table", {"date": "Friday"}) ``` @@ -363,7 +375,7 @@ client ने जो `requestState` token वापस echo किया, server ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: await client.call_tool("forecast", {"city": "London"}, request_state="round-1-from-worker-a") ``` @@ -416,7 +428,7 @@ mcp = MCPServer("Weather", request_state_security=RequestStateSecurity(keys=[key * server log में `Tool already exists:` ही इकलौता संकेत है कि एक ही नाम के दो tools सिमटकर एक रह गए। * एक 421, तीन रूप: `Server returned an error response` (python `Client`), `421 Misdirected Request` / `Invalid Host header` (बाकी सब), `Invalid Host header: ` (server log)। सुधार: `transport_security=TransportSecuritySettings(allowed_hosts=[...])`। * `Task group is not initialized` -> mounted app जिसके host lifespan ने कभी `mcp.session_manager.run()` में enter नहीं किया। -* `Session not found` -> server restart हुआ; reconnect करें। +* `Session not found` -> server restart हुआ या session expire हो गया (`session_idle_timeout`); reconnect करें। * `Cannot send 'elicitation/create': ... no back-channel ...` -> `ctx.elicit()` को server-to-client channel चाहिए: `2026-07-28` connection में वह कभी नहीं होता, `stateless_http=True` legacy वाला छीन लेता है, और `json_response=True` request-scoped वाला। resolver इस्तेमाल करें (legacy client को ऐसा server भी चाहिए जो channel रखता हो)। इसका पड़ोसी `Method not found` ऐसे method की request है जो दूसरी side के protocol revision में है ही नहीं। * `Client did not declare the form elicitation capability ...` और `Elicitation not supported` -> client में `elicitation_callback=` गायब है। * `Invalid or expired requestState` wire पर कभी नहीं बताता कि क्यों। server log बताता है; `unknown key` का मतलब है workers के बीच `RequestStateSecurity(keys=[...])` साझा करें। diff --git a/i18n/hi/pages/whats-new.md b/i18n/hi/pages/whats-new.md index 3550445df7..7178b87f28 100644 --- a/i18n/hi/pages/whats-new.md +++ b/i18n/hi/pages/whats-new.md @@ -1,6 +1,6 @@ --- translation: - sections: [cfe01c0c5863dfa2, 1c58c5cfcc37d455, a7392996acf1ad8f, 875eb2889263424e] + sections: [cfe01c0c5863dfa2, 0dbb68d8b210177b, 80cf193023af3ed4, 875eb2889263424e] tool: 1 --- # v2 में नया क्या है {#whats-new-in-v2} @@ -42,11 +42,11 @@ tool को जो कुछ चाहिए, वह सब model से नह v1 आपको तीन nested परतें थमाता था: raw streams देने वाला transport context manager, उनके चारों ओर लिपटा `ClientSession`, और हाथ से call किया जाने वाला `await session.initialize()`। v2 में एक ही object है: -```python title="client.py" hl_lines="14-18" ---8<-- "docs_src/client/tutorial001.py" +```python title="client.py" hl_lines="7-11" +--8<-- "docs_src/client/tutorial001_client.py" ``` -`Client` server object लेता है (in memory, कोई transport नहीं: testing वाली कहानी), URL (Streamable HTTP), `StdioServerParameters` (stdio subprocess), या `sse_client(...)` जैसा कोई भी दूसरा transport context manager। `async with` में प्रवेश करते ही connect होता है और protocol version negotiate होता है, server चाहे जिस पीढ़ी का हो; उसके बाद `client.server_capabilities` और `client.protocol_version` बस उपलब्ध रहते हैं, और जब server अपनी पहचान बताता है तो `client.server_info` भी (यह अब `Implementation | None` है, क्योंकि 2026 पीढ़ी में identity optional है)। v1 में register किए गए sampling और elicitation callbacks अब भी काम करते हैं (उनकी bodies में वही snake_case attribute rename दिखता है जो इस page की हर चीज़ में), वे अब 2026-style requests-inside-results (नीचे) का जवाब भी देते हैं, और वे एक-एक करके नहीं, बल्कि concurrently चलते हैं। जिसे low-level surface चाहिए, उसके लिए `ClientSession` अब भी नीचे मौजूद है, और `client.session` उसे आपको देता है; वह भी बदला है (वह नए dispatcher engine पर चलता है, और उसके कुछ अपने signatures बदले हैं), इसलिए नीचे उतरने से पहले **[Migration Guide](migration.md#clientsession-now-runs-on-jsonrpcdispatcher-basesession-removed)** पढ़ें। +`Client` URL लेता है (Streamable HTTP), `StdioServerParameters` (stdio subprocess), `sse_client(...)` जैसा कोई भी दूसरा transport context manager, या, tests में, खुद server object (in memory, कोई transport नहीं)। `async with` में प्रवेश करते ही connect होता है और protocol version negotiate होता है, server चाहे जिस पीढ़ी का हो; उसके बाद `client.server_capabilities` और `client.protocol_version` बस उपलब्ध रहते हैं, और जब server अपनी पहचान बताता है तो `client.server_info` भी (यह अब `Implementation | None` है, क्योंकि 2026 पीढ़ी में identity optional है)। v1 में register किए गए sampling और elicitation callbacks अब भी काम करते हैं (उनकी bodies में वही snake_case attribute rename दिखता है जो इस page की हर चीज़ में), वे अब 2026-style requests-inside-results (नीचे) का जवाब भी देते हैं, और वे एक-एक करके नहीं, बल्कि concurrently चलते हैं। जिसे low-level surface चाहिए, उसके लिए `ClientSession` अब भी नीचे मौजूद है, और `client.session` उसे आपको देता है; वह भी बदला है (वह नए dispatcher engine पर चलता है, और उसके कुछ अपने signatures बदले हैं), इसलिए नीचे उतरने से पहले **[Migration Guide](migration.md#clientsession-now-runs-on-jsonrpcdispatcher-basesession-removed)** पढ़ें। **[The Client](client/index.md)** इसका परिचय देता है, **[Client transports](client/transports.md)** connection के चारों रूप समझाता है, **[Client callbacks](client/callbacks.md)** खुद callbacks को, और **[Testing](get-started/testing.md)** वह in-memory pattern दिखाता है जो v1 के `create_connected_server_and_client_session()` helper की जगह लेता है। @@ -153,7 +153,7 @@ renames खुद अपनी घोषणा करते हैं। ये * `McpError`, जिसका नाम बदलकर **`MCPError`** हुआ, सीधे `(code, message, data)` constructor के साथ। * `MCPServer.get_context()`, `mount_path=`, और lowlevel `Server` के decorator methods, ContextVar और handler dicts। -## Protocol: 2025-11-25 से 2026-07-28 {#the-protocol-2025-11-25-to-2026-07-28} +## protocol: 2025-11-25 से 2026-07-28 {#the-protocol-2025-11-25-to-2026-07-28} v2 2026-07-28 revision implement करता है, और यह **दोनों** revisions एक साथ serve करता है: वही `streamable_http_app()` (और वही stdio server) 2025 पीढ़ी के client के `initialize` और 2026 पीढ़ी के client की requests, दोनों का जवाब देता है, बिना कुछ configure किए, बिना कोई flag पलटे, और बिना अलग deployment के। नया revision serve करने से पुराने revision वाला client बीच में नहीं छूटता। आगे वह है जो नया revision खुद बदलता है। @@ -163,19 +163,23 @@ v2 2026-07-28 revision implement करता है, और यह **दोन Streamable HTTP पर 2026 path में कोई `Mcp-Session-Id` नहीं है, और operational headline यही है: **कोई चीज़ modern request को किसी worker से नहीं बाँधती**, इसलिए सादे round-robin load balancer के पीछे कोई भी replica उसका जवाब दे सकता है। दो ईमानदार शर्तें। आपके 2025 पीढ़ी के clients (आज ज़्यादातर clients यही हैं) अब भी sessions खोलते हैं और उन्हें अब भी वही stickiness चाहिए जो v1 पर चाहिए थी; उनके लिए कुछ नहीं बदलता। और एक चीज़ जो **multi-round-trip** retry को workers के पार ले जानी होती है, वह उसका sealed `request_state` है, जिसकी default key हर process में अलग बनती है, इसलिए scaled-out deployment `RequestStateSecurity(keys=[...])` pass करता है। (`stateless_http=True` का इससे लेना-देना नहीं: वह सिर्फ़ यह तय करता है कि 2025 पीढ़ी के clients कैसे serve हों, और 2026 traffic उसे कभी नहीं पढ़ता; अगर आपने v1 में उसे पहले से set किया है, तो कुछ नहीं बदलता।) -इसका client वाला पहलू **[Protocol versions](protocol-versions.md)** है, operator की checklist **[Deploy & scale](run/deploy.md)** है (Host allowlist, `request_state` key, replicas के पार notifications), और दोनों पीढ़ियाँ एक साथ serve करने की कहानी **[Legacy clients को serve करना](run/legacy-clients.md)** है। +इसका client वाला पहलू **[Protocol versions](protocol-versions.md)** है, operator की checklist **[Deploy & scale](run/deploy.md)** है (Host allowlist, `request_state` key, replicas के पार notifications), और दोनों पीढ़ियाँ एक साथ serve करने की कहानी **[legacy clients को serve करना](run/legacy-clients.md)** है। -### Server client को call नहीं कर सकता: multi-round-trip requests {#the-server-cannot-call-the-client-multi-round-trip-requests} +### server client को call नहीं कर सकता: multi-round-trip requests {#the-server-cannot-call-the-client-multi-round-trip-requests} 2026-07-28 पर हर server-initiated request हट गई है: push elicitation, sampling, `roots/list`। 2026 connection पर उनके लिए कोई channel नहीं है, इसलिए `ctx.elicit()` और `ctx.session.create_message()` वहाँ `NoBackChannelError` के साथ fail होते हैं (legacy clients के लिए वे अब भी काम करते हैं)। -इसका विकल्प call को पलट देता है। जिस tool को user से कुछ चाहिए, वह सवाल **लौटाता** है (`InputRequiredResult`), client उन्हीं callbacks से उसका जवाब देता है जो उसके पास हमेशा से थे, और call को जवाबों के साथ retry किया जाता है। `Client` यह loop आपके लिए चलाता है। Server पर आप result शायद ही कभी खुद बनाते हैं, क्योंकि एक **[dependency](handlers/dependencies.md)** यह कर देती है: parameter को `Resolve(ask_quantity)` से annotate करें, जहाँ `ask_quantity` आपका लिखा साधारण function है, और SDK उसी mechanism से पूछता है जिसे connection support करता है, legacy session पर live elicitation request या 2026 पर multi-round-trip। एक tool body, दोनों पीढ़ियाँ: +इसका विकल्प call को पलट देता है। जिस tool को user से कुछ चाहिए, वह सवाल **लौटाता** है (`InputRequiredResult`), client उन्हीं callbacks से उसका जवाब देता है जो उसके पास हमेशा से थे, और call को जवाबों के साथ retry किया जाता है। `Client` यह loop आपके लिए चलाता है। server पर आप result शायद ही कभी खुद बनाते हैं, क्योंकि एक **[dependency](handlers/dependencies.md)** यह कर देती है: parameter को `Resolve(ask_quantity)` से annotate करें, जहाँ `ask_quantity` आपका लिखा साधारण function है, और SDK उसी mechanism से पूछता है जिसे connection support करता है, legacy session पर live elicitation request या 2026 पर multi-round-trip। एक tool body, दोनों पीढ़ियाँ: -```python title="dual_era.py" hl_lines="24 37-38" +```python title="server.py" hl_lines="21" --8<-- "docs_src/legacy_clients/tutorial001.py" ``` -वह file पूरी बात एक जगह कह देती है: एक server, एक `Resolve`-backed tool, और एक legacy client तथा एक modern client, दोनों को अपना जवाब मिलता है, in memory। **[Multi-round-trip requests](handlers/multi-round-trip.md)** mechanism समझाता है (`request_state` समेत, जिसे SDK आपके लिए seal और verify करता है); पूछने का हिस्सा **[Elicitation](handlers/elicitation.md)** में है। +```python title="client.py" hl_lines="14-15" +--8<-- "docs_src/legacy_clients/tutorial001_client.py" +``` + +ये दो files पूरी बात कह देती हैं: एक server, एक `Resolve`-backed tool, और एक legacy client तथा एक modern client, दोनों को उसी चलते हुए server से अपना जवाब मिलता है (**[legacy clients को serve करना](run/legacy-clients.md)** इन्हें कदम दर कदम समझाता है)। **[Multi-round-trip requests](handlers/multi-round-trip.md)** mechanism समझाता है (`request_state` समेत, जिसे SDK आपके लिए seal और verify करता है); पूछने का हिस्सा **[Elicitation](handlers/elicitation.md)** में है। !!! warning "यही वह एक जगह है जहाँ port किए गए v1 server का व्यवहार बदलता है" आपके अपने tests इससे सबसे पहले टकराते हैं: `Client(mcp)` default रूप से आपके v2 server के सामने 2026-07-28 negotiate करता है, @@ -183,28 +187,28 @@ Streamable HTTP पर 2026 path में कोई `Mcp-Session-Id` नही `Resolve(...)` parameter में ले जाएँ (हर पीढ़ी में चलने वाला), या अगर आपको वाकई push व्यवहार चाहिए तो test client को `mode="legacy"` पर pin करें। -### Roots, sampling और protocol logging deprecated हैं; `ping` हटा दिया गया {#roots-sampling-and-protocol-logging-are-deprecated-ping-is-removed} +### roots, sampling और protocol logging deprecated हैं; `ping` हटा दिया गया {#roots-sampling-and-protocol-logging-are-deprecated-ping-is-removed} [SEP-2577](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2577) तीन पूरी **capabilities** को हर protocol version पर deprecated करता है: roots, sampling और MCP-level logging (`ctx.info()` वगैरह)। यह ऊपर के गायब back-channel से अलग धुरी है; deprecated सिर्फ़ सलाह है, 2025 पीढ़ी के sessions के सामने सब कुछ काम करता रहता है, और wire पर कुछ नहीं बदलता। जो आपको दिखता है वह `MCPDeprecationWarning` है, जो `UserWarning` है, इसलिए default रूप से print होता है; मानकर चलें कि upgrade के बाद आपका पहला `ctx.info(...)` यही कहेगा। -`ping` ज़्यादा सख्त है: deprecated नहीं, protocol से हटा दिया गया। Deprecated features के दो standalone methods भी 2026-07-28 पर इसी तरह हटाए गए हैं, `logging/setLevel` और client का `notifications/roots/list_changed`, और progress notifications अब सिर्फ़ server-to-client हैं। +`ping` ज़्यादा सख्त है: deprecated नहीं, protocol से हटा दिया गया। deprecated features के दो standalone methods भी 2026-07-28 पर इसी तरह हटाए गए हैं, `logging/setLevel` और client का `notifications/roots/list_changed`, और progress notifications अब सिर्फ़ server-to-client हैं। **[Deprecated features](deprecated.md)** में पूरी table, हर एक का विकल्प, और legacy clients को serve करते समय शांत log चाहिए तो one-line filter है। -### Change notifications एक stream बन जाते हैं {#change-notifications-become-one-stream} +### change notifications एक stream बन जाते हैं {#change-notifications-become-one-stream} -2026-07-28 पर standalone HTTP GET stream और `resources/subscribe` की जगह `subscriptions/listen` लेता है: client एक long-lived stream खोलता है और बताता है कि उसे किस तरह के notifications चाहिए। `MCPServer` इसे बिना कुछ configure किए serve करता है; आप `await ctx.notify_resource_updated(uri)` (और `notify_tools_changed()`, वगैरह) से publish करते हैं, एक middleware हर caller के लिए listen request ठुकरा सकता है, और multi-replica deployments एक साझा `SubscriptionBus` लगाते हैं। Client पर `async with client.listen(...)` stream खोलता है: filter keyword arguments के रूप में जाता है, typed change events वापस आते हैं, और `sub.honored` वह subset है जिसे server deliver करने पर राज़ी हुआ। +2026-07-28 पर standalone HTTP GET stream और `resources/subscribe` की जगह `subscriptions/listen` लेता है: client एक long-lived stream खोलता है और बताता है कि उसे किस तरह के notifications चाहिए। `MCPServer` इसे बिना कुछ configure किए serve करता है; आप `await ctx.notify_resource_updated(uri)` (और `notify_tools_changed()`, वगैरह) से publish करते हैं, एक middleware हर caller के लिए listen request ठुकरा सकता है, और multi-replica deployments एक साझा `SubscriptionBus` लगाते हैं। client पर `async with client.listen(...)` stream खोलता है: filter keyword arguments के रूप में जाता है, typed change events वापस आते हैं, और `sub.honored` वह subset है जिसे server deliver करने पर राज़ी हुआ। -Publishing और serving **[Subscriptions](handlers/subscriptions.md)** में है, देखने वाला छोर **[इसके Clients वाले जुड़वाँ page](client/subscriptions.md)** में, और bus **[Deploy & scale](run/deploy.md)** में। +publishing और serving **[Subscriptions](handlers/subscriptions.md)** में है, देखने वाला छोर **[इसके Clients वाले जुड़वाँ page](client/subscriptions.md)** में, और bus **[Deploy & scale](run/deploy.md)** में। ### बाकी, फटाफट {#the-rest-quickly} -* **Identity optional, per-message metadata है।** Request-side `clientInfo` `_meta` key optional है (ज़रूरी जोड़ी `protocolVersion` + `clientCapabilities` है), और `serverInfo` `server/discover` result body से बाहर चला गया: servers इसके बजाय उसे हर 2026 पीढ़ी के result के `_meta` में stamp करते हैं ([spec #3002](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/3002))। SDK हमेशा stamp करता है; जब server अपनी पहचान नहीं बताता (उदाहरण के लिए, किसी middleware ने key हटा दी) तो `client.server_info` `None` होता है। **[The low-level Server](advanced/low-level-server.md)** wire पर stamp दिखाता है। -* **Requests bodies parse किए बिना route हो सकती हैं।** Modern HTTP requests `Mcp-Method` ले जाती हैं (और तीन tool जैसी calls के लिए `Mcp-Name`); `x-mcp-header` से annotate की गई tool input-schema property को `Mcp-Param-*` header में mirror किया जाता है और server उसे cross-check करता है ([SEP-2243](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2243))। Gateways और rate limiters सिर्फ़ headers पर route कर सकते हैं; नियम **[Migration Guide](migration.md#servers-validate-mcp-param-headers-against-the-request-body-sep-2243)** में हैं। -* **Results cache hints ले जाते हैं।** List और read results `ttlMs` और `cacheScope` declare करते हैं ([SEP-2549](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2549)); आप उन्हें `cache_hints=` से हर method के लिए set करते हैं, और `Client` built-in response cache के साथ उनका मान रखता है। जो server कोई hints नहीं भेजता (हर pre-2026 server), उसे जस का तस, uncached traffic दिखता है। **[Caching hints](client/caching.md)**। -* **Extensions first class हैं।** Servers और clients reverse-DNS identifiers के नीचे optional capability bundles declare करते हैं ([SEP-2133](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2133)); built-in `Apps` extension (MCP Apps) reference है। **[Extensions](advanced/extensions.md)** और **[MCP Apps](advanced/apps.md)**। -* **Error codes standardized हो गए।** गायब resource `-32602` है, `error.data` में URI के साथ, और नए spec-reserved codes `-32020` (header mismatch), `-32021` (ज़रूरी capability गायब) और `-32022` (unsupported protocol version) के रूप में दिखते हैं। **[Troubleshooting](troubleshooting.md)** ठीक उन्हीं messages के हिसाब से व्यवस्थित है। -* **Authorization को गलत पकड़ना अब मुश्किल है।** Client authorization code के साथ लौटे `iss` को validate करता है ([RFC 9207](https://datatracker.ietf.org/doc/html/rfc9207); आपका `callback_handler` अब `AuthorizationCodeResult` लौटाता है), register करते समय `application_type` भेजता है, और credentials को कभी किसी दूसरे authorization server के सामने replay नहीं करता। Enterprise कोने में नया: [SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990) identity-assertion flow। **[Migration Guide](migration.md)** हर OAuth बदलाव की सूची देता है; pages **[OAuth for clients](client/oauth-clients.md)** और **[Identity assertion](client/identity-assertion.md)** हैं। +* **identity optional, per-message metadata है।** request-side `clientInfo` `_meta` key optional है (ज़रूरी जोड़ी `protocolVersion` + `clientCapabilities` है), और `serverInfo` `server/discover` result body से बाहर चला गया: servers इसके बजाय उसे हर 2026 पीढ़ी के result के `_meta` में stamp करते हैं ([spec #3002](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/3002))। SDK हमेशा stamp करता है; जब server अपनी पहचान नहीं बताता (उदाहरण के लिए, किसी middleware ने key हटा दी) तो `client.server_info` `None` होता है। **[The low-level Server](advanced/low-level-server.md)** wire पर stamp दिखाता है। +* **requests bodies parse किए बिना route हो सकती हैं।** modern HTTP requests `Mcp-Method` ले जाती हैं (और तीन tool जैसी calls के लिए `Mcp-Name`); `x-mcp-header` से annotate की गई tool input-schema property को `Mcp-Param-*` header में mirror किया जाता है और server उसे cross-check करता है ([SEP-2243](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2243))। gateways और rate limiters सिर्फ़ headers पर route कर सकते हैं; नियम **[Migration Guide](migration.md#servers-validate-mcp-param-headers-against-the-request-body-sep-2243)** में हैं। +* **results cache hints ले जाते हैं।** list और read results `ttlMs` और `cacheScope` declare करते हैं ([SEP-2549](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2549)); आप उन्हें `cache_hints=` से हर method के लिए set करते हैं, और `Client` built-in response cache के साथ उनका मान रखता है। जो server कोई hints नहीं भेजता (हर pre-2026 server), उसे जस का तस, uncached traffic दिखता है। **[Caching hints](client/caching.md)**। +* **extensions first class हैं।** servers और clients reverse-DNS identifiers के नीचे optional capability bundles declare करते हैं ([SEP-2133](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2133)); built-in `Apps` extension (MCP Apps) reference है। **[Extensions](advanced/extensions.md)** और **[MCP Apps](advanced/apps.md)**। +* **error codes standardized हो गए।** गायब resource `-32602` है, `error.data` में URI के साथ, और नए spec-reserved codes `-32020` (header mismatch), `-32021` (ज़रूरी capability गायब) और `-32022` (unsupported protocol version) के रूप में दिखते हैं। **[Troubleshooting](troubleshooting.md)** ठीक उन्हीं messages के हिसाब से व्यवस्थित है। +* **authorization को गलत पकड़ना अब मुश्किल है।** client authorization code के साथ लौटे `iss` को validate करता है ([RFC 9207](https://datatracker.ietf.org/doc/html/rfc9207); आपका `callback_handler` अब `AuthorizationCodeResult` लौटाता है), register करते समय `application_type` भेजता है, और credentials को कभी किसी दूसरे authorization server के सामने replay नहीं करता। enterprise कोने में नया: [SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990) identity-assertion flow। **[Migration Guide](migration.md)** हर OAuth बदलाव की सूची देता है; pages **[OAuth for clients](client/oauth-clients.md)** और **[Identity assertion](client/identity-assertion.md)** हैं। * **हर server traceable है।** OpenTelemetry middleware के रूप में default रूप से चालू आता है: हर request को एक server span मिलता है, और जब तक process कोई exporter configure न करे, इसकी कोई लागत नहीं। जब दोनों छोर SDK चलाते हैं, तो client `_meta` में W3C trace context भी propagate करता है, इसलिए traces जुड़ जाते हैं। **[OpenTelemetry](run/opentelemetry.md)**। ## v1 से upgrade कर रहे हैं? {#upgrading-from-v1} diff --git a/i18n/ja/pages/advanced/apps.md b/i18n/ja/pages/advanced/apps.md index 497d06beb6..d1b4bf587e 100644 --- a/i18n/ja/pages/advanced/apps.md +++ b/i18n/ja/pages/advanced/apps.md @@ -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 @@ SDK はこれを組み込みの `Apps` 拡張(`io.modelcontextprotocol/ui`) ## 見た目のある時計 {#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 @@ HTML 自体はホストの `postMessage` を待ち受けて結果を表示しま モデルが読むのは `content` で、iframe は人間のためのものです。UI に対応したホストでもテキストの結果はモデルに渡されますし、テキスト専用のクライアントはそれ「だけ」を受け取ります。ですから定番のパターンは「1 つのツール、2 つの答え」です。もう一度 `get_time` を見てください。 -```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)` が `True` になるのは、クライアントが `io.modelcontextprotocol/ui` 拡張を宣言し、**かつ** `mimeTypes` 設定に `text/html;profile=mcp-app` を含めている場合だけです。このフィールドは必須なので、省略したクライアントは該当しません。同じファイルの `main()` が宣言しているのはまさにこれです。ネゴシエーションのクライアント側であり、その結果リッチな答えが返ってきます。 +`client_supports_apps(ctx)` が `True` になるのは、クライアントが `io.modelcontextprotocol/ui` 拡張を宣言し、**かつ** `mimeTypes` 設定に `text/html;profile=mcp-app` を含めている場合だけです。このフィールドは必須なので、省略したクライアントは該当しません。ネゴシエーションのクライアント側は次のとおりです。 + +```python title="client.py" hl_lines="8 12" +--8<-- "docs_src/apps/tutorial001_client.py" +``` + +`server.py` を HTTP で提供し、別のターミナルからクライアントを実行してください。 + +```console +uv run mcp run server.py --transport streamable-http +``` + +```console +python client.py +``` + +```text +2026-06-26T12:00:00Z +``` + +リッチな答えが返ってきました。`Client` の呼び出しから `extensions=[APPS_SUPPORT]` を外すと、同じプログラムは代わりに `The time is 2026-06-26T12:00:00Z.` と表示します。テキスト専用のクライアントが目にするのは、これがすべてです。 !!! warning `"[Rendered UI]"` のようなプレースホルダーを唯一のコンテンツとして返さないでください。フォールバックのテキストが役に立たなければ、そのツールはテキスト専用のすべてのクライアントにとっても、モデル自身にとっても役に立ちません。きちんと文を書いてください。 diff --git a/i18n/ja/pages/advanced/extensions.md b/i18n/ja/pages/advanced/extensions.md index 5734fafd43..8bf2d07d52 100644 --- a/i18n/ja/pages/advanced/extensions.md +++ b/i18n/ja/pages/advanced/extensions.md @@ -1,6 +1,6 @@ --- translation: - sections: [05891e7cc1938a13, b3c01a6af28c51ee, 7ffc91f5e38bdfe0, 717d3f235a8333a7, f471a13b2fe5d737, ed6af2df4b656dff] + sections: [05891e7cc1938a13, b3c01a6af28c51ee, 6eba6ce094f4417e, 125f28588c85dd7b, ddd8969b698ca11c, ed6af2df4b656dff] tool: 1 --- # 拡張機能 {#extensions} @@ -49,7 +49,7 @@ TypeError: Stamps.identifier must be a `vendor-prefix/name` string 役に立つ最小の拡張機能は、ツール 1 つと設定マップ 1 つです。 -```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 @@ TypeError: Stamps.identifier must be a `vendor-prefix/name` string * `settings()` は `capabilities.extensions["com.example/stamps"]` にアドバタイズされる値です。設定なしで拡張機能をアドバタイズするには `{}`(デフォルト)を返してください。 * 拡張機能がサーバーを受け取ることはありません。提供するものをデータとして宣言し、`MCPServer` がそれを消費します。書き換えられる `self.server` はありません。 -そして `main()` がその証明です。`mcp` に直接つなぐインメモリのクライアントです。 +HTTP で配信すれば、クライアントがその証明になります。 -```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" ``` +このページの `server.py` はどれもこのコマンドで配信し、`client.py` はどれもその横で、2 つ目のターミナルから `python client.py` で実行します。 + ### 独自メソッドの提供 {#serving-your-own-methods} 拡張機能は**新しいリクエストメソッド**を登録できます。仕様のメソッドと並んで配信される、独自の動詞(verb)です。 -```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 @@ TypeError: Stamps.identifier must be a `vendor-prefix/name` string ### クライアント側 {#the-client-side} -同じファイルの `main()` に、クライアント側の話がすべて、その両半分とも入っています。 +クライアントは独立したプログラムで、クライアント側の話の両半分を担っています。 -```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)])` が拡張機能を宣言します。宣言は `ClientCapabilities.extensions` になります。2026-07-28 接続では、このマップはリクエストごとの `_meta` エンベロープで運ばれるため、サーバーは**すべての**リクエストでそれを見ます。レガシー接続では `initialize` ハンドシェイクに載ります。サーバーのコードはどちらでも気にしません。`require_client_extension(ctx, ...)` と `ctx.session.check_client_capability(...)` は、どちらの経路でも正しい情報源を読みます。 * ベンダーメソッドは 1 層下がって `client.session.send_request(...)` を使います。`Client` がファーストクラスのメソッドを増やすのは仕様の動詞に対してだけです。`send_request` はどんな `Request` サブクラスも受け付けるため、ベンダーリクエストはそのまま渡せます。 +* `SearchRequest` と、それが運ぶ 2 つのモデルは拡張機能の通信上の契約なので、クライアントは自分でそれらを宣言します。公開された拡張機能なら、両側がインポートするパッケージに含めて配布するでしょう。 ### `tools/call` のインターセプト {#intercepting-toolscall} @@ -109,12 +116,18 @@ TypeError: Stamps.identifier must be a `vendor-prefix/name` string ## クライアント拡張機能を使う {#using-a-client-extension} -**クライアント拡張機能**は、同じ契約を利用する側から見たものです。1 つの識別子の下にまとめられたクライアント側の振る舞い一式です。インスタンスを `Client(extensions=[...])` に渡し、通常どおりツールを呼び出します。 +**クライアント拡張機能**は、同じ契約を利用する側から見たものです。1 つの識別子の下にまとめられたクライアント側の振る舞い一式です。ここでのサーバーは `buy` に対して、品物ではなく引き換え用のレシートで答えます。ただし、拡張機能を宣言したクライアントに対してだけです。 -```python title="client.py" hl_lines="66-68" +```python title="server.py" hl_lines="22-25" --8<-- "docs_src/extensions/tutorial006.py" ``` +クライアントでは、インスタンスを `Client(extensions=[...])` に渡し、通常どおりツールを呼び出します。 + +```python title="client.py" hl_lines="33-35" +--8<-- "docs_src/extensions/tutorial006_client.py" +``` + `call_tool("buy", ...)` は、他のすべての呼び出しと同様にプレーンな `CallToolResult` を返します。拡張機能が変えたのは次の点です。サーバーは `buy` に対して、最終結果の代わりに `receipt` という**結果の形状**で答えられるようになり、`call_tool` が戻る前に `Receipts` がそれを完了させます(ここでは後続の呼び出しでレシートを引き換えます)。呼び出し側のコードは何も変わりません。 拡張機能を外せば、このどれも存在しません。サーバーのゲートは宣言しなかったクライアントを拒否し(エラー -32021)、ゲートを省いたサーバーから届いた引き受け対象の形状は検証に失敗します。認識できない `resultType` に対して仕様が求めるとおりです。通信路の両端で、デフォルトはオフです。 @@ -124,15 +137,15 @@ TypeError: Stamps.identifier must be a `vendor-prefix/name` string ```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")]) ``` ## クライアント拡張機能を書く {#writing-a-client-extension} `ClientExtension` をサブクラス化し、必要なものだけをオーバーライドします。提供できるものは 3 種類で、それぞれにデフォルトがあります。`settings()`、`claims()`、`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" ``` * 識別子はサーバー側と同じ文法に従い、クラスの定義時に検証されます。 @@ -153,12 +166,18 @@ def notifications(self) -> Sequence[NotificationBinding[Any]]: ### 拡張機能の動詞 {#extension-verbs} -拡張機能独自のリクエストメソッドには、クライアント側の登録は不要です。ベンダーリクエスト型は `mcp.types.Request` をサブクラス化し、[独自メソッドの提供](#serving-your-own-methods)と同様に `client.session.send_request` を通ります。追加が 1 つあります。パラメーターのキーを `Mcp-Name` ヘッダーに載せなければならない場合(tasks のような拡張機能の仕様では、その動詞にこれが必要です)、リクエスト型は `name_param` を宣言します。 +拡張機能独自のリクエストメソッドには、クライアント側の登録は不要です。ベンダーリクエスト型は `mcp.types.Request` をサブクラス化し、[独自メソッドの提供](#serving-your-own-methods)と同様に `client.session.send_request` を通ります。名前付きのジョブに関する動詞を 1 つ、拡張機能が配信するサーバーを例に取りましょう。 -```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" ``` +クライアント側での追加が 1 つあります。パラメーターのキーを `Mcp-Name` ヘッダーに載せなければならない場合(tasks のような拡張機能の仕様では、その動詞にこれが必要です)、リクエスト型は `name_param` を宣言します。 + +```python title="client.py" hl_lines="20-23 28-29" +--8<-- "docs_src/extensions/tutorial007_client.py" +``` + セッションはどの送信経路でも `params["jobId"]` を `Mcp-Name` に反映し、値が欠けている場合は必須ヘッダーを黙って省くのではなく、はっきりとエラーになります。 ## 拡張機能にできないこと {#what-an-extension-cannot-do} diff --git a/i18n/ja/pages/advanced/low-level-server.md b/i18n/ja/pages/advanced/low-level-server.md index c940ae829f..6681b622e2 100644 --- a/i18n/ja/pages/advanced/low-level-server.md +++ b/i18n/ja/pages/advanced/low-level-server.md @@ -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 --- # 低レベルの Server {#the-low-level-server} @@ -36,18 +36,22 @@ translation: ### 試してみる {#try-it} -これには Inspector がありません。`mcp dev` と `mcp run` は `MCPServer` しか受け付けないからです。インメモリの `Client` は気にしません。`MCPServer` を受け取るのとまったく同じように、低レベルの `Server` を受け取ります。 +`mcp dev` と `mcp run` は `MCPServer` しか受け付けないので、このサーバーは自分で起動します。`server.py` の最後の行がここから普通の ASGI アプリを組み立て、uvicorn がそれを実行します。 -```python title="main.py" +```console +uvicorn server:app --port 8000 +``` + +Inspector でも、どのクライアントでも、`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 @@ asyncio.run(main()) * `result.structured_content` は `None` です。高レベルのサーバーは `-> str` を `{"result": ...}` にラップしてくれますが、ここでは自分で組み立てなかったものを誰も組み立ててくれません。 * `list_tools` は**自分で**打ち込んだスキーマを一字一句そのまま返します。高レベル版にはすべてのプロパティに `"title": "Query"` があり、ルートに `"title": "search_booksArguments"` がありました。Pydantic の産物です。この層では、通信上に現れるものはすべて自分が載せたものです。 +テストでは uvicorn もポートも省けます。`Client(server)` は `MCPServer` を受け取るのとまったく同じように、低レベルの `Server` をインプロセスで受け取ります。**[テスト](../get-started/testing.md)** がまさにそのパターンです。 + ## 何もチェックされない {#nothing-is-checked-for-you} `MCPServer` は、生成したスキーマに照らして呼び出しを検証し、関数が実行される前に不正な引数を拒否します(**[ツール](../servers/tools.md)**)。 @@ -214,4 +220,4 @@ use Server.middleware to observe or wrap initialization * `add_request_handler(method, params_type, handler)` は任意のメソッドを提供します。`initialize` は予約されています。 * `Server` が公開するケイパビリティは、どのハンドラーを登録したかから導出されます。 -`Client(server)` が両方のサーバーを同じように扱ったのは、両者がまさに同じプロトコルだからであり、それこそが要点です。さらに下の層はクラスですらありません。**[ミドルウェア](middleware.md)** です。 +クライアントが両方のサーバーを同じように扱ったのは、両者がまさに同じプロトコルだからであり、それこそが要点です。さらに下の層はクラスですらありません。**[ミドルウェア](middleware.md)** です。 diff --git a/i18n/ja/pages/advanced/pagination.md b/i18n/ja/pages/advanced/pagination.md index fb8c489026..6a2a0033a7 100644 --- a/i18n/ja/pages/advanced/pagination.md +++ b/i18n/ja/pages/advanced/pagination.md @@ -1,6 +1,6 @@ --- translation: - sections: [a9aba7a026c7bd85, ed32bda7ba9ae33a, 7e64cc5646abb91f, 22a0129ee78b3c63, d875373c06d8d2f9] + sections: [a9aba7a026c7bd85, 83e2a08b9d46a398, 9fd8a0aa384b3257, 22a0129ee78b3c63, d875373c06d8d2f9] tool: 1 --- # ページネーション {#pagination} @@ -29,9 +29,13 @@ translation: ### 試してみる {#try-it} -`Client(server)` は、`MCPServer` に接続するのとまったく同じように、低レベルの `Server` にメモリ内で接続します。 +`mcp run` は `MCPServer` しか受け付けないので、このサーバーは自分で配信します。`server.py` の最後の行で `Server` から普通の ASGI アプリを組み立てており、それを uvicorn が実行します。 -引数なしで `list_resources()` を呼び出してください。`book-1` から `book-10` までの 10 個のリソースが返り、`next_cursor` は文字列 `"10"` です。 +```console +uvicorn server:app --port 8000 +``` + +任意のクライアント(**[クライアント](../client/index.md)**、または Inspector)を `http://localhost:8000/mcp` に向け、引数なしで `list_resources()` を呼び出してください。`book-1` から `book-10` までの 10 個のリソースが返り、`next_cursor` は文字列 `"10"` です。 それを `list_resources(cursor="10")` として返すと、最初のリソースは `book-11` になり、新しい `next_cursor` は `"20"` です。 @@ -41,7 +45,7 @@ translation: `Client` のすべての `list_*` メソッド(`list_tools`、`list_resources`、`list_resource_templates`、`list_prompts`)は `cursor=` キーワードを受け取ります。ページングされた一覧をすべて取り出すには、`while True` を 1 つ書くだけです。 -```python title="client.py" hl_lines="26-32" +```python title="client.py" hl_lines="9-15" --8<-- "docs_src/pagination/tutorial002.py" ``` @@ -49,7 +53,7 @@ translation: * `next_cursor` を見る**前に** extend してください。最後のページにもリソースはあります。 * `next_cursor is None` が出口です。それ以外はそのまま、手を加えずに `cursor=` に戻します。 -その `main()` を実行すると `100 resources` と表示されます。10 件ずつの 10 ページが、10 ページあることなど知らないループによってつなぎ合わされた結果です。 +uvicorn が `server.py` を配信したままの状態で、2 つ目のターミナルで `python client.py` を実行してください。`100 resources` と表示されます。10 件ずつの 10 ページが、10 ページあることなど知らないループによってつなぎ合わされた結果です。 これは **[クライアント](../client/index.md)** がすべての `list_*` メソッドについて示しているのと同じループで、ページングしないサーバーに対してもコストはかかりません。最初のレスポンスで `next_cursor` が `None` になり、ループは 1 回だけ回ります。 diff --git a/i18n/ja/pages/client/caching.md b/i18n/ja/pages/client/caching.md index 79ffd11917..dc9914a65d 100644 --- a/i18n/ja/pages/client/caching.md +++ b/i18n/ja/pages/client/caching.md @@ -1,6 +1,6 @@ --- translation: - sections: [9e7b9a1710e5aeba, b74ca4c1d2ddddee, fa8714e61bf90c5a, 04db67a886b7271c, 857690fb8f876800] + sections: [9e7b9a1710e5aeba, 66a2e9acc9101d54, d3d25aa802f5145a, 04db67a886b7271c, 857690fb8f876800] tool: 1 --- # キャッシュヒント {#caching-hints} @@ -27,7 +27,7 @@ translation: 低レベルの `Server` では、ハンドラーが結果を手作業で組み立てます。`ttl_ms` と `cache_scope` は結果モデルの単なるフィールドです。これらを明示的に設定したハンドラーは、フィールド単位で常にコンストラクターのマップより優先されます。 -```python title="server.py" hl_lines="10 16" +```python title="server.py" hl_lines="11 17" --8<-- "docs_src/caching/tutorial002.py" ``` @@ -41,10 +41,24 @@ translation: 2026-07-28 のセッションでは、`Client` がヒントに自動で従います。組み込みのレスポンスキャッシュがあり、デフォルトで有効です。`ttlMs` を持って届いた結果は保存され、その TTL 内に同一の呼び出しがあれば、ラウンドトリップなしでキャッシュから返されます。ヒントを「持たない」結果はキャッシュされません。ヒントのない結果には `CacheConfig.default_ttl_ms` が適用され、そのデフォルトは `0`(すぐに古くなる)です。そのため、何も宣言しないサーバーには、これまでとまったく同じ呼び出しごとのトラフィックが届きます。 -```python title="client.py" hl_lines="33 35 38" +これが起こる様子を見るには、前のセクションの `server.py` を uvicorn で起動してください(最後の行が ASGI アプリを組み立てています)。ハンドラーは実際に実行されるたびに 1 行出力します。 + +```console +uvicorn server:app --port 8000 +``` + +```python title="client.py" hl_lines="20 23 28" --8<-- "docs_src/caching/tutorial003.py" ``` +2 つ目のターミナルから `python client.py` を実行してください。最初の結果が持っていたヒント、つまりハンドラーの `ttlMs` とマップの `cacheScope` が並んで出力されます。 + +```text +1000 public +``` + +残りはサーバー側のターミナルを見ればわかります。uvicorn のリクエストログの合間に、`tools/list served` が 3 回現れます。 + 呼び出し 4 回、取得 3 回です。2 回目の呼び出しは新鮮なエントリーを見つけ、サーバーに到達しませんでした。(注入した)クロックを TTL の先へ進めたことで、3 回目は再び取得しました。4 回目は `cache_mode="refresh"` を指定しています。このキーワード引数はキャッシュ対象の 5 つのメソッド(`list_tools`、`list_prompts`、`list_resources`、`list_resource_templates`、`read_resource`)にあります。 * `"use"`(デフォルト)は、新鮮なエントリーがあればそれを返し、なければ取得して保存します。 @@ -53,7 +67,7 @@ translation: `"use"` より上位にルールが 1 つあります。**`meta` を持つ呼び出しは必ずサーバーに到達します。**`meta` を設定したリクエスト(進捗トークンやトレーシング用フィールドなど)は実際のリクエスト送信を前提にしているため、`cache_mode="use"` では `"refresh"` として扱われます。キャッシュの読み込みは省略され、取得した結果は引き続きキャッシュのエントリーを置き換えます。`"bypass"` と明示的な `"refresh"` はいつもどおりに動作します。 -キャッシュを完全に無効にするには、`Client(server, cache=None)` で構築してください。すべての呼び出しが再びラウンドトリップになり、`cache_mode` は受け付けられるものの何もしません。 +キャッシュを完全に無効にするには、`Client` の構築時に `cache=None` を渡してください。すべての呼び出しが再びラウンドトリップになり、`cache_mode` は受け付けられるものの何もしません。 スコープも自動的に尊重されます。`"private"` のエントリーはキャッシュの「パーティション」(後述)をキーにし、`"public"` のエントリーはより広い共有を選べます。そして、名指しされたエントリーについては**通知が TTL に勝ちます**。`list_changed` 通知は対応するキャッシュ済みの一覧を破棄し、`resources/updated` はその URI と完全に一致するキーで保存されたキャッシュ済みの読み込み結果を、どれだけ新鮮でも破棄します。2026-07-28 の接続では、これらの通知は `client.listen(...)` で開く `subscriptions/listen` ストリームに届き、破棄はウォッチャーがイベントを見る前に完了します。詳しくは **[サブスクリプション](subscriptions.md)** を参照してください。 diff --git a/i18n/ja/pages/client/callbacks.md b/i18n/ja/pages/client/callbacks.md index bcf4c266a1..2ad7175985 100644 --- a/i18n/ja/pages/client/callbacks.md +++ b/i18n/ja/pages/client/callbacks.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} @@ -58,7 +58,7 @@ result.content # [TextContent(type='text', text='Card issued to Ada Lovelace.') こちらから `tools/call` が 1 回、サーバーからの折り返しの `elicitation/create` が 1 回、それに答えるのがこちらの関数です。すべてが 1 回のツール呼び出しの中で完結します。 !!! info - `Client(...)` の呼び出しにある `mode="legacy"` は、実際に働いています。デフォルトでは `Client(...)` は新しいプロトコルの経路をネゴシエートしますが、その経路にはサーバーからクライアントへのリクエストのためのバックチャネル(back-channel)がありません。コールバックが動く前に `ctx.elicit` が失敗します。これを決めるのはトランスポートではなく、ネゴシエートされたプロトコルです。インメモリでも URL 越しでも同じです。クライアントがこうしたリクエストに答える必要があるときは、必ず `mode="legacy"` を指定してください。このページの裏にあるテストはすべてそうしています。詳しくは **[プロトコルバージョン](../protocol-versions.md)** を参照してください。 + `Client(...)` の呼び出しにある `mode="legacy"` は、実際に働いています。デフォルトでは `Client(...)` は新しいプロトコルの経路をネゴシエートしますが、その経路にはサーバーからクライアントへのリクエストのためのバックチャネル(back-channel)がありません。コールバックが動く前に `ctx.elicit` が失敗します。これを決めるのはトランスポートではなく、ネゴシエートされたプロトコルです。クライアントがこうしたリクエストに答える必要があるときは、必ず `mode="legacy"` を指定してください。このページの裏にあるテストはすべてそうしています。詳しくは **[プロトコルバージョン](../protocol-versions.md)** を参照してください。 2026-07-28 のセッションでもコールバックが使われなくなるわけではなく、呼ばれ方が変わります。ツールが `ElicitRequest` を含む `InputRequiredResult` を返すと、`Client` はそのエントリを同じ `elicitation_callback` に振り分け、呼び出しを再試行してくれます。この流れは **[マルチラウンドトリップ(multi-round-trip)リクエスト](../handlers/multi-round-trip.md)** で説明しています。 @@ -139,4 +139,4 @@ result.structured_content # {'result': ['elicitation']} * `sampling_callback` と `list_roots_callback` も同じように動きますが、非推奨の機能のためのものです。新しいサーバーは代わりにマルチラウンドトリップリクエストを使います。 * `logging_callback` と `message_handler` は通知を受け取ります。何も宣言しません。 -`Client(...)` の第 1 引数はトランスポートのオブジェクトです。すべての種類は **[クライアントのトランスポート](transports.md)** で扱っています。 +`Client(...)` の第 1 引数でトランスポートが決まります。すべての種類は **[クライアントのトランスポート](transports.md)** で扱っています。 diff --git a/i18n/ja/pages/client/index.md b/i18n/ja/pages/client/index.md index 7ca4241626..7e53cc2050 100644 --- a/i18n/ja/pages/client/index.md +++ b/i18n/ja/pages/client/index.md @@ -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 --- # Client {#the-client} @@ -11,13 +11,23 @@ translation: ## 最初のクライアント {#your-first-client} -```python title="client.py" hl_lines="14-18" +クライアントには、対話する相手のサーバーが必要です。このページのすべてのスニペットが接続するのが、この Bookshop です。`server.py` として保存し、HTTP で起動したままにしてください。 + +```python title="server.py" --8<-- "docs_src/client/tutorial001.py" ``` -冒頭のサーバーは、接続先を用意するためだけにあります。クライアントはハイライトされた 5 行です。 +```console +uv run mcp run server.py --transport streamable-http +``` + +これでサーバーは `http://localhost:8000/mcp` で提供されます。クライアントは別のプログラムです。`client.py` として保存し、2 つ目のターミナルで `python client.py` を実行してください。 -* `Client(mcp)` には**サーバーオブジェクトそのもの**を渡しています。これがインメモリのトランスポートです。サブプロセスもポートも HTTP もありません。このページのすべての例、そして作成するすべてのテストが、この方法で接続します。 +```python title="client.py" hl_lines="7-11" +--8<-- "docs_src/client/tutorial001_client.py" +``` + +* `Client("http://localhost:8000/mcp")` には **URL** を渡しているので、いま起動したサーバーに Streamable HTTP で接続します。 * `async with` が**ライフサイクル**です。入ると接続してネゴシエーションを行い、出ると切断します。`connect()` / `close()` のペアはなく、ブロックが終わった後の `Client` は再利用できません。 * ブロックの中では、接続に関する情報がすでに通常のプロパティとして揃っています。 @@ -25,10 +35,10 @@ translation: `Client` は位置引数を 1 つ取り、その型からトランスポートを決定します。 -* `MCPServer`(または低レベルの `Server`)のインスタンス:**プロセス内**で接続します。 -* URL 文字列(`Client("http://localhost:8000/mcp")`):Streamable HTTP。本番向けの経路です。 -* `StdioServerParameters`:**サブプロセス**として起動するコマンドで、その stdin と stdout を通じて対話します。 +* URL 文字列(`Client("http://localhost:8000/mcp")`):Streamable HTTP。デプロイで使うトランスポートです。 +* `StdioServerParameters`:ローカルの**サブプロセス**として起動するコマンドで、その stdin と stdout を通じて対話します。 * **トランスポート**:`async with ... as (read, write)` できるものなら何でも。たとえば、自分の HTTP クライアントをラップする `streamable_http_client(url, http_client=...)` です。 +* `MCPServer`(または低レベルの `Server`)のインスタンス:**プロセス内**で接続します。サブプロセスもポートもありません。これはテスト向けで、**[テスト](../get-started/testing.md)** がこれを土台にしています。 このページの残りの内容は、4 つのどれでも同じです。ヘッダー、サブプロセス、タイムアウト、そして `Transport` プロトコルについては、専用のページ **[クライアントのトランスポート](transports.md)** があります。 @@ -48,11 +58,11 @@ translation: ## ツールの一覧取得 {#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()` は `ListToolsResult` を返し、ツールは `.tools` に入っています。それぞれが、ホストがモデルに渡す完全な定義です。 +`list_tools()` は `ListToolsResult` を返し、ツールは `.tools` に入っています。それぞれが、ホストがモデルに渡す完全な定義です。最初のツールは次のとおりです。 ```python tool.name # 'search_books' @@ -76,6 +86,8 @@ tool.description # 'Search the catalog by title or author.' このスキーマには、UI が引数フォームを描画するのに必要なものも、モデルが有効な引数を生成するのに必要なものも、すべて含まれています。 +2 つ目のツール `lookup_book` は `title=` なしで登録されているので、その `tool.title` は `None` です。 + !!! tip `title` は省略可能なので、人間にツールを見せる UI はどちらかを選ぶ必要があります。`title` があればそれを、なければ `name` を使います。`from mcp.shared.metadata_utils import get_display_name` がまさにそれを行い、ツール、リソース、リソーステンプレート、プロンプトに対応しています。 @@ -83,7 +95,7 @@ tool.description # 'Search the catalog by title or author.' `call_tool(name, arguments)` はツールを実行し、`CallToolResult` を返します。 -```python title="client.py" hl_lines="27-34" +```python title="client.py" hl_lines="9-16" --8<-- "docs_src/client/tutorial003.py" ``` @@ -131,7 +143,7 @@ result.is_error # False リソースの動詞は組になっています。一覧取得が 2 通り、読み取りが 1 通りです。 -```python title="client.py" hl_lines="22-31" +```python title="client.py" hl_lines="9-18" --8<-- "docs_src/client/tutorial004.py" ``` @@ -141,11 +153,11 @@ result.is_error # False `read_resource` は `contents` を返します。これは `TextResourceContents` または `BlobResourceContents` のリストです。考え方はツールのコンテンツと同じで、`isinstance` で絞り込んでから `.text`(または `.blob`)を読みます。 -クライアントは、リソースが変更されたときに通知を受けることもできます。2025 年世代の接続では `subscribe_resource(uri)` / `unsubscribe_resource(uri)` がそれにあたります。ただしこのメソッドのペアは `MCPServer` が実装していないため、2026-07-28 の通信上(これらの動詞はもう存在しません)ではリクエストに `-32601`、*Method not found* が返ります。2026 年の代替は `subscriptions/listen` ストリームで、こちらは `MCPServer` が実際に提供しています(そこでは `server_capabilities.resources.subscribe` が `True` です)。これを `client.listen(...)` で消費する方法は、このセクションの **[サブスクリプション](subscriptions.md)** のページで説明しています。 +クライアントは、リソースが変更されたときに通知を受けることもできます。2025 年世代の接続では `subscribe_resource(uri)` / `unsubscribe_resource(uri)` がそれにあたります。このメソッドのペアは `MCPServer` が実装していないため、2026-07-28 の通信上(これらの動詞はもう存在しません)ではリクエストに `-32601`、*Method not found* が返ります。2026 年の代替は `subscriptions/listen` ストリームで、こちらは `MCPServer` が実際に提供しています(そこでは `server_capabilities.resources.subscribe` が `True` です)。これを `client.listen(...)` で消費する方法は、このセクションの **[サブスクリプション](subscriptions.md)** のページで説明しています。 ## プロンプト {#prompts} -```python title="client.py" hl_lines="15-20" +```python title="client.py" hl_lines="8-13" --8<-- "docs_src/client/tutorial005.py" ``` @@ -170,7 +182,7 @@ message.content # TextContent(type='text', text='Recommend one poetry book from 補完ハンドラーを持つサーバーは、ユーザーの入力に合わせてプロンプトやリソーステンプレートの引数を自動補完できます。 -```python title="client.py" hl_lines="27-31" +```python title="client.py" hl_lines="9-13" --8<-- "docs_src/client/tutorial006.py" ``` @@ -183,21 +195,21 @@ message.content # TextContent(type='text', text='Recommend one poetry book from `list_*` メソッドはどれも `cursor=` キーワードを取り、結果はどれも `next_cursor` を持ちます。`next_cursor` が `None` なら、すべて取得済みです。 -```python title="client.py" hl_lines="22-30" +```python title="client.py" hl_lines="7-15" --8<-- "docs_src/client/tutorial007.py" ``` -このループはどのサーバーに対しても正しく動きます。`MCPServer` はすべてを 1 ページで返すので、`next_cursor` は `None` になり、ループは 1 回だけ実行されます。ほとんどのコードがこのループを書かないのはそのためです。実際にページ分割するサーバーと、カーソルが従うルールについては **[ページネーション](../advanced/pagination.md)** を参照してください。 +`list_all_tools` はどのサーバーに対しても正しく動きます。`MCPServer` はすべてを 1 ページで返すので、`next_cursor` は `None` になり、ループは 1 回だけ実行されます。ほとんどのコードがこのループを書かないのはそのためです。実際にページ分割するサーバーと、カーソルが従うルールについては **[ページネーション](../advanced/pagination.md)** を参照してください。 ## テストでの利用 {#in-tests} -プロセスもポートも使わない `Client(mcp)` は、それだけでサーバーのテストハーネスになります。 +このページの `client.py` はどれも、HTTP 経由で `server.py` に到達していました。テストではネットワークを省き、`Client` にサーバーオブジェクトそのものを渡します。`from server import mcp` としてから `Client(mcp)` です。プロセスもポートも不要で、上記のメソッドはどれも同じように動きます。 -そのために用意されたコンストラクターのフラグが 1 つあります。`Client(mcp, raise_exceptions=True)` です。効果があるのはインメモリ接続のときだけで、その説明と、それを中心にしたパターン全体の組み立ては **[テスト](../get-started/testing.md)** のページにあります。 +そのために用意されたコンストラクターのフラグが 1 つあります。`Client(mcp, raise_exceptions=True)` です。効果があるのはプロセス内接続のときだけで、その説明と、それを中心にしたパターン全体の組み立ては **[テスト](../get-started/testing.md)** のページにあります。 ## まとめ {#recap} -* `Client(x)` は、サーバーオブジェクトにはインメモリで、URL 文字列には Streamable HTTP で、それ以外にはトランスポート経由で接続します。 +* `Client(x)` は、URL 文字列には Streamable HTTP で接続し、`StdioServerParameters` にはサブプロセスを起動し、トランスポートには直接入り、テストではサーバーオブジェクトそのものを受け取ります。 * `async with` がライフサイクルのすべてです。その中では `server_capabilities` と `protocol_version` にすでに値が入っており、サーバーが提供していれば `server_info` と `instructions` も同様です。 * `list_tools()` で各ツールの `name`、`title`、`description`、`input_schema` が得られます。 * `call_tool()` はモデル向けの `content`、コード向けの `structured_content`、そして `is_error` を返します。例外を送出するツールは、例外ではなく結果として返ってきます。 diff --git a/i18n/ja/pages/client/oauth-clients.md b/i18n/ja/pages/client/oauth-clients.md index e71580ae43..fe05023d58 100644 --- a/i18n/ja/pages/client/oauth-clients.md +++ b/i18n/ja/pages/client/oauth-clients.md @@ -1,6 +1,6 @@ --- translation: - sections: [c6899d3892bd9fa0, 79372cff3cc48a88, 63878d29e87c3e73, 13175843d3588af4, e7e2b9fd516f77de, 758f06399b513c1f, a05d7278487d610b] + sections: [c6899d3892bd9fa0, 79372cff3cc48a88, c2dae1ebe2ebd543, 13175843d3588af4, df06056fb16b3846, 758f06399b513c1f, a05d7278487d610b] tool: 1 --- # OAuth クライアント {#oauth-clients} @@ -77,18 +77,20 @@ translation: `Client` が初めてリクエストを送ると、サーバーは `401` を返します。そこからプロバイダーが引き継ぎます。 -1. **発見。** `WWW-Authenticate` ヘッダーを読み、サーバーの Protected Resource Metadata を `/.well-known/oauth-protected-resource` から取得します。そこからこのリソースを保護している認可サーバーを知り、「その」サーバーのメタデータを取得します。 +1. **発見。** `WWW-Authenticate` ヘッダーを読み、サーバーの Protected Resource Metadata を `/.well-known/oauth-protected-resource` から取得します。そこからこのリソースを保護している認可サーバーを知り、「その」サーバーのメタデータを取得します。(リソースメタデータを公開していない古いサーバーには、代わりにそのサーバー自身のオリジンで認可サーバーのメタデータを問い合わせます。)どちらの場合も、メタデータは取得対象のサーバーを `issuer` として名指ししていなければなりません。それ以外は拒否されます。 2. **登録。** ストレージに何もなければ、`OAuthClientMetadata` を使って動的に登録し、結果を保存します。 3. **認可。** PKCE のペアと `state` を生成し、認可 URL を組み立て、`redirect_handler` を await します。続いて、コードを受け取るために `callback_handler` を await します。 4. **交換。** コードを `OAuthToken` と引き換えて保存し、元のリクエストを `Authorization: Bearer ...` 付きで再送します。 それ以降は静かになります。トークンはストレージから取り出され、期限切れのアクセストークンはリフレッシュトークンで更新されます。そのどれもうまくいかないときだけ、フローをもう一度実行します。 +これらのリクエストすべてに共通するトランスポートのルールが 1 つあります。外側の MCP リクエストと同じく、リダイレクトに従うのは同じオリジンにとどまり、かつメソッドを保つ場合(たとえば末尾スラッシュの 307/308)だけです。それ以外のリダイレクトは、その URL が応答しなかったものとして扱います。 + これらを自分で書く必要はまったくありませんでした。残るキーワード引数は 2 つ(`client_metadata_url` と `validate_resource_url`)で、このファイルではどちらも不要です。知っておく価値があるのは `client_metadata_url` のほうで、下に専用のセクションがあります。 ### 試してみる {#try-it} -このドキュメントの例のほとんどは、インメモリの `Client(server)` で確認できます。これは違います。このフローの要点は HTTP の `401` であり、インメモリのクライアントとサーバーのあいだには HTTP がありません。 +テストで使うインメモリの `Client(server)` は、ここでは役に立ちません。このフローの要点は HTTP の `401` であり、インメモリのクライアントとそのサーバーのあいだには HTTP がないからです。 リポジトリには実際に動くバージョンが同梱されています。`examples/servers/simple-auth/` はスタンドアロンの認可サーバーと保護された MCP サーバーを動かし、`examples/clients/simple-auth-client/` はこのページのクライアントを小さな CLI に育てたものです。その README に 2 つのコマンドが載っています。サーバーを起動し、それに対してクライアントを実行すれば、4 つのステップが進んでいくのを見られます。 @@ -106,13 +108,14 @@ URL は HTTPS で、ルート以外のパスを持っている必要がありま `ClientCredentialsOAuthProvider` は同じ `httpx2.Auth` で、人間がいないだけです。 -```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" ``` 変わった点は次のとおりです。 * `OAuthClientMetadata` もハンドラーもありません。`client_id` と `client_secret` を渡すと、プロバイダーはそれらを中心に最小限の `client_credentials` 登録を組み立て、動的登録を完全に省きます。 +* `issuer` には、そのクレデンシャルを発行した認可サーバーを指定します。そのサーバーの `/.well-known/oauth-authorization-server` ドキュメントが返す `issuer` の値を使ってください。発見は上と同じように実行されますが、トークンリクエストは「その」発行者のメタデータからしか組み立てられません。MCP サーバーがそれ以外の場所を指していれば、フローは代わりに `OAuthFlowError` で止まります。省略するのは非推奨で、3.0 では必須になります(**[非推奨の機能](../deprecated.md#deprecated-sdk-helpers)** を参照してください)。それまでのあいだ、プロバイダーは警告を出したうえで、発見で見つかった認可サーバーをそのまま使います。 * `scope` はスペース区切りの文字列で、OAuth の通信上の形式です。 * その先はすべて同じです。同じ `TokenStorage`、同じ `httpx2.AsyncClient(auth=...)`、同じ `streamable_http_client` です。 @@ -123,7 +126,7 @@ URL は HTTPS で、ルート以外のパスを持っている必要がありま !!! info `mcp.client.auth.extensions.client_credentials` にはもう 1 つプロバイダーがあります。 - **`PrivateKeyJWTOAuthProvider`** は、共有シークレットの代わりに JWT で認証するクライアント向けです(`private_key_jwt`、つまり鍵ペアやワークロードアイデンティティの方式)。パターンは同じで、1 つ構築して `auth=` に載せます。同じモジュールには、そのアサーションを組み立てる 2 つのヘルパー、`SignedJWTParameters` と `static_assertion_provider` も含まれています。 + **`PrivateKeyJWTOAuthProvider`** は、共有シークレットの代わりに JWT で認証するクライアント向けです(`private_key_jwt`、つまり鍵ペアやワークロードアイデンティティの方式)。パターンは同じで、1 つ構築して(同じく省略可能な `issuer` を取ります)`auth=` に載せます。同じモジュールには、そのアサーションを組み立てる 2 つのヘルパー、`SignedJWTParameters` と `static_assertion_provider` も含まれています。 人間がいない状況はもう 1 つあります。クライアントが企業に属していて、どの MCP サーバーに到達してよいかをユーザーではなくその企業のアイデンティティプロバイダーが決める場合です。これは独自の信頼モデルを持つ別のグラントで、専用のページ **[アイデンティティアサーション](identity-assertion.md)** があります。 diff --git a/i18n/ja/pages/client/session-groups.md b/i18n/ja/pages/client/session-groups.md index 88c62acb0b..af1c47679a 100644 --- a/i18n/ja/pages/client/session-groups.md +++ b/i18n/ja/pages/client/session-groups.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-groups} @@ -76,7 +76,7 @@ translation: ## まとめ {#recap} * `ClientSessionGroup` は多数のサーバー接続を保持し、それらのツール、リソース、プロンプトをそれぞれ 1 つの `dict` にまとめます。 -* サーバーごとに `connect_to_server(params)` を呼びます。受け取るのはトランスポートのパラメーターであり、`Client` が受け取るサーバーオブジェクトや URL ではありません。 +* サーバーごとに `connect_to_server(params)` を呼びます。受け取るのはトランスポートのパラメーターであり、`Client` が受け取る URL や `Transport` ではありません。 * `group.call_tool(name, arguments)` は、所有するサーバーへのルーティングを代わりに行います。 * 名前はグループ全体で一意でなければなりません。`search` ツールを持つ 2 つのサーバーは、そのままでは共存できません。 * `component_name_hook=` は登録されるすべての名前を書き換えます。dict のキーは変わりますが、実際に送信される名前は変わりません。 diff --git a/i18n/ja/pages/client/transports.md b/i18n/ja/pages/client/transports.md index 4c61766a1c..409d491633 100644 --- a/i18n/ja/pages/client/transports.md +++ b/i18n/ja/pages/client/transports.md @@ -1,6 +1,6 @@ --- translation: - sections: [9cac816674181eb0, 0700f337babcd4dd, 2bde0dd58cdf00f5, 40b4916d82eaf1d4, 3d0832f39b0d7059, dfa4446556badef0, 5bd93be2ab2ecb9c] + sections: [9cac816674181eb0, 7c157764133fea1f, 40b4916d82eaf1d4, 10d151f2cc75317f, 3d0832f39b0d7059, 92742ba36533633d, 0aeca6145e7bd302] tool: 1 --- # クライアントのトランスポート {#client-transports} @@ -11,30 +11,15 @@ translation: それぞれの「サーバー」側(`mcp.run()` が何をするのか、何をデプロイするのか)については、**[サーバーの実行](../run/index.md)** を参照してください。 -## インメモリ {#in-memory} - -サーバーオブジェクトそのものを渡します。 - -```python title="client.py" hl_lines="14" ---8<-- "docs_src/client_transports/tutorial001.py" -``` - -サブプロセスも、ポートも、通信路を流れるバイト列もありません。クライアントとサーバーは同じプロセス内の 2 つのオブジェクトですが、呼び出しは本物のプロトコル層を通ります。`search_books` は、HTTP 越しの場合とまったく同じように一覧に載り、検証され、呼び出されます。 - -そのため、これは同時に 2 つの役割を果たします。 - -* **テストハーネス。** このドキュメントの例はすべてこの方法で実行されており、**[テスト](../get-started/testing.md)** のページはこのパターンを中心に組み立てられています。 -* **組み込み用の API。** サーバーを自分で構築するアプリケーションなら、そのツールを呼び出すのにネットワーク越しの経路は必要ありません。 - ## Streamable HTTP {#streamable-http} -URL の文字列を渡すと **Streamable HTTP** になります。デプロイ時に使うトランスポートです。 +URL の文字列を渡すと **Streamable HTTP** になります。デプロイ時に使うトランスポートであり、最初に選ぶべきトランスポートです。 ```python title="client.py" hl_lines="5" --8<-- "docs_src/client_transports/tutorial002.py" ``` -本番用のクライアントはこれですべてです。`Client` は URL を `streamable_http_client(...)` で包み、MCP に必要な設定を施した `httpx2.AsyncClient` の上に載せてくれます。具体的には `follow_redirects=True`、connect/write/pool のタイムアウトが 30 秒、そしてサーバーがレスポンスストリームを開いたままにすることがあるため read のタイムアウトが 300 秒です。 +本番用のクライアントはこれですべてです。`Client` は URL を `streamable_http_client(...)` で包み、MCP に必要な設定を施した `httpx2.AsyncClient` の上に載せてくれます。具体的には、connect/write/pool のタイムアウトが 30 秒、そしてサーバーがレスポンスストリームを開いたままにすることがあるため read のタイムアウトが 300 秒です。 !!! check 構築しただけの `Client` は接続されて**いません**。構築時に行われるのはトランスポートの選択だけで、実際に開くのは `async with` です。入る前に接続に手を伸ばすと、SDK がそのことを教えてくれます。 @@ -49,7 +34,7 @@ URL の文字列を渡すと **Streamable HTTP** になります。デプロイ `Authorization` ヘッダー、Cookie、プロキシ、mTLS、あるいは別のタイムアウトが必要になったら、`httpx2.AsyncClient` を自分で組み立てて `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" ``` @@ -70,7 +55,25 @@ TLS について 1 点。`httpx2` は、同梱の CA リストではなく、オ HTTP に関わるものはすべて、渡す 1 つの `httpx2.AsyncClient` に集約されています。 !!! info - `httpx2` はおなじみの `httpx` の API をそのまま保っているので、`httpx` を知っていれば、認証、プロキシ、イベントフック、リトライ、接続数の制限のやり方はすでに知っていることになります。SDK はその上に何も足さず、何も引きません。OAuth が差し込まれるのもここです。`httpx2.AsyncClient(auth=OAuthClientProvider(...))` のように書きます。そのフロー全体については **[OAuth クライアント](oauth-clients.md)** を参照してください。 + `httpx2` はおなじみの `httpx` の API をそのまま保っているので、`httpx` を知っていれば、認証、プロキシ、イベントフック、リトライ、接続数の制限のやり方はすでに知っていることになります。SDK はその上に何も足さず、何も引きません。唯一の例外が[リダイレクトの扱い](#redirects)です。OAuth が差し込まれるのもここです。`httpx2.AsyncClient(auth=OAuthClientProvider(...))` のように書きます。そのフロー全体については **[OAuth クライアント](oauth-clients.md)** を参照してください。 + +### リダイレクト {#redirects} + +トランスポートは渡された URL に接続し、そのオリジンにだけ接続します。 + +* 同じスキーム、ホスト、ポートにとどまる `307`/`308` のリダイレクトには従います。同じホスト上での `http://` → `https://` も同様です。よくある `/mcp` → `/mcp/` という末尾スラッシュのリダイレクトはこれでカバーされます。 +* それ以外の場所へのリダイレクトには従い**ません**。呼び出しは次のエラーで失敗します。 + + ```text + MCPError: Redirect to https://other.example.com/mcp not followed; use that URL as the endpoint if it is the intended server + ``` + + その URL が意図したサーバーなら、設定にその URL を書いてください。そうでなければ、サーバーか、その前段にあるプロキシの設定が誤っています。 + +これは、渡すどの `httpx2.AsyncClient` にも当てはまります。その `follow_redirects` の設定は、MCP のリクエストについてはどちらの方向にも参照されません。SDK の OAuth プロバイダーも、自身のリクエストに同じ規則を適用します。 + +!!! tip + `Redirect to http://… not followed: it would downgrade this HTTPS endpoint to plain HTTP` は、サーバーが自身の知らない TLS 終端プロキシの背後にあり、`http://` のリダイレクトを発行していることを意味します。これはサーバー側で直すか(**[デプロイとスケール](../run/deploy.md#behind-a-tls-terminating-proxy)**)、メッセージが示すとおりの正確な `https://…/` の URL を使うことで解決します。 ## stdio {#stdio} @@ -91,6 +94,18 @@ TLS について 1 点。`httpx2` は、同梱の CA リストではなく、オ API キーを必要とするサーバーは、そこでキーを見つけられません。`env=` で明示的に渡してください。それらの変数は許可リストの上にマージされます。上の例で `BOOKSHOP_API_KEY` がしているのがまさにそれです。 +## インメモリ {#in-memory} + +テストでは、デプロイするものも起動するものもありません。サーバーオブジェクトそのものを渡します。 + +```python hl_lines="14" +--8<-- "docs_src/client_transports/tutorial001.py" +``` + +サブプロセスも、ポートも、通信路を流れるバイト列もありません。クライアントとサーバーは同じプロセス内の 2 つのオブジェクトですが、呼び出しは本物のプロトコル層を通ります。`search_books` は、HTTP 越しの場合とまったく同じように一覧に載り、検証され、呼び出されます。**[テスト](../get-started/testing.md)** のページは、このパターンを中心に組み立てられています。 + +同じ形は組み込み用の API としても使えます。サーバーを自分で構築するアプリケーションなら、ネットワーク越しの経路なしにそのツールを呼び出せます。 + ## SSE {#sse} `mcp.client.sse` の `sse_client(url)` は、Streamable HTTP に取って代わられた HTTP トランスポートです。まだこれを話すサーバーと対話するには、同じように `Client(sse_client("http://localhost:8000/sse"))` と包みます。そして、新しいものをこの上に作らないでください。 @@ -99,15 +114,16 @@ TLS について 1 点。`httpx2` は、同梱の CA リストではなく、オ `Client` から見れば、上記はすべて同じものです。 -**トランスポート**とは、`(read, write)` というメッセージストリームのペアを yield する非同期コンテキストマネージャーのことです。正式には `mcp.client` の `Transport` プロトコルです。`Client` は引数を型で解決します。サーバーオブジェクトならインプロセスで接続し、`str` なら `streamable_http_client(url)` になり、`StdioServerParameters` なら `stdio_client(params)` になり、それ以外は直接トランスポートとして入ります。この最後の規則があるからこそ、`stdio_client(...)`、`streamable_http_client(...)`、`sse_client(...)` はすべて同じ場所に収まり、自分で独自のものを書くこともできます。 +**トランスポート**とは、`(read, write)` というメッセージストリームのペアを yield する非同期コンテキストマネージャーのことです。正式には `mcp.client` の `Transport` プロトコルです。`Client` は引数を型で解決します。`str` なら `streamable_http_client(url)` になり、`StdioServerParameters` なら `stdio_client(params)` になり、サーバーオブジェクトならインプロセスで接続し、それ以外は直接トランスポートとして入ります。この最後の規則があるからこそ、`stdio_client(...)`、`streamable_http_client(...)`、`sse_client(...)` はすべて同じ場所に収まり、自分で独自のものを書くこともできます。 ## まとめ {#recap} -* `Client(mcp)`(サーバーオブジェクト)はインメモリで接続します。テストと組み込みに使ってください。 * `Client("http://.../mcp")`(URL)は、本番用のトランスポートである Streamable HTTP で接続します。 * ヘッダー、認証、プロキシ、タイムアウトは、`streamable_http_client(url, http_client=...)` に渡す `httpx2.AsyncClient` に設定します。`headers=` キーワードはありません。 +* リダイレクトに従うのは、URL 自身のオリジン内(末尾スラッシュの `307`/`308`)と、同じホスト上の `http`→`https` だけです。それ以外は `Redirect to … not followed` で失敗します。最終的な URL を設定してください。 * stdio は `Client(StdioServerParameters(...))` です。自分で `stdio_client(...)` に包むのは、子プロセスの stderr をリダイレクトしたいときだけです。 * サブプロセスが受け取るのは自分の環境ではなく、許可リストに基づく環境です。`env=` でそこに追加します。 +* `Client(mcp)`(サーバーオブジェクト)はインメモリで接続します。テストで使うか、サーバーを構築したアプリケーションにそのサーバーを組み込むために使ってください。 * トランスポートとは、`async with x as (read, write)` と書けるものすべてです。`Client` は、サーバーオブジェクトでも URL でも `StdioServerParameters` でもないものを、そのままこのプロトコルに渡します。 * `Client` の構築でトランスポートが選ばれ、`async with` でそれが開かれます。 diff --git a/i18n/ja/pages/deprecated.md b/i18n/ja/pages/deprecated.md index 8754ecc85d..15d9942f80 100644 --- a/i18n/ja/pages/deprecated.md +++ b/i18n/ja/pages/deprecated.md @@ -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 --- # 非推奨の機能 {#deprecated-features} -2026-07-28 の仕様では、5 つのものが役目を終えます。SDK は今もその 5 つすべてを実装しており、そのすべてに**非推奨の警告**が付くようになりました。SDK のヘルパーが 1 つ、仕様とは別の理由で非推奨になっており、[ページの最後](#deprecated-sdk-helpers)に挙げています。 +2026-07-28 の仕様では、5 つのものが役目を終えます。SDK は今もその 5 つすべてを実装しており、そのすべてに**非推奨の警告**が付くようになりました。これとは別に、SDK 独自の理由で非推奨になったものもいくつかあり、[ページの最後](#deprecated-sdk-helpers)に挙げています。 下の表は、非推奨になった機能それぞれについて、なくなる理由と、代わりに土台にすべきものを挙げています。 @@ -125,11 +125,13 @@ API はこれだけです。メソッドごとのスイッチはありません ## 非推奨の SDK ヘルパー {#deprecated-sdk-helpers} -これらは仕様の変更ではなく、よりよい代替がある SDK の内部実装にすぎません。同じ `MCPDeprecationWarning` で警告し、3.0 で削除されます。 +これらは仕様の変更ではなく、よりよい代替がある SDK の使い方にすぎません。同じ `MCPDeprecationWarning` で警告し、3.0 で古い形式が削除されます。 | 非推奨 | 代わりにすること | |---|---| | `FuncMetadata.call_fn_with_arg_validation()` | `FuncMetadata.validate_arguments()` を呼んでから `FuncMetadata.call_fn()` を呼びます。これを呼んでいたのは、`FuncMetadata` を直接扱うコード(たとえば独自の `Tool` サブクラス)だけです。 | +| `validate_token_resource=` を指定しない `AuthSettings(resource_server_url=...)` | 指定してください。`True` にすると、ベリファイアーが `resource_server_url` 向けに発行されたと報告しないベアラートークンをサーバーが拒否します。`False` は、ベリファイアーがトークンのオーディエンスを自分で検証することを表します(**[認可](run/authorization.md#a-token-verifier)** を参照)。未指定の場合は `False` として振る舞います。3.0 では、`resource_server_url` が設定されているときは常に `True` がデフォルトになります。 | +| `issuer=` を指定しない `ClientCredentialsOAuthProvider(...)` または `PrivateKeyJWTOAuthProvider(...)` | 資格情報を発行した認可サーバーを指す `issuer=` を渡してください(**[OAuth クライアントの作成](client/oauth-clients.md#machine-to-machine)** を参照)。指定しないと、どの認可サーバーが資格情報を受け取るかを MCP サーバーが決めることになります。3.0 ではこのキーワードが必須になります。 | ## まとめ {#recap} @@ -138,7 +140,7 @@ API はこれだけです。メソッドごとのスイッチはありません * 非推奨は勧告にすぎません。通信上の変更はなく、2026 年より前のセッションに対してはすべてが引き続き動作します。そして目に見える `MCPDeprecationWarning` が出ます(`UserWarning` なので、デフォルトで有効です)。 * サンプリングとルートにはさらに、2026-07-28 のセッションにはないバックチャネルが必要です。現行仕様の接続では警告を出し、そのあと例外を送出します。 * `warnings.filterwarnings("ignore", category=MCPDeprecationWarning)` でカテゴリ全体を黙らせます。pytest で `"error::mcp.MCPDeprecationWarning"` を指定すれば、テストの失敗に変わります。 -* SDK のヘルパー `FuncMetadata.call_fn_with_arg_validation()` は、これとは別に非推奨になっており、3.0 で削除されます。 +* [SDK 独自の非推奨](#deprecated-sdk-helpers)も同じルールに従います。今は警告を出し、3.0 で古い形式がなくなります。 * 新しいコードは、これらのどれの上にも築くべきではありません。 このドキュメントのほかのページはすべて、現行の API を扱っています。 diff --git a/i18n/ja/pages/get-started/first-steps.md b/i18n/ja/pages/get-started/first-steps.md index 01798d6d41..d12904a72d 100644 --- a/i18n/ja/pages/get-started/first-steps.md +++ b/i18n/ja/pages/get-started/first-steps.md @@ -1,6 +1,6 @@ --- translation: - sections: [0d6c05bcbf836bf3, 59a7b14eeefc68c1, 7114d8d6daba203f, e8bbb56a98ba7bc9, 5138010f6159901c, f78da7c7c363d4c6, 220a939cab348686] + sections: [0d6c05bcbf836bf3, 9a78b5f6b44b18ab, 7114d8d6daba203f, e8bbb56a98ba7bc9, bfd2fd1153e71dac, 1615a994ef071fdd, 65c599fae991f245] tool: 1 --- # 最初のステップ {#first-steps} @@ -17,7 +17,7 @@ translation: * **クライアント**はホストの中にあり、MCP を話します。ホストは、接続するサーバーごとにクライアントを 1 つずつ動かします。 * **サーバー**は、この SDK で作るものです。クライアントに対して何かを公開します。モデルと直接やり取りすることは決してありません。 -自分で書くのはサーバーです。ホストは別の誰かが作る製品です。SDK には `Client` も用意されています。サーバーのテストに使うもので、このページの後半にも登場します。 +自分で書くのはサーバーです。ホストは別の誰かが作る製品です。SDK には `Client` も用意されています。ホストが URL でサーバーに接続したり、サブプロセスとして起動したりするときに使うのと同じクラスです。このページの後半に登場しますし、サーバーをテストする手段にもなります。 ## 3 つのプリミティブ {#the-three-primitives} @@ -79,22 +79,20 @@ Inspector にはタブが 3 つありました。3 つあると、どうやっ クライアントが接続すると、サーバーは自身の**ケイパビリティ**を宣言します。どの系統のリクエストに応答するか、ということです。クライアントはこの宣言をもとに、そもそも何を要求するかを決めます。この宣言を自分で書いてはいません。`MCPServer` が代わりに宣言します。 -自分の目で確かめてみましょう。SDK の `Client` はサーバーオブジェクトをそのまま受け取り、**インメモリ**で接続します(サブプロセスもポートも使いません)。 - -```python -import asyncio - -from mcp import Client - -from server import mcp +自分の目で確かめてみましょう。1 つのターミナルで `server.py` を HTTP で実行したままにしておきます。 +```console +uv run mcp run server.py --transport streamable-http +``` -async def main() -> None: - async with Client(mcp) as client: - print(client.server_capabilities.model_dump(exclude_none=True)) +そして別のターミナルから、クライアントをそこに向けます。 +```python title="client.py" hl_lines="7-8" +--8<-- "docs_src/first_steps/tutorial001_client.py" +``` -asyncio.run(main()) +```console +python client.py ``` ```text @@ -114,7 +112,7 @@ asyncio.run(main()) ここにないものにも注目してください。`completions`(リソーステンプレートとプロンプトの引数の自動補完)には自分で書くハンドラーが必要ですが、このサーバーにはありません。そのためこのケイパビリティは宣言されず、行儀のよいクライアントなら要求もしません。オプションのものはすべてこのルールに従います。登録すればケイパビリティが現れます。**[補完](../servers/completions.md)** のページがそれを実証しています。 !!! info - `Client(mcp)` は、このドキュメントのすべてのサンプルをテストしているのと同じインメモリクライアントで、自分のサーバーをテストするときにもこれを使います。まるごと 1 ページを割いています。**[テスト](testing.md)** です。 + この `client.py` は完全な MCP クライアントで、専用のページが **[クライアント](../client/index.md)** です。テストではターミナルもポートも省き、`Client` にサーバーオブジェクトそのものを渡します。`Client(mcp)` です。こちらにもまるごと 1 ページを割いています。**[テスト](testing.md)** です。 ## 書かなかったもの {#what-you-did-not-write} @@ -123,7 +121,7 @@ asyncio.run(main()) * JSON Schema。`a: int, b: int` がそのまま `add` のスキーマです。 * リクエストハンドラー。`tools/list`、`resources/read`、`prompts/get` は、すべて代わりに処理されます。 * ケイパビリティの宣言。`MCPServer` が代わりに作りました。 -* プロトコルのコードを 1 行も。バージョンのネゴシエーション、JSON-RPC のフレーミング、ケイパビリティの交換は、すべて `mcp dev` と `Client(mcp)` の内部で行われ、目にすることはありませんでした。 +* プロトコルのコードを 1 行も。バージョンのネゴシエーション、JSON-RPC のフレーミング、ケイパビリティの交換は、すべて `mcp dev` と `client.py` の内部で行われ、目にすることはありませんでした。 この比率こそが、この SDK の存在意義です。 @@ -134,6 +132,6 @@ asyncio.run(main()) * デコレーターはプリミティブごとに 1 つです。`@mcp.tool()`、`@mcp.resource(uri)`、`@mcp.prompt()`。名前、説明、スキーマは関数から取られます。 * `{param}` を含む URI はリソース**テンプレート**を作り、具体的なリソースとは別に一覧表示されます。 * サーバーの**ケイパビリティ**は代わりに宣言され、クライアントはサーバーが宣言したものだけを要求します。 -* `Client(mcp)` はサーバーオブジェクトにインメモリで接続します。初日から使えるテストハーネスです。 +* `Client("http://localhost:8000/mcp")` は実行中のサーバーとやり取りします。代わりにサーバーオブジェクトを渡して `Client(mcp)` とすれば、初日から使えるテストハーネスになります。 次は **[実際のホストに接続する](real-host.md)** です。このサーバーを Claude Desktop や IDE の中で、本当に動かします。その次は **[テスト](testing.md)** です。1 ページ、インメモリクライアント 1 つで、動くかどうかを当て推量することはもうありません。そのあとは各プリミティブに専用のページがあり、まずはモデルが動かすもの、**[ツール](../servers/tools.md)** から始まります。 diff --git a/i18n/ja/pages/get-started/testing.md b/i18n/ja/pages/get-started/testing.md index 2636eec939..2b962251c9 100644 --- a/i18n/ja/pages/get-started/testing.md +++ b/i18n/ja/pages/get-started/testing.md @@ -1,13 +1,13 @@ --- translation: - sections: ['4926721070127497', c52a1de2b6b32f40, 8e792bf8c7489ec6, 627195f7159e24ef] + sections: [5d13c2f0ba42c0d2, c52a1de2b6b32f40, 8e792bf8c7489ec6, 38552ea228b0a04f] tool: 1 --- # テスト {#testing} -Python SDK には、**インメモリトランスポート**を備えた `Client` クラスが付属しています。サーバーオブジェクトを渡せば、そのサーバーに直接接続します。 +SDK の `Client` クラスは、URL に接続したりサブプロセスを起動したりするのと同じクラスですが、**インメモリ**でも接続できます。サーバーオブジェクトを渡せば、そのサーバーと直接やり取りします。 -サブプロセスも、ポートも要りません。トランスポートすら使いません。FastAPI の `TestClient` と同じ発想です。 +サブプロセスも、ポートも要りません。通信路には何も流れません。FastAPI の `TestClient` と同じ発想です。 ## 基本的な使い方 {#basic-usage} @@ -86,10 +86,10 @@ async def test_call_add_tool(client: Client): テストでは有効にしたままにしてください。本番コードでは意味を持ちません。 -## デフォルトはインプロセス {#in-process-by-default} +## デフォルトでは世代を問わない {#era-neutral-by-default} !!! note - `Client(mcp)` はインプロセスで接続し、デフォルトでは**プロトコルの世代を問いません**。サーバーを調べ、適切なプロトコル経路を選びます。テストがレガシー固有のセマンティクス(サンプリングやエリシテーション(elicitation)のプッシュ、`message_handler`)を検証する場合は `mode="legacy"` に固定し、その場合は `raise_exceptions=True` を外してください。レガシー接続はそもそもサニタイズを行わず、このフラグを付けると失敗がテストの中ではなくサーバータスクの中で再送出されてしまうからです。 + `Client(mcp)` はインプロセスで接続し、デフォルトでは**プロトコルの世代を問いません**。サーバーを調べ、適切なプロトコル経路を選びます。テストがレガシー固有のセマンティクス(サンプリングやエリシテーション(elicitation)のプッシュ、`message_handler`)を検証する場合は `mode="legacy"` に固定し、そのときは `raise_exceptions=True` を外してください。レガシー接続はそもそもサニタイズを行わず、このフラグを付けると失敗がテストの中ではなくサーバータスクの中で再送出されてしまうからです。 この 1 行こそが、このドキュメントが「掲載している例は動く」と約束できる理由でもあります。すべてのサンプルファイルは SDK 自身のテストスイートで実行されており、そのほぼすべてがまさにこのクライアントを経由しています。SDK が自分自身に対して使っているのと同じツールを使っているわけです。 diff --git a/i18n/ja/pages/handlers/multi-round-trip.md b/i18n/ja/pages/handlers/multi-round-trip.md index dc7bb0a4e1..201268012d 100644 --- a/i18n/ja/pages/handlers/multi-round-trip.md +++ b/i18n/ja/pages/handlers/multi-round-trip.md @@ -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-round-trip)リクエスト {#multi-round-trip-requests} @@ -162,7 +162,7 @@ TTL、プリンシパルの束縛、リクエストの束縛はコーデック ## 2026-07-28 の結果型 {#a-2026-07-28-result} -`InputRequiredResult` はプロトコルバージョン **2026-07-28** にしか存在しません。インメモリの `Client(server)` はそれを代わりにネゴシエートしてくれます。通信路越しでは `mode="auto"` がそれを検出します。接続後、`client.protocol_version` で何が得られたかが分かります。 +`InputRequiredResult` はプロトコルバージョン **2026-07-28** にしか存在しません。`Client` のデフォルトの `mode="auto"` は、どの接続でもそれを検出します。接続後、`client.protocol_version` で何が得られたかが分かります。 !!! warning 2026 より前のセッションには `InputRequiredResult` を入れる場所がありません。`mode="legacy"` の接続でハンドラーからこれを返すと、ランナーはネゴシエートされたバージョンにシリアライズできず、クライアントには `-32603` *"Handler returned an invalid result"* エラーが返ります。両方の世代に対応するサーバーは、これを使う前に `ctx.protocol_version` をチェックしなければなりません。 diff --git a/i18n/ja/pages/handlers/progress.md b/i18n/ja/pages/handlers/progress.md index 6d695d1d0b..9729ced62c 100644 --- a/i18n/ja/pages/handlers/progress.md +++ b/i18n/ja/pages/handlers/progress.md @@ -1,6 +1,6 @@ --- translation: - sections: [5315262fe26b33e1, 9d8e98840f1b78f0, 0284b215e85366c4, 8534d8dbb4053a70, 2966fac6fe697007] + sections: [5315262fe26b33e1, 9d8e98840f1b78f0, 52d6009a07e770ea, 8534d8dbb4053a70, 2966fac6fe697007] tool: 1 --- # 進捗 {#progress} @@ -29,19 +29,17 @@ translation: クライアントは、`call_tool` に `progress_callback=` を渡すことで、**呼び出しごとに**オプトインします。 -```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,23 +54,27 @@ anyio.run(main) コールバックは `async` 関数で、サーバーが報告したものをそのまま受け取ります。`progress`、`total`、`message` です。 !!! info - `Client(mcp)` はサーバーオブジェクトにメモリ内で直接接続します。**[テスト](../get-started/testing.md)** のページの土台になっているのと同じクライアントです。`progress_callback` は、`Client` がどのトランスポートを使っていても同じパラメーターです。これから目にする「タイミング」はメモリ内接続のものです。メモリ内接続はコールバックをインラインで実行するため、すべての報告が `call_tool` が返る前に届きます。実際のトランスポートでは通知と結果の到着順は保証されず、遅いコールバックは `call_tool` が返ったあともまだ実行中のことがあります。 + `progress_callback` は、`Client` に何を渡したかにかかわらず同じパラメーターです。ここでのような URL でも、`StdioServerParameters` でも、テストでのサーバーオブジェクトでも変わりません。ただし、実際のトランスポートではタイミングに注意してください。通知はそれぞれレスポンスとは別に単独で届くため、遅いコールバックは `call_tool` が返ったあともまだ実行中のことがあります。コールバックをインラインで実行し、すべての報告が先に届くことを保証するのは、プロセス内のテスト接続だけです。 ### 試してみる {#try-it} -`client.py` を `server.py` の隣に置いて、実行してください。 +`server.py` を HTTP で配信し、別のターミナルからクライアントを実行してください。 + +```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.'} ``` -サーバー側の `await ctx.report_progress(...)` はそれぞれ、クライアント側で順番どおりに `show` の 1 回の呼び出しになり、2 行とも `call_tool` が返る**前に**出力されました。進捗は結果にまとめられるのではなく、ツールがまだ動いている間にストリーミングされます。 +サーバー側の `await ctx.report_progress(...)` はそれぞれ、クライアント側で順番どおりに `show` の 1 回の呼び出しになりました。進捗は結果にまとめられるのではありません。ツールがまだ動いている間にストリーミングされます。 !!! warning `progress_callback` は `Client` ではなく、**呼び出し**に属します。そのためのコンストラクター引数はありません。呼び出しごとに必要なコールバックが違うからです。ある呼び出しはダウンロードバーを動かし、次の呼び出しはログの 1 行を出します。 diff --git a/i18n/ja/pages/protocol-versions.md b/i18n/ja/pages/protocol-versions.md index 1e2a6b5161..8eb5f6bf13 100644 --- a/i18n/ja/pages/protocol-versions.md +++ b/i18n/ja/pages/protocol-versions.md @@ -1,6 +1,6 @@ --- translation: - sections: [478fd619e5f90ef8, aef094a00e44e248, bab8cbf3449fa7e9, df1809b15a58335b, 5f9d8c2336ed0239, f54974398e43ddef, b24443dd78584870] + sections: [d98e337bf28845e0, dd642acbba36f615, f547b3e76da19c94, 149513378588da5c, 4e149ed992046709, f54974398e43ddef, b24443dd78584870] tool: 1 --- # プロトコルバージョン {#protocol-versions} @@ -11,9 +11,17 @@ MCP には 2 つの世代があります。 `Client` が代わりにネゴシエーションしてくれるので、気にする必要はほとんどありません。このページで扱うのは、それを制御するたった 1 つのコンストラクター引数 `mode=` と、それを変更する 3 つの場面です。 +このページのスニペットはどれも、**[クライアント](client/index.md)** に出てくる Bookshop の `server.py` と通信する `client.py` です。まず 1 つ目のターミナルでそのサーバーを起動してください。 + +```console +uv run mcp run server.py --transport streamable-http +``` + +次に、2 つ目のターミナルで各スニペットを `python client.py` で実行します。 + ## `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,11 +39,11 @@ MCP には 2 つの世代があります。 機能としてはこれだけです。`Client` は 1 つ、サーバーはどの世代でもよく、コードに分岐は要りません。 !!! info - `MCPServer` はインメモリ、stdio、Streamable HTTP のどのトランスポートでも `server/discover` に応答します。そのため、自分のサーバーが相手なら `auto` は必ず `2026-07-28` になります。フォールバックが発動するのは 2026 年より前の本物のサーバーが相手のときだけで、それはまさにフォールバックしてほしい場面です。 + `MCPServer` は Streamable HTTP、stdio、そしてテストで使うインプロセス接続のどのトランスポートでも `server/discover` に応答します。そのため、自分のサーバーが相手なら `auto` は必ず `2026-07-28` になります。フォールバックが発動するのは 2026 年より前の本物のサーバーが相手のときだけで、それはまさにフォールバックしてほしい場面です。 ## `mode="legacy"` {#modelegacy} -```python title="client.py" hl_lines="14" +```python title="client.py" hl_lines="7" --8<-- "docs_src/protocol_versions/tutorial002.py" ``` @@ -59,7 +67,7 @@ MCP には 2 つの世代があります。 `mode` には新世代のプロトコルバージョン文字列も指定できます。現時点でその集合はちょうど `["2026-07-28"]` です。 -```python title="client.py" hl_lines="14" +```python title="client.py" hl_lines="7" --8<-- "docs_src/protocol_versions/tutorial003.py" ``` @@ -90,7 +98,7 @@ ValueError: mode must be 'legacy', 'auto', or one of ['2026-07-28']; got '2025-0 なので取っておきましょう。`auto` で接続した後、`client.session.discover_result` にはサーバーが送った `DiscoverResult` がそのまま入っています。`supported_versions`、`capabilities`、`instructions`、そしてサーバーが結果の `_meta` に刻んだ識別情報です。次回はそれを `prior_discover=` として渡します。 -```python title="client.py" hl_lines="15 17" +```python title="client.py" hl_lines="8 10" --8<-- "docs_src/protocol_versions/tutorial004.py" ``` diff --git a/i18n/ja/pages/run/asgi.md b/i18n/ja/pages/run/asgi.md index f49b8864fc..7853bd4d6e 100644 --- a/i18n/ja/pages/run/asgi.md +++ b/i18n/ja/pages/run/asgi.md @@ -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 --- # 既存のアプリに組み込む {#add-to-an-existing-app} @@ -86,7 +86,7 @@ Starlette の `Host` ルートも同じように動きます。`Mount("/", ...)` --8<-- "docs_src/asgi/tutorial004.py" ``` -これでクライアントは `/notes/mcp` ではなく `/notes` に接続します。 +これでクライアントは `/notes/mcp` ではなく `/notes/` に接続します。 ## ブラウザークライアント向けの CORS {#cors-for-browser-clients} @@ -127,4 +127,4 @@ Starlette の `Host` ルートも同じように動きます。`Mount("/", ...)` * ブラウザークライアントには CORS が必要です。`Mcp-*` リクエストヘッダーのための `allow_headers` と、レスポンスのための `expose_headers=["Mcp-Session-Id"]` です。 * `@mcp.custom_route()` は、認証なしの素の HTTP エンドポイントを `/mcp` の隣に追加します。 -サーバーに実際の URL で到達できるようになったら、**[クライアント](../client/index.md)** はサーバーオブジェクトの代わりにその URL を使って接続します。 +サーバーに実際の URL で到達できるようになったら、**[クライアント](../client/index.md)** はその URL を使ってサーバーに接続します。 diff --git a/i18n/ja/pages/run/authorization.md b/i18n/ja/pages/run/authorization.md index a1795adab1..df1f0470db 100644 --- a/i18n/ja/pages/run/authorization.md +++ b/i18n/ja/pages/run/authorization.md @@ -1,6 +1,6 @@ --- translation: - sections: [d62c13457fc4a534, 80e73abaca6e0652, d1dc4c54cd00ec9c, 14ad3bc7904036bb, 5225f127bc1b9c77, fe1626fdd5aad1da, 4556cb7ea1a04a31] + sections: [d62c13457fc4a534, 80e73abaca6e0652, 128a492d18295f64, 14ad3bc7904036bb, 54a6697833fcc1d9, fe1626fdd5aad1da, 811d083c1da8bcf6] tool: 1 --- # 認可 {#authorization} @@ -23,12 +23,12 @@ OAuth の用語でいえば、サーバーは**リソースサーバー**です 有効なトークンがどんな形をしているかについて、SDK は何の前提も持ちません。**`TokenVerifier`** を実装して、こちらから伝えます。 -```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` は非同期メソッドを 1 つだけ持つプロトコルです。`verify_token` は `Authorization` ヘッダーから取り出した生のトークンを受け取り、有効なら **`AccessToken`** を、無効なら `None` を返します。実装するものはほかにありません。 -* この例ではトークンをテーブルから引いています。実際のものは JWT の署名を検証するか、認可サーバーのトークンイントロスペクションエンドポイントを呼び出します。そのコードは自分で書きます。SDK はそれを呼び出すだけです。 +* この例ではトークンをテーブルから引いています。各エントリには、そのトークンがどのリソース向けに発行されたかが記録されています。実際のものは JWT の署名を検証するか、認可サーバーのトークンイントロスペクションエンドポイントを呼び出し、トークンが誰向けに発行されたか(`aud`)を `AccessToken.resource` で報告します。そのコードは自分で書きます。SDK はそれを呼び出すだけです。 * `token_verifier=` と `auth=` は必ずセットで渡します。片方だけ渡すと、`MCPServer(...)` はリクエストを 1 つも処理しないうちに `ValueError` を送出します。 `AuthSettings` はリソースサーバーの表向きの顔です。 @@ -36,6 +36,10 @@ OAuth の用語でいえば、サーバーは**リソースサーバー**です * `issuer_url`:トークンを発行する認可サーバー。 * `resource_server_url`:この MCP エンドポイントの公開 URL。トークンが「どの」リソース向けかを示す名前であり、ディスカバリードキュメントが置かれる場所でもあります。 * `required_scopes`:すべてのトークンがこれらをすべて持っている必要があります。 +* `validate_token_resource`:`AccessToken.resource` が `resource_server_url` でないトークンをすべて拒否します。`resource_server_url` を設定したままこれを未設定にすると警告(`MCPDeprecationWarning`)が出て、`False` として動作します。3.0 ではリソースサーバーのデフォルトが `True` になります。 + * 認可サーバーが、クライアントの要求した `resource` にトークンを結び付ける場合は有効にしてください。MCP クライアントはこれを常に送ります。`resource_server_url` は、クライアントが接続する URL と正確に一致させてください。 + * 認可サーバーが独自のオーディエンス識別子(Auth0 の API 識別子、Entra のアプリケーション ID)を使う場合は無効のままにし、代わりにベリファイアーの中で `aud` を確認して、このサーバー向けでないトークンには `None` を返してください。 + * `aud` がリストの場合は、`resource_server_url` と等しいエントリを `resource` に入れてください。 !!! tip SDK リポジトリの `examples/servers/simple-auth/` には、実際の認可サーバーの [RFC 7662](https://datatracker.ietf.org/doc/html/rfc7662) エンドポイントを呼び出す `IntrospectionTokenVerifier` があります。本番用のベリファイアーの多くはこの形になります。 @@ -85,7 +89,7 @@ OAuth の用語でいえば、サーバーは**リソースサーバー**です どのハンドラーの中でも、**`get_access_token()`** は現在のリクエストに対してベリファイアーが返した `AccessToken` です。 -```python title="server.py" hl_lines="4 32-35" +```python title="server.py" hl_lines="4 35-38" --8<-- "docs_src/authorization/tutorial002.py" ``` @@ -117,6 +121,6 @@ SDK が提供するのはリソースサーバーの半分、つまり検証、 * `token_verifier=` と `auth=AuthSettings(issuer_url=..., resource_server_url=..., required_scopes=[...])` は必ずセットで渡します。 * SDK は [RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728) Protected Resource Metadata を `/.well-known/oauth-protected-resource/...` で公開し、未認証のリクエストには、そこを指す `WWW-Authenticate` ヘッダー付きの 401 で応答します。ディスカバリーの仕組みはこれだけです。 * どのハンドラーでも、`get_access_token()` を呼べば誰が呼び出しているかがわかります。 -* 認可は HTTP の関心事です。`stdio` とインメモリクライアントがそれを目にすることはありません。 +* 認可は HTTP の関心事です。`stdio` とインメモリのテストクライアントがそれを目にすることはありません。 クライアント側の半分(認可サーバーを見つけてトークンを取得してくれる部分)については、**[OAuth クライアント](../client/oauth-clients.md)**を参照してください。そして、ユーザーに尋ねる代わりに ID を「アサート」するクライアントについては、**[ID アサーション](../client/identity-assertion.md)**を参照してください。 diff --git a/i18n/ja/pages/run/deploy.md b/i18n/ja/pages/run/deploy.md index dd665f9e20..6a1529a2c7 100644 --- a/i18n/ja/pages/run/deploy.md +++ b/i18n/ja/pages/run/deploy.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 --- # デプロイとスケール {#deploy-scale} @@ -42,6 +42,22 @@ translation: この文言はクライアント側では見つかりません。`421` は JSON-RPC エラーではなくプレーンテキストの HTTP レスポンスなので、MCP クライアントは汎用的なトランスポートエラーを送出します。気に入らなかったホスト名は**サーバー**のログに、警告として 1 行出るだけです。デプロイしたばかりのサーバーがすべての接続を拒否するなら、そうでないと証明されるまでは Host の許可リストが原因です。**[トラブルシューティング](../troubleshooting.md)** もここから始まります。 +## TLS 終端プロキシの背後で {#behind-a-tls-terminating-proxy} + +TLS がプロキシ(イングレス、ロードバランサー、Caddy、nginx)で終端し、その背後で uvicorn が平文の HTTP を配信する場合は、プロキシの `X-Forwarded-*` ヘッダーを信頼するよう uvicorn に指示します。 + +```console +uvicorn server:app --proxy-headers --forwarded-allow-ips='' +``` + +これがないと、アプリは自分が `http://` で配信されていると思い込み、発行するリダイレクト(よくあるのは `/mcp` → `/mcp/`)はすべて `http://…` を指します。Python クライアントは、HTTPS エンドポイントから平文の HTTP へのリダイレクトには従わず、その旨を伝えます。 + +```text +MCPError: Redirect to http://mcp.example.com/mcp/ not followed: it would downgrade this HTTPS endpoint to plain HTTP. +``` + +クライアント側の応急処置は、サーバーが配信する正確な URL(`https://mcp.example.com/mcp/`、末尾のスラッシュ込み)を設定して、リダイレクトが起きないようにすることです。根本的な解決策は上のフラグです。`FORWARDED_ALLOW_IPS` はその環境変数版の書き方です。`*` はすべてのホップを信頼しますが、それが正しいのはプロキシ以外から uvicorn に到達できない場合だけです。 + ## ワーカーと、スティッキーにする必要があるのは誰か {#workers-and-who-has-to-be-sticky} ホスト名が応答するようになったら、その背後に複数のワーカーを置きます。そのための SDK の設定項目はありません。Starlette アプリは、どんな ASGI アプリとも同じ方法でスケールします。fork の仕方を知っているものにオブジェクトを渡すだけです。 @@ -154,6 +170,7 @@ python -c "import secrets; print(secrets.token_hex(32))" ## まとめ {#recap} * デフォルトでは、このアプリは localhost 宛てのリクエストにだけ応答します。`transport_security=TransportSecuritySettings(allowed_hosts=[...], allowed_origins=[...])` が公開時の関門です。これを渡すまでは、本物のホスト名の背後ではすべてのリクエストが `421` になり、理由はサーバーのログにしか出ません。 +* TLS 終端プロキシの背後では、uvicorn を `--proxy-headers --forwarded-allow-ips=...` 付きで実行してください。そうしないとリダイレクトが `http://` を指し、クライアントに拒否されます。 * 2026-07-28 ではセッションはなく、ロードバランサーがスティッキーにすべき対象もありません。`stateless_http=True` がレガシー専用の設定項目なのは、モダンなリクエストはこのフラグが読まれる前にルーティングされ、応答されるからです。 * デフォルトの `requestState` の鍵は、プロセスごとに生成される `os.urandom(32)` です。別のワーカーに届いたマルチラウンドトリップのリトライは、`-32602` *"Invalid or expired requestState"* で失敗します。 * 解決策は `RequestStateSecurity(keys=[...])` **と**、すべてのインスタンスで同じサーバー名にすることです。名前はトークンのデフォルトの audience クレームです。鍵も同じ、名前も同じ。 diff --git a/i18n/ja/pages/run/index.md b/i18n/ja/pages/run/index.md index feaac858bc..7dc38bee55 100644 --- a/i18n/ja/pages/run/index.md +++ b/i18n/ja/pages/run/index.md @@ -1,6 +1,6 @@ --- translation: - sections: [fea8d769ff9edeba, ce8e2ad42f29ef71, 0d705efb19cf99c2, 2fd7cf825e6d2b2c, 9adc400e8c88e854, 318893ad8e2e9924, 6b63ab96b34476c0] + sections: [fea8d769ff9edeba, ce8e2ad42f29ef71, 0d705efb19cf99c2, 9fd2357154a5b7e7, 9adc400e8c88e854, 318893ad8e2e9924, 6b63ab96b34476c0] tool: 1 --- # サーバーの実行 {#running-your-server} @@ -72,6 +72,8 @@ Inspector は本物のホストとまったく同じことをします。`server * `json_response=True`:各 POST に SSE ストリームではなく単一の JSON ボディで応答します。このボディにはレスポンスしか入る余地がありません。そのため、リクエストの途中でクライアントを呼び返すツール(`ctx.elicit()` やサンプリング)は、この区間で `NoBackChannelError` を送出します。進行中の呼び出しに紐づく通知(`ctx.report_progress()` による進捗や呼び出しごとのログメッセージ)は破棄されますが、独立した `GET` ストリームは無関係な通知を引き続き運びます。 * `stateless_http=True`:リクエストごとに新しいトランスポートを作り、セッションを追跡しません。 * `max_request_body_size`:受け付けるリクエストボディの最大サイズ(バイト単位)です。デフォルトは 4 MiB で、これより大きいリクエストはパースやセッション作成の前に HTTP 413 を受け取ります。正当な MCP メッセージがこのサイズを超える場合にだけ引き上げてください。 +* `session_idle_timeout`:レガシーセッションが処理中のものを何も持たないまま待機できる秒数で、これを過ぎるとサーバーがセッションを閉じます。デフォルトは 1800 です。`None` で無効になります。[セッションの存続期間と上限](legacy-clients.md#session-lifetime-and-limits) を参照してください。 +* `max_sessions`:1 つのプロセスが同時に保持するレガシーセッションの数です。デフォルトは 10 000 です。`None` で上限がなくなります。同じセクションで扱っています。 * `event_store`、`retry_interval`、`transport_security`:再開可能性と DNS リバインディング保護です。localhost 以外の場所にデプロイするまでは後回しでかまいません。`transport_security` については **[デプロイとスケール](deploy.md)** で扱います。 !!! warning diff --git a/i18n/ja/pages/run/legacy-clients.md b/i18n/ja/pages/run/legacy-clients.md index 4de105d578..bf2690d4ce 100644 --- a/i18n/ja/pages/run/legacy-clients.md +++ b/i18n/ja/pages/run/legacy-clients.md @@ -1,6 +1,6 @@ --- translation: - sections: [3d1663c18edc824c, d4fd37009a13f03d, af9f398a5a8b679a, 470c2dd144294d69, 8e45827e6d24e8c8, 91dfd0ce98ebb03c] + sections: [3d1663c18edc824c, 90956965ae6a1ca1, af9f398a5a8b679a, 5ce83b1f9d88da62, 0d9b5d13fffc94e5, 8e45827e6d24e8c8, 91dfd0ce98ebb03c] tool: 1 --- # レガシークライアントへの対応 {#serving-legacy-clients} @@ -18,15 +18,25 @@ SDK はすべてのリクエストを `MCP-Protocol-Version` ヘッダーで振 ## 1 つのハンドラーで両方の世代 {#one-handler-both-eras} -ユーザーに何かを尋ねる必要があるツールと、それを呼び出す両方の世代のクライアントを示します。 +ユーザーに何かを尋ねる必要があるツールを示します。 -```python title="server.py" hl_lines="24 37-38" +```python title="server.py" hl_lines="21" --8<-- "docs_src/legacy_clients/tutorial001.py" ``` `reserve` には、モデルが渡してこなかったものが 1 つ必要です。何冊予約するかです。ツールはそれを `Annotated[..., Resolve(ask_quantity)]` で宣言します(詳しくは **[依存関係](../handlers/dependencies.md)** を参照してください)。`reserve` の中には、バージョンを指定する箇所も、ケイパビリティを確認する箇所も、分岐する箇所もありません。 -2 つのクライアントは同じ `mcp` オブジェクトに対して**同時に**開かれています。`mode="legacy"` は `initialize` ハンドシェイクを実行します。2026 年より前のクライアントが開くのとまったく同じ接続です。もう一方はデフォルトのままで、`2026-07-28` になります。 +これを HTTP で公開します。続いて、両方の世代のクライアントがそれを呼び出す様子です。 + +```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" +``` + +2 つのクライアントは、動作中の同じサーバーに対して**同時に**開かれています。`mode="legacy"` は `initialize` ハンドシェイクを実行します。2026 年より前のクライアントが開くのとまったく同じ接続です。もう一方はデフォルトのままで、`2026-07-28` になります。別のターミナルから `python client.py` を実行してください。 ```text 2025-11-25 {'result': "Reserved 2 of 'Dune'."} @@ -53,6 +63,31 @@ SDK はすべてのリクエストを `MCP-Protocol-Version` ヘッダーで振 !!! warning `event_store=` は解決策に見えますが、そうではありません。これは**再開可能性**(「同じ」セッションに再接続するクライアントに、取りこぼした SSE イベントを再送する機能)であって、セッションストアではありません。別のプロセスからセッションに到達できるようにはしません。 +## セッションの有効期間と上限 {#session-lifetime-and-limits} + +レガシーセッションは永遠には生き続けませんし、1 つのプロセスが無制限にセッションを抱えることもありません。これを制御する設定が 2 つあります。どちらも `run()`、`streamable_http_app()`、`Server.streamable_http_app()` のキーワード引数です。モダンな(`2026-07-28` の)接続と `stateless_http=True` にはセッションがないため、どちらの設定も適用されません。 + +| 設定 | デフォルト | 動作 | クライアントから見えるもの | 無効にするには | +|---|---|---|---|---| +| `session_idle_timeout` | `1800`(30 分) | 処理中のものが何もない状態がその時間続いたセッションを閉じます。 | `404 Session not found`。もう一度 `initialize` する必要があります。 | `None` | +| `max_sessions` | `10_000` | その数を超えて新しいセッションを開くことを拒否します。既存のセッションには手を付けず、何も追い出しません。 | `503 Too many open sessions`、JSON-RPC コードは `-32603` です。 | `None` | + +「処理中」と見なされるものは次のとおりです。 + +* 開いている `GET` ストリーム。SDK のクライアントは 1 本を開いたままにするため、接続中のクライアントのセッションが期限切れになることはありません。 +* まだ応答中のリクエスト。タイムアウトより長く動くツール呼び出しが中断されることはなく、カウントダウンはその呼び出しが終わってから始まります。 +* それ以外にはありません。リクエストとリクエストの間は時計が進みます。セッション上のどんなリクエストでも時計はリセットされ、`ping` も例外ではありません。一度期限切れになったセッションを復活させる手段はありません。 + +`DELETE` でセッションを終了したクライアントは、そのセッションを即座に解放します。最初のリクエストが拒否されたクライアントも同様です。 + +```python +mcp.run(transport="streamable-http", session_idle_timeout=None, max_sessions=50_000) +``` + +どちらの出来事もサーバーのログに残ります。期限切れは `INFO` レベルの `Session idle timeout` です。セッションを開くのを拒否した場合は `WARNING` レベルの `Refusing to open a new session: sessions are already open` です。 + +これらの上限はプロセスごとです。ワーカーが 4 つなら上限は `max_sessions` の 4 倍で、各ワーカーは自分のセッションだけを期限切れにします。 + ## 唯一のスイッチ:`stateless_http` {#the-one-knob-stateless_http} スティッキー性というコストを払いたくないなら、変更できるものがちょうど 1 つだけあります。 @@ -73,7 +108,7 @@ SDK はすべてのリクエストを `MCP-Protocol-Version` ヘッダーで振 `json_response=True` はそのスイッチではありませんが、「すべての」レガシーセッションで同じコストの半分を負います。1 つの JSON ボディで応答される `POST` にはリクエストスコープのチャネル用のストリームがないため、リクエスト途中の `ctx.elicit()` は同じ `NoBackChannelError` を送出し、そのリクエストに結び付いた通知は捨てられます。セッションのスタンドアロンストリームには影響しません。無関係な通知は引き続き届きます。 !!! check - あえて間違ったことをしてみましょう。`reserve` は、先ほど両方のクライアントに応答したそのツールです。これを `stateless_http=True` でデプロイし、同じ 2 つのクライアントを HTTP で接続して、それぞれから呼び出してください。 + あえて間違ったことをしてみましょう。`reserve` は、先ほど両方のクライアントに応答したそのツールです。これを `stateless_http=True` でデプロイし、同じ 2 つのクライアントを接続して、それぞれから呼び出してください。 モダンなクライアントには引き続き `Reserved 2 of 'Dune'.` が返ります。モダンな経路は変わっていません。 diff --git a/i18n/ja/pages/troubleshooting.md b/i18n/ja/pages/troubleshooting.md index 77f5d874e1..d351427d0a 100644 --- a/i18n/ja/pages/troubleshooting.md +++ b/i18n/ja/pages/troubleshooting.md @@ -1,6 +1,6 @@ --- translation: - sections: [2efaecdef109a5c5, fcacd3e66b8635a4, 25323d737dcf0261, 8a6e351ec756904d, 137454d469c867f5, 6392596bd6df54f0, 41126fa9c4fe432f, 480b6d7897e30ab4, d83bb682e708dde0, ebbed3449c499db4, 323ef84f6b4bebde, 30fd31be74169d9a, 656943c6cb567218, c2dc3b1007d2e987, 7cf5386b997d04e9, 0b59feed8384456e, 0cba47bae78d04eb, e4355f4c7cf4fb2e] + sections: [3d58228e81b99543, 170514ce901c4139, 17d61fad0a50d62b, 8a6e351ec756904d, 137454d469c867f5, 6392596bd6df54f0, 41126fa9c4fe432f, 480b6d7897e30ab4, d83bb682e708dde0, ebbed3449c499db4, 525cdf1755e29d4c, 30fd31be74169d9a, d2e88333d4f7841f, c2dc3b1007d2e987, d6eabf60cc366341, f798e815252852c2, 0cba47bae78d04eb, 2c218ba829abf74e] tool: 1 --- # トラブルシューティング {#troubleshooting} @@ -13,6 +13,12 @@ translation: --8<-- "docs_src/troubleshooting/tutorial001.py" ``` +それらの項目は `http://localhost:8000/mcp` でこのサーバーにアクセスするので、HTTP で起動したままにしておいてください。 + +```console +uv run mcp run server.py --transport streamable-http +``` + このページで引用しているエラーは本物です。SDK 自身のテストスイートが、そのすべてを再現しています。 ## `ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception)` {#exceptiongroup-unhandled-errors-in-a-taskgroup-1-sub-exception} @@ -23,7 +29,7 @@ translation: ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: await client.read_resource("weather://Atlantis") ``` @@ -49,7 +55,7 @@ async def main() -> None: ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: try: await client.read_resource("weather://Atlantis") except MCPError as e: @@ -65,7 +71,7 @@ async def main() -> None: ```python async def main() -> None: - client = Client(mcp) + client = Client("http://localhost:8000/mcp") tools = await client.list_tools() # RuntimeError ``` @@ -73,7 +79,7 @@ async def main() -> None: ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: tools = await client.list_tools() ``` @@ -245,7 +251,7 @@ app = Starlette(routes=[Mount("/", app=mcp.streamable_http_app())], lifespan=lif ## `MCPError: Session not found` {#mcperror-session-not-found} -クライアントが送った `Mcp-Session-Id` をサーバーが認識していません。ほぼ確実に、サーバーが**再起動した**(または別のインスタンスにルーティングされた)のが原因です。セッションは、その 1 つのプロセスのメモリの中にあります。 +クライアントが送った `Mcp-Session-Id` をサーバーが認識していません。サーバーが**再起動した**(または別のインスタンスにルーティングされた)か、`session_idle_timeout`(デフォルトで 30 分)の間に進行中のものが何もなく、セッションが**期限切れになった**かのどちらかです。[セッションの寿命と上限](run/legacy-clients.md#session-lifetime-and-limits)を参照してください。セッションは、その 1 つのプロセスのメモリの中にあります。 探すべきサーバーのバグはありません。HTTP レスポンスは `404` で、そのボディは JSON-RPC「です」。そのため上の `421` とは違い、python の `Client` はこれをそのまま見せてくれます。 @@ -255,9 +261,9 @@ app = Starlette(routes=[Mount("/", app=mcp.streamable_http_app())], lifespan=lif 直し方は再接続です。`async with Client(...)` ブロックを抜けて新しいブロックに入れば、新しいセッションがネゴシエートされます。長く生きるクライアントであれば、呼び出しの周りで `MCPError` を捕まえ、死んだセッションの中でリトライするのではなく、このメッセージを見たら再接続することになります。 -再起動「なしで」これが起きるなら、スティッキーセッションなしで複数のワーカーを動かしています。ワーカーごとに独自のセッションテーブルを持つので、間違ったワーカーにルーティングされたリクエストはここに行き着きます。この話とその 2 つの直し方(スティッキールーティング、または `stateless_http=True`)は、**[デプロイとスケール](run/deploy.md)** と **[レガシークライアントへの提供](run/legacy-clients.md)** が担当しています。 +再起動「なしで」、かつクライアントがそれほど長く静かだったわけでもないのにこれが起きるなら、スティッキーセッションなしで複数のワーカーを動かしています。ワーカーごとに独自のセッションテーブルを持つので、間違ったワーカーにルーティングされたリクエストはここに行き着きます。この話とその 2 つの直し方(スティッキールーティング、または `stateless_http=True`)は、**[デプロイとスケール](run/deploy.md)** と **[レガシークライアントへの提供](run/legacy-clients.md)** が担当しています。 -サーバー運用者向けには、対応するログ行は `Rejected request with unknown or expired session ID: ` です。`INFO` で記録されるので、通常の `WARNING` のしきい値では見えません。デプロイ直後にまとまって出るのは正常です。接続中のクライアントがすべて再接続しているのです。 +サーバー運用者向けには、対応するログ行は `Rejected request with unknown or expired session ID: ` です。`INFO` で記録されるので、通常の `WARNING` のしきい値では見えません。デプロイ直後にまとまって出るのは正常です。接続中のクライアントがすべて再接続しているのです。セッションが期限切れになった場合は、その行の前に `Session idle timeout` が同じく `INFO` で出ます。 ## `MCPError: Method not found` {#mcperror-method-not-found} @@ -269,7 +275,13 @@ app = Starlette(routes=[Mount("/", app=mcp.streamable_http_app())], lifespan=lif サーバーはユーザーに何かを尋ねたいのに、このクライアントは尋ねられることができると一度も言っていません。 -エリシテーション(elicitation)のリゾルバーは、接続中のクライアントがフォームのエリシテーションを宣言していない場合、最初の時点で拒否します。`e.error.data` には、足りないものが正確に書かれています。 +この Bistro は、予約する前にリゾルバーを通じて尋ねます。 + +```python title="server.py" hl_lines="15-17 21" +--8<-- "docs_src/troubleshooting/tutorial007.py" +``` + +Weather サーバーの代わりにこれを提供し、`elicitation_callback` を渡していないクライアントから `book_table` を呼んでください。接続中のクライアントがフォームのエリシテーション(elicitation)を一度も宣言していないので、リゾルバーは最初の時点で拒否します。`e.error.data` には、足りないものが正確に書かれています。 ```json { @@ -283,7 +295,7 @@ app = Starlette(routes=[Mount("/", app=mcp.streamable_http_app())], lifespan=lif ```python async def main() -> None: - async with Client(mcp, elicitation_callback=handle_elicitation) as client: + async with Client("http://localhost:8000/mcp", elicitation_callback=handle_elicitation) as client: result = await client.call_tool("book_table", {"date": "Friday"}) ``` @@ -302,14 +314,14 @@ async def main() -> None: ハンドラーがリクエストの途中でクライアントに連絡を取ろうとしましたが、その接続の呼び出しには、サーバーからのリクエストを運べるチャネルがありません。呼び出しをそこに置くサーバー設定は 3 つあります。 -**`2026-07-28` の接続。どのトランスポートでも、常に。** モダンなプロトコルにはサーバー起点のリクエストがそもそも存在しないので、サーバーは何かを送る前に拒否します。ツールの中の `ctx.elicit()` が、これに出会う典型的な経路です(`Client(server)` は頼まれなくても `2026-07-28` をネゴシエートするので、最初のインメモリテストで出会います)。`elicitation_callback=` を渡しても何も変わりません。答えるべきリクエストがクライアントに届くことがないからです。 +**`2026-07-28` の接続。どのトランスポートでも、常に。** モダンなプロトコルにはサーバー起点のリクエストがそもそも存在しないので、サーバーは何かを送る前に拒否します。ツールの中の `ctx.elicit()` が、これに出会う典型的な経路です。たいていは、そのツールの最初のインメモリ**[テスト](get-started/testing.md)**で出会います。`Client(mcp)` は頼まれなくても `2026-07-28` をネゴシエートするからです。`elicitation_callback=` を渡しても何も変わりません。答えるべきリクエストがクライアントに届くことがないからです。 ```python title="server.py" hl_lines="16" --8<-- "docs_src/troubleshooting/tutorial006.py" ``` ```python -async def main() -> None: +async def test_book_table() -> None: async with Client(mcp) as client: await client.call_tool("book_table", {"date": "Friday"}) ``` @@ -347,7 +359,7 @@ mcp.shared.exceptions.MCPError: Cannot send 'elicitation/create': this transport ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: await client.call_tool("forecast", {"city": "London"}, request_state="round-1-from-worker-a") ``` @@ -400,7 +412,7 @@ mcp = MCPServer("Weather", request_state_security=RequestStateSecurity(keys=[key * サーバーログの `Tool already exists:` は、同名の 2 つのツールが 1 つに潰れた唯一の合図です。 * 1 つの 421、3 つの綴り:`Server returned an error response`(python の `Client`)、`421 Misdirected Request` / `Invalid Host header`(それ以外すべて)、`Invalid Host header: `(サーバーログ)。直し方:`transport_security=TransportSecuritySettings(allowed_hosts=[...])`。 * `Task group is not initialized` -> マウントされたアプリで、ホストのライフスパンが `mcp.session_manager.run()` に入っていません。 -* `Session not found` -> サーバーが再起動しました。再接続してください。 +* `Session not found` -> サーバーが再起動したか、セッションが期限切れになりました(`session_idle_timeout`)。再接続してください。 * `Cannot send 'elicitation/create': ... no back-channel ...` -> `ctx.elicit()` にはサーバーからクライアントへのチャネルが必要です。`2026-07-28` の接続にはそれが決してなく、`stateless_http=True` はレガシーのチャネルを奪い、`json_response=True` はリクエストスコープのチャネルを奪います。リゾルバーを使ってください(レガシークライアントには、チャネルを保持するサーバーも必要です)。隣の `Method not found` は、相手側のプロトコルリビジョンにないメソッドへのリクエストです。 * `Client did not declare the form elicitation capability ...` と `Elicitation not supported` -> クライアントに `elicitation_callback=` が足りません。 * `Invalid or expired requestState` は、通信上では決して理由を言いません。サーバーログが言います。`unknown key` は、ワーカー間で `RequestStateSecurity(keys=[...])` を共有せよという意味です。 diff --git a/i18n/ja/pages/whats-new.md b/i18n/ja/pages/whats-new.md index eb226e2692..71aacc66ba 100644 --- a/i18n/ja/pages/whats-new.md +++ b/i18n/ja/pages/whats-new.md @@ -1,6 +1,6 @@ --- translation: - sections: [cfe01c0c5863dfa2, 1c58c5cfcc37d455, a7392996acf1ad8f, 875eb2889263424e] + sections: [cfe01c0c5863dfa2, 0dbb68d8b210177b, 80cf193023af3ed4, 875eb2889263424e] tool: 1 --- # v2 の新機能 {#whats-new-in-v2} @@ -37,11 +37,11 @@ mcp = MCPServer("Demo") # v1: FastMCP("Demo") v1 では 3 つの層が入れ子になっていました。生のストリームを返すトランスポートのコンテキストマネージャー、それを包む `ClientSession`、そして手で呼び出す `await session.initialize()` です。v2 にあるのはオブジェクト 1 つです。 -```python title="client.py" hl_lines="14-18" ---8<-- "docs_src/client/tutorial001.py" +```python title="client.py" hl_lines="7-11" +--8<-- "docs_src/client/tutorial001_client.py" ``` -`Client` が受け取るのは、サーバーオブジェクト(インメモリでトランスポートなし。テストで使う形です)、URL(Streamable HTTP)、`StdioServerParameters`(stdio のサブプロセス)、または `sse_client(...)` のようなそれ以外の任意のトランスポートのコンテキストマネージャーです。`async with` に入ると接続し、サーバーがどの世代を話すかにかかわらずプロトコルバージョンをネゴシエートします。その後は `client.server_capabilities` と `client.protocol_version` がそのまま使え、サーバーが自身を名乗る場合は `client.server_info` も使えます(2026 年世代では識別情報が省略可能なので、`Implementation | None` になりました)。v1 で登録したサンプリングとエリシテーションのコールバックは引き続き動作します(コールバックの本体には、このページのほかの項目と同じ snake_case への属性名の変更が及びます)。加えて 2026 形式の「結果に埋め込まれたリクエスト」(後述)にも応答するようになり、1 つずつではなく並行して実行されます。低レベルのインターフェースが必要な人のために `ClientSession` は今も下にあり、`client.session` で取り出せます。ただしこちらも変わっています(新しいディスパッチャーエンジンの上で動き、自身のシグネチャも一部変わりました)。下りていく前に**[移行ガイド](migration.md#clientsession-now-runs-on-jsonrpcdispatcher-basesession-removed)**を読んでください。 +`Client` が受け取るのは、URL(Streamable HTTP)、`StdioServerParameters`(stdio のサブプロセス)、`sse_client(...)` のようなそれ以外の任意のトランスポートのコンテキストマネージャー、またはテストで使うサーバーオブジェクトそのもの(インメモリでトランスポートなし)です。`async with` に入ると接続し、サーバーがどの世代を話すかにかかわらずプロトコルバージョンをネゴシエートします。その後は `client.server_capabilities` と `client.protocol_version` がそのまま使え、サーバーが自身を名乗る場合は `client.server_info` も使えます(2026 年世代では識別情報が省略可能なので、`Implementation | None` になりました)。v1 で登録したサンプリングとエリシテーションのコールバックは引き続き動作します(コールバックの本体には、このページのほかの項目と同じ snake_case への属性名の変更が及びます)。加えて 2026 形式の「結果に埋め込まれたリクエスト」(後述)にも応答するようになり、1 つずつではなく並行して実行されます。低レベルのインターフェースが必要な人のために `ClientSession` は今も下にあり、`client.session` で取り出せます。ただしこちらも変わっています(新しいディスパッチャーエンジンの上で動き、自身のシグネチャも一部変わりました)。下りていく前に**[移行ガイド](migration.md#clientsession-now-runs-on-jsonrpcdispatcher-basesession-removed)**を読んでください。 **[Client](client/index.md)** で紹介し、**[クライアントのトランスポート](client/transports.md)**で 4 つの接続形態を、**[クライアントのコールバック](client/callbacks.md)**でコールバックそのものを扱います。**[テスト](get-started/testing.md)**では、v1 の `create_connected_server_and_client_session()` ヘルパーに代わるインメモリのパターンを示します。 @@ -166,11 +166,15 @@ Streamable HTTP では、2026 の経路に `Mcp-Session-Id` がありません 代わりの仕組みは呼び出しの向きを逆にします。ユーザーから何かを必要とするツールは質問を「返し」(`InputRequiredResult`)、クライアントはこれまでと同じコールバックでそれに答え、答えを添えて呼び出しが再試行されます。そのループは `Client` が回します。サーバー側で結果を自分で組み立てることはめったにありません。**[依存関係](handlers/dependencies.md)**がやってくれるからです。パラメーターを `Resolve(ask_quantity)` で注釈します(`ask_quantity` は自分で書く普通の関数です)。すると SDK は接続が対応している仕組み、つまりレガシーセッションならその場で送るエリシテーションリクエスト、2026 ならマルチラウンドトリップで質問します。ツール本体は 1 つ、世代は両方です。 -```python title="dual_era.py" hl_lines="24 37-38" +```python title="server.py" hl_lines="21" --8<-- "docs_src/legacy_clients/tutorial001.py" ``` -このファイル 1 つに要点が詰まっています。1 つのサーバー、`Resolve` に支えられた 1 つのツール、そしてレガシークライアントと新世代のクライアントの両方がインメモリで答えを受け取ります。仕組み(SDK が封印と検証を行う `request_state` を含む)は**[マルチラウンドトリップリクエスト](handlers/multi-round-trip.md)**が説明し、質問のしかたは**[エリシテーション](handlers/elicitation.md)**が扱います。 +```python title="client.py" hl_lines="14-15" +--8<-- "docs_src/legacy_clients/tutorial001_client.py" +``` + +この 2 つのファイルに要点が詰まっています。1 つのサーバー、`Resolve` に支えられた 1 つのツール、そしてレガシークライアントと新世代のクライアントの両方が、同じ実行中のサーバーから答えを受け取ります(**[レガシークライアントへの対応](run/legacy-clients.md)**がこの 2 つのファイルを順にたどります)。仕組み(SDK が封印と検証を行う `request_state` を含む)は**[マルチラウンドトリップリクエスト](handlers/multi-round-trip.md)**が説明し、質問のしかたは**[エリシテーション](handlers/elicitation.md)**が扱います。 !!! warning "移植した v1 サーバーの動作が変わる唯一の場所" 最初にぶつかるのは自分のテストです。`Client(mcp)` はデフォルトで v2 サーバーに対して 2026-07-28 をネゴシエートするので、`ctx.elicit()` を呼ぶツールは v1 で通っていたテストで失敗します。質問を `Resolve(...)` パラメーターに移す(世代をまたいで使えます)か、本当にプッシュ型の動作が欲しいならテストクライアントを `mode="legacy"` に固定してください。 diff --git a/i18n/ko/pages/advanced/apps.md b/i18n/ko/pages/advanced/apps.md index bb040b9da7..2b10642ac7 100644 --- a/i18n/ko/pages/advanced/apps.md +++ b/i18n/ko/pages/advanced/apps.md @@ -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 @@ SDK는 이를 내장 `Apps` 확장(`io.modelcontextprotocol/ui`)으로 제공합 ## 얼굴을 가진 시계 {#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 @@ HTML 자체는 호스트의 `postMessage`를 수신하고 결과를 표시합니 모델은 `content`를 읽고, iframe은 사람을 위한 것입니다. UI를 지원하는 호스트도 여전히 텍스트 결과를 모델에 전달하며, 텍스트 전용 클라이언트는 **오직** 그 텍스트만 받습니다. 따라서 표준 패턴은 도구 하나에 답 둘입니다. `get_time`을 다시 살펴보세요. -```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)`는 클라이언트가 `io.modelcontextprotocol/ui` 확장을 선언했고 **동시에** `mimeTypes` 설정에 `text/html;profile=mcp-app`을 나열했을 때만 `True`입니다. 이 필드는 필수이므로 생략한 클라이언트는 해당하지 않습니다. 같은 파일의 `main()`이 선언하는 것이 바로 이것입니다. 협상의 클라이언트 쪽 절반을 선언하면 풍부한 답이 돌아옵니다. +`client_supports_apps(ctx)`는 클라이언트가 `io.modelcontextprotocol/ui` 확장을 선언했고 **동시에** `mimeTypes` 설정에 `text/html;profile=mcp-app`을 나열했을 때만 `True`입니다. 이 필드는 필수이므로 생략한 클라이언트는 해당하지 않습니다. 협상의 클라이언트 쪽 절반은 다음과 같습니다. + +```python title="client.py" hl_lines="8 12" +--8<-- "docs_src/apps/tutorial001_client.py" +``` + +`server.py`를 HTTP로 제공한 다음, 두 번째 터미널에서 클라이언트를 실행하세요. + +```console +uv run mcp run server.py --transport streamable-http +``` + +```console +python client.py +``` + +```text +2026-06-26T12:00:00Z +``` + +풍부한 답이 돌아왔습니다. `Client` 호출에서 `extensions=[APPS_SUPPORT]`를 빼면 같은 프로그램이 대신 `The time is 2026-06-26T12:00:00Z.`라고 출력하며, 텍스트 전용 클라이언트가 보게 되는 것은 언제나 이것뿐입니다. !!! warning `"[Rendered UI]"` 같은 자리 표시자를 유일한 content로 반환하지 마세요. 대체 텍스트가 쓸모없다면, 그 도구는 모든 텍스트 전용 클라이언트와 모델 자체에 쓸모없는 도구가 됩니다. 제대로 된 문장을 작성하세요. diff --git a/i18n/ko/pages/advanced/extensions.md b/i18n/ko/pages/advanced/extensions.md index a269ffc28a..6a26bcde47 100644 --- a/i18n/ko/pages/advanced/extensions.md +++ b/i18n/ko/pages/advanced/extensions.md @@ -1,6 +1,6 @@ --- translation: - sections: [05891e7cc1938a13, b3c01a6af28c51ee, 7ffc91f5e38bdfe0, 717d3f235a8333a7, f471a13b2fe5d737, ed6af2df4b656dff] + sections: [05891e7cc1938a13, b3c01a6af28c51ee, 6eba6ce094f4417e, 125f28588c85dd7b, ddd8969b698ca11c, ed6af2df4b656dff] tool: 1 --- # 확장 {#extensions} @@ -49,7 +49,7 @@ TypeError: Stamps.identifier must be a `vendor-prefix/name` string 쓸모 있는 가장 작은 확장은 도구 하나와 설정 맵 하나입니다. -```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 @@ TypeError: Stamps.identifier must be a `vendor-prefix/name` string * `settings()`는 `capabilities.extensions["com.example/stamps"]`에 광고되는 값입니다. 설정 없이 확장을 광고하려면 `{}`(기본값)을 반환하세요. * 확장은 서버를 절대 전달받지 않습니다. 기여할 내용을 데이터로 선언하고, `MCPServer`가 이를 소비합니다. 변경할 `self.server` 같은 것은 없습니다. -그리고 `main()`이 그 증거입니다. `mcp`에 바로 연결하는 인메모리 클라이언트입니다. +HTTP로 서비스하면, 클라이언트가 곧 그 증거가 됩니다. -```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" ``` +이 페이지의 모든 `server.py`는 이 명령으로 서비스하고, 모든 `client.py`는 두 번째 터미널에서 `python client.py`로 그 옆에서 실행합니다. + ### 자체 메서드 제공하기 {#serving-your-own-methods} 확장은 **새로운 요청 메서드**를 등록할 수 있습니다. 사양의 동사 옆에서 함께 서비스되는 자체 동사입니다. -```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 @@ TypeError: Stamps.identifier must be a `vendor-prefix/name` string ### 클라이언트 측 {#the-client-side} -같은 파일의 `main()`이 클라이언트 쪽 이야기의 전부이며, 두 부분을 모두 담고 있습니다. +클라이언트는 독립된 프로그램이며, 클라이언트 쪽 이야기의 두 부분을 모두 담고 있습니다. -```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)])`가 확장을 선언합니다. 선언은 `ClientCapabilities.extensions`가 됩니다. 2026-07-28 연결에서는 이 맵이 요청별 `_meta` 봉투에 실려 이동하므로 서버는 **모든** 요청에서 이를 봅니다. 레거시 연결에서는 `initialize` 핸드셰이크에 실립니다. 서버 코드는 어느 쪽인지 신경 쓰지 않습니다. `require_client_extension(ctx, ...)`와 `ctx.session.check_client_capability(...)`는 두 경로 모두에서 올바른 출처를 읽습니다. * 벤더 메서드는 한 계층 아래인 `client.session.send_request(...)`로 내려갑니다. `Client`는 사양 동사에 대해서만 일급 메서드를 갖춥니다. `send_request`는 모든 `Request` 서브클래스를 받으므로 벤더 요청은 그대로 통과합니다. +* `SearchRequest`와 여기에 실리는 두 모델은 확장의 와이어 계약이므로 클라이언트가 직접 선언합니다. 공개 배포되는 확장이라면 양쪽이 함께 임포트하는 패키지에 담아 제공합니다. ### `tools/call` 가로채기 {#intercepting-toolscall} @@ -109,12 +116,18 @@ TypeError: Stamps.identifier must be a `vendor-prefix/name` string ## 클라이언트 확장 사용하기 {#using-a-client-extension} -**클라이언트 확장**은 소비하는 쪽에서 본 같은 계약으로, 하나의 식별자 아래 묶인 클라이언트 측 동작의 묶음입니다. `Client(extensions=[...])`에 인스턴스를 전달하고 평소처럼 도구를 호출하세요. +**클라이언트 확장**은 소비하는 쪽에서 본 같은 계약으로, 하나의 식별자 아래 묶인 클라이언트 측 동작의 묶음입니다. 여기서 서버는 `buy`에 상품 대신 정산할 영수증으로 응답하며, 그것도 확장을 선언한 클라이언트에게만 그렇게 합니다. -```python title="client.py" hl_lines="66-68" +```python title="server.py" hl_lines="22-25" --8<-- "docs_src/extensions/tutorial006.py" ``` +클라이언트에서는 `Client(extensions=[...])`에 인스턴스를 전달하고 평소처럼 도구를 호출하세요. + +```python title="client.py" hl_lines="33-35" +--8<-- "docs_src/extensions/tutorial006_client.py" +``` + `call_tool("buy", ...)`은 다른 모든 호출처럼 평범한 `CallToolResult`를 반환합니다. 확장이 바꾼 것은 이렇습니다. 이제 서버는 `buy`에 최종 결과 대신 `receipt` **결과 형태**로 응답할 수 있고, `call_tool`이 반환하기 전에 `Receipts`가 이를 마무리합니다(여기서는 후속 호출로 영수증을 정산합니다). 호출 지점에서는 아무것도 달라지지 않습니다. 확장을 빼면 이 중 어떤 것도 존재하지 않습니다. 서버의 관문은 확장을 선언하지 않은 클라이언트를 거부하고(오류 -32021), 관문을 건너뛰는 서버가 보낸 클레임된 형태는 인식되지 않은 `resultType`에 대해 사양이 요구하는 그대로 검증에 실패합니다. 와이어 양 끝 모두에서 기본적으로 꺼져 있습니다. @@ -124,15 +137,15 @@ TypeError: Stamps.identifier must be a `vendor-prefix/name` string ```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")]) ``` ## 클라이언트 확장 작성하기 {#writing-a-client-extension} `ClientExtension`을 서브클래싱하고 필요한 것만 재정의하세요. 기여 종류는 세 가지이며 각각 기본 구현이 있습니다. `settings()`, `claims()`, `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" ``` * 식별자는 서버의 것과 같은 문법을 따르며 클래스가 정의될 때 검증됩니다. @@ -153,12 +166,18 @@ def notifications(self) -> Sequence[NotificationBinding[Any]]: ### 확장 동사 {#extension-verbs} -확장의 자체 요청 메서드에는 클라이언트 측 등록이 필요 없습니다. 벤더 요청 타입은 `mcp.types.Request`를 서브클래싱하고, [자체 메서드 제공하기](#serving-your-own-methods)에서처럼 `client.session.send_request`를 거칩니다. 한 가지가 더 있습니다. params 키가 `Mcp-Name` 헤더에 실려야 할 때(tasks 같은 확장 사양은 자신의 동사에 이를 요구합니다) 요청 타입이 `name_param`을 선언합니다. +확장의 자체 요청 메서드에는 클라이언트 측 등록이 필요 없습니다. 벤더 요청 타입은 `mcp.types.Request`를 서브클래싱하고, [자체 메서드 제공하기](#serving-your-own-methods)에서처럼 `client.session.send_request`를 거칩니다. 확장이 이름 붙은 작업에 관한 동사 하나를 서비스하는 서버를 예로 들어 보겠습니다. -```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" ``` +클라이언트 쪽에는 한 가지가 추가됩니다. params 키가 `Mcp-Name` 헤더에 실려야 할 때(tasks 같은 확장 사양은 자신의 동사에 이를 요구합니다) 요청 타입이 `name_param`을 선언합니다. + +```python title="client.py" hl_lines="20-23 28-29" +--8<-- "docs_src/extensions/tutorial007_client.py" +``` + 세션은 모든 전송 경로에서 `params["jobId"]`를 `Mcp-Name`에 반영하며, 값이 없으면 필수 헤더를 조용히 빠뜨리는 대신 명시적으로 실패합니다. ## 확장이 할 수 없는 것 {#what-an-extension-cannot-do} diff --git a/i18n/ko/pages/advanced/low-level-server.md b/i18n/ko/pages/advanced/low-level-server.md index 811989ad21..218397f0c6 100644 --- a/i18n/ko/pages/advanced/low-level-server.md +++ b/i18n/ko/pages/advanced/low-level-server.md @@ -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 --- # 저수준 Server {#the-low-level-server} @@ -36,18 +36,22 @@ translation: ### 직접 해 보기 {#try-it} -이번에는 Inspector를 쓸 수 없습니다. `mcp dev`와 `mcp run`은 `MCPServer`만 받습니다. 인메모리 `Client`는 상관하지 않으며, `MCPServer`를 받는 것과 똑같이 저수준 `Server`도 받습니다. +`mcp dev`와 `mcp run`은 `MCPServer`만 받으므로 이 서버는 직접 띄워야 합니다. `server.py`의 마지막 줄이 이 서버로 평범한 ASGI 앱을 만들고, uvicorn이 그 앱을 실행합니다. -```python title="main.py" +```console +uvicorn server:app --port 8000 +``` + +Inspector든 다른 어떤 클라이언트든 `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 @@ asyncio.run(main()) * `result.structured_content`가 `None`입니다. 고수준 서버는 `-> str` 반환값을 `{"result": ...}`로 감싸 주지만, 여기서는 직접 만들지 않은 것을 대신 만들어 주는 곳이 없습니다. * `list_tools`는 **직접** 입력한 스키마를 글자 하나까지 그대로 반환합니다. 고수준 버전에는 모든 속성에 `"title": "Query"`가, 루트에 `"title": "search_booksArguments"`가 있었습니다. Pydantic이 남긴 흔적입니다. 여기서는 와이어에 실린 것이라면 전부 직접 넣은 것입니다. +테스트에서는 uvicorn과 포트를 건너뜁니다. `Client(server)`는 `MCPServer`를 받는 것과 똑같이 저수준 `Server`도 프로세스 안에서 받으며, **[테스트](../get-started/testing.md)**가 바로 그 패턴입니다. + ## 자동 검증 없음 {#nothing-is-checked-for-you} `MCPServer`는 함수가 실행되기도 전에, 생성한 스키마에 호출을 대조해 검증하여 잘못된 인수를 거부합니다(**[도구](../servers/tools.md)**). @@ -215,4 +221,4 @@ use Server.middleware to observe or wrap initialization * `add_request_handler(method, params_type, handler)`는 어떤 메서드든 제공합니다. `initialize`는 예약되어 있습니다. * `Server`가 알리는 기능은 등록한 핸들러에서 도출됩니다. -`Client(server)`가 두 서버를 똑같이 다룬 것은 둘이 **같은** 프로토콜이기 때문이며, 바로 그 점이 핵심입니다. 그다음 아래 계층은 클래스가 아닙니다. 바로 **[미들웨어](middleware.md)**입니다. +클라이언트가 두 서버를 똑같이 다룬 것은 둘이 **같은** 프로토콜이기 때문이며, 바로 그 점이 핵심입니다. 그다음 아래 계층은 클래스가 아닙니다. 바로 **[미들웨어](middleware.md)**입니다. diff --git a/i18n/ko/pages/advanced/pagination.md b/i18n/ko/pages/advanced/pagination.md index def248d824..9d5ff4000e 100644 --- a/i18n/ko/pages/advanced/pagination.md +++ b/i18n/ko/pages/advanced/pagination.md @@ -1,6 +1,6 @@ --- translation: - sections: [a9aba7a026c7bd85, ed32bda7ba9ae33a, 7e64cc5646abb91f, 22a0129ee78b3c63, d875373c06d8d2f9] + sections: [a9aba7a026c7bd85, 83e2a08b9d46a398, 9fd8a0aa384b3257, 22a0129ee78b3c63, d875373c06d8d2f9] tool: 1 --- # 페이지네이션 {#pagination} @@ -31,9 +31,13 @@ translation: ### 직접 해 보기 {#try-it} -`Client(server)`는 `MCPServer`에 연결할 때와 똑같이 저수준 `Server`에 인메모리로 연결합니다. +`mcp run`은 `MCPServer`만 받으므로 이 서버는 직접 띄워야 합니다. `server.py`의 마지막 줄이 `Server`로 평범한 ASGI 앱을 만들고, uvicorn이 그 앱을 실행합니다. -인자 없이 `list_resources()`를 호출하세요. `book-1`부터 `book-10`까지 리소스 10개가 돌아오고, `next_cursor`는 문자열 `"10"`입니다. +```console +uvicorn server:app --port 8000 +``` + +아무 클라이언트(**[클라이언트](../client/index.md)** 또는 Inspector)나 `http://localhost:8000/mcp`에 연결하고 인자 없이 `list_resources()`를 호출하세요. `book-1`부터 `book-10`까지 리소스 10개가 돌아오고, `next_cursor`는 문자열 `"10"`입니다. 이를 `list_resources(cursor="10")`으로 다시 넘기면 첫 번째 리소스는 `book-11`이고, 새 `next_cursor`는 `"20"`입니다. @@ -43,7 +47,7 @@ translation: `Client`의 모든 `list_*` 메서드(`list_tools`, `list_resources`, `list_resource_templates`, `list_prompts`)는 `cursor=` 키워드를 받습니다. 페이지로 나뉜 목록을 전부 가져오는 것은 `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 @@ translation: * `next_cursor`를 확인하기 **전에** 결과를 덧붙이세요. 마지막 페이지에도 리소스가 있습니다. * `next_cursor is None`이 종료 조건입니다. 그 외의 값은 손대지 않고 그대로 `cursor=`에 다시 넣습니다. -이 파일의 `main()`을 실행하면 `100 resources`가 출력됩니다. 열 개씩 열 페이지가, 페이지가 열 개라는 사실조차 모르는 루프에 의해 하나로 이어 붙여진 결과입니다. +uvicorn이 `server.py`를 계속 띄워 둔 상태에서 두 번째 터미널에서 `python client.py`를 실행하세요. `100 resources`가 출력됩니다. 열 개씩 열 페이지가, 페이지가 열 개라는 사실조차 모르는 루프를 통해 하나로 이어 붙여진 결과입니다. 이 루프는 **[클라이언트](../client/index.md)**에서 모든 `list_*` 동사에 대해 보여 주는 것과 같은 루프이며, 페이지를 나누지 않는 서버에 대해서도 비용이 들지 않습니다. 첫 응답에서 `next_cursor`가 `None`이므로 루프는 한 번만 돕니다. diff --git a/i18n/ko/pages/client/caching.md b/i18n/ko/pages/client/caching.md index 026272c29f..50625fa117 100644 --- a/i18n/ko/pages/client/caching.md +++ b/i18n/ko/pages/client/caching.md @@ -1,6 +1,6 @@ --- translation: - sections: [9e7b9a1710e5aeba, b74ca4c1d2ddddee, fa8714e61bf90c5a, 04db67a886b7271c, 857690fb8f876800] + sections: [9e7b9a1710e5aeba, 66a2e9acc9101d54, d3d25aa802f5145a, 04db67a886b7271c, 857690fb8f876800] tool: 1 --- # 캐싱 힌트 {#caching-hints} @@ -30,7 +30,7 @@ translation: 저수준 `Server`에서는 핸들러가 결과를 직접 조립하며, `ttl_ms` / `cache_scope`는 결과 모델의 필드일 뿐입니다. 이 필드를 명시적으로 설정한 핸들러는 필드 단위로 언제나 생성자 맵보다 우선합니다. -```python title="server.py" hl_lines="10 16" +```python title="server.py" hl_lines="11 17" --8<-- "docs_src/caching/tutorial002.py" ``` @@ -44,10 +44,24 @@ translation: 2026-07-28 세션에서는 `Client`가 힌트를 대신 따릅니다. 기본으로 켜져 있는 내장 응답 캐시가 있기 때문입니다. `ttlMs`를 싣고 도착한 결과는 저장되고, 그 TTL 안에 동일한 호출이 오면 왕복 없이 캐시에서 제공됩니다. 힌트가 **없는** 결과는 캐시되지 않습니다. 힌트 없는 결과에는 `CacheConfig.default_ttl_ms`가 적용되는데 기본값이 `0`(즉시 만료)이므로, 아무것도 선언하지 않는 서버는 늘 그랬듯 호출마다 요청이 오는 트래픽을 그대로 보게 됩니다. -```python title="client.py" hl_lines="33 35 38" +이 동작을 직접 확인하려면 앞 절의 `server.py`를 uvicorn으로 띄우세요(마지막 줄이 ASGI 앱을 만듭니다). 핸들러는 실제로 실행될 때마다 한 줄을 출력합니다. + +```console +uvicorn server:app --port 8000 +``` + +```python title="client.py" hl_lines="20 23 28" --8<-- "docs_src/caching/tutorial003.py" ``` +두 번째 터미널에서 `python client.py`를 실행하세요. 첫 번째 결과가 싣고 온 힌트, 즉 핸들러의 `ttlMs`와 맵의 `cacheScope`를 나란히 출력합니다. + +```text +1000 public +``` + +나머지는 서버 쪽 터미널이 알려 줍니다. uvicorn의 요청 로그 사이로 `tools/list served`가 세 번 나타납니다. + 호출은 네 번, 서버에서 가져온 것은 세 번입니다. 두 번째 호출은 신선한 항목을 찾았고 서버에 도달하지 않았습니다. (주입된) 시계를 TTL 너머로 진행시키자 세 번째 호출은 다시 가져왔고, 네 번째 호출은 `cache_mode="refresh"`를 지정했습니다. 이 키워드 인자는 캐싱 동사 다섯 개(`list_tools`, `list_prompts`, `list_resources`, `list_resource_templates`, `read_resource`)에 있습니다. * `"use"`(기본값)는 신선한 항목이 있으면 해당 항목을 제공하고, 없으면 가져와서 저장합니다. @@ -56,7 +70,7 @@ translation: `"use"` 위에 규칙이 하나 더 있습니다. **`meta`를 담은 호출은 언제나 서버에 도달합니다.** `meta`가 설정된 요청(진행률 토큰, 추적 필드)은 실제로 전송되는 요청을 기대하므로, `cache_mode="use"`에서는 `"refresh"`로 취급됩니다. 캐시 읽기는 건너뛰고, 가져온 결과는 여전히 캐시된 항목을 대체합니다. `"bypass"`와 명시적 `"refresh"`는 평소대로 동작합니다. -캐싱을 완전히 끄려면 `Client(server, cache=None)`으로 생성하세요. 모든 호출이 다시 왕복이 되며, `cache_mode`는 여전히 받아들여지지만 아무 일도 하지 않습니다. +캐싱을 완전히 끄려면 `Client`를 생성할 때 `cache=None`을 전달하세요. 모든 호출이 다시 왕복이 되며, `cache_mode`는 여전히 받아들여지지만 아무 일도 하지 않습니다. 범위도 자동으로 존중됩니다. `"private"` 항목은 캐시의 **파티션**(아래 참고)을 키로 하고, `"public"` 항목은 더 넓은 공유를 선택할 수 있습니다. 그리고 알림이 지목하는 바로 그 항목에 대해서는 **알림이 TTL보다 우선합니다**. `list_changed` 알림은 일치하는 캐시된 목록을 축출하고, `resources/updated`는 정확히 그 URI로 저장된 캐시된 읽기 결과를 축출합니다. 얼마나 신선했든 상관없습니다. 2026-07-28 연결에서 이 알림은 `client.listen(...)`으로 여는 `subscriptions/listen` 스트림으로 도착하며, 축출은 감시자가 이벤트를 보기 전에 완료됩니다. 자세한 내용은 **[구독](subscriptions.md)**에서 확인하세요. diff --git a/i18n/ko/pages/client/callbacks.md b/i18n/ko/pages/client/callbacks.md index 49bbd347a3..f9b023a773 100644 --- a/i18n/ko/pages/client/callbacks.md +++ b/i18n/ko/pages/client/callbacks.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} @@ -63,7 +63,7 @@ result.content # [TextContent(type='text', text='Card issued to Ada Lovelace.') `Client(...)` 호출의 `mode="legacy"`는 실제로 중요한 역할을 합니다. 기본적으로 `Client(...)`는 최신 프로토콜 경로를 협상하는데, 그 경로에는 서버에서 클라이언트로 가는 요청을 위한 역방향 채널이 없어서 콜백이 실행되기도 전에 `ctx.elicit` 호출이 실패합니다. 이를 결정하는 것은 트랜스포트가 아니라 협상된 - 프로토콜이며, 인메모리든 URL을 통하든 마찬가지입니다. 클라이언트가 이런 요청에 응답해야 할 때마다 + 프로토콜입니다. 클라이언트가 이런 요청에 응답해야 할 때마다 `mode="legacy"`로 고정하세요. 이 페이지를 뒷받침하는 모든 테스트가 그렇게 합니다. 자세한 내용은 **[프로토콜 버전](../protocol-versions.md)**에서 확인하세요. 2026-07-28 세션에서도 콜백이 쓸모없어지는 것은 아니며, 입력을 받는 방식이 다를 뿐입니다. 도구가 @@ -151,4 +151,4 @@ result.structured_content # {'result': ['elicitation']} * `sampling_callback`과 `list_roots_callback`도 같은 방식으로 동작하지만 지원 중단 예정 기능을 처리합니다. 최신 서버는 대신 다중 왕복 요청을 사용합니다. * `logging_callback`과 `message_handler`는 알림을 받습니다. 아무것도 선언하지 않습니다. -`Client(...)`의 첫 번째 인자는 트랜스포트 객체입니다. 모든 종류는 **[클라이언트 트랜스포트](transports.md)**에서 다룹니다. +`Client(...)`의 첫 번째 인자가 트랜스포트를 결정합니다. 모든 종류는 **[클라이언트 트랜스포트](transports.md)**에서 다룹니다. diff --git a/i18n/ko/pages/client/index.md b/i18n/ko/pages/client/index.md index 636b1609a2..51c1e151d5 100644 --- a/i18n/ko/pages/client/index.md +++ b/i18n/ko/pages/client/index.md @@ -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 --- # 클라이언트 {#the-client} @@ -11,13 +11,23 @@ translation: ## 첫 번째 클라이언트 {#your-first-client} -```python title="client.py" hl_lines="14-18" +클라이언트에게는 통신할 서버가 필요합니다. 이 페이지의 모든 예제가 연결하는 대상이 바로 이 Bookshop 서버입니다. `server.py`로 저장하고 HTTP로 실행해 두세요. + +```python title="server.py" --8<-- "docs_src/client/tutorial001.py" ``` -맨 위의 서버는 연결할 대상을 마련하기 위해 있을 뿐입니다. 클라이언트는 강조 표시된 다섯 줄입니다. +```console +uv run mcp run server.py --transport streamable-http +``` + +이렇게 하면 `http://localhost:8000/mcp`에서 서버가 제공됩니다. 클라이언트는 별도의 프로그램입니다. `client.py`로 저장하고 두 번째 터미널에서 `python client.py`를 실행하세요. -* `Client(mcp)`에는 **서버 객체 자체**를 넘깁니다. 이것이 인메모리 트랜스포트입니다. 서브프로세스도, 포트도, HTTP도 없습니다. 이 페이지의 모든 예제와 앞으로 작성할 모든 테스트가 이 방식으로 연결합니다. +```python title="client.py" hl_lines="7-11" +--8<-- "docs_src/client/tutorial001_client.py" +``` + +* `Client("http://localhost:8000/mcp")`에는 **URL**을 넘기므로, 방금 시작한 서버에 Streamable HTTP로 연결합니다. * `async with`가 **생명 주기**입니다. 들어가면 연결하고 협상하며, 나오면 연결을 끊습니다. `connect()` / `close()` 쌍은 없으며, 블록이 끝난 뒤에는 `Client`를 재사용할 수 없습니다. * 블록 안에서는 연결 정보가 이미 평범한 프로퍼티로 준비되어 있습니다. @@ -25,10 +35,10 @@ translation: `Client`는 위치 인자 하나를 받고, 그 타입으로 트랜스포트를 결정합니다. -* `MCPServer`(또는 저수준 `Server`) 인스턴스: **프로세스 내부**에서 연결합니다. -* URL 문자열(`Client("http://localhost:8000/mcp")`): 프로덕션 경로인 Streamable HTTP입니다. -* `StdioServerParameters`: **서브프로세스**로 실행할 명령이며, 그 stdin과 stdout을 통해 통신합니다. +* URL 문자열(`Client("http://localhost:8000/mcp")`): 실제 배포에 쓰는 트랜스포트인 Streamable HTTP입니다. +* `StdioServerParameters`: 로컬 **서브프로세스**로 실행할 명령이며, 그 stdin과 stdout을 통해 통신합니다. * **트랜스포트**: `async with ... as (read, write)`로 사용할 수 있는 모든 것, 예를 들어 직접 만든 HTTP 클라이언트를 감싸는 `streamable_http_client(url, http_client=...)`입니다. +* `MCPServer`(또는 저수준 `Server`) 인스턴스: 서브프로세스도 포트도 없이 **프로세스 내부**에서 연결합니다. 이 방식은 테스트를 위한 것이며, **[테스트](../get-started/testing.md)**가 이를 기반으로 합니다. 이 페이지의 나머지 내용은 네 가지 모두에서 동일합니다. 헤더, 서브프로세스, 타임아웃, `Transport` 프로토콜은 별도의 페이지인 **[클라이언트 트랜스포트](transports.md)**에서 다룹니다. @@ -49,11 +59,11 @@ translation: ## 도구 목록 조회 {#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()`는 `ListToolsResult`를 반환하며, 도구는 `.tools`에 들어 있습니다. 각 도구는 호스트가 모델에 건네는 완전한 정의입니다. +`list_tools()`는 `ListToolsResult`를 반환하며, 도구는 `.tools`에 들어 있습니다. 각 도구는 호스트가 모델에 건네는 완전한 정의입니다. 첫 번째 도구는 다음과 같습니다. ```python tool.name # 'search_books' @@ -77,6 +87,8 @@ tool.description # 'Search the catalog by title or author.' 이 스키마는 UI가 인자 입력 폼을 렌더링하는 데 필요한 전부이자, 모델이 유효한 인자를 만들어 내는 데 필요한 전부입니다. +두 번째 도구인 `lookup_book`은 `title=` 없이 등록되었으므로 `tool.title`이 `None`입니다. + !!! tip `title`은 선택 사항이므로, 사람에게 도구를 보여 주는 UI는 무엇을 표시할지 골라야 합니다. `title`이 있으면 쓰고, 없으면 `name`을 씁니다. `from mcp.shared.metadata_utils import get_display_name`이 정확히 그 일을 하며, @@ -86,7 +98,7 @@ tool.description # 'Search the catalog by title or author.' `call_tool(name, arguments)`는 도구를 실행하고 `CallToolResult`를 돌려줍니다. -```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 @@ result.is_error # False 리소스 동작은 짝을 이룹니다. 목록을 조회하는 방법이 둘, 읽는 방법이 하나입니다. -```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 @@ result.is_error # False ## 프롬프트 {#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 @@ message.content # TextContent(type='text', text='Recommend one poetry book from 자동 완성 핸들러가 있는 서버는 사용자가 입력하는 동안 프롬프트와 리소스 템플릿 인자를 자동 완성할 수 있습니다. -```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 @@ message.content # TextContent(type='text', text='Recommend one poetry book from 모든 `list_*` 메서드는 `cursor=` 키워드를 받고, 모든 결과에는 `next_cursor`가 있습니다. `next_cursor`가 `None`이면 전부 받은 것입니다. -```python title="client.py" hl_lines="22-30" +```python title="client.py" hl_lines="7-15" --8<-- "docs_src/client/tutorial007.py" ``` -이 루프는 어떤 서버에 대해서도 올바릅니다. `MCPServer`는 모든 것을 한 페이지에 반환하므로 `next_cursor`는 `None`이고 루프는 한 번만 돌며, 그래서 대부분의 코드는 이 루프를 작성하지 않습니다. 실제로 페이지를 나누는 서버와 커서가 따르는 규칙은 **[페이지네이션](../advanced/pagination.md)**에서 다룹니다. +`list_all_tools`는 어떤 서버에 대해서도 올바르게 동작합니다. `MCPServer`는 모든 것을 한 페이지에 반환하므로 `next_cursor`는 `None`이고 루프는 한 번만 돌며, 그래서 대부분의 코드는 이 루프를 작성하지 않습니다. 실제로 페이지를 나누는 서버와 커서가 따르는 규칙은 **[페이지네이션](../advanced/pagination.md)**에서 다룹니다. ## 테스트에서 {#in-tests} -프로세스도 포트도 없는 `Client(mcp)`는 그 자체로 이미 서버의 테스트 하네스입니다. +이 페이지의 모든 `client.py`는 HTTP로 `server.py`에 연결했습니다. 테스트에서는 네트워크를 건너뛰고 `Client`에 서버 객체 자체를 넘깁니다. `from server import mcp`를 한 다음 `Client(mcp)`를 만들면 됩니다. 프로세스도 포트도 없으며, 위의 모든 메서드가 똑같이 동작합니다. -이를 위해 만들어진 생성자 플래그가 하나 있습니다. `Client(mcp, raise_exceptions=True)`입니다. 인메모리 연결에서만 효과가 있으며, 이를 설명하고 전체 패턴을 구축하는 페이지는 **[테스트](../get-started/testing.md)**입니다. +이를 위해 만들어진 생성자 플래그가 하나 있습니다. `Client(mcp, raise_exceptions=True)`입니다. 프로세스 내부 연결에서만 효과가 있으며, 이를 설명하고 전체 패턴을 구축하는 페이지는 **[테스트](../get-started/testing.md)**입니다. ## 요약 {#recap} -* `Client(x)`는 서버 객체에는 인메모리로, URL 문자열에는 Streamable HTTP로, 그 밖의 것에는 트랜스포트를 통해 연결합니다. +* `Client(x)`는 URL 문자열에는 Streamable HTTP로 연결하고, `StdioServerParameters`에는 서브프로세스를 실행하며, 트랜스포트에는 직접 들어가고, 테스트에서는 서버 객체 자체를 받습니다. * `async with`가 생명 주기의 전부입니다. 그 안에서는 `server_capabilities`와 `protocol_version`이 이미 채워져 있으며, 서버가 제공하는 경우 `server_info`와 `instructions`도 마찬가지입니다. * `list_tools()`는 각 도구의 `name`, `title`, `description`, `input_schema`를 제공합니다. * `call_tool()`은 모델을 위한 `content`, 코드를 위한 `structured_content`, 그리고 `is_error`를 반환합니다. 예외를 발생시키는 도구는 예외가 아니라 결과입니다. diff --git a/i18n/ko/pages/client/oauth-clients.md b/i18n/ko/pages/client/oauth-clients.md index 534311a96f..91d99477df 100644 --- a/i18n/ko/pages/client/oauth-clients.md +++ b/i18n/ko/pages/client/oauth-clients.md @@ -1,6 +1,6 @@ --- translation: - sections: [c6899d3892bd9fa0, 79372cff3cc48a88, 63878d29e87c3e73, 13175843d3588af4, e7e2b9fd516f77de, 758f06399b513c1f, a05d7278487d610b] + sections: [c6899d3892bd9fa0, 79372cff3cc48a88, c2dae1ebe2ebd543, 13175843d3588af4, df06056fb16b3846, 758f06399b513c1f, a05d7278487d610b] tool: 1 --- # OAuth 클라이언트 {#oauth-clients} @@ -81,18 +81,20 @@ translation: `Client`가 처음 요청을 보내면 서버는 `401`로 응답합니다. 그러면 프로바이더가 이어받습니다. -1. **디스커버리.** `WWW-Authenticate` 헤더를 읽고, `/.well-known/oauth-protected-resource`에서 서버의 Protected Resource Metadata를 가져오고, 어느 인가 서버가 이 리소스를 보호하는지 알아낸 뒤, **그** 서버의 메타데이터를 가져옵니다. +1. **디스커버리.** `WWW-Authenticate` 헤더를 읽고, `/.well-known/oauth-protected-resource`에서 서버의 Protected Resource Metadata를 가져오고, 어느 인가 서버가 이 리소스를 보호하는지 알아낸 뒤, **그** 서버의 메타데이터를 가져옵니다. (리소스 메타데이터를 게시하지 않는 오래된 서버에는 대신 그 서버 자신의 오리진에서 인가 서버 메타데이터를 요청합니다.) 어느 경우든 메타데이터의 `issuer` 값은 그 메타데이터를 가져온 대상 서버를 가리켜야 하며, 다른 값이면 거부합니다. 2. **등록.** 저장소에 아무것도 없으면 `OAuthClientMetadata`로 동적으로 등록하고 결과를 저장합니다. 3. **인가.** PKCE 쌍과 `state`를 생성하고, 인가 URL을 조립하고, `redirect_handler`를 await한 다음, 코드를 받기 위해 `callback_handler`를 await합니다. 4. **교환.** 코드를 `OAuthToken`으로 교환해 저장하고, 원래 요청에 `Authorization: Bearer ...`를 붙여 다시 보냅니다. 그다음부터는 조용합니다. 토큰은 저장소에서 꺼내 쓰고, 만료된 액세스 토큰은 리프레시 토큰으로 갱신하며, 그 어느 것도 통하지 않을 때에만 흐름을 다시 실행합니다. +이 모든 요청에 적용되는 트랜스포트 규칙이 하나 있습니다. 바깥의 MCP 요청과 마찬가지로, 같은 오리진에 머물면서 메서드를 유지하는 리다이렉트(이를테면 끝 슬래시를 붙이는 307/308)만 따르고, 그 밖의 리다이렉트는 해당 URL이 응답하지 않는 것으로 취급합니다. + 이 가운데 직접 작성한 코드는 하나도 없습니다. 키워드 인자가 두 개 더 남아 있는데(`client_metadata_url`과 `validate_resource_url`), 이 파일에는 둘 다 필요 없습니다. 알아 둘 만한 것은 `client_metadata_url`이며, 아래에 별도 섹션이 있습니다. ### 직접 해 보기 {#try-it} -이 문서의 예제 대부분은 인메모리 `Client(server)`로 확인할 수 있습니다. 이 예제는 아닙니다. 이 흐름의 핵심이 HTTP `401`인데, 인메모리 클라이언트와 서버 사이에는 HTTP가 없기 때문입니다. +테스트에서 쓰는 인메모리 `Client(server)`는 여기서는 도움이 되지 않습니다. 이 흐름의 핵심이 HTTP `401`인데, 인메모리 클라이언트와 서버 사이에는 HTTP가 없기 때문입니다. 리포지토리에는 실제로 동작하는 버전이 들어 있습니다. `examples/servers/simple-auth/`는 독립 실행형 인가 서버와 보호된 MCP 서버를 실행하고, `examples/clients/simple-auth-client/`는 이 페이지의 클라이언트를 작은 CLI로 키운 것입니다. 그 README에 두 명령이 있습니다. 서버를 시작하고, 그 서버를 대상으로 클라이언트를 실행하면 네 단계가 지나가는 모습을 볼 수 있습니다. @@ -110,13 +112,14 @@ URL은 루트가 아닌 경로를 가진 HTTPS여야 합니다. 그 외에는 `ClientCredentialsOAuthProvider`는 사람만 빠진 똑같은 `httpx2.Auth`입니다. -```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" ``` 달라진 점은 다음과 같습니다. * `OAuthClientMetadata`도 핸들러도 없습니다. `client_id`와 `client_secret`을 전달하면 프로바이더가 이를 감싸는 최소한의 `client_credentials` 등록을 만들고 동적 등록은 완전히 건너뜁니다. +* `issuer`는 그 자격 증명을 발급한 인가 서버를 지목합니다. 그 서버의 `/.well-known/oauth-authorization-server` 문서가 반환하는 `issuer` 값을 쓰세요. 디스커버리는 위와 똑같이 실행되지만, 토큰 요청은 오직 **그** 발급자의 메타데이터로만 조립됩니다. MCP 서버가 다른 곳을 가리키면 흐름은 대신 `OAuthFlowError`와 함께 멈춥니다. 이 값을 생략하는 것은 지원 중단 예정이며 3.0에서는 필수가 됩니다(**[지원 중단 예정 기능](../deprecated.md#deprecated-sdk-helpers)** 참고). 그때까지는 프로바이더가 경고를 내고 디스커버리가 찾아낸 인가 서버를 그대로 씁니다. * `scope`는 공백으로 구분한 문자열로, OAuth의 전송 형식입니다. * 그 아래는 모두 동일합니다. 같은 `TokenStorage`, 같은 `httpx2.AsyncClient(auth=...)`, 같은 `streamable_http_client`를 씁니다. @@ -129,8 +132,8 @@ URL은 루트가 아닌 경로를 가진 HTTPS여야 합니다. 그 외에는 `mcp.client.auth.extensions.client_credentials`에는 프로바이더가 하나 더 있습니다. 공유 시크릿 대신 JWT로 인증하는 클라이언트를 위한 **`PrivateKeyJWTOAuthProvider`**입니다 (`private_key_jwt`, 즉 키 쌍과 워크로드 아이덴티티 방식). 같은 패턴을 따릅니다. - 하나를 생성해 `auth=`에 넣으면 됩니다. 같은 모듈에는 그 어설션을 만드는 두 헬퍼인 - `SignedJWTParameters`와 `static_assertion_provider`도 들어 있습니다. + 하나를 생성해(선택 인자 `issuer`를 똑같이 받습니다) `auth=`에 넣으면 됩니다. 같은 모듈에는 + 그 어설션을 만드는 두 헬퍼인 `SignedJWTParameters`와 `static_assertion_provider`도 들어 있습니다. 사람이 없는 상황이 하나 더 있습니다. 클라이언트가 기업에 속해 있고, 어느 MCP 서버에 접근할 수 있는지를 사용자가 아니라 그 기업의 아이덴티티 공급자가 결정하는 경우입니다. 이는 고유한 신뢰 모델을 가진 다른 그랜트이며, 별도 페이지인 **[아이덴티티 어설션](identity-assertion.md)**에서 다룹니다. diff --git a/i18n/ko/pages/client/session-groups.md b/i18n/ko/pages/client/session-groups.md index be15ae2084..fc78a899a6 100644 --- a/i18n/ko/pages/client/session-groups.md +++ b/i18n/ko/pages/client/session-groups.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-groups} @@ -78,7 +78,7 @@ translation: ## 요약 {#recap} * `ClientSessionGroup`은 여러 서버 연결을 담고 도구, 리소스, 프롬프트를 각각 하나의 `dict`로 합칩니다. -* 서버마다 `connect_to_server(params)`를 호출합니다. `Client`가 받는 서버 객체나 URL이 아니라 트랜스포트 매개변수를 받습니다. +* 서버마다 `connect_to_server(params)`를 호출합니다. `Client`가 받는 URL이나 `Transport`가 아니라 트랜스포트 매개변수를 받습니다. * `group.call_tool(name, arguments)`는 소유한 서버로 알아서 라우팅합니다. * 이름은 그룹 전체에서 고유해야 합니다. `search` 도구를 가진 두 서버는 그대로는 공존할 수 없습니다. * `component_name_hook=`은 등록되는 모든 이름을 다시 씁니다. 딕셔너리 키는 바뀌지만 전송되는 이름은 바뀌지 않습니다. diff --git a/i18n/ko/pages/client/transports.md b/i18n/ko/pages/client/transports.md index 801f67be94..19b50e2ed5 100644 --- a/i18n/ko/pages/client/transports.md +++ b/i18n/ko/pages/client/transports.md @@ -1,6 +1,6 @@ --- translation: - sections: [9cac816674181eb0, 0700f337babcd4dd, 2bde0dd58cdf00f5, 40b4916d82eaf1d4, 3d0832f39b0d7059, dfa4446556badef0, 5bd93be2ab2ecb9c] + sections: [9cac816674181eb0, 7c157764133fea1f, 40b4916d82eaf1d4, 10d151f2cc75317f, 3d0832f39b0d7059, 92742ba36533633d, 0aeca6145e7bd302] tool: 1 --- # 클라이언트 트랜스포트 {#client-transports} @@ -11,30 +11,15 @@ translation: 각 트랜스포트의 **서버** 쪽(`mcp.run()`이 하는 일과 배포 대상)은 **[서버 실행하기](../run/index.md)**에서 다룹니다. -## 인메모리 {#in-memory} - -서버 객체 자체를 전달하세요. - -```python title="client.py" hl_lines="14" ---8<-- "docs_src/client_transports/tutorial001.py" -``` - -서브프로세스도, 포트도, 네트워크를 오가는 바이트도 없습니다. 클라이언트와 서버는 같은 프로세스 안의 두 객체일 뿐이지만, 호출은 여전히 실제 프로토콜 계층을 거칩니다. `search_books`는 HTTP를 통할 때와 똑같이 나열되고, 검증되고, 호출됩니다. - -덕분에 이 방식은 동시에 두 가지 역할을 합니다. - -* **테스트 도구.** 이 문서의 모든 예제는 이 방식으로 실행되며, **[테스트](../get-started/testing.md)** 페이지는 전체 패턴을 이 방식 위에 구축합니다. -* **임베딩 API.** 서버를 직접 생성하는 애플리케이션은 도구를 호출하기 위해 네트워크를 거칠 필요가 없습니다. - ## Streamable HTTP {#streamable-http} -URL 문자열을 전달하면 **Streamable HTTP**를 얻습니다. 배포 시 사용하는 트랜스포트입니다. +URL 문자열을 전달하면 **Streamable HTTP**를 사용합니다. 배포할 때 그 뒤에 서버를 두는 트랜스포트이자, 가장 먼저 선택해야 할 트랜스포트입니다. ```python title="client.py" hl_lines="5" --8<-- "docs_src/client_transports/tutorial002.py" ``` -이것이 프로덕션 클라이언트의 전부입니다. `Client`가 URL을 `streamable_http_client(...)`로 감싸 주며, 그 아래에는 MCP에 맞게 설정된 `httpx2.AsyncClient`가 있습니다. `follow_redirects=True`, connect/write/pool에 30초 타임아웃, 그리고 서버가 응답 스트림을 열어 둘 수 있으므로 read에는 300초 타임아웃이 적용됩니다. +이것이 프로덕션 클라이언트의 전부입니다. `Client`가 URL을 `streamable_http_client(...)`로 감싸 주며, 그 아래에는 MCP에 맞게 설정된 `httpx2.AsyncClient`가 있습니다. connect/write/pool에는 30초 타임아웃, 그리고 서버가 응답 스트림을 열어 둘 수 있으므로 read에는 300초 타임아웃이 적용됩니다. !!! check 생성만 한 `Client`는 **연결되지 않은** 상태입니다. 생성은 트랜스포트를 고를 뿐이고, @@ -50,7 +35,7 @@ URL 문자열을 전달하면 **Streamable HTTP**를 얻습니다. 배포 시 `Authorization` 헤더, 쿠키, 프록시, mTLS, 다른 타임아웃이 필요해지는 순간, `httpx2.AsyncClient`를 직접 만들어 `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 @@ TLS 관련 참고 사항이 하나 있습니다. `httpx2`는 번들된 CA 목록 !!! info `httpx2`는 익숙한 `httpx` API를 그대로 유지하므로, `httpx`를 안다면 여기서 인증, 프록시, 이벤트 훅, 재시도, 연결 제한을 다루는 방법도 이미 아는 셈입니다. SDK는 그 위에 아무것도 - 더하지 않고 아무것도 빼지 않습니다. OAuth가 연결되는 지점도 여기입니다. + 더하지 않고 아무것도 빼지 않습니다. 단, [리디렉션 처리](#redirects)만은 예외입니다. OAuth가 연결되는 지점도 여기입니다. `httpx2.AsyncClient(auth=OAuthClientProvider(...))`. 전체 흐름은 **[OAuth 클라이언트](oauth-clients.md)**에서 다룹니다. +### 리디렉션 {#redirects} + +트랜스포트는 전달받은 URL에 연결하며, 그 오리진에만 연결합니다. + +* 같은 스킴, 호스트, 포트 안에 머무는 `307`/`308` 리디렉션은 따라가며, 같은 호스트에서 `http://` → `https://`로 가는 리디렉션도 따라갑니다. 흔한 `/mcp` → `/mcp/` 후행 슬래시 리디렉션이 여기에 포함됩니다. +* 그 밖의 곳으로 가는 리디렉션은 따라가지 **않습니다**. 호출은 다음 오류로 실패합니다. + + ```text + MCPError: Redirect to https://other.example.com/mcp not followed; use that URL as the endpoint if it is the intended server + ``` + + 그 URL이 의도한 서버라면 설정에 그 URL을 넣으세요. 아니라면 서버나 그 앞의 프록시 설정이 잘못된 것입니다. + +이 규칙은 전달하는 어떤 `httpx2.AsyncClient`에도 적용됩니다. MCP 요청에는 `follow_redirects` 설정이 어느 방향으로든 참조되지 않습니다. SDK의 OAuth 프로바이더도 자체 요청에 같은 규칙을 적용합니다. + +!!! tip + `Redirect to http://… not followed: it would downgrade this HTTPS endpoint to plain HTTP`는 + 서버가 자신이 모르는 TLS 종료 프록시 뒤에 있어 `http://` 리디렉션을 내보내고 있다는 뜻입니다. + 이는 서버 쪽에서 고치거나(**[배포와 확장](../run/deploy.md#behind-a-tls-terminating-proxy)**), + 메시지가 제안하는 정확한 `https://…/` URL을 사용해 해결합니다. + ## stdio {#stdio} **stdio** 서버는 서브프로세스입니다. 클라이언트가 이를 실행하고, stdin에 JSON-RPC를 쓰고, stdout에서 JSON-RPC를 읽습니다. 데스크톱 호스트가 사용자 컴퓨터에서 서버를 실행하는 방식이 바로 이것입니다. 호스트는 **곧** 이 코드에 UI를 더한 것이며, **[실제 호스트에 연결하기](../get-started/real-host.md)**는 같은 관계를 호스트 쪽에서 설정 파일로 바라본 것입니다. @@ -105,6 +111,18 @@ TLS 관련 참고 사항이 하나 있습니다. `httpx2`는 번들된 CA 목록 API 키가 필요한 서버는 거기서 키를 찾지 못합니다. `env=`로 명시적으로 전달하세요. 해당 변수는 허용 목록 위에 병합됩니다. 위 예제에서 `BOOKSHOP_API_KEY`가 하는 일이 바로 이것입니다. +## 인메모리 {#in-memory} + +테스트에서는 배포할 것도, 실행할 것도 없습니다. 서버 객체 자체를 전달하세요. + +```python hl_lines="14" +--8<-- "docs_src/client_transports/tutorial001.py" +``` + +서브프로세스도, 포트도, 네트워크를 오가는 바이트도 없습니다. 클라이언트와 서버는 같은 프로세스 안의 두 객체이며, 호출은 여전히 실제 프로토콜 계층을 거칩니다. `search_books`는 HTTP를 통할 때와 똑같이 나열되고, 검증되고, 호출됩니다. **[테스트](../get-started/testing.md)** 페이지는 전체 패턴을 이 방식 위에 구축합니다. + +같은 형태가 임베딩 API 역할도 겸합니다. 서버를 직접 생성하는 애플리케이션은 네트워크를 거치지 않고 서버의 도구를 호출할 수 있습니다. + ## SSE {#sse} `mcp.client.sse`의 `sse_client(url)`은 Streamable HTTP로 대체된 이전 HTTP 트랜스포트입니다. 아직 이 방식을 쓰는 서버와 통신하려면 `Client(sse_client("http://localhost:8000/sse"))`처럼 같은 방식으로 감싸서 사용하되, 새로운 것을 이 위에 만들지는 마세요. @@ -113,15 +131,16 @@ TLS 관련 참고 사항이 하나 있습니다. `httpx2`는 번들된 CA 목록 `Client`에게 위의 모든 것은 같은 것입니다. -**트랜스포트**란 `(read, write)` 메시지 스트림 쌍을 내어주는 비동기 컨텍스트 매니저라면 무엇이든 해당합니다. 정식으로는 `mcp.client`의 `Transport` 프로토콜입니다. `Client`는 인자를 타입으로 구분합니다. 서버 객체는 프로세스 내에서 연결하고, `str`은 `streamable_http_client(url)`이 되며, `StdioServerParameters`는 `stdio_client(params)`가 되고, 그 밖의 것은 트랜스포트로 직접 진입합니다. 마지막 규칙 덕분에 `stdio_client(...)`, `streamable_http_client(...)`, `sse_client(...)`가 모두 같은 자리에 들어가고, 직접 만든 트랜스포트도 쓸 수 있습니다. +**트랜스포트**란 `(read, write)` 메시지 스트림 쌍을 내어주는 비동기 컨텍스트 매니저라면 무엇이든 해당합니다. 정식으로는 `mcp.client`의 `Transport` 프로토콜입니다. `Client`는 인자를 타입으로 구분합니다. `str`은 `streamable_http_client(url)`이 되고, `StdioServerParameters`는 `stdio_client(params)`가 되며, 서버 객체는 프로세스 내에서 연결하고, 그 밖의 것은 트랜스포트로 직접 진입합니다. 마지막 규칙 덕분에 `stdio_client(...)`, `streamable_http_client(...)`, `sse_client(...)`가 모두 같은 자리에 들어가고, 직접 만든 트랜스포트도 쓸 수 있습니다. ## 요약 {#recap} -* `Client(mcp)`(서버 객체)는 인메모리로 연결합니다. 테스트와 임베딩에 사용하세요. * `Client("http://.../mcp")`(URL)는 프로덕션 트랜스포트인 Streamable HTTP로 연결합니다. * 헤더, 인증, 프록시, 타임아웃은 `streamable_http_client(url, http_client=...)`에 전달하는 `httpx2.AsyncClient`에 설정합니다. `headers=` 키워드는 없습니다. +* 리디렉션은 URL 자체의 오리진 안에서만(후행 슬래시 `307`/`308`) 따라가며, 같은 호스트의 `http`→`https`도 따라갑니다. 그 밖의 경우는 `Redirect to … not followed`로 실패하므로 최종 URL을 설정하세요. * stdio는 `Client(StdioServerParameters(...))`입니다. 자식 프로세스의 stderr를 다른 곳으로 돌릴 때만 직접 `stdio_client(...)`로 감싸세요. * 서브프로세스는 현재 환경이 아니라 허용 목록에 있는 환경 변수만 받습니다. `env=`로 여기에 추가합니다. +* `Client(mcp)`(서버 객체)는 인메모리로 연결합니다. 테스트에서 사용하거나, 서버를 만든 애플리케이션 안에 서버를 임베딩할 때 사용하세요. * 트랜스포트는 `async with x as (read, write)`로 쓸 수 있는 것이면 무엇이든 됩니다. `Client`는 서버 객체, URL, `StdioServerParameters`가 아닌 것은 모두 그 프로토콜에 그대로 넘깁니다. * `Client`를 생성하면 트랜스포트가 정해집니다. `async with`가 이를 엽니다. diff --git a/i18n/ko/pages/deprecated.md b/i18n/ko/pages/deprecated.md index 63d1d27a41..cc1413a606 100644 --- a/i18n/ko/pages/deprecated.md +++ b/i18n/ko/pages/deprecated.md @@ -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 --- # 지원 중단 예정 기능 {#deprecated-features} -2026-07-28 사양은 다섯 가지를 퇴역시킵니다. SDK는 여전히 이 다섯 가지를 모두 구현하며, 이제 모두에 **지원 중단 예정(deprecated) 경고**가 붙습니다. SDK 헬퍼 하나는 별도의 이유로 지원 중단 예정이며 [페이지 끝](#deprecated-sdk-helpers)에 정리되어 있습니다. +2026-07-28 사양은 다섯 가지를 퇴역시킵니다. SDK는 여전히 이 다섯 가지를 모두 구현하며, 이제 모두에 **지원 중단 예정(deprecated) 경고**가 붙습니다. 몇 가지 SDK 수준의 지원 중단 예정 항목은 사양과 별개의 이유로 지정된 것으로, [페이지 끝](#deprecated-sdk-helpers)에 정리되어 있습니다. 아래 표는 지원 중단 예정인 각 기능의 이름, 사라지는 이유, 그리고 대신 사용할 대체 수단을 정리한 것입니다. @@ -136,11 +136,13 @@ warnings.filterwarnings("ignore", category=MCPDeprecationWarning) ## 지원 중단 예정 SDK 헬퍼 {#deprecated-sdk-helpers} -이것은 사양 변경이 아니라 더 나은 대체 수단이 있는 SDK 내부 구현일 뿐입니다. 같은 `MCPDeprecationWarning`으로 경고하며 3.0에서 제거됩니다. +이것은 사양 변경이 아니라 더 나은 대체 수단이 있는 SDK 사용 방식일 뿐입니다. 같은 `MCPDeprecationWarning`으로 경고하며, 3.0에서 기존 형태가 제거됩니다. | 지원 중단 예정 | 대신 할 일 | |---|---| | `FuncMetadata.call_fn_with_arg_validation()` | `FuncMetadata.validate_arguments()`를 호출한 뒤 `FuncMetadata.call_fn()`을 호출하세요. `FuncMetadata`를 직접 다루는 코드(예를 들어 사용자 정의 `Tool` 하위 클래스)만 이 메서드를 호출했습니다. | +| `validate_token_resource=` 없이 쓰는 `AuthSettings(resource_server_url=...)` | 값을 지정하세요. `True`로 설정하면 검증기가 `resource_server_url`용으로 발급되었다고 보고하지 않는 베어러 토큰을 서버가 거부하고, `False`는 검증기가 토큰의 대상(audience)을 직접 확인한다는 뜻입니다(**[인가](run/authorization.md#a-token-verifier)** 참고). 지정하지 않으면 `False`처럼 동작하며, 3.0부터는 `resource_server_url`이 설정되어 있으면 `True`가 기본값이 됩니다. | +| `issuer=` 없이 쓰는 `ClientCredentialsOAuthProvider(...)` 또는 `PrivateKeyJWTOAuthProvider(...)` | 자격 증명을 발급한 인가 서버를 지정하는 `issuer=`를 전달하세요(**[OAuth 클라이언트 작성하기](client/oauth-clients.md#machine-to-machine)** 참고). 이 값이 없으면 어느 인가 서버가 자격 증명을 받을지를 MCP 서버가 결정하게 됩니다. 3.0부터는 이 키워드가 필수입니다. | ## 요약 {#recap} @@ -149,7 +151,7 @@ warnings.filterwarnings("ignore", category=MCPDeprecationWarning) * 지원 중단 예정은 권고 사항입니다. 와이어 변경은 없고, 2026년 이전 세션에서는 모든 것이 계속 동작하며, 눈에 띄는 `MCPDeprecationWarning`이 나옵니다(`UserWarning`이므로 기본적으로 켜져 있습니다). * 샘플링과 루트는 추가로 2026-07-28 세션에는 없는 백채널이 필요합니다. 최신 연결에서는 경고를 낸 뒤 예외를 발생시킵니다. * `warnings.filterwarnings("ignore", category=MCPDeprecationWarning)`은 카테고리 전체를 끄고, pytest의 `"error::mcp.MCPDeprecationWarning"`은 이를 테스트 실패로 바꿉니다. -* SDK 헬퍼 하나인 `FuncMetadata.call_fn_with_arg_validation()`은 별도로 지원 중단 예정이며 3.0에서 제거됩니다. +* [SDK 수준의 지원 중단 예정 항목](#deprecated-sdk-helpers)도 같은 규칙을 따릅니다. 지금은 경고를 내고, 3.0에서 기존 형태가 제거됩니다. * 새 코드는 이 기능 중 어느 것에도 기반해서는 안 됩니다. 이 문서의 다른 모든 페이지는 현재 API를 설명합니다. diff --git a/i18n/ko/pages/get-started/first-steps.md b/i18n/ko/pages/get-started/first-steps.md index dab57cc989..19e9791b05 100644 --- a/i18n/ko/pages/get-started/first-steps.md +++ b/i18n/ko/pages/get-started/first-steps.md @@ -1,6 +1,6 @@ --- translation: - sections: [0d6c05bcbf836bf3, 59a7b14eeefc68c1, 7114d8d6daba203f, e8bbb56a98ba7bc9, 5138010f6159901c, f78da7c7c363d4c6, 220a939cab348686] + sections: [0d6c05bcbf836bf3, 9a78b5f6b44b18ab, 7114d8d6daba203f, e8bbb56a98ba7bc9, bfd2fd1153e71dac, 1615a994ef071fdd, 65c599fae991f245] tool: 1 --- # 첫걸음 {#first-steps} @@ -17,7 +17,7 @@ translation: * **클라이언트**는 호스트 안에 있으며 MCP로 통신합니다. 호스트는 연결된 서버마다 클라이언트를 하나씩 실행합니다. * **서버**는 이 SDK로 만드는 것입니다. 서버는 클라이언트에 여러 가지를 노출하며, 모델과 직접 대화하는 일은 없습니다. -직접 작성하는 것은 서버입니다. 호스트는 다른 누군가가 만든 제품입니다. SDK는 `Client`도 제공합니다. 서버를 테스트할 때 쓰게 되며, 이 페이지 뒷부분에서 다시 등장합니다. +직접 작성하는 것은 서버입니다. 호스트는 다른 누군가가 만든 제품입니다. SDK는 `Client`도 제공합니다. 호스트가 URL로 서버에 접속하거나 서버를 서브프로세스로 실행할 때 쓰는 바로 그 클래스입니다. 이 페이지 뒷부분에서 다시 등장하며, 작성한 서버를 테스트하는 수단이기도 합니다. ## 세 가지 프리미티브 {#the-three-primitives} @@ -82,22 +82,20 @@ Inspector에서 탭 세 개를 보았습니다. Inspector가 세 개라는 것 클라이언트가 연결하면 서버는 **기능**, 즉 어떤 부류의 요청에 응답할지를 선언합니다. 클라이언트는 이 선언을 보고 애초에 무엇을 요청할지 결정합니다. 이 선언을 작성한 적은 없습니다. `MCPServer`가 대신 선언합니다. -직접 확인해 보세요. SDK의 `Client`는 서버 객체를 그대로 받아 **인메모리**로 연결합니다(서브프로세스도, 포트도 없습니다). - -```python -import asyncio - -from mcp import Client - -from server import mcp +직접 확인해 보세요. 한쪽 터미널에서 `server.py`를 HTTP로 실행해 두세요. +```console +uv run mcp run server.py --transport streamable-http +``` -async def main() -> None: - async with Client(mcp) as client: - print(client.server_capabilities.model_dump(exclude_none=True)) +그리고 다른 터미널에서 클라이언트를 이 서버에 연결하세요. +```python title="client.py" hl_lines="7-8" +--8<-- "docs_src/first_steps/tutorial001_client.py" +``` -asyncio.run(main()) +```console +python client.py ``` ```text @@ -117,8 +115,9 @@ asyncio.run(main()) 없는 것에도 주목하세요. `completions`(리소스 템플릿과 프롬프트의 인자 자동 완성)에는 직접 작성하는 핸들러가 필요한데, 이 서버에는 핸들러가 없으므로 해당 기능이 빠져 있고, 올바르게 동작하는 클라이언트라면 요청하지 않습니다. 선택 사항은 모두 이 규칙을 따릅니다. 등록하면 기능이 나타납니다. **[자동 완성](../servers/completions.md)** 페이지가 이를 보여 줍니다. !!! info - `Client(mcp)`는 이 문서의 모든 예제를 테스트하는 데 쓰이는 바로 그 인메모리 클라이언트이며, - 작성한 서버도 같은 방식으로 테스트하게 됩니다. 이를 다루는 페이지가 따로 있습니다. **[테스트](testing.md)**입니다. + 이 `client.py`는 완전한 MCP 클라이언트이며, 이를 다루는 페이지는 **[클라이언트](../client/index.md)**입니다. + 테스트에서는 터미널도 포트도 건너뛰고 `Client(mcp)`처럼 `Client`에 서버 객체 자체를 넘깁니다. + 이 역시 전용 페이지가 있습니다. **[테스트](testing.md)**입니다. ## 작성하지 않은 것 {#what-you-did-not-write} @@ -127,7 +126,7 @@ asyncio.run(main()) * JSON Schema. `a: int, b: int`가 **곧** `add`의 스키마입니다. * 요청 핸들러. `tools/list`, `resources/read`, `prompts/get`은 모두 대신 처리됩니다. * 기능 선언. `MCPServer`가 대신 만들었습니다. -* 프로토콜 코드 단 한 줄. 버전 협상, JSON-RPC 프레이밍, 기능 교환은 모두 `mcp dev`와 `Client(mcp)` 안에서 일어났고, 눈에 보이지도 않았습니다. +* 프로토콜 코드 단 한 줄. 버전 협상, JSON-RPC 프레이밍, 기능 교환은 모두 `mcp dev`와 `client.py` 안에서 일어났고, 눈에 보이지도 않았습니다. 이 비율이야말로 SDK가 존재하는 이유입니다. @@ -138,6 +137,6 @@ asyncio.run(main()) * 프리미티브마다 데코레이터 하나면 됩니다. `@mcp.tool()`, `@mcp.resource(uri)`, `@mcp.prompt()`입니다. 이름, 설명, 스키마는 함수에서 가져옵니다. * `{param}`이 들어간 URI는 리소스 **템플릿**을 만들며, 구체적인 리소스와는 따로 나열됩니다. * 서버의 **기능**은 자동으로 선언되며, 클라이언트는 서버가 선언한 것만 요청합니다. -* `Client(mcp)`는 서버 객체에 인메모리로 연결합니다. 첫날부터 갖추는 테스트 하네스입니다. +* `Client("http://localhost:8000/mcp")`는 실행 중인 서버와 통신합니다. 대신 서버 객체를 넘겨 `Client(mcp)`로 만들면 첫날부터 쓰는 테스트 하네스가 됩니다. 다음은 **[실제 호스트에 연결하기](real-host.md)**입니다. 이 서버를 Claude Desktop이나 IDE 안에서 실제로 돌려 봅니다. 그다음은 **[테스트](testing.md)**입니다. 페이지 하나, 인메모리 클라이언트 하나면 동작하는지 추측할 일이 없어집니다. 그 뒤로는 프리미티브마다 전용 페이지가 이어지며, 모델이 주도하는 프리미티브인 **[도구](../servers/tools.md)**부터 시작합니다. diff --git a/i18n/ko/pages/get-started/testing.md b/i18n/ko/pages/get-started/testing.md index 2537f1b054..db1a83dc69 100644 --- a/i18n/ko/pages/get-started/testing.md +++ b/i18n/ko/pages/get-started/testing.md @@ -1,13 +1,13 @@ --- translation: - sections: ['4926721070127497', c52a1de2b6b32f40, 8e792bf8c7489ec6, 627195f7159e24ef] + sections: [5d13c2f0ba42c0d2, c52a1de2b6b32f40, 8e792bf8c7489ec6, 38552ea228b0a04f] tool: 1 --- # 테스트 {#testing} -Python SDK는 **인메모리 트랜스포트**를 갖춘 `Client` 클래스를 제공합니다. 서버 객체를 넘기면 그 서버에 직접 연결합니다. +SDK의 `Client` 클래스는 URL에 연결하거나 서브프로세스를 띄울 때 쓰는 바로 그 클래스인데, **인메모리**로도 연결합니다. 서버 객체를 넘기면 그 서버와 직접 통신합니다. -서브프로세스도 없습니다. 포트도 없습니다. 트랜스포트도 전혀 없습니다. FastAPI의 `TestClient`와 같은 발상입니다. +서브프로세스도 없습니다. 포트도 없습니다. 네트워크를 오가는 것도 전혀 없습니다. FastAPI의 `TestClient`와 같은 발상입니다. ## 기본 사용법 {#basic-usage} @@ -96,14 +96,14 @@ async def test_call_add_tool(client: Client): 테스트에서는 켜 두세요. 프로덕션 코드에서는 아무 의미가 없습니다. -## 기본값은 인프로세스 연결 {#in-process-by-default} +## 기본값은 세대 중립 {#era-neutral-by-default} !!! note `Client(mcp)`는 인프로세스로 연결하며 기본적으로 **세대 중립적**(era-neutral)입니다. 서버를 - 조사해 알맞은 프로토콜 경로를 고릅니다. 테스트가 레거시 전용 동작, 즉 샘플링이나 - 엘리시테이션(elicitation) 푸시, `message_handler` 같은 것을 검증한다면 `mode="legacy"`로 - 고정하고, 그 경우에는 `raise_exceptions=True`를 빼세요. 레거시 연결은 애초에 정제를 하지 - 않으며, 이 플래그는 실패를 테스트가 아니라 서버 태스크 안에서 다시 발생시키기 때문입니다. + 조사해 알맞은 프로토콜 경로를 고릅니다. 테스트가 레거시 전용 동작(샘플링이나 + 엘리시테이션(elicitation) 푸시, `message_handler`)을 검증한다면 `mode="legacy"`로 고정하고, + 그 경우에는 `raise_exceptions=True`를 빼세요. 레거시 연결은 애초에 정제를 하지 않으며, 이 + 플래그는 실패를 테스트가 아니라 서버 태스크 안에서 다시 발생시키기 때문입니다. 이 문서의 예제가 실제로 동작한다고 약속할 수 있는 것도 바로 그 한 줄 덕분입니다. 모든 예제 파일은 SDK 자체의 테스트 스위트에서 실행되며, 거의 전부가 정확히 이 클라이언트를 거칩니다. diff --git a/i18n/ko/pages/handlers/multi-round-trip.md b/i18n/ko/pages/handlers/multi-round-trip.md index 3955da968e..f2c577981c 100644 --- a/i18n/ko/pages/handlers/multi-round-trip.md +++ b/i18n/ko/pages/handlers/multi-round-trip.md @@ -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-round-trip-requests} @@ -164,7 +164,7 @@ TTL, 주체 바인딩, 요청 바인딩은 코덱의 일이 **아닙니다**. SD ## 2026-07-28 전용 결과 {#a-2026-07-28-result} -`InputRequiredResult`는 프로토콜 버전 **2026-07-28**에만 존재합니다. 인메모리 `Client(server)`는 이를 대신 협상하고, 네트워크를 통할 때는 `mode="auto"`가 이를 발견합니다. 연결한 뒤에는 `client.protocol_version`이 무엇을 얻었는지 알려 줍니다. +`InputRequiredResult`는 프로토콜 버전 **2026-07-28**에만 존재합니다. `Client`의 기본값인 `mode="auto"`는 어떤 연결에서든 이를 발견합니다. 연결한 뒤에는 `client.protocol_version`이 무엇을 얻었는지 알려 줍니다. !!! warning 2026 이전 세션에는 `InputRequiredResult`를 넣을 곳이 없습니다. `mode="legacy"` 연결에서 diff --git a/i18n/ko/pages/handlers/progress.md b/i18n/ko/pages/handlers/progress.md index 0b0bbd9876..d576a9171c 100644 --- a/i18n/ko/pages/handlers/progress.md +++ b/i18n/ko/pages/handlers/progress.md @@ -1,6 +1,6 @@ --- translation: - sections: [5315262fe26b33e1, 9d8e98840f1b78f0, 0284b215e85366c4, 8534d8dbb4053a70, 2966fac6fe697007] + sections: [5315262fe26b33e1, 9d8e98840f1b78f0, 52d6009a07e770ea, 8534d8dbb4053a70, 2966fac6fe697007] tool: 1 --- # 진행 상황 {#progress} @@ -29,19 +29,17 @@ translation: 클라이언트는 **호출 단위로** 수신을 선택합니다. `call_tool`에 `progress_callback=` 인자를 전달하면 됩니다. -```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,27 +54,31 @@ anyio.run(main) 콜백은 서버가 보고한 값 그대로, 즉 `progress`, `total`, `message`를 받는 `async` 함수입니다. !!! info - `Client(mcp)`는 서버 객체에 메모리 안에서 직접 연결하며, **[테스트](../get-started/testing.md)** - 페이지의 기반이 되는 것과 같은 클라이언트입니다. `progress_callback`은 `Client`가 어떤 트랜스포트를 - 쓰든 같은 매개변수입니다. 다만 곧 보게 될 **타이밍**은 인메모리 연결의 타이밍입니다. 인메모리 연결은 - 콜백을 인라인으로 실행하므로 모든 보고가 `call_tool`이 반환되기 전에 도착합니다. 실제 트랜스포트에서는 - 알림과 결과가 경쟁하므로, 느린 콜백은 `call_tool`이 반환된 뒤에도 여전히 실행 중일 수 있습니다. + `progress_callback`은 `Client`에 무엇을 넘겼든 같은 매개변수입니다. 여기서처럼 URL이든, + `StdioServerParameters`든, 테스트에서 쓰는 서버 객체든 마찬가지입니다. 다만 실제 트랜스포트에서는 + 타이밍에 주의하세요. 각 알림은 응답과 별도로 따로 전달되므로, 느린 콜백은 `call_tool`이 반환된 + 뒤에도 여전히 실행 중일 수 있습니다. 프로세스 내 테스트 연결만이 콜백을 인라인으로 실행하여 모든 + 보고가 먼저 도착하도록 보장합니다. ### 직접 해 보기 {#try-it} -`client.py`를 `server.py` 옆에 두고 실행하세요. +`server.py`를 HTTP로 서비스한 다음, 두 번째 터미널에서 클라이언트를 실행하세요. + +```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.'} ``` -서버의 `await ctx.report_progress(...)` 하나하나가 클라이언트에서 `show` 호출 하나가 되었고, 순서도 그대로이며, 두 줄 모두 `call_tool`이 반환되기 **전에** 출력되었습니다. 진행 상황은 결과에 묶여 오지 않고, 도구가 아직 작업하는 동안 스트리밍됩니다. +서버의 `await ctx.report_progress(...)` 하나하나가 클라이언트에서 `show` 호출 하나가 되었고, 순서도 그대로입니다. 진행 상황은 결과에 묶여 오지 않습니다. 도구가 아직 작업하는 동안 스트리밍됩니다. !!! warning `progress_callback`은 `Client`가 아니라 **호출**에 속합니다. 이를 위한 생성자 인자는 없습니다. diff --git a/i18n/ko/pages/protocol-versions.md b/i18n/ko/pages/protocol-versions.md index 033e968cea..be44b46c30 100644 --- a/i18n/ko/pages/protocol-versions.md +++ b/i18n/ko/pages/protocol-versions.md @@ -1,6 +1,6 @@ --- translation: - sections: [478fd619e5f90ef8, aef094a00e44e248, bab8cbf3449fa7e9, df1809b15a58335b, 5f9d8c2336ed0239, f54974398e43ddef, b24443dd78584870] + sections: [d98e337bf28845e0, dd642acbba36f615, f547b3e76da19c94, 149513378588da5c, 4e149ed992046709, f54974398e43ddef, b24443dd78584870] tool: 1 --- # 프로토콜 버전 {#protocol-versions} @@ -11,9 +11,17 @@ MCP에는 두 시대가 있습니다. `Client`가 대신 협상하므로 신경 쓸 일은 거의 없습니다. 이 페이지는 이를 제어하는 단 하나의 생성자 인자인 `mode=`와 이 값을 바꾸게 되는 세 가지 경우를 다룹니다. +이 페이지의 모든 코드 조각은 **[클라이언트](client/index.md)**에 나온 Bookshop `server.py`와 통신하는 `client.py`입니다. 한 터미널에서 그 서버를 시작하세요. + +```console +uv run mcp run server.py --transport streamable-http +``` + +그런 다음 두 번째 터미널에서 `python client.py`로 각 코드 조각을 실행하세요. + ## `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 @@ MCP에는 두 시대가 있습니다. 이것이 기능의 전부입니다. `Client` 하나로 어느 시대의 서버든 상대하며, 코드에 분기가 필요 없습니다. !!! info - `MCPServer`는 인메모리, stdio, Streamable HTTP 등 모든 트랜스포트에서 `server/discover`에 - 응답하므로, 직접 작성한 서버를 상대로는 `auto`가 항상 `2026-07-28`에 도달합니다. 폴백은 - 실제 2026년 이전 서버를 상대할 때만 발동하며, 바로 그때가 폴백이 필요한 순간입니다. + `MCPServer`는 Streamable HTTP, stdio, 그리고 테스트에서 쓰는 프로세스 내 연결까지 모든 + 트랜스포트에서 `server/discover`에 응답하므로, 직접 작성한 서버를 상대로는 `auto`가 항상 + `2026-07-28`에 도달합니다. 폴백은 실제 2026년 이전 서버를 상대할 때만 발동하며, 바로 그때가 + 폴백이 필요한 순간입니다. ## `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 @@ MCP에는 두 시대가 있습니다. `mode`에는 최신 프로토콜 버전 문자열도 넣을 수 있습니다. 현재 그 집합은 정확히 `["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 @@ ValueError: mode must be 'legacy', 'auto', or one of ['2026-07-28']; got '2025-0 그러니 보관해 두세요. `auto` 연결 후 `client.session.discover_result`에는 서버가 보낸 `DiscoverResult`가 그대로 담겨 있습니다. `supported_versions`, `capabilities`, `instructions`, 그리고 서버가 결과의 `_meta`에 새겨 넣은 신원 정보까지 포함됩니다. 다음번에는 이를 `prior_discover=`로 다시 넘기세요. -```python title="client.py" hl_lines="15 17" +```python title="client.py" hl_lines="8 10" --8<-- "docs_src/protocol_versions/tutorial004.py" ``` diff --git a/i18n/ko/pages/run/asgi.md b/i18n/ko/pages/run/asgi.md index 1c1795f046..46fe8f24fd 100644 --- a/i18n/ko/pages/run/asgi.md +++ b/i18n/ko/pages/run/asgi.md @@ -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 --- # 기존 앱에 추가하기 {#add-to-an-existing-app} @@ -98,7 +98,7 @@ Starlette의 `Host` 라우트도 같은 방식으로 동작합니다. 경로 대 --8<-- "docs_src/asgi/tutorial004.py" ``` -이제 클라이언트는 `/notes/mcp`가 아니라 `/notes`에 연결합니다. +이제 클라이언트는 `/notes/mcp`가 아니라 `/notes/`에 연결합니다. ## 브라우저 클라이언트를 위한 CORS {#cors-for-browser-clients} @@ -141,4 +141,4 @@ Starlette의 `Host` 라우트도 같은 방식으로 동작합니다. 경로 대 * 브라우저 클라이언트에는 CORS가 필요합니다. `Mcp-*` 요청 헤더를 위한 `allow_headers`, 응답을 위한 `expose_headers=["Mcp-Session-Id"]`입니다. * `@mcp.custom_route()`는 `/mcp` 옆에 인증되지 않는 평범한 HTTP 엔드포인트를 추가합니다. -서버가 실제 URL로 도달 가능해지면, **[클라이언트](../client/index.md)**는 서버 객체 대신 그 URL로 연결합니다. +서버가 실제 URL로 도달 가능해지면, **[클라이언트](../client/index.md)**는 그 URL로 서버에 연결합니다. diff --git a/i18n/ko/pages/run/authorization.md b/i18n/ko/pages/run/authorization.md index 3e2e68a83a..e26d3a9fb5 100644 --- a/i18n/ko/pages/run/authorization.md +++ b/i18n/ko/pages/run/authorization.md @@ -1,6 +1,6 @@ --- translation: - sections: [d62c13457fc4a534, 80e73abaca6e0652, d1dc4c54cd00ec9c, 14ad3bc7904036bb, 5225f127bc1b9c77, fe1626fdd5aad1da, 4556cb7ea1a04a31] + sections: [d62c13457fc4a534, 80e73abaca6e0652, 128a492d18295f64, 14ad3bc7904036bb, 54a6697833fcc1d9, fe1626fdd5aad1da, 811d083c1da8bcf6] tool: 1 --- # 인가 {#authorization} @@ -23,12 +23,12 @@ OAuth 용어로 말하면 서버는 **리소스 서버**입니다. 누구도 로 SDK는 유효한 토큰이 어떤 모습인지에 대해 아무런 의견이 없습니다. **`TokenVerifier`**를 구현해서 알려 주면 됩니다. -```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`는 async 메서드 하나를 가진 프로토콜입니다. `verify_token`은 `Authorization` 헤더에서 꺼낸 원시 토큰을 받아, 유효하면 **`AccessToken`**을, 유효하지 않으면 `None`을 반환합니다. 그 외에 구현할 것은 없습니다. -* 이 예제는 테이블에서 토큰을 조회합니다. 실제 구현은 JWT 서명을 검증하거나 인가 서버의 토큰 인트로스펙션 엔드포인트를 호출합니다. 그 코드는 직접 작성하는 것이고, SDK는 호출만 합니다. +* 이 예제는 테이블에서 토큰을 조회하며, 각 항목에는 그 토큰이 발급된 대상 리소스가 기록되어 있습니다. 실제 구현은 JWT 서명을 검증하거나 인가 서버의 토큰 인트로스펙션 엔드포인트를 호출하고, 토큰이 누구를 위해 발급되었는지(토큰의 `aud`)를 `AccessToken.resource`에 담아 알려 줍니다. 그 코드는 직접 작성하는 것이고, SDK는 호출만 합니다. * `token_verifier=`와 `auth=`는 항상 함께 다닙니다. 한쪽만 전달하면 `MCPServer(...)`가 요청을 하나도 처리하기 전에 `ValueError`를 발생시킵니다. `AuthSettings`는 리소스 서버의 공개 정보입니다. @@ -36,6 +36,10 @@ SDK는 유효한 토큰이 어떤 모습인지에 대해 아무런 의견이 없 * `issuer_url`: 토큰을 발급하는 인가 서버입니다. * `resource_server_url`: 이 MCP 엔드포인트의 공개 URL입니다. 토큰이 **어떤** 리소스를 위한 것인지 지칭하며, 디스커버리 문서가 위치하는 곳이기도 합니다. * `required_scopes`: 모든 토큰이 이 스코프를 전부 가지고 있어야 합니다. +* `validate_token_resource`: `AccessToken.resource`가 `resource_server_url`이 아닌 토큰은 모두 거부합니다. `resource_server_url`은 설정하고 이 값을 설정하지 않은 채로 두면 경고(`MCPDeprecationWarning`)가 발생하며 `False`로 동작합니다. 3.0에서는 리소스 서버의 기본값이 `True`가 됩니다. + * 인가 서버가 클라이언트가 요청한 `resource`에 토큰을 묶어 발급한다면 이 옵션을 켜세요. MCP 클라이언트는 이 값을 항상 보냅니다. `resource_server_url`은 클라이언트가 실제로 연결하는 URL과 정확히 같게 유지하세요. + * 인가 서버가 자체 audience 식별자(Auth0 API 식별자, Entra 애플리케이션 ID)를 사용한다면 이 옵션을 끄고, 대신 검증기에서 `aud`를 확인해 이 서버를 위한 토큰이 아니면 `None`을 반환하세요. + * `aud` 값이 리스트라면 `resource_server_url`과 같은 항목을 `resource`에 넣으세요. !!! tip SDK 저장소의 `examples/servers/simple-auth/`에는 실제 인가 서버의 @@ -91,7 +95,7 @@ SDK는 유효한 토큰이 어떤 모습인지에 대해 아무런 의견이 없 어떤 핸들러 안에서든 **`get_access_token()`**은 현재 요청에 대해 검증기가 반환한 `AccessToken`입니다. -```python title="server.py" hl_lines="4 32-35" +```python title="server.py" hl_lines="4 35-38" --8<-- "docs_src/authorization/tutorial002.py" ``` @@ -125,6 +129,6 @@ SDK는 리소스 서버 쪽 절반을 제공합니다. 검증하고, 알리고, * `token_verifier=`와 `auth=AuthSettings(issuer_url=..., resource_server_url=..., required_scopes=[...])`는 항상 함께 다닙니다. * SDK는 `/.well-known/oauth-protected-resource/...`에 [RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728) Protected Resource Metadata를 게시하고, 인증되지 않은 요청에는 이 문서를 가리키는 `WWW-Authenticate` 헤더가 담긴 401로 응답합니다. 디스커버리는 이것이 전부입니다. * 어떤 핸들러에서든 `get_access_token()`이 곧 호출자입니다. -* 인가는 HTTP의 관심사입니다. `stdio`와 인메모리 클라이언트에서는 인가가 전혀 보이지 않습니다. +* 인가는 HTTP의 관심사입니다. `stdio`와 인메모리 테스트 클라이언트에서는 인가가 전혀 보이지 않습니다. 클라이언트 쪽 절반(인가 서버를 찾아내고 토큰을 대신 가져오는 일)은 **[OAuth 클라이언트](../client/oauth-clients.md)**에서 확인하세요. 그리고 사용자에게 신원을 묻는 대신 신원을 **어설션**하는 클라이언트는 **[ID 어설션](../client/identity-assertion.md)**에서 확인하세요. diff --git a/i18n/ko/pages/run/deploy.md b/i18n/ko/pages/run/deploy.md index 8c3384bad1..c994ca1f04 100644 --- a/i18n/ko/pages/run/deploy.md +++ b/i18n/ko/pages/run/deploy.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 --- # 배포와 확장 {#deploy-scale} @@ -46,6 +46,22 @@ translation: 경고 한 줄로만 나타납니다. 새로 배포한 서버가 모든 연결을 거부한다면, 달리 밝혀지기 전까지는 Host 허용 목록 문제입니다. **[문제 해결](../troubleshooting.md)**도 여기서 시작합니다. +## TLS 종료 프록시 뒤에서 {#behind-a-tls-terminating-proxy} + +TLS가 프록시(인그레스, 로드 밸런서, Caddy, nginx)에서 끝나고 그 뒤에서 uvicorn이 평문 HTTP를 서비스한다면, uvicorn이 프록시의 `X-Forwarded-*` 헤더를 신뢰하도록 설정하세요. + +```console +uvicorn server:app --proxy-headers --forwarded-allow-ips='' +``` + +이 설정이 없으면 앱은 자신이 `http://`로 서비스되고 있다고 믿고, 앱이 내보내는 모든 리디렉션(흔한 것은 `/mcp` → `/mcp/`)이 `http://…`를 가리킵니다. Python 클라이언트는 HTTPS 엔드포인트에서 평문 HTTP로 따라가기를 거부하며 그 이유를 알려 줍니다. + +```text +MCPError: Redirect to http://mcp.example.com/mcp/ not followed: it would downgrade this HTTPS endpoint to plain HTTP. +``` + +클라이언트 쪽 임시방편은 서버가 서비스하는 정확한 URL(`https://mcp.example.com/mcp/`, 슬래시 포함)을 설정해 리디렉션이 일어나지 않게 하는 것입니다. 제대로 된 해결책은 위의 플래그입니다. `FORWARDED_ALLOW_IPS`는 같은 설정의 환경 변수 이름이며, `*` 값은 모든 홉을 신뢰하므로 프록시 말고는 아무것도 uvicorn에 닿을 수 없을 때만 맞는 설정입니다. + ## 워커, 그리고 스티키가 필요한 쪽 {#workers-and-who-has-to-be-sticky} 호스트명이 응답하기 시작했다면 그 뒤에 워커를 둘 이상 두세요. 이를 위한 SDK 설정은 없습니다. Starlette 앱은 다른 ASGI 앱과 똑같이, 포크할 줄 아는 도구에 객체를 넘겨서 확장합니다. @@ -170,6 +186,7 @@ python -c "import secrets; print(secrets.token_hex(32))" ## 요약 {#recap} * 기본적으로 이 앱은 localhost로 오는 요청만 받습니다. `transport_security=TransportSecuritySettings(allowed_hosts=[...], allowed_origins=[...])`가 서비스 개시의 관문입니다. 이를 전달하기 전까지 실제 호스트명 뒤의 모든 요청은 `421`이 되며, 그 이유는 서버 로그에만 남습니다. +* TLS 종료 프록시 뒤에서는 uvicorn을 `--proxy-headers --forwarded-allow-ips=...` 옵션으로 실행하세요. 그러지 않으면 리디렉션이 `http://`를 가리키고 클라이언트가 이를 거부합니다. * 2026-07-28에는 세션이 없고, 로드 밸런서가 스티키로 붙들 대상도 없습니다. `stateless_http=True`는 레거시 전용 설정입니다. 최신 요청은 이 플래그를 읽기도 전에 라우팅되고 응답되기 때문입니다. * 기본 `requestState` 키는 프로세스마다 만들어지는 `os.urandom(32)`입니다. 다른 워커에 도달한 다중 왕복 재시도는 `-32602` *"Invalid or expired requestState"*로 실패합니다. * 해결책은 `RequestStateSecurity(keys=[...])`를 쓰는 것, **그리고** 모든 인스턴스에 같은 서버 이름을 쓰는 것입니다. 이름은 토큰의 기본 audience 클레임입니다. 같은 키, 같은 이름. diff --git a/i18n/ko/pages/run/index.md b/i18n/ko/pages/run/index.md index a2a69a98c3..32590892c7 100644 --- a/i18n/ko/pages/run/index.md +++ b/i18n/ko/pages/run/index.md @@ -1,6 +1,6 @@ --- translation: - sections: [fea8d769ff9edeba, ce8e2ad42f29ef71, 0d705efb19cf99c2, 2fd7cf825e6d2b2c, 9adc400e8c88e854, 318893ad8e2e9924, 6b63ab96b34476c0] + sections: [fea8d769ff9edeba, ce8e2ad42f29ef71, 0d705efb19cf99c2, 9fd2357154a5b7e7, 9adc400e8c88e854, 318893ad8e2e9924, 6b63ab96b34476c0] tool: 1 --- # 서버 실행하기 {#running-your-server} @@ -75,6 +75,11 @@ Inspector는 실제 호스트가 하는 일을 그대로 합니다. `server.py` * `max_request_body_size`: 허용되는 요청 본문의 최대 크기(바이트). 기본값은 4MiB이며, 더 큰 요청은 파싱이나 세션 생성 전에 HTTP 413을 받습니다. 정상적인 MCP 메시지가 이 크기를 넘을 때만 올리세요. +* `session_idle_timeout`: 진행 중인 것이 아무것도 없는 상태로 레거시 세션이 머물 수 있는 시간(초)이며, 이 시간이 지나면 + 서버가 세션을 닫습니다. 기본값은 1800입니다. `None`이면 비활성화됩니다. + [세션 수명과 제한](legacy-clients.md#session-lifetime-and-limits)을 참고하세요. +* `max_sessions`: 한 프로세스가 동시에 유지하는 레거시 세션의 수. 기본값은 10 000입니다. `None`이면 + 제한이 없어집니다. 같은 섹션에서 다룹니다. * `event_store`, `retry_interval`, `transport_security`: 재개 가능성과 DNS 리바인딩 보호. localhost가 아닌 곳에 배포하기 전까지는 미뤄도 됩니다. `transport_security`는 **[배포와 확장](deploy.md)**에서 다룹니다. !!! warning diff --git a/i18n/ko/pages/run/legacy-clients.md b/i18n/ko/pages/run/legacy-clients.md index b762492463..6ec6aafcee 100644 --- a/i18n/ko/pages/run/legacy-clients.md +++ b/i18n/ko/pages/run/legacy-clients.md @@ -1,6 +1,6 @@ --- translation: - sections: [3d1663c18edc824c, d4fd37009a13f03d, af9f398a5a8b679a, 470c2dd144294d69, 8e45827e6d24e8c8, 91dfd0ce98ebb03c] + sections: [3d1663c18edc824c, 90956965ae6a1ca1, af9f398a5a8b679a, 5ce83b1f9d88da62, 0d9b5d13fffc94e5, 8e45827e6d24e8c8, 91dfd0ce98ebb03c] tool: 1 --- # 레거시 클라이언트 지원 {#serving-legacy-clients} @@ -21,15 +21,25 @@ SDK는 모든 요청을 `MCP-Protocol-Version` 헤더에 따라 라우팅합니 ## 하나의 핸들러, 두 시대 {#one-handler-both-eras} -다음은 사용자에게 무언가를 물어봐야 하는 도구와, 그 도구를 호출하는 두 시대의 클라이언트입니다. +다음은 사용자에게 무언가를 물어봐야 하는 도구입니다. -```python title="server.py" hl_lines="24 37-38" +```python title="server.py" hl_lines="21" --8<-- "docs_src/legacy_clients/tutorial001.py" ``` `reserve`에는 모델이 제공하지 않은 정보가 하나 필요합니다. 몇 권인지입니다. `Annotated[..., Resolve(ask_quantity)]`는 도구가 이를 선언하는 방법입니다(자세한 내용은 **[의존성](../handlers/dependencies.md)**에서 확인하세요). `reserve` 안에는 버전을 명시하거나, 기능을 확인하거나, 분기하는 코드가 전혀 없습니다. -두 클라이언트는 같은 `mcp` 객체에 **동시에** 열려 있습니다. `mode="legacy"`는 `initialize` 핸드셰이크를 실행합니다. 2026 이전 클라이언트가 여는 바로 그 연결입니다. 다른 하나는 기본값을 사용해 `2026-07-28` 버전으로 연결됩니다. +HTTP로 띄우세요. 다음은 이 도구를 호출하는 두 시대의 클라이언트입니다. + +```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" +``` + +두 클라이언트는 실행 중인 같은 서버를 상대로 **동시에** 열려 있습니다. `mode="legacy"`는 `initialize` 핸드셰이크를 실행합니다. 2026 이전 클라이언트가 여는 바로 그 연결입니다. 다른 하나는 기본값을 사용해 `2026-07-28` 버전으로 연결됩니다. 두 번째 터미널에서 `python client.py`를 실행하세요. ```text 2025-11-25 {'result': "Reserved 2 of 'Dune'."} @@ -61,6 +71,31 @@ SDK는 모든 요청을 `MCP-Protocol-Version` 헤더에 따라 라우팅합니 세션에 다시 연결하는 클라이언트에게 놓친 SSE 이벤트를 재생하는 것)입니다. 다른 프로세스에서 세션에 도달할 수 있게 해 주는 일은 결코 없습니다. +## 세션 수명과 제한 {#session-lifetime-and-limits} + +레거시 세션은 영원히 살지 않으며, 한 프로세스가 무한히 많은 세션을 보유하지도 않습니다. 이를 제어하는 설정이 두 가지 있습니다. 둘 다 `run()`, `streamable_http_app()`, `Server.streamable_http_app()`의 키워드 인자입니다. 현대(`2026-07-28`) 연결과 `stateless_http=True`에는 세션이 없으므로 두 설정 모두 적용되지 않습니다. + +| 설정 | 기본값 | 하는 일 | 클라이언트가 보는 것 | 끄는 방법 | +|---|---|---|---|---| +| `session_idle_timeout` | `1800`(30분) | 그 시간 동안 진행 중인 것이 아무것도 없었던 세션을 닫습니다. | `404 Session not found`. 다시 `initialize`해야 합니다. | `None` | +| `max_sessions` | `10_000` | 그 수를 넘는 세션은 열기를 거부합니다. 기존 세션은 건드리지 않으며 아무것도 쫓아내지 않습니다. | JSON-RPC 코드 `-32603`과 함께 `503 Too many open sessions`. | `None` | + +"진행 중"으로 간주되는 것은 다음과 같습니다. + +* 열린 `GET` 스트림. SDK 클라이언트는 하나를 열어 두므로, 연결된 클라이언트의 세션은 만료되지 않습니다. +* 아직 응답 중인 요청. 타임아웃보다 오래 실행되는 도구 호출은 중단되지 않으며, 카운트다운은 호출이 끝난 뒤에야 시작됩니다. +* 그 밖에는 없습니다. 요청 사이에는 시계가 돌아갑니다. 세션에 들어오는 요청은 무엇이든 시계를 다시 시작하며, `ping`도 포함됩니다. 세션이 한 번 만료되면 무엇으로도 되살릴 수 없습니다. + +`DELETE`로 세션을 끝내는 클라이언트는 세션을 즉시 해제합니다. 여는 요청이 거부된 클라이언트도 마찬가지입니다. + +```python +mcp.run(transport="streamable-http", session_idle_timeout=None, max_sessions=50_000) +``` + +두 이벤트 모두 서버 로그에 나타납니다. 만료는 `INFO` 수준의 `Session idle timeout`입니다. 열기 거부는 `WARNING` 수준의 `Refusing to open a new session: sessions are already open`입니다. + +제한은 프로세스별입니다. 워커가 넷이면 상한은 `max_sessions`의 네 배이며, 각 워커는 자기 세션을 스스로 만료시킵니다. + ## 유일한 옵션: `stateless_http` {#the-one-knob-stateless_http} 스티키 라우팅이 치르기 싫은 비용이라면, 바꿀 수 있는 것은 정확히 하나입니다. @@ -85,7 +120,7 @@ SDK는 모든 요청을 `MCP-Protocol-Version` 헤더에 따라 라우팅합니 !!! check 일부러 잘못된 설정을 해 보세요. `reserve`는 방금 두 클라이언트를 모두 지원한 바로 그 도구입니다. - `stateless_http=True`로 배포하고, 같은 두 클라이언트를 HTTP로 연결한 뒤, 각각에서 호출해 보세요. + `stateless_http=True`로 배포하고, 같은 두 클라이언트를 연결한 뒤, 각각에서 호출해 보세요. 현대 클라이언트는 여전히 `Reserved 2 of 'Dune'.`을 받습니다. 현대 경로는 바뀌지 않았습니다. diff --git a/i18n/ko/pages/troubleshooting.md b/i18n/ko/pages/troubleshooting.md index caaf97c46d..7a7bed2ca8 100644 --- a/i18n/ko/pages/troubleshooting.md +++ b/i18n/ko/pages/troubleshooting.md @@ -1,6 +1,6 @@ --- translation: - sections: [2efaecdef109a5c5, fcacd3e66b8635a4, 25323d737dcf0261, 8a6e351ec756904d, 137454d469c867f5, 6392596bd6df54f0, 41126fa9c4fe432f, 480b6d7897e30ab4, d83bb682e708dde0, ebbed3449c499db4, 323ef84f6b4bebde, 30fd31be74169d9a, 656943c6cb567218, c2dc3b1007d2e987, 7cf5386b997d04e9, 0b59feed8384456e, 0cba47bae78d04eb, e4355f4c7cf4fb2e] + sections: [3d58228e81b99543, 170514ce901c4139, 17d61fad0a50d62b, 8a6e351ec756904d, 137454d469c867f5, 6392596bd6df54f0, 41126fa9c4fe432f, 480b6d7897e30ab4, d83bb682e708dde0, ebbed3449c499db4, 525cdf1755e29d4c, 30fd31be74169d9a, d2e88333d4f7841f, c2dc3b1007d2e987, d6eabf60cc366341, f798e815252852c2, 0cba47bae78d04eb, 2c218ba829abf74e] tool: 1 --- # 문제 해결 {#troubleshooting} @@ -13,6 +13,12 @@ translation: --8<-- "docs_src/troubleshooting/tutorial001.py" ``` +해당 항목은 `http://localhost:8000/mcp`로 이 서버에 접속하므로, HTTP로 실행한 채로 두세요. + +```console +uv run mcp run server.py --transport streamable-http +``` + 이 페이지에서 인용하는 오류는 모두 실제 오류입니다. SDK 자체의 테스트 스위트가 하나하나 전부 재현합니다. ## `ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception)` {#exceptiongroup-unhandled-errors-in-a-taskgroup-1-sub-exception} @@ -23,7 +29,7 @@ translation: ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: await client.read_resource("weather://Atlantis") ``` @@ -42,14 +48,14 @@ async def main() -> None: +------------------------------------ ``` -이에 대해 할 일은 두 가지입니다. +여기서 할 일은 두 가지입니다. 1. **맨 아래를 읽으세요.** `MCPError: No forecast for 'Atlantis'.`가 실패의 원인입니다. 이 페이지에서 찾아야 할 것은 바로 **이** 텍스트입니다. 2. **블록 안에서 잡으세요.** `ExceptionGroup`은 예외가 `async with`를 **벗어날** 때만 나타납니다. 안에서 잡으면 같은 실패가 그룹 없이 평범한 `MCPError`로 나타납니다. ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: try: await client.read_resource("weather://Atlantis") except MCPError as e: @@ -67,7 +73,7 @@ async def main() -> None: ```python async def main() -> None: - client = Client(mcp) + client = Client("http://localhost:8000/mcp") tools = await client.list_tools() # RuntimeError ``` @@ -75,7 +81,7 @@ async def main() -> None: ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: tools = await client.list_tools() ``` @@ -251,7 +257,7 @@ app = Starlette(routes=[Mount("/", app=mcp.streamable_http_app())], lifespan=lif ## `MCPError: Session not found` {#mcperror-session-not-found} -서버가 클라이언트가 보낸 `Mcp-Session-Id`를 알아보지 못하는 경우이며, 거의 언제나 서버가 **재시작**되었기(또는 다른 인스턴스로 라우팅되었기) 때문입니다. 세션은 해당 프로세스 하나의 메모리에만 존재합니다. +서버가 클라이언트가 보낸 `Mcp-Session-Id`를 알아보지 못하는 경우입니다. 서버가 **재시작**되었거나(또는 다른 인스턴스로 라우팅되었거나), `session_idle_timeout` 동안 진행 중인 요청이 없어서 세션이 **만료**된 것입니다. 이 값은 기본적으로 30분입니다. [세션 수명과 제한](run/legacy-clients.md#session-lifetime-and-limits)을 참고하세요. 세션은 해당 프로세스 하나의 메모리에만 존재합니다. 찾아야 할 서버 버그는 없습니다. HTTP 응답은 본문이 **실제로** JSON-RPC인 `404`이므로, 위의 `421`과 달리 Python `Client`가 이번에는 그대로 보여 줍니다. @@ -261,9 +267,9 @@ app = Starlette(routes=[Mount("/", app=mcp.streamable_http_app())], lifespan=lif 해결책은 다시 연결하는 것입니다. `async with Client(...)` 블록을 벗어나 새 블록에 진입하면 새 세션을 협상합니다. 오래 실행되는 클라이언트라면, 호출을 감싸 `MCPError`를 잡은 뒤 이 메시지가 나오면 죽은 세션 안에서 재시도하지 말고 다시 연결해야 한다는 뜻입니다. -재시작 **없이** 이 문제가 발생한다면, 스티키 세션 없이 워커를 둘 이상 실행하고 있는 것입니다. 각 워커가 자기만의 세션 테이블을 가지므로, 엉뚱한 워커로 라우팅된 요청이 여기에 도달합니다. 이 이야기와 두 가지 해결책(스티키 라우팅 또는 `stateless_http=True`)은 **[배포와 확장](run/deploy.md)**과 **[레거시 클라이언트 지원](run/legacy-clients.md)**에서 다룹니다. +재시작도 **없었고** 클라이언트가 그렇게 오래 조용히 있지도 않았는데 이 문제가 발생한다면, 스티키 세션 없이 워커를 둘 이상 실행하고 있는 것입니다. 각 워커가 자기만의 세션 테이블을 가지므로, 엉뚱한 워커로 라우팅된 요청이 여기에 도달합니다. 이 이야기와 두 가지 해결책(스티키 라우팅 또는 `stateless_http=True`)은 **[배포와 확장](run/deploy.md)**과 **[레거시 클라이언트 지원](run/legacy-clients.md)**에서 다룹니다. -서버 운영자 쪽에서 대응하는 로그 줄은 `Rejected request with unknown or expired session ID: `입니다. `INFO` 수준으로 기록되므로 일반적인 `WARNING` 임계값에서는 보이지 않습니다. 배포 직후 이 줄이 한꺼번에 쏟아지는 것은 정상입니다. 연결되어 있던 모든 클라이언트가 다시 연결하는 중이기 때문입니다. +서버 운영자 쪽에서 대응하는 로그 줄은 `Rejected request with unknown or expired session ID: `입니다. `INFO` 수준으로 기록되므로 일반적인 `WARNING` 임계값에서는 보이지 않습니다. 배포 직후 이 줄이 한꺼번에 쏟아지는 것은 정상입니다. 연결되어 있던 모든 클라이언트가 다시 연결하는 중이기 때문입니다. 세션이 만료된 경우라면 그 줄 앞에 `Session idle timeout`이 먼저 찍히며, 역시 `INFO` 수준입니다. ## `MCPError: Method not found` {#mcperror-method-not-found} @@ -275,7 +281,13 @@ app = Starlette(routes=[Mount("/", app=mcp.streamable_http_app())], lifespan=lif 서버가 사용자에게 무언가를 물어보려 하는데, 이 클라이언트가 물어볼 수 있다고 밝힌 적이 없는 경우입니다. -엘리시테이션(elicitation) 리졸버는 연결된 클라이언트가 폼 엘리시테이션을 선언하지 않았으면 처음부터 거부하며, `e.error.data`가 정확히 무엇이 빠졌는지 알려 줍니다. +이 Bistro 서버는 예약하기 전에 리졸버를 통해 먼저 물어봅니다. + +```python title="server.py" hl_lines="15-17 21" +--8<-- "docs_src/troubleshooting/tutorial007.py" +``` + +Weather 서버 대신 이 서버를 띄우고, `elicitation_callback`을 전달하지 않은 클라이언트에서 `book_table`을 호출하세요. 연결된 클라이언트가 폼 엘리시테이션(elicitation)을 선언한 적이 없으므로 리졸버는 처음부터 거부하며, `e.error.data`가 정확히 무엇이 빠졌는지 알려 줍니다. ```json { @@ -289,7 +301,7 @@ app = Starlette(routes=[Mount("/", app=mcp.streamable_http_app())], lifespan=lif ```python async def main() -> None: - async with Client(mcp, elicitation_callback=handle_elicitation) as client: + async with Client("http://localhost:8000/mcp", elicitation_callback=handle_elicitation) as client: result = await client.call_tool("book_table", {"date": "Friday"}) ``` @@ -314,14 +326,14 @@ async def main() -> None: 핸들러가 요청 도중에 클라이언트에 손을 뻗으려 했는데, 그 호출에 서버의 요청을 실어 나를 채널이 없는 연결이었던 경우입니다. 호출을 이런 상황에 놓는 서버 구성은 세 가지입니다. -**`2026-07-28` 연결. 트랜스포트와 무관하게 항상.** 최신 프로토콜에는 서버가 시작하는 요청이 아예 없으므로, 서버는 무엇을 보내기도 전에 거부합니다. 도구 안에서 `ctx.elicit()`을 호출하는 것이 이 오류를 만나는 전형적인 길이며(`Client(server)`는 따로 요청하지 않아도 `2026-07-28`을 협상하므로, 첫 인메모리 테스트에서 바로 만납니다), `elicitation_callback=` 인자를 전달해도 달라지는 것은 없습니다. 클라이언트가 답할 요청 자체가 도달하지 않기 때문입니다. +**`2026-07-28` 연결. 트랜스포트와 무관하게 항상.** 최신 프로토콜에는 서버가 시작하는 요청이 아예 없으므로, 서버는 무엇을 보내기도 전에 거부합니다. 도구 안에서 `ctx.elicit()`을 호출하는 것이 이 오류를 만나는 전형적인 길이며, `Client(mcp)`는 따로 요청하지 않아도 `2026-07-28`을 협상하므로 대개 그 도구의 첫 인메모리 **[테스트](get-started/testing.md)**에서 바로 만납니다. `elicitation_callback=` 인자를 전달해도 달라지는 것은 없습니다. 클라이언트가 답할 요청 자체가 도달하지 않기 때문입니다. ```python title="server.py" hl_lines="16" --8<-- "docs_src/troubleshooting/tutorial006.py" ``` ```python -async def main() -> None: +async def test_book_table() -> None: async with Client(mcp) as client: await client.call_tool("book_table", {"date": "Friday"}) ``` @@ -363,7 +375,7 @@ mcp.shared.exceptions.MCPError: Cannot send 'elicitation/create': this transport ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: await client.call_tool("forecast", {"city": "London"}, request_state="round-1-from-worker-a") ``` @@ -416,7 +428,7 @@ mcp = MCPServer("Weather", request_state_security=RequestStateSecurity(keys=[key * 서버 로그의 `Tool already exists:`는 이름이 같은 두 도구가 하나로 합쳐졌다는 유일한 신호입니다. * 421 하나에 표기는 세 가지입니다. `Server returned an error response`(Python `Client`), `421 Misdirected Request` / `Invalid Host header`(그 밖의 모든 곳), `Invalid Host header: `(서버 로그). 해결책은 `transport_security=TransportSecuritySettings(allowed_hosts=[...])`입니다. * `Task group is not initialized` -> 마운트된 앱에서 호스트 lifespan이 `mcp.session_manager.run()`에 진입하지 않은 경우입니다. -* `Session not found` -> 서버가 재시작되었습니다. 다시 연결하세요. +* `Session not found` -> 서버가 재시작되었거나 세션이 만료되었습니다(`session_idle_timeout`). 다시 연결하세요. * `Cannot send 'elicitation/create': ... no back-channel ...` -> `ctx.elicit()`에는 서버에서 클라이언트로 가는 채널이 필요합니다. `2026-07-28` 연결에는 그런 채널이 아예 없고, `stateless_http=True`는 레거시 채널을 없애며, `json_response=True`는 요청 범위 채널을 없앱니다. 리졸버를 사용하세요(레거시 클라이언트라면 채널을 유지하는 서버도 필요합니다). 이웃인 `Method not found`는 상대편 프로토콜 리비전에 없는 메서드를 요청한 경우입니다. * `Client did not declare the form elicitation capability ...` 및 `Elicitation not supported` -> 클라이언트에 `elicitation_callback=` 인자가 빠져 있습니다. * `Invalid or expired requestState`는 와이어에서 이유를 절대 말하지 않습니다. 서버 로그가 말해 주며, `unknown key`는 워커 간에 `RequestStateSecurity(keys=[...])` 설정을 공유하라는 뜻입니다. diff --git a/i18n/ko/pages/whats-new.md b/i18n/ko/pages/whats-new.md index 84077c3ff6..6c50e98d21 100644 --- a/i18n/ko/pages/whats-new.md +++ b/i18n/ko/pages/whats-new.md @@ -1,6 +1,6 @@ --- translation: - sections: [cfe01c0c5863dfa2, 1c58c5cfcc37d455, a7392996acf1ad8f, 875eb2889263424e] + sections: [cfe01c0c5863dfa2, 0dbb68d8b210177b, 80cf193023af3ed4, 875eb2889263424e] tool: 1 --- # v2에서 달라진 점 {#whats-new-in-v2} @@ -42,11 +42,11 @@ mcp = MCPServer("Demo") # v1: FastMCP("Demo") v1은 세 겹으로 중첩된 계층을 건네주었습니다. 원시 스트림을 내놓는 트랜스포트 컨텍스트 매니저, 이를 감싸는 `ClientSession`, 그리고 직접 호출해야 하는 `await session.initialize()`입니다. v2에는 객체가 하나뿐입니다. -```python title="client.py" hl_lines="14-18" ---8<-- "docs_src/client/tutorial001.py" +```python title="client.py" hl_lines="7-11" +--8<-- "docs_src/client/tutorial001_client.py" ``` -`Client`는 서버 객체(트랜스포트 없이 인메모리로 동작하며, 테스트에 쓰는 방식입니다), URL(Streamable HTTP), `StdioServerParameters`(stdio 하위 프로세스), 또는 `sse_client(...)` 같은 그 밖의 임의의 트랜스포트 컨텍스트 매니저를 받습니다. `async with`에 진입하면 서버가 어느 시대의 프로토콜을 말하든 연결을 맺고 프로토콜 버전을 협상합니다. 그 뒤에는 `client.server_capabilities`와 `client.protocol_version`이 그냥 준비되어 있고, 서버가 자신을 식별하는 경우에는 `client.server_info`도 마찬가지입니다(2026 시대에는 식별 정보가 선택 사항이므로 이제 타입은 `Implementation | None`입니다). v1에서 등록한 샘플링 및 엘리시테이션 콜백은 여전히 동작하며(콜백 본문에는 이 페이지의 다른 모든 것과 마찬가지로 snake_case 속성 이름 변경이 적용됩니다), 이제 2026 방식의 결과 속 요청(아래 참고)에도 응답하고, 한 번에 하나씩이 아니라 동시에 실행됩니다. 저수준 인터페이스를 원하는 경우를 위해 `ClientSession`은 여전히 그 아래에 있으며 `client.session`으로 얻을 수 있습니다. 다만 이 클래스 역시 바뀌었으므로(새 디스패처 엔진 위에서 실행되고, 자체 시그니처 일부도 변경되었습니다) 아래 계층으로 내려가기 전에 **[마이그레이션 가이드](migration.md#clientsession-now-runs-on-jsonrpcdispatcher-basesession-removed)**를 읽어 보세요. +`Client`는 URL(Streamable HTTP), `StdioServerParameters`(stdio 하위 프로세스), `sse_client(...)` 같은 그 밖의 임의의 트랜스포트 컨텍스트 매니저, 또는 테스트에서는 서버 객체 자체(트랜스포트 없이 인메모리로 동작합니다)를 받습니다. `async with`에 진입하면 서버가 어느 시대의 프로토콜을 말하든 연결을 맺고 프로토콜 버전을 협상합니다. 그 뒤에는 `client.server_capabilities`와 `client.protocol_version`이 그냥 준비되어 있고, 서버가 자신을 식별하는 경우에는 `client.server_info`도 마찬가지입니다(2026 시대에는 식별 정보가 선택 사항이므로 이제 타입은 `Implementation | None`입니다). v1에서 등록한 샘플링 및 엘리시테이션 콜백은 여전히 동작하며(콜백 본문에는 이 페이지의 다른 모든 것과 마찬가지로 snake_case 속성 이름 변경이 적용됩니다), 이제 2026 방식의 결과 속 요청(아래 참고)에도 응답하고, 한 번에 하나씩이 아니라 동시에 실행됩니다. 저수준 인터페이스를 원하는 경우를 위해 `ClientSession`은 여전히 그 아래에 있으며 `client.session`으로 얻을 수 있습니다. 다만 이 클래스 역시 바뀌었으므로(새 디스패처 엔진 위에서 실행되고, 자체 시그니처 일부도 변경되었습니다) 아래 계층으로 내려가기 전에 **[마이그레이션 가이드](migration.md#clientsession-now-runs-on-jsonrpcdispatcher-basesession-removed)**를 읽어 보세요. **[클라이언트](client/index.md)**에서 소개하고, **[클라이언트 트랜스포트](client/transports.md)**에서 네 가지 연결 형태를, **[클라이언트 콜백](client/callbacks.md)**에서 콜백 자체를 다루며, **[테스트](get-started/testing.md)**에서는 v1의 `create_connected_server_and_client_session()` 헬퍼를 대체하는 인메모리 패턴을 보여 줍니다. @@ -171,11 +171,15 @@ Streamable HTTP에서는 2026 경로에 `Mcp-Session-Id`가 없으며, 운영 대체 방식은 호출의 방향을 뒤집습니다. 사용자로부터 무언가가 필요한 도구는 질문을 **반환**하고(`InputRequiredResult`), 클라이언트는 늘 갖고 있던 것과 같은 콜백으로 질문에 답하며, 답이 첨부된 채로 호출이 재시도됩니다. 이 루프는 `Client`가 대신 돌려 줍니다. 서버에서 결과를 직접 만드는 일은 드뭅니다. **[의존성](handlers/dependencies.md)**이 대신 해 주기 때문입니다. 매개변수에 `Resolve(ask_quantity)` 어노테이션을 달면(`ask_quantity`는 직접 작성하는 평범한 함수입니다), SDK가 연결이 지원하는 메커니즘, 즉 레거시 세션에서는 실시간 엘리시테이션 요청, 2026에서는 다중 왕복을 통해 질문합니다. 도구 본문 하나로 두 시대를 지원합니다. -```python title="dual_era.py" hl_lines="24 37-38" +```python title="server.py" hl_lines="21" --8<-- "docs_src/legacy_clients/tutorial001.py" ``` -이 파일 하나에 핵심이 모두 담겨 있습니다. 서버 하나, `Resolve` 기반 도구 하나, 그리고 레거시 클라이언트와 최신 클라이언트가 모두 인메모리로 답을 받습니다. 메커니즘(SDK가 대신 봉인하고 검증하는 `request_state` 포함)은 **[다중 왕복 요청](handlers/multi-round-trip.md)**에서 설명하고, 질문하는 쪽은 **[엘리시테이션](handlers/elicitation.md)**에서 다룹니다. +```python title="client.py" hl_lines="14-15" +--8<-- "docs_src/legacy_clients/tutorial001_client.py" +``` + +이 두 파일에 핵심이 모두 담겨 있습니다. 서버 하나, `Resolve` 기반 도구 하나, 그리고 레거시 클라이언트와 최신 클라이언트가 모두 실행 중인 같은 서버에서 답을 받습니다(**[레거시 클라이언트 지원](run/legacy-clients.md)**에서 이 두 파일을 하나씩 짚어 봅니다). 메커니즘(SDK가 대신 봉인하고 검증하는 `request_state` 포함)은 **[다중 왕복 요청](handlers/multi-round-trip.md)**에서 설명하고, 질문하는 쪽은 **[엘리시테이션](handlers/elicitation.md)**에서 다룹니다. !!! warning "포팅한 v1 서버의 동작이 바뀌는 유일한 지점입니다" 가장 먼저 부딪히는 것은 직접 작성한 테스트입니다. `Client(mcp)`는 기본적으로 v2 서버와 2026-07-28을 diff --git a/i18n/pt/pages/advanced/apps.md b/i18n/pt/pages/advanced/apps.md index 06a5c15df5..49a412660f 100644 --- a/i18n/pt/pages/advanced/apps.md +++ b/i18n/pt/pages/advanced/apps.md @@ -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} @@ -25,7 +25,7 @@ e depois volte aqui. ## Um relógio com uma cara {#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" ``` @@ -56,15 +56,36 @@ O modelo lê `content`; o iframe é para humanos. Um host com suporte a UI ainda o resultado em texto para o modelo, e um cliente só de texto recebe *apenas* isso. Então o padrão canônico é uma ferramenta, duas respostas. Olhe `get_time` de novo: -```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)` é `True` somente quando o cliente declarou a extensão `io.modelcontextprotocol/ui` **e** listou `text/html;profile=mcp-app` nas suas configurações `mimeTypes`. O campo é obrigatório, então um cliente que o omite -não conta. É exatamente isso que `main()` no mesmo arquivo declara: a -metade cliente da negociação, e a resposta rica volta. +não conta. Aqui está a metade cliente da negociação: + +```python title="client.py" hl_lines="8 12" +--8<-- "docs_src/apps/tutorial001_client.py" +``` + +Sirva `server.py` por HTTP e depois execute o cliente em um segundo terminal: + +```console +uv run mcp run server.py --transport streamable-http +``` + +```console +python client.py +``` + +```text +2026-06-26T12:00:00Z +``` + +A resposta rica voltou. Tire `extensions=[APPS_SUPPORT]` da chamada a `Client` +e o mesmo programa imprime `The time is 2026-06-26T12:00:00Z.` no lugar, que é +tudo o que um cliente só de texto chega a ver. !!! warning Nunca retorne um placeholder como `"[Rendered UI]"` como único conteúdo. Se o diff --git a/i18n/pt/pages/advanced/extensions.md b/i18n/pt/pages/advanced/extensions.md index c9ceb86f8c..51debad3db 100644 --- a/i18n/pt/pages/advanced/extensions.md +++ b/i18n/pt/pages/advanced/extensions.md @@ -1,6 +1,6 @@ --- translation: - sections: [05891e7cc1938a13, b3c01a6af28c51ee, 7ffc91f5e38bdfe0, 717d3f235a8333a7, f471a13b2fe5d737, ed6af2df4b656dff] + sections: [05891e7cc1938a13, b3c01a6af28c51ee, 6eba6ce094f4417e, 125f28588c85dd7b, ddd8969b698ca11c, ed6af2df4b656dff] tool: 1 --- # Extensões {#extensions} @@ -49,7 +49,7 @@ Use como prefixo um domínio que você controla. `io.modelcontextprotocol/*` é A menor extensão útil é uma ferramenta e um mapa de configurações: -```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 @@ A menor extensão útil é uma ferramenta e um mapa de configurações: * `settings()` é o valor anunciado em `capabilities.extensions["com.example/stamps"]`. Retorne `{}` (o padrão) para anunciar a extensão sem configurações. * A extensão nunca recebe o servidor. Ela declara contribuições como dados; o `MCPServer` as consome. Não existe um `self.server` para modificar. -E `main()` é a prova, um cliente em memória direto contra `mcp`: +Sirva-a por HTTP, e um cliente é a prova: -```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" ``` +Todo `server.py` desta página é servido com esse comando, e todo `client.py` roda ao lado dele com `python client.py` em um segundo terminal. + ### Servindo seus próprios métodos {#serving-your-own-methods} Uma extensão pode registrar **novos métodos de requisição**: seus próprios verbos, servidos ao lado dos da especificação: -```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 @@ Os métodos são **estritamente aditivos**. O SDK impõe isso na construção, n ### O lado do cliente {#the-client-side} -O `main()` do mesmo arquivo é a história inteira do cliente, com as duas metades: +O cliente é um programa à parte, e carrega as duas metades da história do cliente: -```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)])` declara a extensão. As declarações viram `ClientCapabilities.extensions`: em uma conexão 2026-07-28 o mapa viaja no envelope `_meta` de cada requisição, então o servidor o vê em **toda** requisição; em uma conexão legada ele vai no handshake `initialize`. O código do servidor não se importa com qual: `require_client_extension(ctx, ...)` e `ctx.session.check_client_capability(...)` leem a fonte certa nos dois caminhos. * Métodos de fornecedor descem uma camada para `client.session.send_request(...)`; `Client` só ganha métodos de primeira classe para verbos da especificação. `send_request` aceita qualquer subclasse de `Request`, então a requisição do fornecedor passa como está. +* `SearchRequest` e os dois modelos que ela carrega são o contrato de protocolo da extensão, então o cliente os declara por conta própria. Uma extensão publicada os distribuiria em um pacote que os dois lados importam. ### Interceptando `tools/call` {#intercepting-toolscall} @@ -109,12 +116,18 @@ O hook envolve `tools/call` e nada mais. Para preocupações que valem para toda ## Usando uma extensão de cliente {#using-a-client-extension} -Uma **extensão de cliente** é o mesmo contrato visto do lado consumidor: um pacote de comportamento do lado do cliente reunido sob um único identificador. Passe as instâncias para `Client(extensions=[...])` e chame as ferramentas normalmente: +Uma **extensão de cliente** é o mesmo contrato visto do lado consumidor: um pacote de comportamento do lado do cliente reunido sob um único identificador. Aqui o servidor responde a `buy` com um recibo a resgatar em vez da mercadoria, e só para um cliente que declarou a extensão: -```python title="client.py" hl_lines="66-68" +```python title="server.py" hl_lines="22-25" --8<-- "docs_src/extensions/tutorial006.py" ``` +No cliente, passe as instâncias para `Client(extensions=[...])` e chame as ferramentas normalmente: + +```python title="client.py" hl_lines="33-35" +--8<-- "docs_src/extensions/tutorial006_client.py" +``` + `call_tool("buy", ...)` retorna um `CallToolResult` comum, como toda outra chamada. O que a extensão mudou: o servidor agora pode responder a `buy` com um **formato de resultado** `receipt` em vez de um resultado final, e `Receipts` o finaliza (aqui, resgatando o recibo com uma chamada seguinte) antes de `call_tool` retornar. Nada muda no ponto da chamada. Tire a extensão e nada disso existe: a barreira do servidor recusa um cliente que não a declarou (erro -32021), e um formato reivindicado vindo de um servidor que pula a barreira falha na validação, exatamente como a especificação exige para um `resultType` não reconhecido. Desligado por padrão, nas duas pontas da conexão. @@ -124,15 +137,15 @@ Para anunciar um identificador **sem** nenhum comportamento do lado do cliente ( ```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")]) ``` ## Escrevendo uma extensão de cliente {#writing-a-client-extension} Herde de `ClientExtension` e sobrescreva apenas o que precisar. Três tipos de contribuição, cada um com um padrão: `settings()`, `claims()` e `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" ``` * O identificador segue a mesma gramática do servidor, validada quando a classe é definida. @@ -153,12 +166,18 @@ Duas regras discretas. As claims ficam ativas apenas em conexões 2026-07-28, e ### Verbos de extensão {#extension-verbs} -Os métodos de requisição próprios de uma extensão não precisam de registro no lado do cliente. Um tipo de requisição de fornecedor herda de `mcp.types.Request` e passa por `client.session.send_request`, como em [Servindo seus próprios métodos](#serving-your-own-methods). Um acréscimo: quando uma chave de params precisa viajar no header `Mcp-Name` (especificações de extensão como tasks exigem isso para seus verbos), o tipo de requisição declara `name_param`: +Os métodos de requisição próprios de uma extensão não precisam de registro no lado do cliente. Um tipo de requisição de fornecedor herda de `mcp.types.Request` e passa por `client.session.send_request`, como em [Servindo seus próprios métodos](#serving-your-own-methods). Considere um servidor cuja extensão serve um único verbo sobre um job nomeado: -```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" ``` +Um acréscimo no cliente: quando uma chave de params precisa viajar no header `Mcp-Name` (especificações de extensão como tasks exigem isso para seus verbos), o tipo de requisição declara `name_param`: + +```python title="client.py" hl_lines="20-23 28-29" +--8<-- "docs_src/extensions/tutorial007_client.py" +``` + A sessão espelha `params["jobId"]` em `Mcp-Name` em todo caminho de envio, e um valor ausente falha de forma explícita em vez de omitir silenciosamente um header obrigatório. ## O que uma extensão não pode fazer {#what-an-extension-cannot-do} diff --git a/i18n/pt/pages/advanced/low-level-server.md b/i18n/pt/pages/advanced/low-level-server.md index 9c9cd540ac..a75014bb2d 100644 --- a/i18n/pt/pages/advanced/low-level-server.md +++ b/i18n/pt/pages/advanced/low-level-server.md @@ -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 --- # O Server de baixo nível {#the-low-level-server} @@ -36,18 +36,22 @@ Três coisas mudaram, e elas são a API de baixo nível inteira: ### Experimente {#try-it} -Não existe Inspector para este aqui: `mcp dev` e `mcp run` só aceitam um `MCPServer`. O `Client` em memória não se importa; ele recebe um `Server` de baixo nível exatamente como recebe um `MCPServer`: +`mcp dev` e `mcp run` só aceitam um `MCPServer`, então este aqui você serve por conta própria. A última linha de `server.py` monta um app ASGI comum a partir dele, e o uvicorn o executa: -```python title="main.py" +```console +uvicorn server:app --port 8000 +``` + +Aponte o Inspector, ou qualquer cliente, para `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 @@ O mesmo texto que a versão com `@mcp.tool()` produziu. Duas diferenças honesta * `result.structured_content` é `None`. O servidor de alto nível encapsula um `-> str` em `{"result": ...}` para você; aqui ninguém monta o que você não montou. * `list_tools` retorna o schema que **você** digitou, caractere por caractere. A versão de alto nível tinha `"title": "Query"` em cada propriedade e um `"title": "search_booksArguments"` na raiz: artefatos do Pydantic. Aqui embaixo, se está no fio, foi você quem colocou lá. +Em um teste você dispensa o uvicorn e a porta: `Client(server)` recebe um `Server` de baixo nível no mesmo processo exatamente como recebe um `MCPServer`, e **[Testes](../get-started/testing.md)** é esse padrão. + ## Nada é verificado por você {#nothing-is-checked-for-you} O `MCPServer` rejeita um argumento ruim antes mesmo de a sua função executar, validando a chamada contra o schema que ele gerou (**[Ferramentas](../servers/tools.md)**). @@ -208,11 +214,11 @@ Cada um destes é uma ideia para a qual você já tem o vocabulário; cada um te ## Recapitulando {#recap} * O `Server` de baixo nível recebe os seus handlers como **parâmetros do construtor** `on_*`; todo handler é `async (ctx, params) -> result`. -* Você escreve o dict `input_schema` e você monta o `CallToolResult`. Nada é derivado, encapsulado ou validado por você. +* Você escreve o dict `input_schema` e você monta o `CallToolResult`. Nada é derivado, encapsulado ou validado para você. * Uma exceção em um handler é um erro de protocolo `-32603`. Um erro de ferramenta que o modelo consegue ler é um `CallToolResult` com `is_error=True` que **você** retorna. * O `_meta` no resultado é endereçado à aplicação cliente, não ao modelo. * `Server[T]` é genérico no que o seu lifespan produz; `ctx.lifespan_context` é um `T` tipado. * `add_request_handler(method, params_type, handler)` serve qualquer método. `initialize` é reservado. * As capacidades que um `Server` anuncia são derivadas de quais handlers você registrou. -`Client(server)` tratou os dois servidores de forma idêntica porque eles *são* o mesmo protocolo, e essa é justamente a ideia. A próxima camada abaixo nem é uma classe: é **[Middleware](middleware.md)**. +O cliente tratou os dois servidores de forma idêntica porque eles *são* o mesmo protocolo, e essa é justamente a ideia. A próxima camada abaixo nem é uma classe: é **[Middleware](middleware.md)**. diff --git a/i18n/pt/pages/advanced/pagination.md b/i18n/pt/pages/advanced/pagination.md index e0ff987638..e1e1f777e0 100644 --- a/i18n/pt/pages/advanced/pagination.md +++ b/i18n/pt/pages/advanced/pagination.md @@ -1,6 +1,6 @@ --- translation: - sections: [a9aba7a026c7bd85, ed32bda7ba9ae33a, 7e64cc5646abb91f, 22a0129ee78b3c63, d875373c06d8d2f9] + sections: [a9aba7a026c7bd85, 83e2a08b9d46a398, 9fd8a0aa384b3257, 22a0129ee78b3c63, d875373c06d8d2f9] tool: 1 --- # Paginação {#pagination} @@ -31,9 +31,13 @@ O `@mcp.resource()` não tem nenhum gancho para isso. Para paginar, você mesmo ### Experimente {#try-it} -`Client(server)` se conecta a um `Server` de baixo nível em memória exatamente como se conecta a um `MCPServer`. +O `mcp run` só aceita um `MCPServer`, então este aqui você mesmo serve. A última linha de `server.py` monta um app ASGI comum a partir do `Server`, e o uvicorn executa esse app: -Chame `list_resources()` sem argumentos. Você recebe dez recursos, de `book-1` a `book-10`, e `next_cursor` é a string `"10"`. +```console +uvicorn server:app --port 8000 +``` + +Aponte qualquer cliente (**[O cliente](../client/index.md)**, ou o Inspector) para `http://localhost:8000/mcp` e chame `list_resources()` sem argumentos. Você recebe dez recursos, de `book-1` a `book-10`, e `next_cursor` é a string `"10"`. Devolva-a com `list_resources(cursor="10")` e o primeiro recurso é `book-11`, o novo `next_cursor` é `"20"`. @@ -43,7 +47,7 @@ A décima página volta com `next_cursor` definido como `None`. Pronto. Todo método `list_*` do `Client` (`list_tools`, `list_resources`, `list_resource_templates`, `list_prompts`) aceita a palavra-chave `cursor=`. Esgotar uma lista paginada é um único `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 @@ Todo método `list_*` do `Client` (`list_tools`, `list_resources`, `list_resourc * Acumule **antes** de olhar para `next_cursor`: a última página também tem recursos. * `next_cursor is None` é a saída. Qualquer outra coisa volta direto para `cursor=`, intocada. -Execute o `main()` dele e ele imprime `100 resources`: dez páginas de dez, costuradas por um loop que nunca soube que havia dez páginas. +Com o uvicorn ainda servindo `server.py`, execute `python client.py` em um segundo terminal. Ele imprime `100 resources`: dez páginas de dez, costuradas por um loop que nunca soube que havia dez páginas. Este é o mesmo loop que **[O cliente](../client/index.md)** mostra para todo verbo `list_*`, e ele não custa nada contra um servidor que não pagina: `next_cursor` é `None` na primeira resposta e o loop roda uma vez. diff --git a/i18n/pt/pages/client/caching.md b/i18n/pt/pages/client/caching.md index e6cfdf837a..73b242b32f 100644 --- a/i18n/pt/pages/client/caching.md +++ b/i18n/pt/pages/client/caching.md @@ -1,6 +1,6 @@ --- translation: - sections: [9e7b9a1710e5aeba, b74ca4c1d2ddddee, fa8714e61bf90c5a, 04db67a886b7271c, 857690fb8f876800] + sections: [9e7b9a1710e5aeba, 66a2e9acc9101d54, d3d25aa802f5145a, 04db67a886b7271c, 857690fb8f876800] tool: 1 --- # Dicas de cache {#caching-hints} @@ -30,7 +30,7 @@ Por padrão, todo resultado diz `ttlMs: 0, cacheScope: "private"`: obsoleto imed No `Server` de baixo nível, os handlers montam seus resultados à mão, e `ttl_ms` / `cache_scope` são apenas campos nos modelos de resultado. Um handler que os define explicitamente sempre vence o mapa do construtor, campo a campo: -```python title="server.py" hl_lines="10 16" +```python title="server.py" hl_lines="11 17" --8<-- "docs_src/caching/tutorial002.py" ``` @@ -44,10 +44,24 @@ Uma ressalva sobre listas paginadas: o protocolo exige o **mesmo `cacheScope` em Numa sessão 2026-07-28, o `Client` respeita as dicas por você: ele tem um cache de respostas embutido, ligado por padrão. Um resultado que chega carregando um `ttlMs` é armazenado, e uma chamada idêntica dentro desse TTL é servida do cache sem viagem de ida e volta. Um resultado que não carrega *nenhuma* dica não é armazenado: resultados sem dica recebem `CacheConfig.default_ttl_ms`, cujo padrão é `0` (obsoleto imediatamente), então um servidor que não declara nada vê exatamente o mesmo tráfego chamada a chamada de sempre. -```python title="client.py" hl_lines="33 35 38" +Para ver isso acontecer, sirva o `server.py` da seção anterior com o uvicorn (a última linha dele monta o app ASGI). O handler imprime uma linha toda vez que executa de verdade: + +```console +uvicorn server:app --port 8000 +``` + +```python title="client.py" hl_lines="20 23 28" --8<-- "docs_src/caching/tutorial003.py" ``` +Execute `python client.py` em um segundo terminal. Ele imprime as dicas que o primeiro resultado carregava, o `ttlMs` do handler ao lado do `cacheScope` do mapa: + +```text +1000 public +``` + +O terminal do servidor conta o resto da história: entre os logs de requisição do uvicorn, `tools/list served` aparece três vezes. + Quatro chamadas, três buscas. A segunda chamada encontrou uma entrada fresca e nunca chegou ao servidor; avançar o relógio (injetado) além do TTL fez a terceira buscar de novo; a quarta disse `cache_mode="refresh"`. Esse argumento nomeado existe nos cinco verbos com cache (`list_tools`, `list_prompts`, `list_resources`, `list_resource_templates`, `read_resource`): * `"use"` (o padrão) serve uma entrada fresca se houver uma, e armazena a busca se não houver. @@ -56,7 +70,7 @@ Quatro chamadas, três buscas. A segunda chamada encontrou uma entrada fresca e Uma regra fica acima de `"use"`: **chamadas que carregam `meta` sempre chegam ao servidor.** Uma requisição com `meta` definido (um token de progresso, campos de rastreamento) espera uma requisição no fio, então sob `cache_mode="use"` ela é tratada como `"refresh"`: a leitura do cache é pulada, e o resultado buscado ainda substitui a entrada em cache. `"bypass"` e um `"refresh"` explícito se comportam como sempre. -Para desligar o cache por completo, construa com `Client(server, cache=None)`: toda chamada volta a ser uma viagem de ida e volta, e `cache_mode`, embora ainda aceito, não faz nada. +Para desligar o cache por completo, passe `cache=None` ao construir o `Client`: toda chamada volta a ser uma viagem de ida e volta, e `cache_mode`, embora ainda aceito, não faz nada. O escopo também é respeitado automaticamente: entradas `"private"` são indexadas pela *partição* do cache (abaixo), enquanto as `"public"` podem optar por um compartilhamento mais amplo. E **notificações vencem o TTL** para as entradas exatas que nomeiam: uma notificação `list_changed` remove a listagem em cache correspondente, e `resources/updated` remove a leitura em cache armazenada sob exatamente a sua URI, por mais frescas que estivessem. Numa conexão 2026-07-28 essas notificações chegam num stream `subscriptions/listen` que você abre com `client.listen(...)`, e a remoção se completa antes de o seu observador ver o evento; **[Assinaturas](subscriptions.md)** é essa página. diff --git a/i18n/pt/pages/client/callbacks.md b/i18n/pt/pages/client/callbacks.md index 39308a3e3a..5fa7b2661b 100644 --- a/i18n/pt/pages/client/callbacks.md +++ b/i18n/pt/pages/client/callbacks.md @@ -1,6 +1,6 @@ --- translation: - sections: [adf3c545b5be46b6, 916cd3ab1c03f461, e9be7a8d0eb0a456, 565890a636288ecf, 6af7e49db9129ec3, 06b0238c174186af, 90c6043be435fcb0] + sections: [adf3c545b5be46b6, 916cd3ab1c03f461, 32ef568335dd95a7, 565890a636288ecf, 6af7e49db9129ec3, 06b0238c174186af, 0abc5ea5cb7ff6b3] tool: 1 --- # Callbacks do cliente {#client-callbacks} @@ -63,7 +63,7 @@ Um `tools/call` seu, um `elicitation/create` de volta do servidor, respondido pe O `mode="legacy"` na chamada `Client(...)` está fazendo trabalho de verdade. Por padrão, `Client(...)` negocia o caminho moderno do protocolo, e esse caminho não tem canal de retorno (back-channel) para requisições do servidor ao cliente: `ctx.elicit` falha antes mesmo de o seu callback rodar. Não é o transporte que decide isso; é o protocolo - negociado, tanto em memória quanto por uma URL. Fixe `mode="legacy"` sempre que o seu cliente tiver + negociado. Fixe `mode="legacy"` sempre que o seu cliente tiver que responder a uma; todos os testes por trás desta página fazem isso. **[Versões do protocolo](../protocol-versions.md)** tem a história completa. Em uma sessão 2026-07-28 o callback não está morto, ele é alimentado de outro jeito: quando uma ferramenta retorna um @@ -151,4 +151,4 @@ Mais dois. Nenhum deles declara nada. * `sampling_callback` e `list_roots_callback` funcionam do mesmo jeito, mas atendem funcionalidades descontinuadas; servidores modernos usam requisições de múltiplas idas e voltas no lugar. * `logging_callback` e `message_handler` recebem notificações. Eles não declaram nada. -O primeiro argumento de `Client(...)` é um objeto de transporte. **[Transportes do cliente](transports.md)** cobre todos os tipos. +O primeiro argumento de `Client(...)` escolhe o transporte. **[Transportes do cliente](transports.md)** cobre todos os tipos. diff --git a/i18n/pt/pages/client/index.md b/i18n/pt/pages/client/index.md index abffbbdee7..72c13cdf81 100644 --- a/i18n/pt/pages/client/index.md +++ b/i18n/pt/pages/client/index.md @@ -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 --- # O cliente {#the-client} @@ -11,13 +11,23 @@ Um **`Client`** é como um programa Python conversa com um servidor MCP. ## Seu primeiro cliente {#your-first-client} -```python title="client.py" hl_lines="14-18" +Um cliente precisa de um servidor com quem conversar. Esta Bookshop é o servidor a que todo trecho desta página se conecta. Salve-o como `server.py` e deixe-o rodando via HTTP: + +```python title="server.py" --8<-- "docs_src/client/tutorial001.py" ``` -O servidor no topo só está ali para você ter algo a que se conectar. O cliente são as cinco linhas destacadas. +```console +uv run mcp run server.py --transport streamable-http +``` + +Isso o serve em `http://localhost:8000/mcp`. O cliente é um programa à parte. Salve-o como `client.py` e execute `python client.py` em um segundo terminal: -* `Client(mcp)` recebe o **próprio objeto servidor**. Esse é o transporte em memória: sem subprocesso, sem porta, sem HTTP. É assim que todo exemplo nesta página, e todo teste que você escrever, se conecta. +```python title="client.py" hl_lines="7-11" +--8<-- "docs_src/client/tutorial001_client.py" +``` + +* `Client("http://localhost:8000/mcp")` recebe uma **URL**, então se conecta via Streamable HTTP ao servidor que você acabou de iniciar. * `async with` é o **ciclo de vida**. Entrar nele conecta e negocia; sair dele desconecta. Não há um par `connect()` / `close()`, e um `Client` não pode ser reutilizado depois que o bloco termina. * Dentro do bloco, os fatos da conexão já estão ali como propriedades comuns. @@ -25,10 +35,10 @@ O servidor no topo só está ali para você ter algo a que se conectar. O client `Client` recebe um argumento posicional e resolve o transporte a partir do tipo dele: -* Uma instância de `MCPServer` (ou do `Server` de baixo nível): conectada **no mesmo processo**. -* Uma string de URL (`Client("http://localhost:8000/mcp")`): Streamable HTTP, o caminho de produção. -* Um `StdioServerParameters`: o comando a iniciar como **subprocesso**, com o qual se conversa pelo stdin e stdout dele. +* Uma string de URL (`Client("http://localhost:8000/mcp")`): Streamable HTTP, o transporte atrás do qual você faz o deploy. +* Um `StdioServerParameters`: o comando a iniciar como **subprocesso** local, com o qual se conversa pelo stdin e stdout dele. * Um **transporte**: qualquer coisa com que você possa fazer `async with ... as (read, write)`, como `streamable_http_client(url, http_client=...)` em volta do seu próprio cliente HTTP. +* Uma instância de `MCPServer` (ou do `Server` de baixo nível): conectada **no mesmo processo**, sem subprocesso e sem porta. Essa é para testes, e **[Testes](../get-started/testing.md)** se apoia nela. Todo o resto desta página é idêntico entre os quatro. Cabeçalhos, subprocessos, timeouts e o protocolo `Transport` têm sua própria página: **[Transportes do cliente](transports.md)**. @@ -49,11 +59,11 @@ Você nunca escolheu uma versão do protocolo. Por padrão, o `Client` sonda o s ## Listando ferramentas {#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()` retorna um `ListToolsResult`; as ferramentas estão em `.tools`. Cada uma é a definição completa que um host entregaria a um modelo: +`list_tools()` retorna um `ListToolsResult`; as ferramentas estão em `.tools`. Cada uma é a definição completa que um host entregaria a um modelo. Eis a primeira: ```python tool.name # 'search_books' @@ -77,6 +87,8 @@ e `tool.input_schema` é o JSON Schema que o servidor derivou das anotações de Esse schema é tudo o que uma UI precisa para renderizar um formulário de argumentos, e tudo o que um modelo precisa para produzir argumentos válidos. +A segunda ferramenta, `lookup_book`, foi registrada sem um `title=`, então o `tool.title` dela é `None`. + !!! tip `title` é opcional, então uma UI que mostra ferramentas a um humano tem que escolher: o `title` se houver um, o `name` se não. `from mcp.shared.metadata_utils import get_display_name` faz exatamente isso, @@ -86,7 +98,7 @@ Esse schema é tudo o que uma UI precisa para renderizar um formulário de argum `call_tool(name, arguments)` executa a ferramenta e devolve um `CallToolResult`. -```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 @@ Uma ferramenta que lança uma exceção **não** lança no seu cliente. Ela volt Os verbos de recurso vêm em pares: duas formas de listar, uma forma de ler. -```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 @@ Um cliente também pode ser avisado quando um recurso muda. Em conexões da era ## 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 @@ Um host entrega essas mensagens direto ao modelo. A funcionalidade inteira é es Um servidor com um handler de completion pode autocompletar argumentos de prompts e de templates de recurso enquanto o usuário digita. -```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 @@ A resposta está em `result.completion.values`. Digite `"p"` e o servidor volta Todo método `list_*` aceita um argumento nomeado `cursor=` e todo resultado carrega um `next_cursor`. Quando `next_cursor` é `None`, você tem tudo. -```python title="client.py" hl_lines="22-30" +```python title="client.py" hl_lines="7-15" --8<-- "docs_src/client/tutorial007.py" ``` -Esse loop está correto contra qualquer servidor. O `MCPServer` retorna tudo em uma página só, então `next_cursor` é `None` e o loop roda uma vez, e é por isso que a maioria do código nunca o escreve. Servidores que paginam de verdade, e as regras que os cursores obedecem, estão em **[Paginação](../advanced/pagination.md)**. +`list_all_tools` está correta contra qualquer servidor. O `MCPServer` retorna tudo em uma página só, então `next_cursor` é `None` e o loop roda uma vez, e é por isso que a maioria do código nunca o escreve. Servidores que paginam de verdade, e as regras que os cursores obedecem, estão em **[Paginação](../advanced/pagination.md)**. ## Em testes {#in-tests} -`Client(mcp)`, sem processo e sem porta, já é um harness de teste para o seu servidor. +Todo `client.py` desta página alcançou o `server.py` via HTTP. Em um teste você pula a rede e entrega ao `Client` o próprio objeto servidor: `from server import mcp`, depois `Client(mcp)`. Sem processo, sem porta, e todo método acima funciona igual. -Existe uma flag do construtor feita para isso: `Client(mcp, raise_exceptions=True)`. Ela só tem efeito em conexões em memória, e **[Testes](../get-started/testing.md)** é a página que a explica e constrói todo o padrão em torno dela. +Existe uma flag do construtor feita para isso: `Client(mcp, raise_exceptions=True)`. Ela só tem efeito em conexões no mesmo processo, e **[Testes](../get-started/testing.md)** é a página que a explica e constrói todo o padrão em torno dela. ## Recapitulando {#recap} -* `Client(x)` conecta em memória a um objeto servidor, via Streamable HTTP a uma string de URL, e por qualquer outra coisa via um transporte. +* `Client(x)` conecta via Streamable HTTP a uma string de URL, inicia um subprocesso para um `StdioServerParameters`, entra direto em um transporte e, em testes, recebe o próprio objeto servidor. * `async with` é o ciclo de vida inteiro. Dentro dele, `server_capabilities` e `protocol_version` já estão preenchidos; `server_info` e `instructions` também, quando o servidor os fornece. * `list_tools()` dá a você o `name`, `title`, `description` e `input_schema` de cada ferramenta. * `call_tool()` retorna `content` para o modelo, `structured_content` para o seu código e `is_error`. Uma ferramenta que lança exceção é um resultado, não uma exceção. diff --git a/i18n/pt/pages/client/oauth-clients.md b/i18n/pt/pages/client/oauth-clients.md index 51d1824f2b..7464435f47 100644 --- a/i18n/pt/pages/client/oauth-clients.md +++ b/i18n/pt/pages/client/oauth-clients.md @@ -1,6 +1,6 @@ --- translation: - sections: [c6899d3892bd9fa0, 79372cff3cc48a88, 63878d29e87c3e73, 13175843d3588af4, e7e2b9fd516f77de, 758f06399b513c1f, a05d7278487d610b] + sections: [c6899d3892bd9fa0, 79372cff3cc48a88, c2dae1ebe2ebd543, 13175843d3588af4, df06056fb16b3846, 758f06399b513c1f, a05d7278487d610b] tool: 1 --- # Clientes OAuth {#oauth-clients} @@ -81,18 +81,20 @@ Veja `main()`. O provider vai no **cliente httpx2**, o cliente httpx2 vai em `st Na primeira vez que `Client` envia uma requisição, o servidor responde `401`. O provider assume: -1. **Descoberta.** Ele lê o cabeçalho `WWW-Authenticate`, busca os Protected Resource Metadata do servidor em `/.well-known/oauth-protected-resource`, descobre qual servidor de autorização protege este recurso e busca os metadados *desse* servidor. +1. **Descoberta.** Ele lê o cabeçalho `WWW-Authenticate`, busca os Protected Resource Metadata do servidor em `/.well-known/oauth-protected-resource`, descobre qual servidor de autorização protege este recurso e busca os metadados *desse* servidor. (A um servidor mais antigo, que não publica metadados de recurso, o provider pede os metadados do servidor de autorização na própria origem dele.) De um jeito ou de outro, os metadados precisam nomear, como `issuer`, o servidor para o qual foram buscados; qualquer outra coisa é recusada. 2. **Registro.** Nada no armazenamento? Ele registra você dinamicamente com o seu `OAuthClientMetadata` e armazena o resultado. 3. **Autorização.** Ele gera o par PKCE e um `state`, monta a URL de autorização, faz await no seu `redirect_handler` e depois faz await no seu `callback_handler` para obter o code. 4. **Troca.** Ele troca o code por um `OAuthToken`, armazena e reenvia a sua requisição original com `Authorization: Bearer ...`. Depois disso ele fica quieto. Os tokens saem do armazenamento, um token de acesso expirado é renovado com o refresh token, e só quando nada disso funciona ele executa o fluxo de novo. +Uma regra de transporte vale para todas essas requisições: assim como a requisição MCP dentro da qual elas acontecem, elas só seguem um redirecionamento quando ele fica na mesma origem e mantém o método (um 307/308 de barra final, digamos), e tratam qualquer outro redirecionamento como se aquela URL não respondesse. + Você não escreveu nada disso. Restam dois argumentos nomeados (`client_metadata_url` e `validate_resource_url`), e este arquivo não precisa de nenhum dos dois. `client_metadata_url` é o que vale a pena conhecer; ele ganha uma seção própria abaixo. ### Experimente {#try-it} -A maioria dos exemplos nesta documentação você consegue conferir com um `Client(server)` em memória. Este não: o ponto central do fluxo é um `401` HTTP, e não há HTTP entre um cliente em memória e o seu servidor. +O `Client(server)` em memória que os seus testes usam não ajuda aqui: o ponto central do fluxo é um `401` HTTP, e não há HTTP entre um cliente em memória e o seu servidor. O repositório traz a versão ao vivo. `examples/servers/simple-auth/` executa um servidor de autorização independente e um servidor MCP protegido; `examples/clients/simple-auth-client/` é o cliente desta página crescido até virar uma pequena CLI. O README dele tem os dois comandos: inicie os servidores, execute o cliente contra eles e veja as quatro etapas passarem. @@ -110,13 +112,14 @@ Um job noturno, uma etapa de CI, outro serviço. Não há navegador nem ninguém `ClientCredentialsOAuthProvider` é o mesmo `httpx2.Auth`, sem o humano: -```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" ``` O que mudou: * Sem `OAuthClientMetadata`, sem handlers. Você passa `client_id` e `client_secret`; o provider monta um registro `client_credentials` mínimo em torno deles e pula o registro dinâmico por completo. +* `issuer` nomeia o servidor de autorização que emitiu essas credenciais; use o valor `issuer` que o documento `/.well-known/oauth-authorization-server` dele retorna. A descoberta ainda acontece como acima, mas as requisições de token só são montadas a partir dos metadados *desse* issuer; se o servidor MCP apontar para qualquer outro lugar, o fluxo para com um `OAuthFlowError`. Deixá-lo de fora está obsoleto e ele passa a ser obrigatório na 3.0 (veja **[Funcionalidades obsoletas](../deprecated.md#deprecated-sdk-helpers)**); até lá, o provider emite um aviso e usa o servidor de autorização que a descoberta encontrar. * `scope` é uma string separada por espaços, o formato OAuth usado na comunicação. * Tudo a partir daí é idêntico: o mesmo `TokenStorage`, o mesmo `httpx2.AsyncClient(auth=...)`, o mesmo `streamable_http_client`. @@ -129,7 +132,7 @@ Por padrão, o secret viaja como HTTP Basic auth na requisição de token (`clie Mais um provider mora em `mcp.client.auth.extensions.client_credentials`: **`PrivateKeyJWTOAuthProvider`**, para clientes que se autenticam com um JWT em vez de um segredo compartilhado (`private_key_jwt`, a variante de par de chaves e workload identity). Ele segue - o mesmo padrão: construa um, coloque em `auth=`. O mesmo módulo traz + o mesmo padrão: construa um (ele aceita o mesmo `issuer` opcional), coloque em `auth=`. O mesmo módulo traz `SignedJWTParameters` e `static_assertion_provider`, dois helpers que montam a assertion dele. Há mais uma situação sem humano: o cliente pertence a uma empresa cujo provedor de identidade, e não o usuário, decide quais servidores MCP ele pode alcançar. Esse é um grant diferente, com seu próprio modelo de confiança e sua própria página, **[Asserção de identidade](identity-assertion.md)**. diff --git a/i18n/pt/pages/client/session-groups.md b/i18n/pt/pages/client/session-groups.md index 7f42531d44..8b2324b2ff 100644 --- a/i18n/pt/pages/client/session-groups.md +++ b/i18n/pt/pages/client/session-groups.md @@ -1,6 +1,6 @@ --- translation: - sections: [09c857a25a9dc37a, 43bc6a76a243a50e, 0a716022a88768df, 4b7f78042bfcfff7, c112662e61b03315, 58974ba1f489a8b4, d18adbdbb835ea73] + sections: [09c857a25a9dc37a, 43bc6a76a243a50e, 0a716022a88768df, 4b7f78042bfcfff7, c112662e61b03315, 58974ba1f489a8b4, ed4d17e894864056] tool: 1 --- # Grupos de sessões {#session-groups} @@ -78,7 +78,7 @@ Se você já tem em mãos uma `ClientSession` conectada (`Client.session` é uma ## Recapitulando {#recap} * `ClientSessionGroup` mantém várias conexões de servidor e reúne as ferramentas, recursos e prompts delas em um `dict` para cada tipo. -* `connect_to_server(params)` por servidor. Ele recebe parâmetros de transporte, nunca o objeto de servidor ou a URL que um `Client` recebe. +* `connect_to_server(params)` por servidor. Ele recebe parâmetros de transporte, nunca a URL ou o `Transport` que um `Client` recebe. * `group.call_tool(name, arguments)` roteia para o servidor dono por você. * Os nomes precisam ser únicos no grupo inteiro; dois servidores com uma ferramenta `search` não conseguem coexistir por conta própria. * `component_name_hook=` reescreve cada nome registrado. A chave do dict muda, o nome na rede não. diff --git a/i18n/pt/pages/client/transports.md b/i18n/pt/pages/client/transports.md index 31504bac38..7d8c48018e 100644 --- a/i18n/pt/pages/client/transports.md +++ b/i18n/pt/pages/client/transports.md @@ -1,6 +1,6 @@ --- translation: - sections: [9cac816674181eb0, 0700f337babcd4dd, 2bde0dd58cdf00f5, 40b4916d82eaf1d4, 3d0832f39b0d7059, dfa4446556badef0, 5bd93be2ab2ecb9c] + sections: [9cac816674181eb0, 7c157764133fea1f, 40b4916d82eaf1d4, 10d151f2cc75317f, 3d0832f39b0d7059, 92742ba36533633d, 0aeca6145e7bd302] tool: 1 --- # Transportes do cliente {#client-transports} @@ -11,30 +11,15 @@ Você nunca configura um transporte separadamente. `Client` recebe um único arg O lado do *servidor* de cada um (o que `mcp.run()` faz e o que você coloca no deploy) está em **[Executando seu servidor](../run/index.md)**. -## Em memória {#in-memory} - -Passe o próprio objeto do servidor: - -```python title="client.py" hl_lines="14" ---8<-- "docs_src/client_transports/tutorial001.py" -``` - -Sem subprocesso, sem porta, sem bytes trafegando na rede. O cliente e o servidor são dois objetos no mesmo processo, e a chamada ainda passa pela camada real do protocolo: `search_books` é listada, validada e invocada exatamente como seria sobre HTTP. - -Isso faz dele duas coisas ao mesmo tempo: - -* **Uma estrutura de testes.** Todo exemplo desta documentação é exercitado dessa forma, e a página **[Testes](../get-started/testing.md)** constrói o padrão inteiro em torno disso. -* **Uma API de embutimento.** Uma aplicação que constrói o servidor não precisa de um salto pela rede para chamar as ferramentas dele. - ## Streamable HTTP {#streamable-http} -Passe uma string de URL e você tem **Streamable HTTP**, o transporte atrás do qual você faz o deploy: +Passe uma string de URL e você tem **Streamable HTTP**, o transporte atrás do qual você faz o deploy e o primeiro a que você deve recorrer: ```python title="client.py" hl_lines="5" --8<-- "docs_src/client_transports/tutorial002.py" ``` -Esse é o cliente de produção inteiro. `Client` envolve a URL em `streamable_http_client(...)` para você, sobre um `httpx2.AsyncClient` configurado do jeito que o MCP precisa: `follow_redirects=True`, um timeout de 30 segundos para connect/write/pool e um timeout de leitura de 300 segundos, porque o servidor pode manter um stream de resposta aberto. +Esse é o cliente de produção inteiro. `Client` envolve a URL em `streamable_http_client(...)` para você, sobre um `httpx2.AsyncClient` configurado do jeito que o MCP precisa: um timeout de 30 segundos para connect/write/pool e um timeout de leitura de 300 segundos, porque o servidor pode manter um stream de resposta aberto. !!! check Um `Client` que você construiu **não** está conectado. A construção só escolhe o transporte; @@ -50,7 +35,7 @@ Esse é o cliente de produção inteiro. `Client` envolve a URL em `streamable_h No momento em que você precisar de um header `Authorization`, um cookie, um proxy, mTLS ou um timeout diferente, construa o `httpx2.AsyncClient` você mesmo e entregue-o a `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 @@ sem um repositório de CAs do sistema utilizável (alguns contêineres mínimos) !!! info `httpx2` mantém a API conhecida do `httpx`, então se você conhece `httpx` já sabe como fazer auth, proxies, event hooks, retentativas e limites de conexão aqui. O SDK não acrescenta nada por cima nem - tira nada. É também onde o OAuth se encaixa: + tira nada, exceto o [tratamento de redirecionamentos](#redirects). É também onde o OAuth se encaixa: `httpx2.AsyncClient(auth=OAuthClientProvider(...))`. Esse fluxo inteiro está em **[Clientes OAuth](oauth-clients.md)**. +### Redirecionamentos {#redirects} + +O transporte se conecta à URL que você forneceu, e somente a essa origem. + +* Um redirecionamento `307`/`308` que permanece no mesmo esquema, host e porta é seguido, e `http://` → `https://` no mesmo host também. Isso cobre o redirecionamento habitual de barra final `/mcp` → `/mcp/`. +* Um redirecionamento para qualquer outro lugar **não** é seguido. A chamada falha com: + + ```text + MCPError: Redirect to https://other.example.com/mcp not followed; use that URL as the endpoint if it is the intended server + ``` + + Se essa URL é o servidor que você queria, coloque-a na sua configuração. Se não é, o servidor ou um proxy na frente dele está mal configurado. + +Isso vale para qualquer `httpx2.AsyncClient` que você passe: a configuração `follow_redirects` dele não é consultada para requisições MCP, em nenhuma das direções. Os provedores OAuth do SDK aplicam a mesma regra às próprias requisições. + +!!! tip + `Redirect to http://… not followed: it would downgrade this HTTPS endpoint to plain HTTP` significa que o + servidor está atrás de um proxy com terminação TLS que ele desconhece e está emitindo redirecionamentos `http://`. + Isso se corrige no servidor (**[Deploy e escala](../run/deploy.md#behind-a-tls-terminating-proxy)**) + ou usando a URL exata `https://…/` que a mensagem sugere. + ## stdio {#stdio} Um servidor **stdio** é um subprocesso. O cliente o inicia, escreve JSON-RPC no stdin dele e lê JSON-RPC do stdout dele. É assim que um host de desktop executa um servidor na sua máquina: um host *é* este código mais uma interface, e **[Conecte a um host real](../get-started/real-host.md)** é a mesma relação vista do lado do host, como um arquivo de configuração. @@ -105,6 +111,18 @@ O stderr do processo filho vai para o seu. Para mandá-lo para outro lugar, cons Um servidor que precise de uma chave de API não vai encontrá-la ali. Passe-a explicitamente com `env=`; essas variáveis são mescladas por cima da allow-list. É isso que `BOOKSHOP_API_KEY` está fazendo acima. +## Em memória {#in-memory} + +Em um teste não há nada para colocar no deploy nem nada para iniciar. Passe o próprio objeto do servidor: + +```python hl_lines="14" +--8<-- "docs_src/client_transports/tutorial001.py" +``` + +Sem subprocesso, sem porta, sem bytes trafegando na rede. O cliente e o servidor são dois objetos no mesmo processo, e a chamada ainda passa pela camada real do protocolo: `search_books` é listada, validada e invocada exatamente como seria sobre HTTP. A página **[Testes](../get-started/testing.md)** constrói o padrão inteiro em torno disso. + +A mesma forma serve também como API de embutimento: uma aplicação que constrói o servidor por conta própria pode chamar as ferramentas dele sem um salto pela rede. + ## SSE {#sse} `sse_client(url)`, de `mcp.client.sse`, é o transporte HTTP que o Streamable HTTP substituiu. Envolva-o da mesma forma, `Client(sse_client("http://localhost:8000/sse"))`, para conversar com um servidor que ainda o fala, e não construa nada novo em cima dele. @@ -113,15 +131,16 @@ O stderr do processo filho vai para o seu. Para mandá-lo para outro lugar, cons Para o `Client`, tudo acima é a mesma coisa. -Um **transporte** é qualquer gerenciador de contexto assíncrono que produz um par `(read, write)` de streams de mensagens: formalmente, o protocolo `Transport` em `mcp.client`. `Client` resolve seu argumento pelo tipo: um objeto de servidor conecta no próprio processo, uma `str` vira `streamable_http_client(url)`, um `StdioServerParameters` vira `stdio_client(params)` e qualquer outra coisa é aberta diretamente como transporte. É por causa dessa última regra que `stdio_client(...)`, `streamable_http_client(...)` e `sse_client(...)` se encaixam todos no mesmo lugar, e que você pode escrever o seu próprio. +Um **transporte** é qualquer gerenciador de contexto assíncrono que produz um par `(read, write)` de streams de mensagens: formalmente, o protocolo `Transport` em `mcp.client`. `Client` resolve seu argumento pelo tipo: uma `str` vira `streamable_http_client(url)`, um `StdioServerParameters` vira `stdio_client(params)`, um objeto de servidor conecta no próprio processo e qualquer outra coisa é aberta diretamente como transporte. É por causa dessa última regra que `stdio_client(...)`, `streamable_http_client(...)` e `sse_client(...)` se encaixam todos no mesmo lugar, e que você pode escrever o seu próprio. ## Recapitulando {#recap} -* `Client(mcp)` (o objeto do servidor) conecta em memória. Use para testes e para embutir. * `Client("http://.../mcp")` (uma URL) conecta por Streamable HTTP, o transporte de produção. * Headers, auth, proxies e timeouts pertencem a um `httpx2.AsyncClient` que você passa a `streamable_http_client(url, http_client=...)`. Não existe o argumento `headers=`. +* Redirecionamentos só são seguidos dentro da própria origem da URL (um `307`/`308` de barra final), mais `http`→`https` no mesmo host. Qualquer outra coisa falha com `Redirect to … not followed`; configure a URL final. * stdio é `Client(StdioServerParameters(...))`. Envolva-o em `stdio_client(...)` você mesmo apenas para redirecionar o stderr do processo filho. * O subprocesso recebe um ambiente em allow-list, não o seu; `env=` acrescenta a ele. +* `Client(mcp)` (o objeto do servidor) conecta em memória. Use em testes, ou para embutir um servidor na aplicação que o construiu. * Um transporte é qualquer coisa com que você possa fazer `async with x as (read, write)`. `Client` entrega direto a esse protocolo tudo que não for um objeto de servidor, uma URL ou um `StdioServerParameters`. * Construir um `Client` escolhe o transporte. `async with` o abre. diff --git a/i18n/pt/pages/deprecated.md b/i18n/pt/pages/deprecated.md index 564d9ae243..a4d8f0e1f2 100644 --- a/i18n/pt/pages/deprecated.md +++ b/i18n/pt/pages/deprecated.md @@ -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 --- # Funcionalidades descontinuadas {#deprecated-features} -A especificação 2026-07-28 aposenta cinco coisas. O SDK ainda implementa cada uma delas, e cada uma agora carrega um **aviso de descontinuação**. Um helper do SDK está descontinuado por conta própria e aparece listado [no final](#deprecated-sdk-helpers). +A especificação 2026-07-28 aposenta cinco coisas. O SDK ainda implementa cada uma delas, e cada uma agora carrega um **aviso de descontinuação**. Algumas descontinuações no nível do SDK valem por conta própria e aparecem listadas [no final](#deprecated-sdk-helpers). A tabela abaixo nomeia cada funcionalidade descontinuada, o motivo de ela estar saindo e o substituto sobre o qual construir. @@ -137,11 +137,13 @@ A API inteira é essa. Não há uma chave por método, e você não quer uma: o ## Helpers descontinuados do SDK {#deprecated-sdk-helpers} -Estas não são mudanças de especificação, apenas detalhes internos do SDK com um substituto melhor. Avisam com o mesmo `MCPDeprecationWarning` e serão removidos na 3.0. +Estas não são mudanças de especificação, apenas usos do SDK com um substituto melhor. Avisam com o mesmo `MCPDeprecationWarning`, e a 3.0 remove a forma antiga. | Descontinuado | O que fazer no lugar | |---|---| | `FuncMetadata.call_fn_with_arg_validation()` | `FuncMetadata.validate_arguments()` e depois `FuncMetadata.call_fn()`. Só código que conduz `FuncMetadata` diretamente (uma subclasse personalizada de `Tool`, digamos) chegou a chamá-lo. | +| `AuthSettings(resource_server_url=...)` sem `validate_token_resource=` | Defina-o: `True` faz o servidor recusar bearer tokens que o seu verificador não informa como emitidos para `resource_server_url`; `False` diz que o seu verificador confere a audiência do token por conta própria (veja **[Autorização](run/authorization.md#a-token-verifier)**). Sem definir, ele se comporta como `False`; a 3.0 torna `True` o padrão sempre que `resource_server_url` estiver definido. | +| `ClientCredentialsOAuthProvider(...)` ou `PrivateKeyJWTOAuthProvider(...)` sem `issuer=` | Passe `issuer=` nomeando o servidor de autorização que emitiu as credenciais (veja **[Escrevendo clientes OAuth](client/oauth-clients.md#machine-to-machine)**). Sem ele, é o servidor MCP que decide qual servidor de autorização as recebe; a 3.0 torna o parâmetro obrigatório. | ## Recapitulando {#recap} @@ -150,7 +152,7 @@ Estas não são mudanças de especificação, apenas detalhes internos do SDK co * Descontinuado é consultivo: sem mudanças no protocolo de transmissão, tudo continua funcionando em sessões pré-2026, e você recebe um `MCPDeprecationWarning` visível (um `UserWarning`, então está ligado por padrão). * Amostragem e roots precisam, além disso, de um canal de retorno que uma sessão 2026-07-28 não tem. Em uma conexão moderna elas avisam e depois lançam uma exceção. * `warnings.filterwarnings("ignore", category=MCPDeprecationWarning)` silencia a categoria inteira; `"error::mcp.MCPDeprecationWarning"` no pytest a transforma em falha de teste. -* Um helper do SDK, `FuncMetadata.call_fn_with_arg_validation()`, está descontinuado separadamente para remoção na 3.0. +* As [descontinuações no nível do SDK](#deprecated-sdk-helpers) seguem a mesma regra: avisam agora, e a 3.0 remove a forma antiga. * Código novo não deve ser construído sobre nenhuma delas. Todas as outras páginas desta documentação ensinam a API atual. diff --git a/i18n/pt/pages/get-started/first-steps.md b/i18n/pt/pages/get-started/first-steps.md index 32b8bb46b1..baeb501a3b 100644 --- a/i18n/pt/pages/get-started/first-steps.md +++ b/i18n/pt/pages/get-started/first-steps.md @@ -1,6 +1,6 @@ --- translation: - sections: [0d6c05bcbf836bf3, 59a7b14eeefc68c1, 7114d8d6daba203f, e8bbb56a98ba7bc9, 5138010f6159901c, f78da7c7c363d4c6, 220a939cab348686] + sections: [0d6c05bcbf836bf3, 9a78b5f6b44b18ab, 7114d8d6daba203f, e8bbb56a98ba7bc9, bfd2fd1153e71dac, 1615a994ef071fdd, 65c599fae991f245] tool: 1 --- # Primeiros passos {#first-steps} @@ -17,7 +17,7 @@ Três palavras que você vai ver em todas as páginas daqui em diante: * Um **cliente** vive dentro do host e fala MCP. O host executa um cliente para cada servidor ao qual está conectado. * Um **servidor** é o que você constrói com este SDK. Ele expõe coisas aos clientes. Nunca fala diretamente com o modelo. -Você escreve o servidor. Os hosts são produto de terceiros. O SDK também traz um `Client`. Você vai usá-lo para testar seus servidores, e ele aparece mais adiante nesta página. +Você escreve o servidor. Os hosts são produto de terceiros. O SDK também traz um `Client`, a mesma classe que um host usaria para alcançar um servidor por URL ou iniciá-lo como subprocesso. Ele aparece mais adiante nesta página, e é também com ele que você vai testar seus servidores. ## As três primitivas {#the-three-primitives} @@ -83,22 +83,20 @@ Você viu três abas no Inspector. Como ele sabia que eram três? Quando um cliente se conecta, o servidor declara suas **capacidades**: quais famílias de requisições ele vai responder. O cliente usa essa declaração para decidir o que faz sentido pedir. Você nunca escreveu isso; o `MCPServer` declara por você. -Veja você mesmo. O `Client` do SDK aceita o objeto do servidor diretamente e se conecta a ele **em memória** (sem subprocesso, sem porta): - -```python -import asyncio - -from mcp import Client - -from server import mcp +Veja você mesmo. Deixe o `server.py` rodando via HTTP em um terminal: +```console +uv run mcp run server.py --transport streamable-http +``` -async def main() -> None: - async with Client(mcp) as client: - print(client.server_capabilities.model_dump(exclude_none=True)) +e aponte um cliente para ele a partir de outro: +```python title="client.py" hl_lines="7-8" +--8<-- "docs_src/first_steps/tutorial001_client.py" +``` -asyncio.run(main()) +```console +python client.py ``` ```text @@ -118,8 +116,9 @@ O `MCPServer` serve as três primitivas, então as três são sempre declaradas. Repare no que não aparece ali. `completions` (autocompletar de argumentos para templates de recurso e prompts) precisa de um handler escrito por você; este servidor não tem nenhum, então a capacidade fica de fora e um cliente bem-comportado nem pede. Essa é a regra para tudo que é opcional: registre a coisa e a capacidade aparece; **[Completions](../servers/completions.md)** comprova isso. !!! info - `Client(mcp)` é o mesmo cliente em memória com que todos os exemplos desta documentação são - testados, e é assim que você vai testar os seus. Ele ganha uma página inteira: **[Testes](testing.md)**. + Esse `client.py` é um cliente MCP completo, e **[O cliente](../client/index.md)** é a página dele. + Em um teste, você pula o terminal e a porta e entrega ao `Client` o próprio objeto do servidor, + `Client(mcp)`. Isso também ganha uma página inteira: **[Testes](testing.md)**. ## O que você não escreveu {#what-you-did-not-write} @@ -128,7 +127,7 @@ Olhe de novo esta página. Você escreveu três funções Python pequenas. Você * Um JSON Schema. `a: int, b: int` *é* o schema de `add`. * Um handler de requisição. `tools/list`, `resources/read`, `prompts/get`: o SDK atende todos por você. * Uma declaração de capacidades. O `MCPServer` fez isso por você. -* Uma linha de protocolo. A negociação de versão, o enquadramento JSON-RPC, a troca de capacidades: tudo isso aconteceu dentro de `mcp dev` e `Client(mcp)`, e você nunca viu. +* Uma linha de protocolo. A negociação de versão, o enquadramento JSON-RPC, a troca de capacidades: tudo isso aconteceu dentro de `mcp dev` e `client.py`, e você nunca viu. Essa proporção é a razão de ser do SDK. @@ -139,6 +138,6 @@ Essa proporção é a razão de ser do SDK. * Um decorador por primitiva: `@mcp.tool()`, `@mcp.resource(uri)`, `@mcp.prompt()`. Nome, descrição e schema vêm da função. * Uma URI com um `{param}` cria um **template** de recurso, listado separadamente dos recursos concretos. * As **capacidades** do servidor já vêm declaradas para você, e um cliente só pede o que o servidor declara. -* `Client(mcp)` se conecta ao objeto do servidor em memória: seu ambiente de testes desde o primeiro dia. +* `Client("http://localhost:8000/mcp")` conversa com seu servidor em execução. Entregue a ele o objeto do servidor no lugar, `Client(mcp)`, e ele vira seu ambiente de testes desde o primeiro dia. A seguir vem **[Conecte a um host real](real-host.md)**: este servidor dentro do Claude Desktop ou de uma IDE, de verdade. Depois, **[Testes](testing.md)**: uma página, um cliente em memória, e você nunca mais fica adivinhando se funciona. Depois disso, cada primitiva ganha sua própria página, começando pela que o modelo comanda: **[Ferramentas](../servers/tools.md)**. diff --git a/i18n/pt/pages/get-started/testing.md b/i18n/pt/pages/get-started/testing.md index 2eb6c0621a..d2b49686e4 100644 --- a/i18n/pt/pages/get-started/testing.md +++ b/i18n/pt/pages/get-started/testing.md @@ -1,13 +1,13 @@ --- translation: - sections: ['4926721070127497', c52a1de2b6b32f40, 8e792bf8c7489ec6, 627195f7159e24ef] + sections: [5d13c2f0ba42c0d2, c52a1de2b6b32f40, 8e792bf8c7489ec6, 38552ea228b0a04f] tool: 1 --- # Testes {#testing} -O SDK Python traz uma classe `Client` com um **transporte em memória**: passe a ela o objeto do seu servidor e ela se conecta diretamente a ele. +A classe `Client` do SDK, a mesma que se conecta a uma URL ou inicia um subprocesso, também se conecta **em memória**: passe a ela o objeto do seu servidor e ela conversa diretamente com ele. -Sem subprocesso. Sem porta. Sem transporte nenhum. É a mesma ideia do `TestClient` do FastAPI. +Sem subprocesso. Sem porta. Nada trafegando na rede. É a mesma ideia do `TestClient` do FastAPI. ## Uso básico {#basic-usage} @@ -97,7 +97,7 @@ em vez da sanitizada. Deixe-a ligada nos testes. Em código de produção, ela não significa nada. -## No mesmo processo por padrão {#in-process-by-default} +## Neutro quanto à era por padrão {#era-neutral-by-default} !!! note `Client(mcp)` se conecta no mesmo processo e é **neutro quanto à era** por padrão: ele sonda o servidor e diff --git a/i18n/pt/pages/handlers/multi-round-trip.md b/i18n/pt/pages/handlers/multi-round-trip.md index 77432a5764..ff7b0448a2 100644 --- a/i18n/pt/pages/handlers/multi-round-trip.md +++ b/i18n/pt/pages/handlers/multi-round-trip.md @@ -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 --- # Requisições com múltiplas idas e voltas {#multi-round-trip-requests} @@ -164,7 +164,7 @@ O `Server` de baixo nível é o nível sem pilhas inclusas: diferente do `MCPSer ## Um resultado de 2026-07-28 {#a-2026-07-28-result} -`InputRequiredResult` só existe na versão de protocolo **2026-07-28**. O `Client(server)` em memória a negocia por você; pela rede, `mode="auto"` a descobre. Depois de conectar, `client.protocol_version` diz o que você obteve. +`InputRequiredResult` só existe na versão de protocolo **2026-07-28**. O `mode="auto"` padrão do `Client` a descobre em qualquer conexão. Depois de conectar, `client.protocol_version` diz o que você obteve. !!! warning Uma sessão pré-2026 não tem onde colocar um `InputRequiredResult`. Retorne um do seu handler em uma diff --git a/i18n/pt/pages/handlers/progress.md b/i18n/pt/pages/handlers/progress.md index caa2c09484..f6b78f8ccd 100644 --- a/i18n/pt/pages/handlers/progress.md +++ b/i18n/pt/pages/handlers/progress.md @@ -1,6 +1,6 @@ --- translation: - sections: [5315262fe26b33e1, 9d8e98840f1b78f0, 0284b215e85366c4, 8534d8dbb4053a70, 2966fac6fe697007] + sections: [5315262fe26b33e1, 9d8e98840f1b78f0, 52d6009a07e770ea, 8534d8dbb4053a70, 2966fac6fe697007] tool: 1 --- # Progresso {#progress} @@ -29,19 +29,17 @@ Três argumentos, e você decide o que eles significam: O cliente opta por receber **por chamada**, passando `progress_callback=` para `call_tool`: -```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) O callback é uma função `async` que recebe exatamente o que o servidor informou: `progress`, `total`, `message`. !!! info - `Client(mcp)` conecta direto ao objeto do servidor, em memória, o mesmo cliente sobre o qual a página **[Testes](../get-started/testing.md)** - é construída. `progress_callback` é o mesmo parâmetro seja qual for o transporte que o `Client` - usa; o *timing* que você está prestes a ver é o da conexão em memória. Ela executa seu callback - inline, então todo informe chega antes de `call_tool` retornar. Em um transporte real, as - notificações disputam corrida com o resultado, e um callback lento ainda pode estar executando depois que `call_tool` - retornou. + `progress_callback` é o mesmo parâmetro seja lá o que você tenha passado ao `Client`: uma URL como aqui, um + `StdioServerParameters`, ou o objeto do servidor em um teste. Mas fique atento ao timing em um transporte + real. Cada notificação é entregue por conta própria, ao lado da resposta, então um callback lento + ainda pode estar executando depois que `call_tool` retornou. Só a conexão de teste em processo + executa o callback inline e garante que todo informe chegue antes. ### Experimente {#try-it} -Coloque `client.py` ao lado de `server.py` e execute: +Sirva `server.py` por HTTP e depois execute o cliente em um segundo 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.'} ``` -Cada `await ctx.report_progress(...)` no servidor virou uma chamada a `show` no cliente, em ordem, e as duas linhas foram impressas **antes** de `call_tool` retornar. O progresso não vem embutido no resultado; ele é transmitido enquanto a ferramenta ainda está trabalhando. +Cada `await ctx.report_progress(...)` no servidor virou uma chamada a `show` no cliente, em ordem. O progresso não vem embutido no resultado. Ele é transmitido enquanto a ferramenta ainda está trabalhando. !!! warning `progress_callback` pertence à **chamada**, não ao `Client`. Não há argumento de construtor diff --git a/i18n/pt/pages/protocol-versions.md b/i18n/pt/pages/protocol-versions.md index 6ce3ae2f2a..44cec718a3 100644 --- a/i18n/pt/pages/protocol-versions.md +++ b/i18n/pt/pages/protocol-versions.md @@ -1,6 +1,6 @@ --- translation: - sections: [478fd619e5f90ef8, aef094a00e44e248, bab8cbf3449fa7e9, df1809b15a58335b, 5f9d8c2336ed0239, f54974398e43ddef, b24443dd78584870] + sections: [d98e337bf28845e0, dd642acbba36f615, f547b3e76da19c94, 149513378588da5c, 4e149ed992046709, f54974398e43ddef, b24443dd78584870] tool: 1 --- # Versões do protocolo {#protocol-versions} @@ -11,9 +11,17 @@ Os servidores lançados antes de 2026-07-28 abrem toda conexão com o **handshak Você quase nunca precisa se preocupar com isso, porque o `Client` negocia por você. Esta página trata do único argumento do construtor que controla isso, `mode=`, e das três situações em que você o altera. +Cada trecho de código desta página é um `client.py` que conversa com o `server.py` da Bookshop de **[O cliente](client/index.md)**. Inicie esse servidor em um terminal: + +```console +uv run mcp run server.py --transport streamable-http +``` + +Depois execute cada trecho em um segundo terminal com `python client.py`. + ## `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 @@ De um jeito ou de outro você sai conectado, e `client.protocol_version` diz qua A funcionalidade inteira é essa. Um `Client`, qualquer era de servidor, sem ramificações no seu código. !!! info - O `MCPServer` responde a `server/discover` em todos os transportes — em memória, stdio, streamable - HTTP — então, contra o seu próprio servidor, `auto` sempre chega em `2026-07-28`. O fallback só - dispara contra um servidor real anterior a 2026, que é exatamente quando você quer que ele dispare. + O `MCPServer` responde a `server/discover` em todos os transportes — Streamable HTTP, stdio e a + conexão no mesmo processo que os seus testes usam —, então, contra o seu próprio servidor, `auto` + sempre chega em `2026-07-28`. O fallback só dispara contra um servidor real anterior a 2026, que é + exatamente quando você quer que ele dispare. ## `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 @@ Em 2026-07-28 ele não existe mais. O servidor *retorna* suas perguntas e você `mode` também aceita uma string de versão moderna do protocolo. Hoje esse conjunto é exatamente `["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 @@ A sondagem é barata, mas ainda é uma ida e volta que você paga a cada reconex Então guarde-a. Depois de uma conexão `auto`, `client.session.discover_result` contém o `DiscoverResult` exato que o servidor enviou: seu `supported_versions`, seu `capabilities`, seu `instructions` e a identidade que o servidor carimbou no `_meta` do resultado. Passe-o de volta como `prior_discover=` na próxima vez: -```python title="client.py" hl_lines="15 17" +```python title="client.py" hl_lines="8 10" --8<-- "docs_src/protocol_versions/tutorial004.py" ``` diff --git a/i18n/pt/pages/run/asgi.md b/i18n/pt/pages/run/asgi.md index 563ef3d9b7..940c732b92 100644 --- a/i18n/pt/pages/run/asgi.md +++ b/i18n/pt/pages/run/asgi.md @@ -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 --- # Adicione a um app existente {#add-to-an-existing-app} @@ -99,7 +99,7 @@ Aquele `/mcp` no final é o `streamable_http_path`. Defina-o como `"/"` e o pref --8<-- "docs_src/asgi/tutorial004.py" ``` -Agora os clientes se conectam a `/notes`, não a `/notes/mcp`. +Agora os clientes se conectam a `/notes/`, não a `/notes/mcp`. ## CORS para clientes no navegador {#cors-for-browser-clients} @@ -142,4 +142,4 @@ Um cliente que roda no navegador precisa de duas permissões suas: para **enviar * Clientes no navegador precisam de CORS: `allow_headers` para os headers de requisição `Mcp-*`, `expose_headers=["Mcp-Session-Id"]` para a resposta. * `@mcp.custom_route()` adiciona endpoints HTTP comuns, sem autenticação, ao lado de `/mcp`. -Com o servidor acessível em uma URL real, **[O cliente](../client/index.md)** se conecta a ele com essa URL em vez de um objeto servidor. +Com o servidor acessível em uma URL real, **[O cliente](../client/index.md)** se conecta a ele com essa URL. diff --git a/i18n/pt/pages/run/authorization.md b/i18n/pt/pages/run/authorization.md index 468206c7ea..1adb357676 100644 --- a/i18n/pt/pages/run/authorization.md +++ b/i18n/pt/pages/run/authorization.md @@ -1,6 +1,6 @@ --- translation: - sections: [d62c13457fc4a534, 80e73abaca6e0652, d1dc4c54cd00ec9c, 14ad3bc7904036bb, 5225f127bc1b9c77, fe1626fdd5aad1da, 4556cb7ea1a04a31] + sections: [d62c13457fc4a534, 80e73abaca6e0652, 128a492d18295f64, 14ad3bc7904036bb, 54a6697833fcc1d9, fe1626fdd5aad1da, 811d083c1da8bcf6] tool: 1 --- # Autorização {#authorization} @@ -23,12 +23,12 @@ O triângulo inteiro é esse. Tudo nesta página é o item do meio. O SDK não tem opinião sobre como é um token válido. Você diz a ele, implementando **`TokenVerifier`**: -```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` é um protocolo com um único método assíncrono. `verify_token` recebe o token bruto do header `Authorization` e retorna um **`AccessToken`** se ele for válido, `None` se não for. Não há mais nada a implementar. -* Este aqui procura o token em uma tabela. Um de verdade verifica a assinatura de um JWT ou chama o endpoint de introspecção de tokens do servidor de autorização. Esse código é seu; o SDK apenas o chama. +* Este aqui procura o token em uma tabela; cada entrada registra o recurso para o qual ele foi emitido. Um de verdade verifica a assinatura de um JWT ou chama o endpoint de introspecção de tokens do servidor de autorização, e informa para quem o token foi emitido (seu `aud`) em `AccessToken.resource`. Esse código é seu; o SDK apenas o chama. * `token_verifier=` e `auth=` sempre andam juntos. Passe um sem o outro e `MCPServer(...)` levanta um `ValueError` antes mesmo de atender uma requisição. `AuthSettings` é a face pública do seu resource server: @@ -36,6 +36,10 @@ O SDK não tem opinião sobre como é um token válido. Você diz a ele, impleme * `issuer_url`: o servidor de autorização que emite seus tokens. * `resource_server_url`: a URL pública deste endpoint MCP. Ela indica *a qual* recurso um token se destina, e é onde fica o documento de descoberta. * `required_scopes`: todo token deve conter todos eles. +* `validate_token_resource`: recusa qualquer token cujo `AccessToken.resource` não seja `resource_server_url`. Deixá-lo sem definir enquanto `resource_server_url` está definido emite um aviso (`MCPDeprecationWarning`) e se comporta como `False`; a versão 3.0 torna `True` o padrão para resource servers. + * Ative-o quando seu servidor de autorização vincula os tokens ao `resource` que o cliente pediu, o que clientes MCP sempre enviam. Mantenha `resource_server_url` como a URL exata à qual os clientes se conectam. + * Deixe-o desligado quando seu servidor de autorização usa seus próprios identificadores de audiência (um identificador de API do Auth0, um ID de aplicação do Entra) e, em vez disso, verifique `aud` no seu verificador, retornando `None` para um token que não é para este servidor. + * Se `aud` for uma lista, coloque em `resource` a entrada que é igual a `resource_server_url`. !!! tip `examples/servers/simple-auth/` no repositório do SDK tem um `IntrospectionTokenVerifier` que chama @@ -91,7 +95,7 @@ Esse documento é como um cliente que nunca ouviu falar do seu servidor encontra Dentro de qualquer handler, **`get_access_token()`** é o `AccessToken` que seu verificador retornou para a requisição atual: -```python title="server.py" hl_lines="4 32-35" +```python title="server.py" hl_lines="4 35-38" --8<-- "docs_src/authorization/tutorial002.py" ``` @@ -125,6 +129,6 @@ Um servidor de autorização também pode aceitar a asserção assinada de um pr * `token_verifier=` e `auth=AuthSettings(issuer_url=..., resource_server_url=..., required_scopes=[...])` sempre andam juntos. * O SDK publica o Protected Resource Metadata da [RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728) em `/.well-known/oauth-protected-resource/...` e responde a requisições não autenticadas com um 401 cujo header `WWW-Authenticate` aponta para ele. A história da descoberta é toda essa. * `get_access_token()` em qualquer handler diz quem está chamando. -* Autorização é assunto do HTTP. O `stdio` e o cliente em memória nunca a veem. +* Autorização é assunto do HTTP. O `stdio` e o cliente de teste em memória nunca a veem. A metade do cliente (descobrir seu servidor de autorização e buscar o token para você) está em **[Clientes OAuth](../client/oauth-clients.md)**. E um cliente que *afirma* uma identidade em vez de pedir uma ao usuário está em **[Asserção de identidade](../client/identity-assertion.md)**. diff --git a/i18n/pt/pages/run/deploy.md b/i18n/pt/pages/run/deploy.md index 8561e0e13d..c4d0544871 100644 --- a/i18n/pt/pages/run/deploy.md +++ b/i18n/pt/pages/run/deploy.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 --- # Deploy e escala {#deploy-scale} @@ -47,6 +47,22 @@ Depois do deploy atrás de um hostname de verdade, esse mesmo padrão rejeita ** que recusa toda conexão é uma allowlist de Host até que se prove o contrário. **[Solução de problemas](../troubleshooting.md)** também começa por aqui. +## Atrás de um proxy que termina TLS {#behind-a-tls-terminating-proxy} + +Se o TLS termina em um proxy (um ingress, um balanceador de carga, Caddy, nginx) e o uvicorn serve HTTP puro atrás dele, diga ao uvicorn para confiar nos headers `X-Forwarded-*` do proxy: + +```console +uvicorn server:app --proxy-headers --forwarded-allow-ips='' +``` + +Sem isso, o app acredita que está sendo servido por `http://`, e qualquer redirecionamento que ele emita (o habitual é `/mcp` → `/mcp/`) aponta para `http://…`. O cliente Python se recusa a seguir um endpoint HTTPS para HTTP puro e avisa: + +```text +MCPError: Redirect to http://mcp.example.com/mcp/ not followed: it would downgrade this HTTPS endpoint to plain HTTP. +``` + +O paliativo do lado do cliente é configurar a URL exata que o servidor serve (`https://mcp.example.com/mcp/`, com a barra) para que nenhum redirecionamento aconteça. A correção é a flag acima. `FORWARDED_ALLOW_IPS` é a grafia como variável de ambiente; `*` confia em todo salto, o que só está certo quando nada além do proxy consegue alcançar o uvicorn. + ## Workers, e quem precisa de afinidade {#workers-and-who-has-to-be-sticky} Quando o hostname responder, coloque mais de um worker atrás dele. Não há botão no SDK para isso; você escala um app Starlette do jeito que escala qualquer app ASGI, entregando o objeto a algo que saiba fazer fork: @@ -170,6 +186,7 @@ Um `MCPServer` é uma implementação de protocolo, não um servidor de aplicaç ## Recapitulando {#recap} * Por padrão, o app responde apenas a requisições endereçadas ao localhost. `transport_security=TransportSecuritySettings(allowed_hosts=[...], allowed_origins=[...])` é o portão para ir ao ar: até você passar isso, toda requisição atrás de um hostname de verdade é um `421` e o motivo só está no log do servidor. +* Atrás de um proxy que termina TLS, execute o uvicorn com `--proxy-headers --forwarded-allow-ips=...`, senão os redirecionamentos dele apontam para `http://` e o cliente os recusa. * No 2026-07-28 não há sessão e nada em que um balanceador de carga possa ter afinidade. `stateless_http=True` é um botão só para o legado porque uma requisição moderna é roteada e respondida antes de essa flag ser lida. * A chave padrão do `requestState` é `os.urandom(32)`, cunhada por processo. Uma nova tentativa de múltiplas idas e voltas que chega a um worker diferente falha com `-32602` *"Invalid or expired requestState"*. * A correção é `RequestStateSecurity(keys=[...])` **e** o mesmo nome de servidor em toda instância. O nome é a claim de audiência padrão do token. Mesmas chaves, mesmo nome. diff --git a/i18n/pt/pages/run/index.md b/i18n/pt/pages/run/index.md index 791f05e4a1..f190bd5cfd 100644 --- a/i18n/pt/pages/run/index.md +++ b/i18n/pt/pages/run/index.md @@ -1,6 +1,6 @@ --- translation: - sections: [fea8d769ff9edeba, ce8e2ad42f29ef71, 0d705efb19cf99c2, 2fd7cf825e6d2b2c, 9adc400e8c88e854, 318893ad8e2e9924, 6b63ab96b34476c0] + sections: [fea8d769ff9edeba, ce8e2ad42f29ef71, 0d705efb19cf99c2, 9fd2357154a5b7e7, 9adc400e8c88e854, 318893ad8e2e9924, 6b63ab96b34476c0] tool: 1 --- # Executando seu servidor {#running-your-server} @@ -75,6 +75,11 @@ Cada transporte tem seus próprios argumentos nomeados, todos em `run()`: * `max_request_body_size`: maior corpo de requisição aceito, em bytes. O padrão é 4 MiB; requisições maiores recebem HTTP 413 antes do parsing ou da criação da sessão. Aumente apenas quando mensagens MCP legítimas ultrapassarem esse tamanho. +* `session_idle_timeout`: segundos que uma sessão legada pode ficar sem nada em andamento antes de o + servidor fechá-la. Padrão 1800. `None` desativa. Veja + [Tempo de vida e limites de sessão](legacy-clients.md#session-lifetime-and-limits). +* `max_sessions`: quantas sessões legadas um processo mantém ao mesmo tempo. Padrão 10 000. `None` + remove o limite. Tratado na mesma seção. * `event_store`, `retry_interval`, `transport_security`: retomada e proteção contra DNS rebinding. Podem esperar até você fazer o deploy em algum lugar que não seja o localhost; **[Deploy e escala](deploy.md)** cobre `transport_security`. !!! warning diff --git a/i18n/pt/pages/run/legacy-clients.md b/i18n/pt/pages/run/legacy-clients.md index e6f0349dbb..23adca9c2b 100644 --- a/i18n/pt/pages/run/legacy-clients.md +++ b/i18n/pt/pages/run/legacy-clients.md @@ -1,6 +1,6 @@ --- translation: - sections: [3d1663c18edc824c, d4fd37009a13f03d, af9f398a5a8b679a, 470c2dd144294d69, 8e45827e6d24e8c8, 91dfd0ce98ebb03c] + sections: [3d1663c18edc824c, 90956965ae6a1ca1, af9f398a5a8b679a, 5ce83b1f9d88da62, 0d9b5d13fffc94e5, 8e45827e6d24e8c8, 91dfd0ce98ebb03c] tool: 1 --- # Atendendo clientes legados {#serving-legacy-clients} @@ -21,15 +21,25 @@ Então um cliente legado não é algo *para* o qual você constrói. É algo que ## Um handler, as duas eras {#one-handler-both-eras} -Aqui está uma ferramenta (tool) que precisa perguntar algo ao usuário, e clientes das duas eras chamando-a: +Aqui está uma ferramenta (tool) que precisa perguntar algo ao usuário: -```python title="server.py" hl_lines="24 37-38" +```python title="server.py" hl_lines="21" --8<-- "docs_src/legacy_clients/tutorial001.py" ``` `reserve` precisa de uma coisa que o modelo não forneceu: quantas cópias. `Annotated[..., Resolve(ask_quantity)]` é como uma ferramenta declara isso (**[Dependências](../handlers/dependencies.md)** tem essa história completa). Nada em `reserve` cita uma versão, verifica uma capacidade ou ramifica. -Os dois clientes ficam abertos **ao mesmo tempo**, no mesmo objeto `mcp`. `mode="legacy"` executa o handshake `initialize`: exatamente a conexão que um cliente pré-2026 abre. O outro usa o padrão e cai em `2026-07-28`. +Sirva-a via HTTP, e aqui estão clientes das duas eras chamando-a: + +```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" +``` + +Os dois clientes ficam abertos **ao mesmo tempo**, contra o mesmo servidor em execução. `mode="legacy"` executa o handshake `initialize`: exatamente a conexão que um cliente pré-2026 abre. O outro usa o padrão e cai em `2026-07-28`. Execute `python client.py` em um segundo terminal: ```text 2025-11-25 {'result': "Reserved 2 of 'Dune'."} @@ -61,6 +71,40 @@ Com um worker, isso é invisível. Com dois, é o problema inteiro: uma requisi perdidos para um cliente que se reconecta à *mesma* sessão), não um armazenamento de sessões. Ele nunca torna uma sessão alcançável a partir de outro processo. +## Tempo de vida e limites de sessão {#session-lifetime-and-limits} + +Uma sessão legada não vive para sempre, e um processo não mantém um número ilimitado +delas. Duas configurações controlam isso. Ambas são argumentos nomeados em `run()`, `streamable_http_app()` +e `Server.streamable_http_app()`. Conexões modernas (`2026-07-28`) e `stateless_http=True` +não têm sessões, então nenhuma das duas configurações se aplica a elas. + +| Configuração | Padrão | O que faz | O que o cliente vê | Para desligar | +|---|---|---|---|---| +| `session_idle_timeout` | `1800` (30 min) | Fecha uma sessão que não teve nada em andamento por esse tempo. | `404 Session not found`. Ele precisa fazer `initialize` de novo. | `None` | +| `max_sessions` | `10_000` | Recusa abrir uma sessão além dessa quantidade. As sessões existentes ficam intactas e nada é despejado. | `503 Too many open sessions` com o código JSON-RPC `-32603`. | `None` | + +O que conta como "em andamento": + +* Um stream `GET` aberto. Os clientes do SDK mantêm um aberto, então a sessão de um cliente conectado nunca + expira. +* Uma requisição que ainda está sendo respondida. Uma chamada de ferramenta que roda por mais tempo que o timeout não é + interrompida, e a contagem regressiva só começa quando ela termina. +* Nada mais. Entre requisições, o relógio corre. Qualquer requisição na sessão o reinicia, + `ping` inclusive. Depois que uma sessão expirou, nada a revive. + +Um cliente que encerra sua sessão com `DELETE` a libera imediatamente. O mesmo vale para um cliente cuja +requisição de abertura foi recusada. + +```python +mcp.run(transport="streamable-http", session_idle_timeout=None, max_sessions=50_000) +``` + +Os dois eventos aparecem no log do servidor. Uma expiração é `Session idle timeout` em `INFO`. Uma +abertura recusada é `Refusing to open a new session: sessions are already open` em `WARNING`. + +Os limites são por processo. Com quatro workers, o teto é quatro vezes `max_sessions`, e cada +worker expira suas próprias sessões. + ## A única chave: `stateless_http` {#the-one-knob-stateless_http} Se stickiness é um custo que você se recusa a pagar, existe exatamente uma coisa que você pode mudar. @@ -86,7 +130,7 @@ Duas coisas sobre ele importam mais do que o que ele faz. !!! check Faça a coisa errada. `reserve` é exatamente a ferramenta que acabou de atender os dois clientes. Faça o deploy dela com - `stateless_http=True`, conecte os mesmos dois clientes via HTTP e chame-a de cada um. + `stateless_http=True`, conecte os mesmos dois clientes e chame-a de cada um. O cliente moderno ainda recebe `Reserved 2 of 'Dune'.` A perna moderna não mudou. diff --git a/i18n/pt/pages/troubleshooting.md b/i18n/pt/pages/troubleshooting.md index ed7b4b7ed6..29e6e3c1a7 100644 --- a/i18n/pt/pages/troubleshooting.md +++ b/i18n/pt/pages/troubleshooting.md @@ -1,6 +1,6 @@ --- translation: - sections: [2efaecdef109a5c5, fcacd3e66b8635a4, 25323d737dcf0261, 8a6e351ec756904d, 137454d469c867f5, 6392596bd6df54f0, 41126fa9c4fe432f, 480b6d7897e30ab4, d83bb682e708dde0, ebbed3449c499db4, 323ef84f6b4bebde, 30fd31be74169d9a, 656943c6cb567218, c2dc3b1007d2e987, 7cf5386b997d04e9, 0b59feed8384456e, 0cba47bae78d04eb, e4355f4c7cf4fb2e] + sections: [3d58228e81b99543, 170514ce901c4139, 17d61fad0a50d62b, 8a6e351ec756904d, 137454d469c867f5, 6392596bd6df54f0, 41126fa9c4fe432f, 480b6d7897e30ab4, d83bb682e708dde0, ebbed3449c499db4, 525cdf1755e29d4c, 30fd31be74169d9a, d2e88333d4f7841f, c2dc3b1007d2e987, d6eabf60cc366341, f798e815252852c2, 0cba47bae78d04eb, 2c218ba829abf74e] tool: 1 --- # Solução de problemas {#troubleshooting} @@ -13,6 +13,12 @@ Várias entradas usam este mesmo servidor. Uma ferramenta (tool) e um recurso co --8<-- "docs_src/troubleshooting/tutorial001.py" ``` +Essas entradas o acessam em `http://localhost:8000/mcp`, então deixe-o rodando via HTTP: + +```console +uv run mcp run server.py --transport streamable-http +``` + Os erros que esta página cita são reais: a própria suíte de testes do SDK reproduz cada um deles. ## `ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception)` {#exceptiongroup-unhandled-errors-in-a-taskgroup-1-sub-exception} @@ -23,7 +29,7 @@ Isto não é um erro do MCP. É ruído do anyio, e o seu erro de verdade é a ** ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: await client.read_resource("weather://Atlantis") ``` @@ -49,7 +55,7 @@ Duas coisas a fazer com isso: ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: try: await client.read_resource("weather://Atlantis") except MCPError as e: @@ -67,7 +73,7 @@ async def main() -> None: ```python async def main() -> None: - client = Client(mcp) + client = Client("http://localhost:8000/mcp") tools = await client.list_tools() # RuntimeError ``` @@ -75,7 +81,7 @@ Entre nele. `__aenter__` é a conexão: ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: tools = await client.list_tools() ``` @@ -251,7 +257,7 @@ app = Starlette(routes=[Mount("/", app=mcp.streamable_http_app())], lifespan=lif ## `MCPError: Session not found` {#mcperror-session-not-found} -O servidor não reconhece o `Mcp-Session-Id` que o seu cliente enviou, quase sempre porque o servidor **reiniciou** (ou você foi roteado para uma instância diferente). As sessões vivem na memória daquele único processo. +O servidor não reconhece o `Mcp-Session-Id` que o seu cliente enviou. Ou o servidor **reiniciou** (ou você foi roteado para uma instância diferente), ou a sessão **expirou** porque nada estava em andamento durante o `session_idle_timeout`, que é de 30 minutos por padrão. Veja [Tempo de vida e limites da sessão](run/legacy-clients.md#session-lifetime-and-limits). As sessões vivem na memória daquele único processo. Não há bug de servidor para encontrar. A resposta HTTP é um `404` cujo corpo *é* JSON-RPC, então, ao contrário do `421` acima, o `Client` python mostra esta aqui palavra por palavra: @@ -261,9 +267,9 @@ Não há bug de servidor para encontrar. A resposta HTTP é um `404` cujo corpo A correção é reconectar: saia do bloco `async with Client(...)` e entre em um novo, que negocia uma sessão nova. Para um cliente de vida longa, isso significa capturar `MCPError` em volta das suas chamadas e reconectar ao ver esta mensagem, em vez de tentar de novo dentro de uma sessão morta. -Se isso acontece *sem* um reinício, você está rodando mais de um worker sem sticky sessions: cada worker mantém a própria tabela de sessões, então uma requisição roteada para o errado cai aqui. **[Deploy e escala](run/deploy.md)** e **[Atendendo clientes legados](run/legacy-clients.md)** são donos dessa história e das suas duas correções (roteamento sticky, ou `stateless_http=True`). +Se isso acontece *sem* um reinício e sem que o cliente tenha ficado quieto por tanto tempo, você está rodando mais de um worker sem sticky sessions: cada worker mantém a própria tabela de sessões, então uma requisição roteada para o errado cai aqui. **[Deploy e escala](run/deploy.md)** e **[Atendendo clientes legados](run/legacy-clients.md)** são donos dessa história e das suas duas correções (roteamento sticky, ou `stateless_http=True`). -Para quem opera o servidor, a linha de log correspondente é `Rejected request with unknown or expired session ID: `. Ela é registrada em `INFO`, então é invisível no limite usual de `WARNING`. Vê-la em rajadas logo depois de um deploy é normal; todo cliente conectado está reconectando. +Para quem opera o servidor, a linha de log correspondente é `Rejected request with unknown or expired session ID: `. Ela é registrada em `INFO`, então é invisível no limite usual de `WARNING`. Vê-la em rajadas logo depois de um deploy é normal; todo cliente conectado está reconectando. Quando, em vez disso, a sessão expirou, essa linha vem precedida de `Session idle timeout`, também em `INFO`. ## `MCPError: Method not found` {#mcperror-method-not-found} @@ -275,7 +281,13 @@ Uma coisa **não** produz este erro, apesar de ser uma requisição que o protoc Seu servidor quer perguntar algo ao usuário, e este cliente nunca disse que pode receber perguntas. -Um resolvedor de elicitação (elicitation) recusa logo de início quando o cliente conectado não declarou elicitação por formulário, e `e.error.data` nomeia exatamente o que falta: +Este Bistro pergunta antes de reservar, por meio de um resolvedor: + +```python title="server.py" hl_lines="15-17 21" +--8<-- "docs_src/troubleshooting/tutorial007.py" +``` + +Sirva-o no lugar do servidor Weather e chame `book_table` a partir de um cliente que não passou nenhum `elicitation_callback`. O resolvedor recusa logo de início, porque o cliente conectado nunca declarou elicitação (elicitation) por formulário, e `e.error.data` nomeia exatamente o que falta: ```json { @@ -289,7 +301,7 @@ Passe `elicitation_callback=` para `Client(...)`. Registrar o callback *é* a de ```python async def main() -> None: - async with Client(mcp, elicitation_callback=handle_elicitation) as client: + async with Client("http://localhost:8000/mcp", elicitation_callback=handle_elicitation) as client: result = await client.call_tool("book_table", {"date": "Friday"}) ``` @@ -314,14 +326,14 @@ Você vê esta a partir de `ctx.elicit()` em uma conexão legada, e em qualquer Seu handler tentou alcançar o cliente no meio da requisição, em uma conexão cuja chamada não tem canal capaz de carregar uma requisição vinda do servidor. Há três configurações de servidor que colocam uma chamada nessa situação. -**Uma conexão `2026-07-28`: qualquer transporte, sempre.** O protocolo moderno não tem nenhuma requisição iniciada pelo servidor, então o servidor recusa antes que qualquer coisa seja enviada. `ctx.elicit()` dentro de uma ferramenta é o jeito clássico de topar com isso (logo no primeiro teste em memória, já que `Client(server)` negocia `2026-07-28` sem que ninguém peça), e passar `elicitation_callback=` não muda nada, porque nenhuma requisição chega ao cliente para ele responder: +**Uma conexão `2026-07-28`: qualquer transporte, sempre.** O protocolo moderno não tem nenhuma requisição iniciada pelo servidor, então o servidor recusa antes que qualquer coisa seja enviada. `ctx.elicit()` dentro de uma ferramenta é o jeito clássico de topar com isso, em geral logo no primeiríssimo **[teste](get-started/testing.md)** em memória dessa ferramenta, já que `Client(mcp)` negocia `2026-07-28` sem que ninguém peça. Passar `elicitation_callback=` não muda nada, porque nenhuma requisição chega ao cliente para ele responder: ```python title="server.py" hl_lines="16" --8<-- "docs_src/troubleshooting/tutorial006.py" ``` ```python -async def main() -> None: +async def test_book_table() -> None: async with Client(mcp) as client: await client.call_tool("book_table", {"date": "Friday"}) ``` @@ -363,7 +375,7 @@ O servidor não conseguiu verificar o token `requestState` que o seu cliente dev ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: await client.call_tool("forecast", {"city": "London"}, request_state="round-1-from-worker-a") ``` @@ -416,7 +428,7 @@ mcp = MCPServer("Weather", request_state_security=RequestStateSecurity(keys=[key * `Tool already exists:` no log do servidor é o único sinal de que duas ferramentas com o mesmo nome viraram uma só. * Um 421, três grafias: `Server returned an error response` (o `Client` python), `421 Misdirected Request` / `Invalid Host header` (todo o resto), `Invalid Host header: ` (o log do servidor). Correção: `transport_security=TransportSecuritySettings(allowed_hosts=[...])`. * `Task group is not initialized` -> um app montado cujo lifespan do host nunca entrou em `mcp.session_manager.run()`. -* `Session not found` -> o servidor reiniciou; reconecte. +* `Session not found` -> o servidor reiniciou ou a sessão expirou (`session_idle_timeout`); reconecte. * `Cannot send 'elicitation/create': ... no back-channel ...` -> `ctx.elicit()` precisa de um canal do servidor para o cliente: uma conexão `2026-07-28` nunca tem um, `stateless_http=True` tira o legado, e `json_response=True` tira o de escopo de requisição. Use um resolvedor (um cliente legado também precisa de um servidor que mantenha o canal). O vizinho `Method not found` é uma requisição para um método que a revisão do protocolo do outro lado não tem. * `Client did not declare the form elicitation capability ...` e `Elicitation not supported` -> falta `elicitation_callback=` no cliente. * `Invalid or expired requestState` nunca diz o porquê na rede. O log do servidor diz; `unknown key` significa compartilhar `RequestStateSecurity(keys=[...])` entre os workers. diff --git a/i18n/pt/pages/whats-new.md b/i18n/pt/pages/whats-new.md index 6957e00143..8c68702e82 100644 --- a/i18n/pt/pages/whats-new.md +++ b/i18n/pt/pages/whats-new.md @@ -1,6 +1,6 @@ --- translation: - sections: [cfe01c0c5863dfa2, 1c58c5cfcc37d455, a7392996acf1ad8f, 875eb2889263424e] + sections: [cfe01c0c5863dfa2, 0dbb68d8b210177b, 80cf193023af3ed4, 875eb2889263424e] tool: 1 --- # O que há de novo na v2 {#whats-new-in-v2} @@ -43,11 +43,11 @@ Nem tudo de que uma ferramenta (tool) precisa deve vir do modelo. Novidade na v2 A v1 entregava três camadas aninhadas: um gerenciador de contexto de transporte que produzia streams brutos, uma `ClientSession` em volta deles e um `await session.initialize()` chamado à mão. A v2 tem um objeto só: -```python title="client.py" hl_lines="14-18" ---8<-- "docs_src/client/tutorial001.py" +```python title="client.py" hl_lines="7-11" +--8<-- "docs_src/client/tutorial001_client.py" ``` -`Client` recebe um objeto de servidor (em memória, sem transporte: é o cenário dos testes), uma URL (Streamable HTTP), um `StdioServerParameters` (um subprocesso stdio) ou qualquer outro gerenciador de contexto de transporte, como `sse_client(...)`. Entrar no `async with` conecta e negocia a versão do protocolo, seja qual for a era que o servidor fale; `client.server_capabilities` e `client.protocol_version` simplesmente estão lá depois disso, e `client.server_info` também, quando o servidor se identifica (agora ele é `Implementation | None`, já que na era 2026 a identidade é opcional). Os callbacks de amostragem e de elicitação que você registrou na v1 continuam funcionando (o corpo deles passa pela mesma renomeação de atributos para snake_case que todo o resto desta página), agora também respondem às requisições-dentro-de-resultados no estilo 2026 (abaixo), e rodam de forma concorrente em vez de um por vez. `ClientSession` continua por baixo para quem quer a superfície de baixo nível, e `client.session` a entrega para você; ela também mudou (roda sobre o novo motor de dispatcher, e algumas das próprias assinaturas dela mudaram), então leia o **[Guia de migração](migration.md#clientsession-now-runs-on-jsonrpcdispatcher-basesession-removed)** antes de descer de nível. +`Client` recebe uma URL (Streamable HTTP), um `StdioServerParameters` (um subprocesso stdio), qualquer outro gerenciador de contexto de transporte, como `sse_client(...)`, ou, nos testes, o próprio objeto do servidor (em memória, sem transporte). Entrar no `async with` conecta e negocia a versão do protocolo, seja qual for a era que o servidor fale; `client.server_capabilities` e `client.protocol_version` simplesmente estão lá depois disso, e `client.server_info` também, quando o servidor se identifica (agora ele é `Implementation | None`, já que na era 2026 a identidade é opcional). Os callbacks de amostragem e de elicitação que você registrou na v1 continuam funcionando (o corpo deles passa pela mesma renomeação de atributos para snake_case que todo o resto desta página), agora também respondem às requisições-dentro-de-resultados no estilo 2026 (abaixo), e rodam de forma concorrente em vez de um por vez. `ClientSession` continua por baixo para quem quer a superfície de baixo nível, e `client.session` a entrega para você; ela também mudou (roda sobre o novo motor de dispatcher, e algumas das próprias assinaturas dela mudaram), então leia o **[Guia de migração](migration.md#clientsession-now-runs-on-jsonrpcdispatcher-basesession-removed)** antes de descer de nível. **[O Client](client/index.md)** o apresenta, **[Transportes do cliente](client/transports.md)** cobre as quatro formas de conexão, **[Callbacks do cliente](client/callbacks.md)** cobre os callbacks em si, e **[Testes](get-started/testing.md)** mostra o padrão em memória que substitui o helper `create_connected_server_and_client_session()` da v1. @@ -172,11 +172,15 @@ Toda requisição iniciada pelo servidor sumiu na 2026-07-28: elicitação por p A substituição inverte a chamada. Uma ferramenta que precisa de algo do usuário *retorna* a pergunta (`InputRequiredResult`), o cliente a responde com os mesmos callbacks que sempre teve, e a chamada é repetida com as respostas anexadas. `Client` conduz esse loop para você. No servidor você raramente monta o resultado por conta própria, porque uma **[dependência](handlers/dependencies.md)** faz isso: anote um parâmetro com `Resolve(ask_quantity)`, onde `ask_quantity` é uma função comum que você escreve, e o SDK pergunta pelo mecanismo que a conexão suportar, uma requisição de elicitação ao vivo em uma sessão legada ou um multi-round-trip na 2026. Um corpo de ferramenta, as duas eras: -```python title="dual_era.py" hl_lines="24 37-38" +```python title="server.py" hl_lines="21" --8<-- "docs_src/legacy_clients/tutorial001.py" ``` -Esse arquivo é a proposta inteira em um lugar só: um servidor, uma ferramenta apoiada em `Resolve`, e um cliente legado mais um cliente moderno, os dois recebendo a sua resposta, em memória. **[Requisições multi-round-trip](handlers/multi-round-trip.md)** explica o mecanismo (incluindo o `request_state`, que o SDK sela e verifica para você); **[Elicitação](handlers/elicitation.md)** cobre a parte de perguntar. +```python title="client.py" hl_lines="14-15" +--8<-- "docs_src/legacy_clients/tutorial001_client.py" +``` + +Esses dois arquivos são a proposta inteira: um servidor, uma ferramenta apoiada em `Resolve`, e um cliente legado mais um cliente moderno, os dois recebendo a sua resposta do mesmo servidor em execução (**[Servindo clientes legados](run/legacy-clients.md)** percorre os dois). **[Requisições multi-round-trip](handlers/multi-round-trip.md)** explica o mecanismo (incluindo o `request_state`, que o SDK sela e verifica para você); **[Elicitação](handlers/elicitation.md)** cobre a parte de perguntar. !!! warning "Este é o único lugar em que um servidor v1 portado muda de comportamento" Os seus próprios testes esbarram nisso primeiro: `Client(mcp)` negocia 2026-07-28 com o seu diff --git a/i18n/ru/pages/advanced/apps.md b/i18n/ru/pages/advanced/apps.md index c4de3c2d01..772437d023 100644 --- a/i18n/ru/pages/advanced/apps.md +++ b/i18n/ru/pages/advanced/apps.md @@ -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 @@ translation: ## Часы с циферблатом {#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 @@ translation: Модель читает `content`; iframe — для людей. Хост с поддержкой UI всё равно передаёт текстовый результат модели, а чисто текстовый клиент получает *только* его. Поэтому канонический паттерн — один инструмент, два ответа. Взгляните на `get_time` ещё раз: -```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)` возвращает `True`, только когда клиент объявил расширение `io.modelcontextprotocol/ui` **и** указал `text/html;profile=mcp-app` в настройке `mimeTypes`. Поле обязательное, так что клиент, который его опустил, не считается. Именно это объявляет `main()` в том же файле: клиентскую половину согласования — и в ответ приходит развёрнутый результат. +`client_supports_apps(ctx)` возвращает `True`, только когда клиент объявил расширение `io.modelcontextprotocol/ui` **и** указал `text/html;profile=mcp-app` в настройке `mimeTypes`. Поле обязательное, так что клиент, который его опустил, не считается. Вот клиентская половина согласования: + +```python title="client.py" hl_lines="8 12" +--8<-- "docs_src/apps/tutorial001_client.py" +``` + +Запустите `server.py` по HTTP, затем во втором терминале запустите клиент: + +```console +uv run mcp run server.py --transport streamable-http +``` + +```console +python client.py +``` + +```text +2026-06-26T12:00:00Z +``` + +Пришёл развёрнутый ответ. Уберите `extensions=[APPS_SUPPORT]` из вызова `Client` — и та же программа напечатает `The time is 2026-06-26T12:00:00Z.`: это всё, что когда-либо увидит чисто текстовый клиент. !!! warning Никогда не возвращайте заглушку вроде `"[Rendered UI]"` в качестве единственного содержимого. Если запасной текст бесполезен, инструмент бесполезен для любого текстового клиента и для самой модели. Напишите нормальное предложение. diff --git a/i18n/ru/pages/advanced/extensions.md b/i18n/ru/pages/advanced/extensions.md index a2d233679c..688c4a2d78 100644 --- a/i18n/ru/pages/advanced/extensions.md +++ b/i18n/ru/pages/advanced/extensions.md @@ -1,6 +1,6 @@ --- translation: - sections: [05891e7cc1938a13, b3c01a6af28c51ee, 7ffc91f5e38bdfe0, 717d3f235a8333a7, f471a13b2fe5d737, ed6af2df4b656dff] + sections: [05891e7cc1938a13, b3c01a6af28c51ee, 6eba6ce094f4417e, 125f28588c85dd7b, ddd8969b698ca11c, ed6af2df4b656dff] tool: 1 --- # Расширения {#extensions} @@ -49,7 +49,7 @@ TypeError: Stamps.identifier must be a `vendor-prefix/name` string Самое маленькое полезное расширение — один инструмент и карта настроек: -```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 @@ TypeError: Stamps.identifier must be a `vendor-prefix/name` string * `settings()` — значение, объявляемое в `capabilities.extensions["com.example/stamps"]`. Верните `{}` (значение по умолчанию), чтобы объявить расширение без настроек. * Расширение никогда не получает сервер. Оно описывает свой вклад как данные; `MCPServer` их потребляет. Никакого `self.server`, который можно было бы менять, нет. -А `main()` служит доказательством: клиент в памяти, подключённый напрямую к `mcp`: +Запустите его по HTTP — и доказательством послужит клиент: -```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" ``` +Каждый `server.py` на этой странице запускается этой командой, а каждый `client.py` работает рядом с ним: `python client.py` во втором терминале. + ### Обслуживание собственных методов {#serving-your-own-methods} Расширение может регистрировать **новые методы запросов** — собственные глаголы, обслуживаемые рядом с методами спецификации: -```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 @@ TypeError: Stamps.identifier must be a `vendor-prefix/name` string ### Клиентская сторона {#the-client-side} -`main()` из того же файла — это вся клиентская часть, обе её половины: +Клиент — отдельная программа, и в ней обе половины клиентской части: -```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)])` объявляет расширение. Объявления превращаются в `ClientCapabilities.extensions`: на подключении версии 2026-07-28 карта передаётся в конверте `_meta` каждого запроса, так что сервер видит её в **каждом** запросе; на подключении старого поколения она едет в рукопожатии `initialize`. Серверному коду всё равно, какой из вариантов: `require_client_extension(ctx, ...)` и `ctx.session.check_client_capability(...)` читают нужный источник на обоих путях. * Вендорные методы спускаются на уровень ниже, к `client.session.send_request(...)`; `Client` обзаводится полноценными методами только для глаголов спецификации. `send_request` принимает любой подкласс `Request`, так что вендорный запрос проходит как есть. +* `SearchRequest` и две модели, которые он несёт, — это контракт расширения в передаваемых данных, поэтому клиент объявляет их у себя сам. Опубликованное расширение поставляло бы их в пакете, который импортируют обе стороны. ### Перехват `tools/call` {#intercepting-toolscall} @@ -109,12 +116,18 @@ TypeError: Stamps.identifier must be a `vendor-prefix/name` string ## Использование клиентского расширения {#using-a-client-extension} -**Клиентское расширение** — тот же контракт со стороны потребителя: набор клиентского поведения за одним идентификатором. Передайте экземпляры в `Client(extensions=[...])` и вызывайте инструменты как обычно: +**Клиентское расширение** — тот же контракт со стороны потребителя: набор клиентского поведения за одним идентификатором. Сервер здесь отвечает на `buy` не самим товаром, а квитанцией, которую нужно погасить, — и только клиенту, объявившему расширение: -```python title="client.py" hl_lines="66-68" +```python title="server.py" hl_lines="22-25" --8<-- "docs_src/extensions/tutorial006.py" ``` +На клиенте передайте экземпляры в `Client(extensions=[...])` и вызывайте инструменты как обычно: + +```python title="client.py" hl_lines="33-35" +--8<-- "docs_src/extensions/tutorial006_client.py" +``` + `call_tool("buy", ...)` возвращает обычный `CallToolResult`, как и любой другой вызов. Что изменило расширение: теперь сервер может ответить на `buy` **формой результата** `receipt` вместо окончательного результата, а `Receipts` доводит её до конца (здесь — погашая квитанцию дополнительным вызовом) до того, как `call_tool` вернёт управление. В месте вызова не меняется ничего. Уберите расширение — и ничего этого не будет: пропускной пункт сервера отклонит клиент, который его не объявил (ошибка -32021), а заявленная форма от сервера, пропускающего эту проверку, не пройдёт валидацию — ровно так, как спецификация требует для нераспознанного `resultType`. Выключено по умолчанию, на обоих концах соединения. @@ -124,15 +137,15 @@ TypeError: Stamps.identifier must be a `vendor-prefix/name` string ```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")]) ``` ## Написание клиентского расширения {#writing-a-client-extension} Унаследуйтесь от `ClientExtension` и переопределите только то, что нужно. Три вида вклада, у каждого реализация по умолчанию: `settings()`, `claims()` и `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" ``` * Идентификатор подчиняется той же грамматике, что и на сервере, и проверяется при определении класса. @@ -153,12 +166,18 @@ def notifications(self) -> Sequence[NotificationBinding[Any]]: ### Глаголы расширения {#extension-verbs} -Собственные методы запросов расширения не требуют регистрации на стороне клиента. Тип вендорного запроса наследуется от `mcp.types.Request` и отправляется через `client.session.send_request`, как в разделе [Обслуживание собственных методов](#serving-your-own-methods). Одно дополнение: когда ключ из params должен передаваться в заголовке `Mcp-Name` (спецификации расширений, например tasks, требуют этого для своих глаголов), тип запроса объявляет `name_param`: +Собственные методы запросов расширения не требуют регистрации на стороне клиента. Тип вендорного запроса наследуется от `mcp.types.Request` и отправляется через `client.session.send_request`, как в разделе [Обслуживание собственных методов](#serving-your-own-methods). Возьмём сервер, расширение которого обслуживает один глагол, относящийся к именованному заданию: -```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" ``` +Одно дополнение на клиенте: когда ключ из params должен передаваться в заголовке `Mcp-Name` (спецификации расширений, например tasks, требуют этого для своих глаголов), тип запроса объявляет `name_param`: + +```python title="client.py" hl_lines="20-23 28-29" +--8<-- "docs_src/extensions/tutorial007_client.py" +``` + Сессия дублирует `params["jobId"]` в `Mcp-Name` на каждом пути отправки, а отсутствующее значение приводит к явной ошибке, а не к молчаливому пропуску обязательного заголовка. ## Чего расширение не может {#what-an-extension-cannot-do} diff --git a/i18n/ru/pages/advanced/low-level-server.md b/i18n/ru/pages/advanced/low-level-server.md index a982fb0fbd..2bd34e6074 100644 --- a/i18n/ru/pages/advanced/low-level-server.md +++ b/i18n/ru/pages/advanced/low-level-server.md @@ -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 --- # Низкоуровневый Server {#the-low-level-server} @@ -36,18 +36,22 @@ translation: ### Попробуйте сами {#try-it} -Inspector здесь не поможет: `mcp dev` и `mcp run` принимают только `MCPServer`. Клиенту `Client`, работающему в памяти, всё равно — он принимает низкоуровневый `Server` точно так же, как `MCPServer`: +`mcp dev` и `mcp run` принимают только `MCPServer`, так что этот сервер вы запускаете сами. Последняя строка `server.py` строит из него обычное ASGI-приложение, а uvicorn его запускает: -```python title="main.py" +```console +uvicorn server:app --port 8000 +``` + +Направьте Inspector или любой клиент на `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 @@ asyncio.run(main()) * `result.structured_content` равен `None`. Высокоуровневый сервер сам оборачивает `-> str` в `{"result": ...}`; здесь никто не соберёт то, чего не собрали вы. * `list_tools` возвращает схему, которую набрали **вы**, символ в символ. В высокоуровневой версии у каждого свойства было `"title": "Query"`, а в корне — `"title": "search_booksArguments"`: артефакты Pydantic. Здесь всё, что есть в передаваемых данных, положили туда вы. +В тесте uvicorn и порт не нужны: `Client(server)` принимает низкоуровневый `Server` внутри процесса точно так же, как `MCPServer`, и именно этот подход описан на странице **[Тестирование](../get-started/testing.md)**. + ## За вас ничего не проверяют {#nothing-is-checked-for-you} `MCPServer` отклоняет некорректный аргумент ещё до запуска вашей функции, проверяя вызов по сгенерированной им схеме (**[Инструменты](../servers/tools.md)**). @@ -215,4 +221,4 @@ use Server.middleware to observe or wrap initialization * `add_request_handler(method, params_type, handler)` обслуживает любой метод. `initialize` зарезервирован. * Возможности, которые объявляет `Server`, выводятся из того, какие обработчики вы зарегистрировали. -`Client(server)` обращался с обоими серверами одинаково, потому что это *и есть* один и тот же протокол — в этом весь смысл. Следующий уровень вниз — вообще не класс: это **[Middleware](middleware.md)**. +Клиент обращался с обоими серверами одинаково, потому что это *и есть* один и тот же протокол — в этом весь смысл. Следующий уровень вниз — вообще не класс: это **[Middleware](middleware.md)**. diff --git a/i18n/ru/pages/advanced/pagination.md b/i18n/ru/pages/advanced/pagination.md index adc3596339..dba7d7bd08 100644 --- a/i18n/ru/pages/advanced/pagination.md +++ b/i18n/ru/pages/advanced/pagination.md @@ -1,6 +1,6 @@ --- translation: - sections: [a9aba7a026c7bd85, ed32bda7ba9ae33a, 7e64cc5646abb91f, 22a0129ee78b3c63, d875373c06d8d2f9] + sections: [a9aba7a026c7bd85, 83e2a08b9d46a398, 9fd8a0aa384b3257, 22a0129ee78b3c63, d875373c06d8d2f9] tool: 1 --- # Пагинация {#pagination} @@ -31,9 +31,13 @@ translation: ### Попробуйте сами {#try-it} -`Client(server)` подключается к низкоуровневому `Server` в памяти точно так же, как к `MCPServer`. +`mcp run` принимает только `MCPServer`, поэтому этот сервер придётся запускать самостоятельно. Последняя строка `server.py` собирает из `Server` обычное ASGI-приложение, и его запускает uvicorn: -Вызовите `list_resources()` без аргументов. Придут десять ресурсов, от `book-1` до `book-10`, а `next_cursor` будет строкой `"10"`. +```console +uvicorn server:app --port 8000 +``` + +Направьте любой клиент (**[Клиент](../client/index.md)** или Inspector) на `http://localhost:8000/mcp` и вызовите `list_resources()` без аргументов. Придут десять ресурсов, от `book-1` до `book-10`, а `next_cursor` будет строкой `"10"`. Передайте его обратно через `list_resources(cursor="10")` — первым ресурсом окажется `book-11`, а новый `next_cursor` будет `"20"`. @@ -43,7 +47,7 @@ translation: Каждый метод `list_*` у `Client` (`list_tools`, `list_resources`, `list_resource_templates`, `list_prompts`) принимает именованный аргумент `cursor=`. Вычитать список постранично целиком — это один `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 @@ translation: * Добавляйте элементы **до** того, как смотреть на `next_cursor`: на последней странице тоже есть ресурсы. * `next_cursor is None` — условие выхода. Всё остальное без изменений отправляется обратно в `cursor=`. -Запустите его `main()`, и он напечатает `100 resources`: десять страниц по десять, сшитых циклом, который и не знал, что страниц было десять. +Пока uvicorn продолжает обслуживать `server.py`, запустите `python client.py` во втором терминале. Он напечатает `100 resources`: десять страниц по десять, сшитых циклом, который и не знал, что страниц было десять. Это тот же цикл, который **[Клиент](../client/index.md)** показывает для каждого метода `list_*`, и против сервера без пагинации он ничего не стоит: `next_cursor` равен `None` уже в первом ответе, и цикл выполняется один раз. diff --git a/i18n/ru/pages/client/caching.md b/i18n/ru/pages/client/caching.md index f23dbc7b06..fa35cdcc27 100644 --- a/i18n/ru/pages/client/caching.md +++ b/i18n/ru/pages/client/caching.md @@ -1,6 +1,6 @@ --- translation: - sections: [9e7b9a1710e5aeba, b74ca4c1d2ddddee, fa8714e61bf90c5a, 04db67a886b7271c, 857690fb8f876800] + sections: [9e7b9a1710e5aeba, 66a2e9acc9101d54, d3d25aa802f5145a, 04db67a886b7271c, 857690fb8f876800] tool: 1 --- # Подсказки по кэшированию {#caching-hints} @@ -30,7 +30,7 @@ translation: В низкоуровневом классе `Server` обработчики собирают результаты вручную, а `ttl_ms` и `cache_scope` — это просто поля моделей результата. Обработчик, который задаёт их явно, всегда берёт верх над словарём из конструктора, поле за полем: -```python title="server.py" hl_lines="10 16" +```python title="server.py" hl_lines="11 17" --8<-- "docs_src/caching/tutorial002.py" ``` @@ -44,10 +44,24 @@ translation: В сессии 2026-07-28 `Client` учитывает подсказки за вас: у него есть встроенный кэш ответов, включённый по умолчанию. Результат, пришедший с `ttlMs`, сохраняется, и идентичный вызов в пределах этого TTL обслуживается из кэша без обращения к серверу. Результат *без* подсказки не кэшируется: результаты без подсказок получают `CacheConfig.default_ttl_ms`, по умолчанию равный `0` (сразу устаревший), так что сервер, ничего не объявляющий, видит ровно тот же трафик — вызов за вызовом, — что и всегда. -```python title="client.py" hl_lines="33 35 38" +Чтобы увидеть это в действии, запустите `server.py` из предыдущего раздела под uvicorn (его последняя строка собирает ASGI-приложение). Обработчик печатает строку каждый раз, когда действительно выполняется: + +```console +uvicorn server:app --port 8000 +``` + +```python title="client.py" hl_lines="20 23 28" --8<-- "docs_src/caching/tutorial003.py" ``` +Запустите `python client.py` во втором терминале. Он напечатает подсказки, с которыми пришёл первый результат: `ttlMs` обработчика рядом с `cacheScope` из словаря: + +```text +1000 public +``` + +Остальное видно в терминале сервера: между логами запросов uvicorn строка `tools/list served` появляется три раза. + Четыре вызова, три обращения к серверу. Второй вызов нашёл свежую запись и до сервера не дошёл; перевод (внедрённых) часов за пределы TTL заставил третий снова обратиться к серверу; четвёртый указал `cache_mode="refresh"`. Этот именованный аргумент есть у пяти кэширующих методов (`list_tools`, `list_prompts`, `list_resources`, `list_resource_templates`, `read_resource`): * `"use"` (по умолчанию) отдаёт свежую запись, если она есть, а если нет — запрашивает и сохраняет результат. @@ -56,7 +70,7 @@ translation: Над `"use"` стоит одно правило: **вызовы с `meta` всегда доходят до сервера.** Запрос с заданным `meta` (токен прогресса, поля трассировки) рассчитывает на настоящий сетевой запрос, поэтому при `cache_mode="use"` он обрабатывается как `"refresh"`: чтение из кэша пропускается, а полученный результат всё равно заменяет закэшированную запись. `"bypass"` и явный `"refresh"` ведут себя как обычно. -Чтобы совсем выключить кэширование, создайте клиент как `Client(server, cache=None)`: каждый вызов снова обращается к серверу, а `cache_mode`, хотя и принимается, ничего не делает. +Чтобы совсем выключить кэширование, передайте `cache=None` при создании `Client`: каждый вызов снова обращается к серверу, а `cache_mode`, хотя и принимается, ничего не делает. Область тоже учитывается автоматически: записи `"private"` привязаны к *разделу* (partition) кэша (о нём ниже), тогда как записи `"public"` могут быть разделены шире. И **уведомления важнее TTL** для ровно тех записей, которые они называют: уведомление `list_changed` вытесняет соответствующий закэшированный список, а `resources/updated` вытесняет закэшированное чтение, сохранённое ровно под его URI, какими бы свежими они ни были. На подключении 2026-07-28 эти уведомления приходят по потоку `subscriptions/listen`, который открывается через `client.listen(...)`, и вытеснение завершается раньше, чем наблюдатель увидит событие; подробнее — на странице **[Подписки](subscriptions.md)**. diff --git a/i18n/ru/pages/client/callbacks.md b/i18n/ru/pages/client/callbacks.md index c0c29f503a..c539214636 100644 --- a/i18n/ru/pages/client/callbacks.md +++ b/i18n/ru/pages/client/callbacks.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} @@ -63,7 +63,7 @@ result.content # [TextContent(type='text', text='Card issued to Ada Lovelace.') `mode="legacy"` в вызове `Client(...)` стоит не просто так. По умолчанию `Client(...)` согласовывает современный вариант протокола, а в нём нет обратного канала (back-channel) для запросов от сервера к клиенту: `ctx.elicit` завершается ошибкой ещё до того, как колбэк успевает запуститься. Решает это не транспорт, а согласованный - протокол — и в памяти, и по URL одинаково. Фиксируйте `mode="legacy"` всякий раз, когда клиент должен + протокол. Фиксируйте `mode="legacy"` всякий раз, когда клиент должен отвечать на такие запросы; так делает каждый тест, стоящий за этой страницей. Подробнее — на странице **[Версии протокола](../protocol-versions.md)**. В сессии 2026-07-28 колбэк не бесполезен — просто данные поступают к нему иначе: когда инструмент возвращает @@ -151,4 +151,4 @@ result.structured_content # {'result': ['elicitation']} * `sampling_callback` и `list_roots_callback` работают так же, но обслуживают устаревшие возможности; современные серверы вместо этого используют многораундовые запросы. * `logging_callback` и `message_handler` получают уведомления. Они ничего не объявляют. -Первый аргумент `Client(...)` — объект транспорта. Все их виды описаны на странице **[Транспорты клиента](transports.md)**. +Первый аргумент `Client(...)` выбирает транспорт. Все их виды описаны на странице **[Транспорты клиента](transports.md)**. diff --git a/i18n/ru/pages/client/index.md b/i18n/ru/pages/client/index.md index 5a86d3dc10..d653c88674 100644 --- a/i18n/ru/pages/client/index.md +++ b/i18n/ru/pages/client/index.md @@ -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 --- # Объект Client {#the-client} @@ -11,13 +11,23 @@ translation: ## Первый клиент {#your-first-client} -```python title="client.py" hl_lines="14-18" +Клиенту нужен сервер, с которым он будет говорить. К этому серверу Bookshop подключается каждый пример на этой странице. Сохраните его как `server.py` и оставьте работать по HTTP: + +```python title="server.py" --8<-- "docs_src/client/tutorial001.py" ``` -Сервер в начале файла нужен лишь для того, чтобы было к чему подключаться. Клиент — это пять выделенных строк. +```console +uv run mcp run server.py --transport streamable-http +``` + +Теперь он доступен по адресу `http://localhost:8000/mcp`. Клиент — отдельная программа. Сохраните её как `client.py` и запустите `python client.py` во втором терминале: -* `Client(mcp)` получает **сам объект сервера**. Это транспорт в памяти: ни подпроцесса, ни порта, ни HTTP. Именно так подключается каждый пример на этой странице и каждый тест, который вы напишете. +```python title="client.py" hl_lines="7-11" +--8<-- "docs_src/client/tutorial001_client.py" +``` + +* `Client("http://localhost:8000/mcp")` получает **URL**, поэтому подключается по Streamable HTTP к серверу, который вы только что запустили. * `async with` — это **жизненный цикл**. Вход в блок подключает и согласовывает возможности; выход — отключает. Пары `connect()` / `close()` нет, и `Client` нельзя использовать повторно после завершения блока. * Внутри блока сведения о подключении уже доступны как обычные свойства. @@ -25,10 +35,10 @@ translation: `Client` принимает один позиционный аргумент и определяет транспорт по его типу: -* Экземпляр `MCPServer` (или низкоуровневого `Server`): подключение **внутри процесса**. -* Строка с URL (`Client("http://localhost:8000/mcp")`): Streamable HTTP, основной вариант для реального развёртывания. -* `StdioServerParameters`: команда, которая запускается как **подпроцесс**; общение с ним идёт через его stdin и stdout. +* Строка с URL (`Client("http://localhost:8000/mcp")`): Streamable HTTP, транспорт для реального развёртывания. +* `StdioServerParameters`: команда, которая запускается как локальный **подпроцесс**; общение с ним идёт через его stdin и stdout. * **Транспорт**: всё, что можно использовать как `async with ... as (read, write)`, например `streamable_http_client(url, http_client=...)` поверх вашего собственного HTTP-клиента. +* Экземпляр `MCPServer` (или низкоуровневого `Server`): подключение **внутри процесса**, без подпроцесса и без порта. Этот вариант — для тестов, и на нём строится страница **[Тестирование](../get-started/testing.md)**. Всё остальное на этой странице одинаково для всех четырёх вариантов. Заголовкам, подпроцессам, тайм-аутам и протоколу `Transport` посвящена отдельная страница: **[Транспорты клиента](transports.md)**. @@ -49,11 +59,11 @@ translation: ## Получение списка инструментов {#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()` возвращает `ListToolsResult`; инструменты лежат в `.tools`. Каждый из них — полное определение, которое хост передал бы модели: +`list_tools()` возвращает `ListToolsResult`; инструменты лежат в `.tools`. Каждый из них — полное определение, которое хост передал бы модели. Вот первый: ```python tool.name # 'search_books' @@ -77,6 +87,8 @@ tool.description # 'Search the catalog by title or author.' Этой схемы достаточно и интерфейсу, чтобы отрисовать форму аргументов, и модели, чтобы сформировать корректные аргументы. +Второй инструмент, `lookup_book`, зарегистрирован без `title=`, поэтому его `tool.title` равен `None`. + !!! tip `title` необязателен, поэтому интерфейсу, показывающему инструменты человеку, приходится выбирать: `title`, если он есть, иначе `name`. `from mcp.shared.metadata_utils import get_display_name` делает именно это — @@ -86,7 +98,7 @@ tool.description # 'Search the catalog by title or author.' `call_tool(name, arguments)` запускает инструмент и возвращает `CallToolResult`. -```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 @@ result.is_error # False Операции с ресурсами идут парами: два способа получить список и один способ прочитать. -```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 @@ result.is_error # False ## Промпты {#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 @@ message.content # TextContent(type='text', text='Recommend one poetry book from Сервер с обработчиком автодополнения может дополнять аргументы промптов и шаблонов ресурсов по мере того, как пользователь их вводит. -```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 @@ message.content # TextContent(type='text', text='Recommend one poetry book from Каждый метод `list_*` принимает именованный аргумент `cursor=`, а каждый результат содержит `next_cursor`. Когда `next_cursor` равен `None`, у вас есть всё. -```python title="client.py" hl_lines="22-30" +```python title="client.py" hl_lines="7-15" --8<-- "docs_src/client/tutorial007.py" ``` -Этот цикл корректен для любого сервера. `MCPServer` возвращает всё одной страницей, так что `next_cursor` равен `None` и цикл выполняется один раз — поэтому в большинстве программ его и не пишут. О серверах, которые действительно разбивают выдачу на страницы, и о правилах, которым подчиняются курсоры, — на странице **[Пагинация](../advanced/pagination.md)**. +Функция `list_all_tools` корректна для любого сервера. `MCPServer` возвращает всё одной страницей, так что `next_cursor` равен `None` и цикл выполняется один раз — поэтому в большинстве программ его и не пишут. О серверах, которые действительно разбивают выдачу на страницы, и о правилах, которым подчиняются курсоры, — на странице **[Пагинация](../advanced/pagination.md)**. ## В тестах {#in-tests} -`Client(mcp)` без процесса и без порта — уже готовая тестовая обвязка для сервера. +Каждый `client.py` на этой странице обращался к `server.py` по HTTP. В тесте сеть не нужна: `Client` получает сам объект сервера — `from server import mcp`, затем `Client(mcp)`. Ни процесса, ни порта, и все описанные выше методы работают так же. -Для этого есть один специальный флаг конструктора: `Client(mcp, raise_exceptions=True)`. Он действует только на подключениях в памяти, а объясняет его и строит вокруг него весь подход страница **[Тестирование](../get-started/testing.md)**. +Для этого есть один специальный флаг конструктора: `Client(mcp, raise_exceptions=True)`. Он действует только на подключениях внутри процесса, а объясняет его и строит вокруг него весь подход страница **[Тестирование](../get-started/testing.md)**. ## Итоги {#recap} -* `Client(x)` подключается в памяти к объекту сервера, по Streamable HTTP — к строке с URL, а ко всему остальному — через транспорт. +* `Client(x)` подключается по Streamable HTTP к строке с URL, запускает подпроцесс для `StdioServerParameters`, напрямую входит в переданный транспорт, а в тестах принимает сам объект сервера. * `async with` — это весь жизненный цикл. Внутри него `server_capabilities` и `protocol_version` уже заполнены; `server_info` и `instructions` — тоже, если сервер их предоставляет. * `list_tools()` даёт `name`, `title`, `description` и `input_schema` каждого инструмента. * `call_tool()` возвращает `content` для модели, `structured_content` для кода и `is_error`. Инструмент, выбросивший исключение, — это результат, а не исключение. diff --git a/i18n/ru/pages/client/oauth-clients.md b/i18n/ru/pages/client/oauth-clients.md index 3af177a2fa..987455f01a 100644 --- a/i18n/ru/pages/client/oauth-clients.md +++ b/i18n/ru/pages/client/oauth-clients.md @@ -1,6 +1,6 @@ --- translation: - sections: [c6899d3892bd9fa0, 79372cff3cc48a88, 63878d29e87c3e73, 13175843d3588af4, e7e2b9fd516f77de, 758f06399b513c1f, a05d7278487d610b] + sections: [c6899d3892bd9fa0, 79372cff3cc48a88, c2dae1ebe2ebd543, 13175843d3588af4, df06056fb16b3846, 758f06399b513c1f, a05d7278487d610b] tool: 1 --- # OAuth-клиенты {#oauth-clients} @@ -82,18 +82,20 @@ translation: Когда `Client` отправляет первый запрос, сервер отвечает `401`. Дальше действует провайдер: -1. **Обнаружение.** Он читает заголовок `WWW-Authenticate`, загружает метаданные защищённого ресурса (Protected Resource Metadata) сервера с `/.well-known/oauth-protected-resource`, узнаёт, какой сервер авторизации защищает этот ресурс, и загружает метаданные уже *того* сервера. +1. **Обнаружение.** Он читает заголовок `WWW-Authenticate`, загружает метаданные защищённого ресурса (Protected Resource Metadata) сервера с `/.well-known/oauth-protected-resource`, узнаёт, какой сервер авторизации защищает этот ресурс, и загружает метаданные уже *того* сервера. (У сервера постарше, который не публикует метаданные ресурса, вместо этого запрашиваются метаданные сервера авторизации по его собственному origin.) В любом случае метаданные должны называть в поле `issuer` тот сервер, для которого они были загружены; всё остальное отклоняется. 2. **Регистрация.** В хранилище пусто? Он динамически регистрирует вас с вашим `OAuthClientMetadata` и сохраняет результат. 3. **Авторизация.** Он генерирует пару PKCE и `state`, собирает URL авторизации, ждёт ваш `redirect_handler`, а затем ждёт от `callback_handler` код. 4. **Обмен.** Он обменивает код на `OAuthToken`, сохраняет его и повторяет исходный запрос уже с `Authorization: Bearer ...`. После этого он работает незаметно. Токены берутся из хранилища, истёкший токен доступа обновляется с помощью токена обновления, и только когда ничего из этого не срабатывает, сценарий запускается заново. +Ко всем этим запросам применяется одно транспортное правило: как и MCP-запрос, внутри которого они выполняются, они следуют перенаправлению только тогда, когда оно остаётся на том же origin и сохраняет метод (скажем, 307/308 на завершающий слеш), а любое другое перенаправление считают тем, что этот URL не отвечает. + Ничего из этого вы не писали. Остаются два именованных аргумента (`client_metadata_url` и `validate_resource_url`), и этому файлу не нужен ни один из них. О `client_metadata_url` стоит знать — ему посвящён отдельный раздел ниже. ### Попробуйте сами {#try-it} -Большинство примеров в этой документации можно проверить с `Client(server)` в памяти. Этот — нет: весь смысл сценария в HTTP-ответе `401`, а между клиентом в памяти и его сервером никакого HTTP нет. +`Client(server)` в памяти, которым пользуются ваши тесты, здесь не поможет: весь смысл сценария в HTTP-ответе `401`, а между клиентом в памяти и его сервером никакого HTTP нет. В репозитории есть живая версия. `examples/servers/simple-auth/` запускает отдельный сервер авторизации и защищённый MCP-сервер; `examples/clients/simple-auth-client/` — это клиент с этой страницы, выросший в небольшой CLI. В его README — две команды: запустите серверы, запустите клиент, подключив его к ним, — и наблюдайте, как проходят все четыре шага. @@ -111,13 +113,14 @@ URL должен быть HTTPS и с некорневым путём; всё о `ClientCredentialsOAuthProvider` — тот же `httpx2.Auth`, только без человека: -```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" ``` Что изменилось: * Нет `OAuthClientMetadata`, нет обработчиков. Вы передаёте `client_id` и `client_secret`; провайдер строит вокруг них минимальную регистрацию `client_credentials` и полностью пропускает динамическую регистрацию. +* `issuer` называет сервер авторизации, который выдал эти учётные данные; используйте значение `issuer`, которое возвращает его документ `/.well-known/oauth-authorization-server`. Обнаружение по-прежнему проходит, как описано выше, но запросы токена строятся только по метаданным *этого* издателя; если MCP-сервер указывает куда-то ещё, сценарий останавливается с `OAuthFlowError`. Не указывать его — устаревший вариант, и в 3.0 параметр станет обязательным (см. **[Устаревшие возможности](../deprecated.md#deprecated-sdk-helpers)**); до тех пор провайдер выдаёт предупреждение и использует тот сервер авторизации, который найдёт обнаружение. * `scope` — строка с разделением пробелами, формат OAuth для передачи по сети. * Всё дальше по цепочке идентично: тот же `TokenStorage`, тот же `httpx2.AsyncClient(auth=...)`, тот же `streamable_http_client`. @@ -130,7 +133,7 @@ URL должен быть HTTPS и с некорневым путём; всё о Ещё один провайдер находится в `mcp.client.auth.extensions.client_credentials`: **`PrivateKeyJWTOAuthProvider`** — для клиентов, которые аутентифицируются с помощью JWT вместо общего секрета (`private_key_jwt`, вариант с парой ключей и workload identity). - Схема та же: создайте экземпляр и передайте его в `auth=`. В том же модуле есть + Схема та же: создайте экземпляр (он принимает тот же необязательный `issuer`) и передайте его в `auth=`. В том же модуле есть `SignedJWTParameters` и `static_assertion_provider` — два вспомогательных средства, которые собирают для него утверждение (assertion). diff --git a/i18n/ru/pages/client/session-groups.md b/i18n/ru/pages/client/session-groups.md index 7014c96012..7b0fade07d 100644 --- a/i18n/ru/pages/client/session-groups.md +++ b/i18n/ru/pages/client/session-groups.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-groups} @@ -78,7 +78,7 @@ translation: ## Итоги {#recap} * `ClientSessionGroup` держит много подключений к серверам и сводит их инструменты, ресурсы и промпты в один `dict` для каждого вида. -* `connect_to_server(params)` — по одному на сервер. Принимает параметры транспорта, а не объект сервера или URL, как `Client`. +* `connect_to_server(params)` — по одному на сервер. Принимает параметры транспорта, а не URL или `Transport`, которые принимает `Client`. * `group.call_tool(name, arguments)` сам направляет вызов на сервер-владелец. * Имена должны быть уникальны в пределах всей группы; два сервера с инструментом `search` сами по себе ужиться не могут. * `component_name_hook=` переписывает каждое регистрируемое имя. Меняется ключ словаря, но не имя в передаваемых данных. diff --git a/i18n/ru/pages/client/transports.md b/i18n/ru/pages/client/transports.md index 1c24a01ebe..28a44383c7 100644 --- a/i18n/ru/pages/client/transports.md +++ b/i18n/ru/pages/client/transports.md @@ -1,6 +1,6 @@ --- translation: - sections: [9cac816674181eb0, 0700f337babcd4dd, 2bde0dd58cdf00f5, 40b4916d82eaf1d4, 3d0832f39b0d7059, dfa4446556badef0, 5bd93be2ab2ecb9c] + sections: [9cac816674181eb0, 7c157764133fea1f, 40b4916d82eaf1d4, 10d151f2cc75317f, 3d0832f39b0d7059, 92742ba36533633d, 0aeca6145e7bd302] tool: 1 --- # Клиентские транспорты {#client-transports} @@ -11,30 +11,15 @@ translation: *Серверная* сторона каждого из них (то, что делает `mcp.run()` и что вы развёртываете) описана на странице **[Запуск сервера](../run/index.md)**. -## В памяти {#in-memory} - -Передайте сам объект сервера: - -```python title="client.py" hl_lines="14" ---8<-- "docs_src/client_transports/tutorial001.py" -``` - -Ни подпроцесса, ни порта, ни байтов в передаваемых данных. Клиент и сервер — два объекта в одном процессе, и вызов всё равно проходит через настоящий протокольный уровень: `search_books` перечисляется, валидируется и вызывается ровно так же, как это было бы по HTTP. - -Поэтому это сразу две вещи: - -* **Тестовый стенд.** Каждый пример в этой документации проверяется именно так, а страница **[Тестирование](../get-started/testing.md)** строит вокруг этого весь подход. -* **API для встраивания.** Приложению, которое создаёт сервер, не нужен сетевой переход, чтобы вызывать его инструменты. - ## Streamable HTTP {#streamable-http} -Передайте строку с URL — и получите **Streamable HTTP**, транспорт, за которым вы развёртываете сервер: +Передайте строку с URL — и получите **Streamable HTTP**, транспорт, за которым вы развёртываете сервер и с которого стоит начинать: ```python title="client.py" hl_lines="5" --8<-- "docs_src/client_transports/tutorial002.py" ``` -Это уже готовый клиент для продакшена. `Client` сам оборачивает URL в `streamable_http_client(...)` поверх `httpx2.AsyncClient`, настроенного так, как нужно MCP: `follow_redirects=True`, таймаут 30 секунд на connect/write/pool и таймаут чтения 300 секунд, потому что сервер может держать поток ответа открытым. +Это уже готовый клиент для продакшена. `Client` сам оборачивает URL в `streamable_http_client(...)` поверх `httpx2.AsyncClient`, настроенного так, как нужно MCP: таймаут 30 секунд на connect/write/pool и таймаут чтения 300 секунд, потому что сервер может держать поток ответа открытым. !!! check Созданный `Client` **не** подключён. Конструктор только выбирает транспорт; @@ -50,7 +35,7 @@ translation: Как только понадобится заголовок `Authorization`, cookie, прокси, mTLS или другой таймаут, создайте `httpx2.AsyncClient` сами и передайте его в `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 @@ translation: !!! info `httpx2` сохраняет привычный API `httpx`, так что, если вы знаете `httpx`, вы уже умеете делать здесь аутентификацию, прокси, хуки событий, повторные попытки и ограничения соединений. SDK ничего не добавляет сверху - и ничего не убирает. Здесь же подключается OAuth: + и ничего не убирает, кроме [обработки редиректов](#redirects). Здесь же подключается OAuth: `httpx2.AsyncClient(auth=OAuthClientProvider(...))`. Весь этот сценарий — на странице **[OAuth-клиенты](oauth-clients.md)**. +### Редиректы {#redirects} + +Транспорт подключается к URL, который вы ему передали, и ни к какому другому источнику (origin). + +* Редирект `307`/`308`, который остаётся на той же схеме, хосте и порту, выполняется; то же касается перехода `http://` → `https://` на том же хосте. Это покрывает обычный редирект с добавлением косой черты `/mcp` → `/mcp/`. +* Редирект куда-либо ещё **не** выполняется. Вызов завершается ошибкой: + + ```text + MCPError: Redirect to https://other.example.com/mcp not followed; use that URL as the endpoint if it is the intended server + ``` + + Если этот URL и есть нужный сервер, укажите его в конфигурации. Если нет — сервер или прокси перед ним настроен неправильно. + +Это верно для любого `httpx2.AsyncClient`, который вы передаёте: его настройка `follow_redirects` для MCP-запросов не учитывается — ни в одну, ни в другую сторону. OAuth-провайдеры SDK применяют то же правило к своим собственным запросам. + +!!! tip + Сообщение `Redirect to http://… not followed: it would downgrade this HTTPS endpoint to plain HTTP` означает, что + сервер стоит за прокси с терминацией TLS, о котором не знает, и выдаёт редиректы на `http://`. + Это исправляется на сервере (**[Развёртывание и масштабирование](../run/deploy.md#behind-a-tls-terminating-proxy)**) + либо использованием ровно того URL `https://…/`, который предлагает сообщение. + ## stdio {#stdio} Сервер **stdio** — это подпроцесс. Клиент запускает его, пишет JSON-RPC в его stdin и читает JSON-RPC из его stdout. Именно так десктопный хост запускает сервер на вашей машине: хост — это *и есть* этот код плюс UI, а страница **[Подключение к реальному хосту](../get-started/real-host.md)** показывает те же отношения со стороны хоста, в виде файла конфигурации. @@ -105,6 +111,18 @@ stderr дочернего процесса идёт в ваш. Чтобы нап Сервер, которому нужен API-ключ, там его не найдёт. Передайте его явно через `env=`; эти переменные добавляются поверх разрешённого списка. Именно это делает `BOOKSHOP_API_KEY` выше. +## В памяти {#in-memory} + +В тесте нечего развёртывать и нечего запускать. Передайте сам объект сервера: + +```python hl_lines="14" +--8<-- "docs_src/client_transports/tutorial001.py" +``` + +Ни подпроцесса, ни порта, ни байтов в сети. Клиент и сервер — два объекта в одном процессе, и вызов всё равно проходит через настоящий протокольный уровень: `search_books` перечисляется, валидируется и вызывается ровно так же, как это было бы по HTTP. Страница **[Тестирование](../get-started/testing.md)** строит вокруг этого весь подход. + +Та же форма служит и API для встраивания: приложение, которое само создаёт сервер, может вызывать его инструменты без обращения к сети. + ## SSE {#sse} `sse_client(url)` из `mcp.client.sse` — это HTTP-транспорт, который заменил Streamable HTTP. Оборачивайте его так же, `Client(sse_client("http://localhost:8000/sse"))`, чтобы общаться с сервером, который всё ещё на нём говорит, и не стройте на нём ничего нового. @@ -113,15 +131,16 @@ stderr дочернего процесса идёт в ваш. Чтобы нап Для `Client` всё перечисленное — одно и то же. -**Транспорт** — это любой асинхронный контекстный менеджер, который отдаёт пару потоков сообщений `(read, write)`: формально — протокол `Transport` из `mcp.client`. `Client` разрешает свой аргумент по типу: объект сервера подключается внутри процесса, `str` превращается в `streamable_http_client(url)`, `StdioServerParameters` — в `stdio_client(params)`, а всё остальное используется как транспорт напрямую. Благодаря последнему правилу `stdio_client(...)`, `streamable_http_client(...)` и `sse_client(...)` подходят в одно и то же место — и вы можете написать свой. +**Транспорт** — это любой асинхронный контекстный менеджер, который отдаёт пару потоков сообщений `(read, write)`: формально — протокол `Transport` из `mcp.client`. `Client` разрешает свой аргумент по типу: `str` превращается в `streamable_http_client(url)`, `StdioServerParameters` — в `stdio_client(params)`, объект сервера подключается внутри процесса, а всё остальное открывается напрямую как транспорт. Благодаря последнему правилу `stdio_client(...)`, `streamable_http_client(...)` и `sse_client(...)` встают в одно и то же место — и поэтому же можно написать свой собственный. ## Итоги {#recap} -* `Client(mcp)` (объект сервера) подключается в памяти. Используйте для тестов и для встраивания. * `Client("http://.../mcp")` (URL) подключается по Streamable HTTP, транспорту для продакшена. * Заголовки, аутентификация, прокси и таймауты задаются на `httpx2.AsyncClient`, который передаётся в `streamable_http_client(url, http_client=...)`. Именованного аргумента `headers=` нет. +* Редиректы выполняются только в пределах источника самого URL (`307`/`308` с добавлением косой черты) плюс `http`→`https` на том же хосте. Всё остальное завершается ошибкой `Redirect to … not followed`; укажите в конфигурации конечный URL. * stdio — это `Client(StdioServerParameters(...))`. Оборачивайте его в `stdio_client(...)` сами, только чтобы перенаправить stderr дочернего процесса. * Подпроцесс получает окружение из разрешённого списка, а не ваше; `env=` добавляет к нему. +* `Client(mcp)` (объект сервера) подключается в памяти. Используйте его в тестах или чтобы встроить сервер в приложение, которое его создало. * Транспорт — это всё, с чем можно написать `async with x as (read, write)`. `Client` передаёт всё, что не объект сервера, не URL и не `StdioServerParameters`, прямо в этот протокол. * Создание `Client` выбирает транспорт. `async with` его открывает. diff --git a/i18n/ru/pages/deprecated.md b/i18n/ru/pages/deprecated.md index ba399dd31a..b014bc3f10 100644 --- a/i18n/ru/pages/deprecated.md +++ b/i18n/ru/pages/deprecated.md @@ -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 --- # Устаревшие возможности {#deprecated-features} -Спецификация 2026-07-28 выводит из обращения пять возможностей. SDK по-прежнему реализует каждую из них, и каждая теперь выдаёт **предупреждение об устаревании**. Один вспомогательный метод SDK объявлен устаревшим по собственным причинам и описан [в конце страницы](#deprecated-sdk-helpers). +Спецификация 2026-07-28 выводит из обращения пять возможностей. SDK по-прежнему реализует каждую из них, и каждая теперь выдаёт **предупреждение об устаревании**. Несколько устареваний на уровне самого SDK стоят особняком и перечислены [в конце страницы](#deprecated-sdk-helpers). В таблице ниже перечислены все устаревшие возможности, причина, по которой каждая уходит, и замена, на которую стоит опираться. @@ -137,13 +137,15 @@ warnings.filterwarnings("ignore", category=MCPDeprecationWarning) Одна строка в конфигурации pytest — и устаревший вызов уже не сможет незаметно вернуться в кодовую базу, не провалив тест. -## Устаревшие вспомогательные методы SDK {#deprecated-sdk-helpers} +## Устаревшие вспомогательные средства SDK {#deprecated-sdk-helpers} -Это не изменения спецификации, а лишь внутренние детали SDK, у которых появилась лучшая замена. Они выдают то же предупреждение `MCPDeprecationWarning` и будут удалены в версии 3.0. +Это не изменения спецификации, а лишь способы использования SDK, у которых появилась лучшая замена. Они выдают то же предупреждение `MCPDeprecationWarning`, а в версии 3.0 прежняя форма будет удалена. | Устарело | Что делать вместо этого | |---|---| | `FuncMetadata.call_fn_with_arg_validation()` | `FuncMetadata.validate_arguments()`, а затем `FuncMetadata.call_fn()`. Его вызывал только код, работающий с `FuncMetadata` напрямую (скажем, собственный подкласс `Tool`). | +| `AuthSettings(resource_server_url=...)` без `validate_token_resource=` | Задайте его: при `True` сервер отклоняет bearer-токены, которые ваш верификатор не признаёт выпущенными для `resource_server_url`; `False` означает, что верификатор сам проверяет аудиторию токена (см. **[Авторизация](run/authorization.md#a-token-verifier)**). Незаданное значение ведёт себя как `False`; в версии 3.0 `True` станет значением по умолчанию всякий раз, когда задан `resource_server_url`. | +| `ClientCredentialsOAuthProvider(...)` или `PrivateKeyJWTOAuthProvider(...)` без `issuer=` | Передайте `issuer=` с указанием сервера авторизации, который выдал учётные данные (см. **[Написание OAuth-клиентов](client/oauth-clients.md#machine-to-machine)**). Без него MCP-сервер сам решает, какой сервер авторизации их получит; в версии 3.0 этот именованный аргумент станет обязательным. | ## Итоги {#recap} @@ -152,7 +154,7 @@ warnings.filterwarnings("ignore", category=MCPDeprecationWarning) * Устаревание носит рекомендательный характер: в передаваемых данных ничего не меняется, всё продолжает работать в сессиях до 2026 года, и появляется заметное предупреждение `MCPDeprecationWarning` (это `UserWarning`, поэтому оно включено по умолчанию). * Сэмплированию и корневым каталогам дополнительно нужен обратный канал, которого в сессии 2026-07-28 нет. На современном подключении они выдают предупреждение, а затем выбрасывают исключение. * `warnings.filterwarnings("ignore", category=MCPDeprecationWarning)` заглушает всю категорию; `"error::mcp.MCPDeprecationWarning"` в pytest превращает её в провал теста. -* Один вспомогательный метод SDK, `FuncMetadata.call_fn_with_arg_validation()`, объявлен устаревшим отдельно и будет удалён в версии 3.0. +* [Устаревания на уровне SDK](#deprecated-sdk-helpers) подчиняются тому же правилу: сейчас они предупреждают, а в версии 3.0 прежняя форма будет удалена. * Новый код не следует строить ни на одной из этих возможностей. Все остальные страницы этой документации описывают актуальный API. diff --git a/i18n/ru/pages/get-started/first-steps.md b/i18n/ru/pages/get-started/first-steps.md index 0a2d279030..f1ef017b7d 100644 --- a/i18n/ru/pages/get-started/first-steps.md +++ b/i18n/ru/pages/get-started/first-steps.md @@ -1,6 +1,6 @@ --- translation: - sections: [0d6c05bcbf836bf3, 59a7b14eeefc68c1, 7114d8d6daba203f, e8bbb56a98ba7bc9, 5138010f6159901c, f78da7c7c363d4c6, 220a939cab348686] + sections: [0d6c05bcbf836bf3, 9a78b5f6b44b18ab, 7114d8d6daba203f, e8bbb56a98ba7bc9, bfd2fd1153e71dac, 1615a994ef071fdd, 65c599fae991f245] tool: 1 --- # Первые шаги {#first-steps} @@ -17,7 +17,7 @@ translation: * **Клиент** живёт внутри хоста и говорит на MCP. Хост запускает по одному клиенту на каждый сервер, к которому подключён. * **Сервер** — это то, что вы строите с помощью этого SDK. Он предоставляет клиентам разные вещи. С моделью напрямую он никогда не общается. -Вы пишете сервер. Хосты — это чужой продукт. SDK также даёт класс `Client`. Он пригодится для тестирования серверов и появится ниже на этой странице. +Вы пишете сервер. Хосты — это чужой продукт. SDK также даёт класс `Client` — тот же самый, которым хост подключился бы к серверу по URL или запустил бы его как подпроцесс. Он появится ниже на этой странице, и им же вы будете тестировать свои серверы. ## Три примитива {#the-three-primitives} @@ -83,22 +83,20 @@ Inspector запустил ваш сервер через **stdio** — один Когда клиент подключается, сервер объявляет свои **возможности**: на какие семейства запросов он будет отвечать. По этому объявлению клиент решает, о чём вообще имеет смысл спрашивать. Вы его не писали; `MCPServer` объявляет его за вас. -Посмотрите сами. Класс `Client` из SDK принимает объект сервера напрямую и подключается к нему **в памяти** (без подпроцесса, без порта): - -```python -import asyncio - -from mcp import Client - -from server import mcp +Посмотрите сами. Оставьте `server.py` работать по HTTP в одном терминале: +```console +uv run mcp run server.py --transport streamable-http +``` -async def main() -> None: - async with Client(mcp) as client: - print(client.server_capabilities.model_dump(exclude_none=True)) +а из другого направьте на него клиент: +```python title="client.py" hl_lines="7-8" +--8<-- "docs_src/first_steps/tutorial001_client.py" +``` -asyncio.run(main()) +```console +python client.py ``` ```text @@ -118,9 +116,9 @@ asyncio.run(main()) Обратите внимание на то, чего здесь нет. `completions` (автодополнение аргументов для шаблонов ресурсов и промптов) требует обработчика, который пишете вы; у этого сервера его нет, поэтому возможность отсутствует, и корректный клиент о ней не спросит. Таково правило для всего необязательного: зарегистрируйте нужное — и возможность появится; страница **[Автодополнение](../servers/completions.md)** это демонстрирует. !!! info - `Client(mcp)` — тот самый клиент в памяти, которым тестируется каждый пример в этой - документации, и именно так вы будете тестировать свои. Ему отведена целая страница: - **[Тестирование](testing.md)**. + Этот `client.py` — полноценный MCP-клиент, и ему посвящена страница **[Клиент](../client/index.md)**. + В тесте терминал и порт не нужны: передайте `Client` сам объект сервера — + `Client(mcp)`. Этому тоже отведена целая страница: **[Тестирование](testing.md)**. ## Чего вы не писали {#what-you-did-not-write} @@ -129,7 +127,7 @@ asyncio.run(main()) * JSON Schema. `a: int, b: int` — это *и есть* схема для `add`. * Обработчик запросов. `tools/list`, `resources/read`, `prompts/get` — всё это обслуживается за вас. * Объявление возможностей. `MCPServer` составил его за вас. -* Ни строчки протокола. Согласование версии, обрамление JSON-RPC, обмен возможностями — всё это произошло внутри `mcp dev` и `Client(mcp)`, и вы этого не видели. +* Ни строчки протокола. Согласование версии, обрамление JSON-RPC, обмен возможностями — всё это произошло внутри `mcp dev` и `client.py`, и вы этого не видели. В этом соотношении — весь смысл SDK. @@ -140,6 +138,6 @@ asyncio.run(main()) * По одному декоратору на примитив: `@mcp.tool()`, `@mcp.resource(uri)`, `@mcp.prompt()`. Имя, описание и схема берутся из функции. * URI с `{param}` создаёт **шаблон** ресурса, который отображается отдельно от конкретных ресурсов. * **Возможности** сервера объявляются за вас, а клиент спрашивает только о том, что сервер объявил. -* `Client(mcp)` подключается к объекту сервера в памяти: ваш тестовый стенд с первого дня. +* `Client("http://localhost:8000/mcp")` разговаривает с запущенным сервером. Передайте ему вместо этого сам объект сервера, `Client(mcp)`, — и это ваш тестовый стенд с первого дня. Дальше — **[Подключение к настоящему хосту](real-host.md)**: этот же сервер внутри Claude Desktop или IDE, по-настоящему. Затем **[Тестирование](testing.md)**: одна страница, один клиент в памяти — и больше не придётся гадать, работает ли оно. После этого каждому примитиву отведена своя страница, начиная с того, которым управляет модель: **[Инструменты](../servers/tools.md)**. diff --git a/i18n/ru/pages/get-started/testing.md b/i18n/ru/pages/get-started/testing.md index 1f4d623e41..24a0a0ad10 100644 --- a/i18n/ru/pages/get-started/testing.md +++ b/i18n/ru/pages/get-started/testing.md @@ -1,13 +1,13 @@ --- translation: - sections: ['4926721070127497', c52a1de2b6b32f40, 8e792bf8c7489ec6, 627195f7159e24ef] + sections: [5d13c2f0ba42c0d2, c52a1de2b6b32f40, 8e792bf8c7489ec6, 38552ea228b0a04f] tool: 1 --- # Тестирование {#testing} -В Python SDK есть класс `Client` со **встроенным in-memory транспортом**: передайте ему объект сервера, и он подключится к нему напрямую. +Класс `Client` из SDK — тот самый, что подключается к URL или запускает подпроцесс, — умеет подключаться и **в памяти**: передайте ему объект сервера, и он будет говорить с ним напрямую. -Ни подпроцесса. Ни порта. Вообще никакого транспорта. Та же идея, что и `TestClient` в FastAPI. +Ни подпроцесса. Ни порта. Ничего не передаётся по сети. Та же идея, что и `TestClient` в FastAPI. ## Базовое использование {#basic-usage} @@ -97,7 +97,7 @@ async def test_call_add_tool(client: Client): В тестах оставляйте его включённым. В продакшен-коде он не имеет смысла. -## Внутри процесса по умолчанию {#in-process-by-default} +## Без привязки к поколению протокола по умолчанию {#era-neutral-by-default} !!! note `Client(mcp)` подключается внутри процесса и по умолчанию **не привязан к поколению diff --git a/i18n/ru/pages/handlers/multi-round-trip.md b/i18n/ru/pages/handlers/multi-round-trip.md index 2c0ba39534..172e2c3347 100644 --- a/i18n/ru/pages/handlers/multi-round-trip.md +++ b/i18n/ru/pages/handlers/multi-round-trip.md @@ -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-round-trip) {#multi-round-trip-requests} @@ -164,7 +164,7 @@ TTL, привязка к принципалу и привязка к запро ## Результат версии 2026-07-28 {#a-2026-07-28-result} -`InputRequiredResult` существует только в версии протокола **2026-07-28**. `Client(server)` в памяти согласует её за вас; по сети её обнаруживает `mode="auto"`. После подключения `client.protocol_version` сообщает, что именно получилось. +`InputRequiredResult` существует только в версии протокола **2026-07-28**. Режим `mode="auto"`, который `Client` использует по умолчанию, обнаруживает её на любом подключении. После подключения `client.protocol_version` сообщает, что именно получилось. !!! warning В сессии до 2026 года `InputRequiredResult` некуда положить. Верните его из обработчика на diff --git a/i18n/ru/pages/handlers/progress.md b/i18n/ru/pages/handlers/progress.md index 77fcb71fb1..4bea42be1a 100644 --- a/i18n/ru/pages/handlers/progress.md +++ b/i18n/ru/pages/handlers/progress.md @@ -1,6 +1,6 @@ --- translation: - sections: [5315262fe26b33e1, 9d8e98840f1b78f0, 0284b215e85366c4, 8534d8dbb4053a70, 2966fac6fe697007] + sections: [5315262fe26b33e1, 9d8e98840f1b78f0, 52d6009a07e770ea, 8534d8dbb4053a70, 2966fac6fe697007] tool: 1 --- # Ход выполнения {#progress} @@ -29,19 +29,17 @@ translation: Клиент подписывается **на каждый вызов отдельно**, передавая `progress_callback=` в `call_tool`: -```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,29 +54,32 @@ anyio.run(main) Колбэк — это `async`-функция, принимающая ровно то, что сообщил сервер: `progress`, `total`, `message`. !!! info - `Client(mcp)` подключается напрямую к объекту сервера, в памяти, — это тот же клиент, на котором - построена страница **[Тестирование](../get-started/testing.md)**. Параметр `progress_callback` один - и тот же, какой бы транспорт ни использовал `Client`; а вот *временны́е характеристики*, которые вы - сейчас увидите, относятся к подключению в памяти. Оно выполняет колбэк прямо на месте, поэтому - каждый отчёт приходит до того, как `call_tool` вернёт управление. По настоящему транспорту - уведомления соревнуются с результатом, и медленный колбэк может всё ещё работать после того, как - `call_tool` уже вернул управление. + Параметр `progress_callback` один и тот же, что бы вы ни передали в `Client`: URL, как здесь, + `StdioServerParameters` или объект сервера в тесте. Но по настоящему транспорту учитывайте + временны́е характеристики. Каждое уведомление доставляется само по себе, отдельно от ответа, поэтому + медленный колбэк может всё ещё работать после того, как `call_tool` уже вернул управление. Только + внутрипроцессное тестовое подключение выполняет колбэк прямо на месте и гарантирует, что каждый + отчёт придёт раньше результата. ### Попробуйте сами {#try-it} -Положите `client.py` рядом с `server.py` и запустите: +Запустите `server.py` по HTTP, затем из второго терминала запустите клиент: + +```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.'} ``` -Каждый `await ctx.report_progress(...)` на сервере превратился в один вызов `show` на клиенте, в том же порядке, и обе строки напечатались **до** того, как `call_tool` вернул управление. Ход выполнения не упаковывается в результат: он передаётся потоком, пока инструмент ещё работает. +Каждый `await ctx.report_progress(...)` на сервере превратился в один вызов `show` на клиенте, в том же порядке. Ход выполнения не упаковывается в результат. Он передаётся потоком, пока инструмент ещё работает. !!! warning `progress_callback` относится к **вызову**, а не к `Client`. Аргумента конструктора для него нет, diff --git a/i18n/ru/pages/protocol-versions.md b/i18n/ru/pages/protocol-versions.md index 195d3026b7..719373e047 100644 --- a/i18n/ru/pages/protocol-versions.md +++ b/i18n/ru/pages/protocol-versions.md @@ -1,6 +1,6 @@ --- translation: - sections: [478fd619e5f90ef8, aef094a00e44e248, bab8cbf3449fa7e9, df1809b15a58335b, 5f9d8c2336ed0239, f54974398e43ddef, b24443dd78584870] + sections: [d98e337bf28845e0, dd642acbba36f615, f547b3e76da19c94, 149513378588da5c, 4e149ed992046709, f54974398e43ddef, b24443dd78584870] tool: 1 --- # Версии протокола {#protocol-versions} @@ -11,9 +11,17 @@ translation: Заботиться об этом почти никогда не приходится: `Client` договаривается за вас. Эта страница — об одном аргументе конструктора, который этим управляет, `mode=`, и о трёх случаях, когда его меняют. +Каждый фрагмент на этой странице — это `client.py`, который общается с сервером Bookshop `server.py` со страницы **[Клиент](client/index.md)**. Запустите этот сервер в одном терминале: + +```console +uv run mcp run server.py --transport streamable-http +``` + +Затем запускайте каждый фрагмент во втором терминале командой `python client.py`. + ## `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 @@ translation: Вот и вся механика. Один `Client`, сервер любого поколения, никаких ветвлений в коде. !!! info - `MCPServer` отвечает на `server/discover` на любом транспорте — в памяти, stdio, Streamable - HTTP, — поэтому с собственным сервером `auto` всегда приходит к `2026-07-28`. Откат - срабатывает только с настоящим сервером до 2026 года — ровно тогда, когда он и нужен. + `MCPServer` отвечает на `server/discover` на любом транспорте — Streamable HTTP, stdio и + внутрипроцессном подключении, которое используют ваши тесты, — поэтому с собственным сервером + `auto` всегда приходит к `2026-07-28`. Откат срабатывает только с настоящим сервером до + 2026 года — ровно тогда, когда он и нужен. ## `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 @@ translation: `mode` принимает и строку современной версии протокола. Сегодня это множество ровно `["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 @@ ValueError: mode must be 'legacy', 'auto', or one of ['2026-07-28']; got '2025-0 Так что сохраните его. После подключения в режиме `auto` в `client.session.discover_result` лежит ровно тот `DiscoverResult`, который прислал сервер: его `supported_versions`, `capabilities`, `instructions` и идентификационные данные, которые сервер записал в `_meta` результата. В следующий раз передайте его обратно как `prior_discover=`: -```python title="client.py" hl_lines="15 17" +```python title="client.py" hl_lines="8 10" --8<-- "docs_src/protocol_versions/tutorial004.py" ``` diff --git a/i18n/ru/pages/run/asgi.md b/i18n/ru/pages/run/asgi.md index 8fd397b480..bacb4f750b 100644 --- a/i18n/ru/pages/run/asgi.md +++ b/i18n/ru/pages/run/asgi.md @@ -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 --- # Добавление в существующее приложение {#add-to-an-existing-app} @@ -102,7 +102,7 @@ uvicorn server:app --8<-- "docs_src/asgi/tutorial004.py" ``` -Теперь клиенты подключаются к `/notes`, а не к `/notes/mcp`. +Теперь клиенты подключаются к `/notes/`, а не к `/notes/mcp`. ## CORS для браузерных клиентов {#cors-for-browser-clients} @@ -137,7 +137,7 @@ uvicorn server:app ## Итоги {#recap} * `mcp.streamable_http_app()` возвращает приложение Starlette с одним маршрутом, `/mcp`. Запустить его может любой ASGI-сервер. -* По умолчанию приложение отвечает только на запросы, адресованные localhost, а за настоящим именем хоста отклоняет всё кодом `421`, пока вы не передадите список разрешённых хостов в `transport_security=`. За это и за остальной путь к продакшену отвечает страница **[Развёртывание и масштабирование](deploy.md)**. +* По умолчанию приложение отвечает только на запросы, адресованные localhost, а за настоящим именем хоста отклоняет всё кодом `421`, пока вы не передадите в `transport_security=` список разрешённых хостов. За это и за остальной путь к продакшену отвечает страница **[Развёртывание и масштабирование](deploy.md)**. * `Mount` (или `Host`) помещает его внутрь более крупного приложения Starlette или FastAPI. * **Монтирование отключает встроенный жизненный цикл.** Жизненный цикл хост-приложения должен войти в `mcp.session_manager.run()`, иначе первый запрос завершится ошибкой. * Несколько серверов в одном приложении — это несколько монтирований и один жизненный цикл, который входит в каждый менеджер сессий. @@ -145,4 +145,4 @@ uvicorn server:app * Браузерным клиентам нужен CORS: `allow_headers` для заголовков запроса `Mcp-*`, `expose_headers=["Mcp-Session-Id"]` для ответа. * `@mcp.custom_route()` добавляет обычные HTTP-точки без аутентификации рядом с `/mcp`. -Когда сервер доступен по настоящему URL, **[Клиент](../client/index.md)** подключается к нему по этому URL вместо объекта сервера. +Когда сервер доступен по настоящему URL, **[Клиент](../client/index.md)** подключается к нему по этому URL. diff --git a/i18n/ru/pages/run/authorization.md b/i18n/ru/pages/run/authorization.md index c4059c1ae6..68dd7b77eb 100644 --- a/i18n/ru/pages/run/authorization.md +++ b/i18n/ru/pages/run/authorization.md @@ -1,19 +1,19 @@ --- translation: - sections: [d62c13457fc4a534, 80e73abaca6e0652, d1dc4c54cd00ec9c, 14ad3bc7904036bb, 5225f127bc1b9c77, fe1626fdd5aad1da, 4556cb7ea1a04a31] + sections: [d62c13457fc4a534, 80e73abaca6e0652, 128a492d18295f64, 14ad3bc7904036bb, 54a6697833fcc1d9, fe1626fdd5aad1da, 811d083c1da8bcf6] tool: 1 --- # Авторизация {#authorization} -Через Streamable HTTP MCP-сервер — это обычный веб-сервис, и защищается он так же, как любой веб-сервис: с помощью bearer-токенов OAuth 2.1. +При работе через Streamable HTTP ваш MCP-сервер — обычный веб-сервис, и защищают его так же, как любой веб-сервис: bearer-токенами OAuth 2.1. -В терминах OAuth ваш сервер — это **сервер ресурсов**. Он никого не аутентифицирует и не выдаёт токенов. Он делает ровно одно: смотрит на заголовок `Authorization` каждого запроса и решает, годится ли токен в нём. +В терминах OAuth ваш сервер — это **сервер ресурсов**. Он никогда не выполняет вход пользователей и никогда не выдаёт токены. Он делает одно: смотрит на заголовок `Authorization` в каждом запросе и решает, годится ли токен в нём. -Эта страница — о серверной стороне. Клиент, который обнаруживает ваш сервер авторизации и получает токен, описан на странице **[OAuth-клиенты](../client/oauth-clients.md)**. +Эта страница — о серверной стороне. Клиент, который находит ваш сервер авторизации и получает токен, описан на странице **[OAuth-клиенты](../client/oauth-clients.md)**. ## Три стороны {#the-three-parties} -* **Сервер авторизации** аутентифицирует пользователей и выдаёт токены доступа. Его вы не пишете. Это ваш провайдер идентификации (Auth0, Keycloak, Entra или ваш собственный). +* **Сервер авторизации** выполняет вход пользователей и выдаёт токены доступа. Его вы не пишете. Это ваш провайдер идентификации (Auth0, Keycloak, Entra или собственный). * **Сервер ресурсов** — это ваш MCP-сервер. Он проверяет токен в каждом запросе. * **Клиент** выясняет, какому серверу авторизации вы доверяете, получает у него токен и присылает его вам в виде `Authorization: Bearer `. @@ -21,29 +21,33 @@ translation: ## Верификатор токенов {#a-token-verifier} -SDK ничего не предполагает о том, как выглядит действительный токен. Это определяете вы, реализуя **`TokenVerifier`**: +У SDK нет мнения о том, как выглядит действительный токен. Это сообщаете вы, реализуя **`TokenVerifier`**: -```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` — это протокол с одним асинхронным методом. `verify_token` получает сырой токен из заголовка `Authorization` и возвращает **`AccessToken`**, если токен действителен, или `None`, если нет. Больше реализовывать нечего. -* Этот верификатор ищет токен в таблице. Настоящий проверяет подпись JWT или обращается к эндпоинту интроспекции токенов на сервере авторизации. Этот код — ваш; SDK его только вызывает. +* `TokenVerifier` — протокол с одним асинхронным методом. `verify_token` получает сырой токен из заголовка `Authorization` и возвращает **`AccessToken`**, если токен действителен, и `None`, если нет. Больше реализовывать нечего. +* Этот верификатор ищет токен в таблице; в каждой записи указан ресурс, для которого токен выдан. Настоящий проверяет подпись JWT или вызывает эндпоинт интроспекции токенов сервера авторизации и сообщает, для кого выдан токен (его `aud`), в `AccessToken.resource`. Этот код пишете вы; SDK его только вызывает. * `token_verifier=` и `auth=` всегда идут в паре. Передайте один без другого — и `MCPServer(...)` выбросит `ValueError` ещё до того, как обслужит хоть один запрос. -`AuthSettings` — это публичное лицо вашего сервера ресурсов: +`AuthSettings` — публичное лицо вашего сервера ресурсов: * `issuer_url`: сервер авторизации, который выдаёт ваши токены. -* `resource_server_url`: публичный URL этого MCP-эндпоинта. Он указывает, *для какого* ресурса предназначен токен, и по нему же размещается документ обнаружения. +* `resource_server_url`: публичный URL этого MCP-эндпоинта. Он указывает, для *какого* ресурса предназначен токен, и по нему же располагается документ обнаружения. * `required_scopes`: каждый токен должен содержать их все. +* `validate_token_resource`: отклонять любой токен, у которого `AccessToken.resource` не равен `resource_server_url`. Если оставить его незаданным при заданном `resource_server_url`, выдаётся предупреждение (`MCPDeprecationWarning`), а поведение такое же, как при `False`; в версии 3.0 значением по умолчанию для серверов ресурсов станет `True`. + * Включите его, если ваш сервер авторизации привязывает токены к параметру `resource`, который запросил клиент, — MCP-клиенты передают его всегда. Следите, чтобы `resource_server_url` в точности совпадал с URL, к которому подключаются клиенты. + * Оставьте выключенным, если сервер авторизации использует собственные идентификаторы аудитории (идентификатор API в Auth0, идентификатор приложения в Entra), и вместо этого проверяйте `aud` в верификаторе, возвращая `None` для токена, предназначенного не этому серверу. + * Если `aud` — список, поместите в `resource` тот элемент, который равен `resource_server_url`. !!! tip - В `examples/servers/simple-auth/` в репозитории SDK есть `IntrospectionTokenVerifier`, который обращается - к эндпоинту [RFC 7662](https://datatracker.ietf.org/doc/html/rfc7662) настоящего сервера авторизации. Именно так устроено большинство верификаторов в продакшене. + В репозитории SDK, в `examples/servers/simple-auth/`, есть `IntrospectionTokenVerifier`, который обращается + к эндпоинту [RFC 7662](https://datatracker.ietf.org/doc/html/rfc7662) настоящего сервера авторизации. Так устроено большинство верификаторов в реальных развёртываниях. -## Что вы получаете через HTTP {#what-you-get-over-http} +## Что появляется по HTTP {#what-you-get-over-http} -Авторизация живёт в HTTP-заголовках, поэтому существует только на HTTP-транспортах. Запускайте её на том транспорте, который развёртываете: `mcp.run(transport="streamable-http")` поднимает её на `http://127.0.0.1:8000/mcp`, а остальное — на странице **[Запуск сервера](index.md)**. Теперь у приложения два маршрута: +Авторизация живёт в HTTP-заголовках, поэтому существует только на HTTP-транспортах. Запускайте её на том, который развёртываете: `mcp.run(transport="streamable-http")` поднимает сервер на `http://127.0.0.1:8000/mcp`, а остальное — на странице **[Запуск сервера](index.md)**. Теперь у приложения два маршрута: ```text /mcp @@ -65,11 +69,11 @@ SDK ничего не предполагает о том, как выгляди } ``` -Именно по этому документу клиент, который никогда не слышал о вашем сервере, находит к нему дорогу: читает `authorization_servers` и идёт туда за токеном. Ничего из этого вы не писали. +По этому документу клиент, никогда не слышавший о вашем сервере, находит дорогу внутрь: читает `authorization_servers` и идёт туда за токеном. Ни строчки из него вы не писали. !!! check - Обратитесь к `/mcp` без токена (или с таким, для которого верификатор вернул `None`) — и запрос - остановят на входе: + Вызовите `/mcp` без токена (или с токеном, для которого верификатор вернул `None`) — и запрос + остановят на пороге: ```text HTTP/1.1 401 Unauthorized @@ -78,26 +82,26 @@ SDK ничего не предполагает о том, как выгляди {"error": "invalid_token", "error_description": "Authentication required"} ``` - Ничего не разбиралось, ни один инструмент не запускался. А указатель `resource_metadata` в `WWW-Authenticate` — - это то, что делает обнаружение автоматическим: 401 -> документ метаданных -> сервер авторизации -> токен -> повтор. + Ничего не было разобрано, и ни один инструмент не выполнился. А указатель `resource_metadata` в `WWW-Authenticate` — + именно то, что делает обнаружение автоматическим: 401 -> документ метаданных -> сервер авторизации -> токен -> повтор запроса. !!! warning Ничто из этого не защищает `stdio`. У канала нет заголовка `Authorization`, поэтому к `token_verifier` там никогда - не обращаются. Граница безопасности `stdio`-сервера — это процесс, который его запустил. То же - относится к `Client(mcp)` в памяти, который используется в тестах: он подключается напрямую к объекту сервера + не обращаются. Граница безопасности `stdio`-сервера — процесс, который его запустил. То же + относится к `Client(mcp)` в памяти, который используется в тестах: он подключается прямо к объекту сервера и минует HTTP-уровень вместе с авторизацией. -## Личность вызывающего {#the-callers-identity} +## Личность вызывающей стороны {#the-callers-identity} Внутри любого обработчика **`get_access_token()`** — это `AccessToken`, который ваш верификатор вернул для текущего запроса: -```python title="server.py" hl_lines="4 32-35" +```python title="server.py" hl_lines="4 35-38" --8<-- "docs_src/authorization/tutorial002.py" ``` -* Это работает в инструментах, ресурсах и промптах, и ничего передавать не нужно: middleware авторизации сохраняет его в контекстной переменной для каждого запроса. -* Возвращается **тот самый объект, который собрал ваш верификатор**: `client_id`, `scopes`, `subject`, `expires_at` и любые дополнительные `claims`, которые вы прикрепили. Это и есть точка для правил на уровне отдельных инструментов: прочитайте scopes и откажите. -* Вне аутентифицированного HTTP-запроса функция возвращает `None`. В памяти и через `stdio` это всегда `None`. +* Работает в инструментах, ресурсах и промптах, и передавать ничего не нужно: middleware авторизации сохраняет его в контекстной переменной для каждого запроса. +* Возвращается **тот самый объект, который собрал ваш верификатор**: `client_id`, `scopes`, `subject`, `expires_at` и любые дополнительные `claims`, которые вы прикрепили. Это и есть точка для правил на уровне инструмента: прочитайте области действия и откажите. +* Вне аутентифицированного HTTP-запроса возвращается `None`. В памяти и по `stdio` это всегда `None`. Вызовите `whoami` с `Authorization: Bearer alice-token` — и модель прочитает: @@ -105,26 +109,26 @@ SDK ничего не предполагает о том, как выгляди alice (scopes: notes:read) ``` -## Половина, которую SDK не делает {#the-half-the-sdk-doesnt-do} +## Половина, которой в SDK нет {#the-half-the-sdk-doesnt-do} -SDK даёт вам половину сервера ресурсов: проверить, объявить, отказать. Он не даёт страницу входа, экран согласия или токен. +SDK даёт вам половину сервера ресурсов: проверить, объявить, отказать. Он не даёт ни страницы входа, ни экрана согласия, ни токена. -Чтобы увидеть все три стороны в действии, запустите `examples/servers/simple-auth/` из репозитория SDK (небольшой сервер авторизации и сервер ресурсов, настроенный ровно так, как на этой странице), а затем направьте на него `examples/clients/simple-auth-client/` — и пройдите весь путь от обнаружения до токена. +Чтобы увидеть в движении все три стороны, запустите `examples/servers/simple-auth/` из репозитория SDK (небольшой сервер авторизации и сервер ресурсов, настроенный ровно как на этой странице), а затем направьте на него `examples/clients/simple-auth-client/` — получится полный цикл обнаружения и получения токена. !!! info - Есть второй аргумент конструктора, `auth_server_provider=`, который встраивает полноценный сервер - авторизации внутрь MCP-сервера. Он появился раньше разделения AS/RS, вокруг которого построена спецификация - авторизации MCP. В новых серверах к нему прибегать не следует. + Есть и второй аргумент конструктора, `auth_server_provider=`, который встраивает полноценный сервер + авторизации внутрь MCP-сервера. Он появился раньше разделения на AS и RS, вокруг которого построена + спецификация авторизации MCP. В новых серверах обращаться к нему не следует. -Сервер авторизации также может принять подписанное утверждение от корпоративного провайдера идентификации вместо того, чтобы пользователь проходил через экран согласия, и SDK поддерживает обе стороны этого обмена. Сам грант и клиент, который его предъявляет, описаны на странице **[Утверждение личности](../client/identity-assertion.md)**. +Сервер авторизации может также принять подписанное утверждение корпоративного провайдера идентификации вместо того, чтобы пользователь проходил экран согласия, — и SDK поддерживает обе стороны этого обмена. Сам грант и клиент, который его предъявляет, описаны на странице **[Утверждение идентичности](../client/identity-assertion.md)**. ## Итоги {#recap} -* Через Streamable HTTP ваш сервер — это **сервер ресурсов** OAuth 2.1: он проверяет токены, но никогда их не выдаёт. -* `TokenVerifier` — вся поверхность интеграции: один асинхронный метод, токен на входе, `AccessToken | None` на выходе. +* По Streamable HTTP ваш сервер — **сервер ресурсов** OAuth 2.1: он проверяет токены и никогда их не выдаёт. +* `TokenVerifier` — вся поверхность интеграции: один асинхронный метод, на входе токен, на выходе `AccessToken | None`. * `token_verifier=` и `auth=AuthSettings(issuer_url=..., resource_server_url=..., required_scopes=[...])` всегда идут в паре. -* SDK публикует Protected Resource Metadata по [RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728) по адресу `/.well-known/oauth-protected-resource/...` и отвечает на неаутентифицированные запросы кодом 401, заголовок `WWW-Authenticate` которого указывает на них. Это и есть вся история обнаружения. +* SDK публикует Protected Resource Metadata по [RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728) по адресу `/.well-known/oauth-protected-resource/...` и отвечает на неаутентифицированные запросы кодом 401, заголовок `WWW-Authenticate` которого указывает на этот документ. В этом и состоит всё обнаружение. * `get_access_token()` в любом обработчике — это тот, кто вызывает. -* Авторизация — забота HTTP. `stdio` и клиент в памяти её никогда не видят. +* Авторизация — дело HTTP. `stdio` и тестовый клиент в памяти её никогда не видят. -Клиентская половина (обнаружение вашего сервера авторизации и получение токена за вас) — на странице **[OAuth-клиенты](../client/oauth-clients.md)**. А клиент, который *утверждает* личность вместо того, чтобы запрашивать её у пользователя, — на странице **[Утверждение личности](../client/identity-assertion.md)**. +Клиентская половина (найти ваш сервер авторизации и получить токен за вас) — на странице **[OAuth-клиенты](../client/oauth-clients.md)**. А клиент, который *утверждает* идентичность, вместо того чтобы спрашивать её у пользователя, — на странице **[Утверждение идентичности](../client/identity-assertion.md)**. diff --git a/i18n/ru/pages/run/deploy.md b/i18n/ru/pages/run/deploy.md index 1671d9230c..1ddeb70d37 100644 --- a/i18n/ru/pages/run/deploy.md +++ b/i18n/ru/pages/run/deploy.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 --- # Развёртывание и масштабирование {#deploy-scale} @@ -47,6 +47,22 @@ translation: Свежеразвёрнутый сервер, который отклоняет все подключения, — это список разрешённых Host, пока не доказано обратное. **[Устранение неполадок](../troubleshooting.md)** тоже начинается отсюда. +## За прокси, терминирующим TLS {#behind-a-tls-terminating-proxy} + +Если TLS завершается на прокси (ingress, балансировщик нагрузки, Caddy, nginx), а uvicorn за ним отдаёт обычный HTTP, велите uvicorn доверять заголовкам `X-Forwarded-*` от прокси: + +```console +uvicorn server:app --proxy-headers --forwarded-allow-ips='' +``` + +Без этого приложение считает, что его отдают по `http://`, и любое перенаправление, которое оно выдаёт (обычно это `/mcp` → `/mcp/`), указывает на `http://…`. Python-клиент отказывается переходить с HTTPS-эндпоинта на обычный HTTP и прямо об этом говорит: + +```text +MCPError: Redirect to http://mcp.example.com/mcp/ not followed: it would downgrade this HTTPS endpoint to plain HTTP. +``` + +Временное решение на стороне клиента — указать точный URL, который обслуживает сервер (`https://mcp.example.com/mcp/`, со слэшем на конце), чтобы перенаправления не было вовсе. Настоящее решение — флаг выше. `FORWARDED_ALLOW_IPS` — то же самое в виде переменной окружения; `*` доверяет каждому узлу по пути, что правильно, только если до uvicorn не может добраться никто, кроме прокси. + ## Рабочие процессы и кому нужна привязка {#workers-and-who-has-to-be-sticky} Как только доменное имя отвечает, поставьте за ним больше одного рабочего процесса. В SDK для этого нет никакой ручки; приложение Starlette масштабируется так же, как любое ASGI-приложение: объект передаётся тому, кто умеет порождать процессы: @@ -171,6 +187,7 @@ python -c "import secrets; print(secrets.token_hex(32))" ## Итоги {#recap} * По умолчанию приложение отвечает только на запросы, адресованные localhost. `transport_security=TransportSecuritySettings(allowed_hosts=[...], allowed_origins=[...])` — это ворота в продакшен: пока вы его не передадите, каждый запрос за настоящим доменным именем получает `421`, а причина есть только в логе сервера. +* За прокси, терминирующим TLS, запускайте uvicorn с `--proxy-headers --forwarded-allow-ips=...`, иначе его перенаправления указывают на `http://`, и клиент их отклоняет. * На 2026-07-28 нет сессии, и балансировщику не к чему привязываться. `stateless_http=True` — ручка только для старого поколения, потому что современный запрос маршрутизируется и получает ответ раньше, чем этот флаг вообще читается. * Ключ `requestState` по умолчанию — `os.urandom(32)`, выпускаемый в каждом процессе. Многораундовый повтор, попавший к другому рабочему процессу, падает с `-32602` *«Invalid or expired requestState»*. * Решение — `RequestStateSecurity(keys=[...])` **и** одно и то же имя сервера на каждом экземпляре. Имя — это audience claim токена по умолчанию. Одни ключи, одно имя. diff --git a/i18n/ru/pages/run/index.md b/i18n/ru/pages/run/index.md index 879daf7259..a4ad8692aa 100644 --- a/i18n/ru/pages/run/index.md +++ b/i18n/ru/pages/run/index.md @@ -1,6 +1,6 @@ --- translation: - sections: [fea8d769ff9edeba, ce8e2ad42f29ef71, 0d705efb19cf99c2, 2fd7cf825e6d2b2c, 9adc400e8c88e854, 318893ad8e2e9924, 6b63ab96b34476c0] + sections: [fea8d769ff9edeba, ce8e2ad42f29ef71, 0d705efb19cf99c2, 9fd2357154a5b7e7, 9adc400e8c88e854, 318893ad8e2e9924, 6b63ab96b34476c0] tool: 1 --- # Запуск сервера {#running-your-server} @@ -75,6 +75,11 @@ Inspector делает ровно то же, что и настоящий хос * `max_request_body_size`: максимальный принимаемый размер тела запроса в байтах. По умолчанию 4 МиБ; более крупные запросы получают HTTP 413 ещё до разбора и создания сессии. Увеличивайте его, только если легитимные MCP-сообщения превышают этот размер. +* `session_idle_timeout`: сколько секунд сессия старого поколения может простаивать без единого выполняющегося запроса, прежде чем + сервер её закроет. По умолчанию 1800. `None` отключает таймаут. См. + [Время жизни сессий и лимиты](legacy-clients.md#session-lifetime-and-limits). +* `max_sessions`: сколько сессий старого поколения один процесс удерживает одновременно. По умолчанию 10 000. `None` + снимает ограничение. Описан в том же разделе. * `event_store`, `retry_interval`, `transport_security`: возобновляемость и защита от DNS-rebinding. Они могут подождать, пока вы не развернётесь где-то кроме localhost; `transport_security` разобран на странице **[Развёртывание и масштабирование](deploy.md)**. !!! warning diff --git a/i18n/ru/pages/run/legacy-clients.md b/i18n/ru/pages/run/legacy-clients.md index 254ec014fc..bd6708c229 100644 --- a/i18n/ru/pages/run/legacy-clients.md +++ b/i18n/ru/pages/run/legacy-clients.md @@ -1,6 +1,6 @@ --- translation: - sections: [3d1663c18edc824c, d4fd37009a13f03d, af9f398a5a8b679a, 470c2dd144294d69, 8e45827e6d24e8c8, 91dfd0ce98ebb03c] + sections: [3d1663c18edc824c, 90956965ae6a1ca1, af9f398a5a8b679a, 5ce83b1f9d88da62, 0d9b5d13fffc94e5, 8e45827e6d24e8c8, 91dfd0ce98ebb03c] tool: 1 --- # Обслуживание клиентов старого поколения {#serving-legacy-clients} @@ -21,15 +21,25 @@ SDK маршрутизирует каждый запрос по его заго ## Один обработчик, оба поколения {#one-handler-both-eras} -Вот инструмент, которому нужно кое-что спросить у пользователя, и клиенты обоих поколений, которые его вызывают: +Вот инструмент, которому нужно кое-что спросить у пользователя: -```python title="server.py" hl_lines="24 37-38" +```python title="server.py" hl_lines="21" --8<-- "docs_src/legacy_clients/tutorial001.py" ``` Инструменту `reserve` нужно одно, чего модель не сообщила: сколько экземпляров. `Annotated[..., Resolve(ask_quantity)]` — так инструмент это объявляет (подробнее — на странице **[Зависимости](../handlers/dependencies.md)**). Ничто в `reserve` не называет версию, не проверяет возможность и не ветвится. -Оба клиента открыты **одновременно**, на одном и том же объекте `mcp`. `mode="legacy"` выполняет рукопожатие `initialize` — ровно такое подключение открывает клиент до 2026 года. Второй клиент берёт значение по умолчанию и оказывается на `2026-07-28`. +Запустите его по HTTP — и вот клиенты обоих поколений, которые его вызывают: + +```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" +``` + +Оба клиента открыты **одновременно**, к одному и тому же работающему серверу. `mode="legacy"` выполняет рукопожатие `initialize` — ровно такое подключение открывает клиент до 2026 года. Второй клиент берёт значение по умолчанию и оказывается на `2026-07-28`. Запустите `python client.py` во втором терминале: ```text 2025-11-25 {'result': "Reserved 2 of 'Dune'."} @@ -62,6 +72,40 @@ SDK маршрутизирует каждый запрос по его заго отправка пропущенных SSE-событий клиенту, который переподключается к *той же* сессии), а не хранилище сессий. Сессию доступной из другого процесса он не делает никогда. +## Время жизни и лимиты сессий {#session-lifetime-and-limits} + +Сессия старого поколения не живёт вечно, и один процесс не держит их неограниченное количество. +За это отвечают две настройки. Обе — именованные аргументы `run()`, `streamable_http_app()` +и `Server.streamable_http_app()`. У современных подключений (`2026-07-28`) и при `stateless_http=True` +сессий нет, так что ни одна из настроек к ним не относится. + +| Настройка | По умолчанию | Что делает | Что видит клиент | Как отключить | +|---|---|---|---|---| +| `session_idle_timeout` | `1800` (30 мин) | Закрывает сессию, в которой столько времени ничего не было в работе. | `404 Session not found`. Придётся заново выполнить `initialize`. | `None` | +| `max_sessions` | `10_000` | Отказывается открывать сессии сверх этого числа. Существующие сессии не трогает и ничего не вытесняет. | `503 Too many open sessions` с кодом JSON-RPC `-32603`. | `None` | + +Что считается «в работе»: + +* Открытый `GET`-поток. Клиенты SDK держат такой поток открытым, поэтому сессия подключённого + клиента никогда не истекает. +* Запрос, на который ещё готовится ответ. Вызов инструмента, работающий дольше тайм-аута, + не прерывается, а обратный отсчёт начинается только после его завершения. +* Больше ничего. Между запросами часы идут. Любой запрос в сессии запускает их заново, + включая `ping`. Истёкшую сессию уже ничто не оживит. + +Клиент, завершающий сессию запросом `DELETE`, освобождает её сразу. То же происходит +с клиентом, чей открывающий запрос был отклонён. + +```python +mcp.run(transport="streamable-http", session_idle_timeout=None, max_sessions=50_000) +``` + +Оба события попадают в лог сервера. Истечение — `Session idle timeout` на уровне `INFO`. +Отказ в открытии — `Refusing to open a new session: sessions are already open` на уровне `WARNING`. + +Лимиты действуют на процесс. При четырёх рабочих процессах потолок — четыре раза по `max_sessions`, +и каждый рабочий процесс сам отсчитывает время жизни своих сессий. + ## Единственный переключатель: `stateless_http` {#the-one-knob-stateless_http} Если привязка — цена, которую вы платить не готовы, изменить можно ровно одно. @@ -87,7 +131,7 @@ SDK маршрутизирует каждый запрос по его заго !!! check Сделайте заведомо неправильно. `reserve` — тот самый инструмент, который только что обслужил - оба клиента. Разверните его с `stateless_http=True`, подключите те же два клиента по HTTP и + оба клиента. Разверните его с `stateless_http=True`, подключите те же два клиента и вызовите его из каждого. Современный клиент по-прежнему получает `Reserved 2 of 'Dune'.` Современная ветка не изменилась. diff --git a/i18n/ru/pages/troubleshooting.md b/i18n/ru/pages/troubleshooting.md index 189c9d0a0e..b364dc4a8c 100644 --- a/i18n/ru/pages/troubleshooting.md +++ b/i18n/ru/pages/troubleshooting.md @@ -1,6 +1,6 @@ --- translation: - sections: [2efaecdef109a5c5, fcacd3e66b8635a4, 25323d737dcf0261, 8a6e351ec756904d, 137454d469c867f5, 6392596bd6df54f0, 41126fa9c4fe432f, 480b6d7897e30ab4, d83bb682e708dde0, ebbed3449c499db4, 323ef84f6b4bebde, 30fd31be74169d9a, 656943c6cb567218, c2dc3b1007d2e987, 7cf5386b997d04e9, 0b59feed8384456e, 0cba47bae78d04eb, e4355f4c7cf4fb2e] + sections: [3d58228e81b99543, 170514ce901c4139, 17d61fad0a50d62b, 8a6e351ec756904d, 137454d469c867f5, 6392596bd6df54f0, 41126fa9c4fe432f, 480b6d7897e30ab4, d83bb682e708dde0, ebbed3449c499db4, 525cdf1755e29d4c, 30fd31be74169d9a, d2e88333d4f7841f, c2dc3b1007d2e987, d6eabf60cc366341, f798e815252852c2, 0cba47bae78d04eb, 2c218ba829abf74e] tool: 1 --- # Устранение неполадок {#troubleshooting} @@ -13,6 +13,12 @@ translation: --8<-- "docs_src/troubleshooting/tutorial001.py" ``` +Эти записи обращаются к нему по адресу `http://localhost:8000/mcp`, так что оставьте его запущенным по HTTP: + +```console +uv run mcp run server.py --transport streamable-http +``` + Ошибки, которые цитирует эта страница, настоящие: собственный набор тестов SDK воспроизводит каждую из них. ## `ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception)` {#exceptiongroup-unhandled-errors-in-a-taskgroup-1-sub-exception} @@ -23,7 +29,7 @@ translation: ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: await client.read_resource("weather://Atlantis") ``` @@ -49,7 +55,7 @@ async def main() -> None: ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: try: await client.read_resource("weather://Atlantis") except MCPError as e: @@ -67,7 +73,7 @@ async def main() -> None: ```python async def main() -> None: - client = Client(mcp) + client = Client("http://localhost:8000/mcp") tools = await client.list_tools() # RuntimeError ``` @@ -75,7 +81,7 @@ async def main() -> None: ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: tools = await client.list_tools() ``` @@ -251,7 +257,7 @@ app = Starlette(routes=[Mount("/", app=mcp.streamable_http_app())], lifespan=lif ## `MCPError: Session not found` {#mcperror-session-not-found} -Сервер не узнаёт `Mcp-Session-Id`, который отправил клиент, — почти всегда потому, что сервер **перезапустился** (или вас направили на другой экземпляр). Сессии живут в памяти одного этого процесса. +Сервер не узнаёт `Mcp-Session-Id`, который отправил клиент. Либо сервер **перезапустился** (или вас направили на другой экземпляр), либо сессия **истекла**, потому что в течение `session_idle_timeout` — по умолчанию 30 минут — не выполнялось ни одного запроса. См. раздел [Время жизни сессии и ограничения](run/legacy-clients.md#session-lifetime-and-limits). Сессии живут в памяти одного этого процесса. Искать ошибку в сервере незачем. HTTP-ответ — `404`, тело которого — *настоящий* JSON-RPC, поэтому, в отличие от `421` выше, `Client` на Python показывает его дословно: @@ -261,9 +267,9 @@ app = Starlette(routes=[Mount("/", app=mcp.streamable_http_app())], lifespan=lif Исправление — переподключиться: выйти из блока `async with Client(...)` и войти в новый, который согласует свежую сессию. Для долгоживущего клиента это означает перехватывать `MCPError` вокруг вызовов и переподключаться по этому сообщению, а не повторять попытки внутри мёртвой сессии. -Если это происходит *без* перезапуска, значит, у вас больше одного воркера без закрепления сессий за ними: каждый воркер держит собственную таблицу сессий, поэтому запрос, направленный не на тот воркер, оказывается здесь. Эта история и два её решения (маршрутизация с привязкой сессий или `stateless_http=True`) — на страницах **[Развёртывание и масштабирование](run/deploy.md)** и **[Обслуживание клиентов старого поколения](run/legacy-clients.md)**. +Если это происходит *без* перезапуска и клиент не молчал так долго, значит, у вас больше одного воркера без закрепления сессий за ними: каждый воркер держит собственную таблицу сессий, поэтому запрос, направленный не на тот воркер, оказывается здесь. Эта история и два её решения (маршрутизация с привязкой сессий или `stateless_http=True`) — на страницах **[Развёртывание и масштабирование](run/deploy.md)** и **[Обслуживание клиентов старого поколения](run/legacy-clients.md)**. -Для оператора сервера соответствующая строка лога — `Rejected request with unknown or expired session ID: `. Она пишется на уровне `INFO`, поэтому при обычном пороге `WARNING` её не видно. Видеть её пачками сразу после развёртывания — нормально: все подключённые клиенты переподключаются. +Для оператора сервера соответствующая строка лога — `Rejected request with unknown or expired session ID: `. Она пишется на уровне `INFO`, поэтому при обычном пороге `WARNING` её не видно. Видеть её пачками сразу после развёртывания — нормально: все подключённые клиенты переподключаются. Если же сессия истекла, этой строке предшествует `Session idle timeout`, тоже на уровне `INFO`. ## `MCPError: Method not found` {#mcperror-method-not-found} @@ -275,7 +281,13 @@ app = Starlette(routes=[Mount("/", app=mcp.streamable_http_app())], lifespan=lif Сервер хочет что-то спросить у пользователя, а этот клиент никогда не говорил, что его можно спрашивать. -Резолвер элицитации (elicitation) отказывает заранее, если подключённый клиент не объявил элицитацию через формы, и `e.error.data` называет ровно то, чего не хватает: +Этот Bistro перед бронированием задаёт вопрос через резолвер: + +```python title="server.py" hl_lines="15-17 21" +--8<-- "docs_src/troubleshooting/tutorial007.py" +``` + +Запустите его вместо сервера Weather и вызовите `book_table` из клиента, который не передал `elicitation_callback`. Резолвер отказывает заранее, потому что подключённый клиент так и не объявил элицитацию (elicitation) через формы, а `e.error.data` называет ровно то, чего не хватает: ```json { @@ -289,7 +301,7 @@ app = Starlette(routes=[Mount("/", app=mcp.streamable_http_app())], lifespan=lif ```python async def main() -> None: - async with Client(mcp, elicitation_callback=handle_elicitation) as client: + async with Client("http://localhost:8000/mcp", elicitation_callback=handle_elicitation) as client: result = await client.call_tool("book_table", {"date": "Friday"}) ``` @@ -314,14 +326,14 @@ async def main() -> None: Обработчик попытался обратиться к клиенту посреди запроса на подключении, где у вызова нет канала, способного донести запрос от сервера. В такое положение вызов ставят три конфигурации сервера. -**Подключение `2026-07-28`: любой транспорт, всегда.** В современном протоколе вообще нет запросов, инициируемых сервером, поэтому сервер отказывает ещё до того, как что-либо отправлено. `ctx.elicit()` внутри инструмента — классический способ с этим столкнуться (в самом первом тесте в памяти, поскольку `Client(server)` согласовывает `2026-07-28`, не спрашивая), и передача `elicitation_callback=` ничего не меняет: никакой запрос до клиента не доходит, так что отвечать ему не на что: +**Подключение `2026-07-28`: любой транспорт, всегда.** В современном протоколе вообще нет запросов, инициируемых сервером, поэтому сервер отказывает ещё до того, как что-либо отправлено. `ctx.elicit()` внутри инструмента — классический способ с этим столкнуться, обычно в самом первом **[тесте](get-started/testing.md)** этого инструмента в памяти, ведь `Client(mcp)` согласовывает `2026-07-28`, даже если его об этом не просили. Передача `elicitation_callback=` ничего не меняет: никакой запрос до клиента не доходит, так что отвечать ему не на что: ```python title="server.py" hl_lines="16" --8<-- "docs_src/troubleshooting/tutorial006.py" ``` ```python -async def main() -> None: +async def test_book_table() -> None: async with Client(mcp) as client: await client.call_tool("book_table", {"date": "Friday"}) ``` @@ -363,7 +375,7 @@ mcp.shared.exceptions.MCPError: Cannot send 'elicitation/create': this transport ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: await client.call_tool("forecast", {"city": "London"}, request_state="round-1-from-worker-a") ``` @@ -416,7 +428,7 @@ mcp = MCPServer("Weather", request_state_security=RequestStateSecurity(keys=[key * `Tool already exists:` в логе сервера — единственный признак того, что два одноимённых инструмента схлопнулись в один. * Один 421, три написания: `Server returned an error response` (`Client` на Python), `421 Misdirected Request` / `Invalid Host header` (всё остальное), `Invalid Host header: ` (лог сервера). Исправление: `transport_security=TransportSecuritySettings(allowed_hosts=[...])`. * `Task group is not initialized` -> смонтированное приложение, жизненный цикл хоста которого так и не вошёл в `mcp.session_manager.run()`. -* `Session not found` -> сервер перезапустился; переподключитесь. +* `Session not found` -> сервер перезапустился или сессия истекла (`session_idle_timeout`); переподключитесь. * `Cannot send 'elicitation/create': ... no back-channel ...` -> `ctx.elicit()` нужен канал от сервера к клиенту: у подключения `2026-07-28` его не бывает никогда, `stateless_http=True` отнимает его у подключений старого поколения, а `json_response=True` отнимает канал, привязанный к запросу. Используйте резолвер (клиенту старого поколения к тому же нужен сервер, который сохраняет канал). Соседнее `Method not found` — это запрос метода, которого нет в ревизии протокола другой стороны. * `Client did not declare the form elicitation capability ...` и `Elicitation not supported` -> у клиента не хватает `elicitation_callback=`. * `Invalid or expired requestState` никогда не говорит по сети, почему. Лог сервера говорит; `unknown key` означает, что `RequestStateSecurity(keys=[...])` нужно сделать общим для всех воркеров. diff --git a/i18n/ru/pages/whats-new.md b/i18n/ru/pages/whats-new.md index 2e9f324ae5..3382e2d0ae 100644 --- a/i18n/ru/pages/whats-new.md +++ b/i18n/ru/pages/whats-new.md @@ -1,6 +1,6 @@ --- translation: - sections: [cfe01c0c5863dfa2, 1c58c5cfcc37d455, a7392996acf1ad8f, 875eb2889263424e] + sections: [cfe01c0c5863dfa2, 0dbb68d8b210177b, 80cf193023af3ed4, 875eb2889263424e] tool: 1 --- # Что нового в v2 {#whats-new-in-v2} @@ -42,11 +42,11 @@ mcp = MCPServer("Demo") # v1: FastMCP("Demo") v1 выдавала три вложенных слоя: контекстный менеджер транспорта, отдающий сырые потоки, обёрнутый вокруг них `ClientSession` и вызываемый вручную `await session.initialize()`. В v2 объект один: -```python title="client.py" hl_lines="14-18" ---8<-- "docs_src/client/tutorial001.py" +```python title="client.py" hl_lines="7-11" +--8<-- "docs_src/client/tutorial001_client.py" ``` -`Client` принимает объект сервера (в памяти, без транспорта — это сценарий для тестов), URL (Streamable HTTP), `StdioServerParameters` (подпроцесс stdio) или любой другой контекстный менеджер транспорта, например `sse_client(...)`. Вход в `async with` подключается и согласует версию протокола, на каком бы поколении ни говорил сервер; после этого `client.server_capabilities` и `client.protocol_version` просто доступны, как и `client.server_info`, когда сервер себя идентифицирует (теперь это `Implementation | None`, поскольку в поколении 2026 идентификация необязательна). Колбэки сэмплирования и элицитации, зарегистрированные в v1, по-прежнему работают (их тела затрагивает то же переименование атрибутов в snake_case, что и всё остальное на этой странице), теперь они ещё и отвечают на запросы внутри результатов в стиле 2026 (см. ниже) и выполняются параллельно, а не по одному. `ClientSession` по-прежнему лежит в основе для тех, кому нужна низкоуровневая поверхность, и `client.session` её отдаёт; она тоже изменилась (работает на новом движке-диспетчере, и некоторые её собственные сигнатуры поменялись), так что прежде чем спускаться на этот уровень, прочитайте **[Руководство по миграции](migration.md#clientsession-now-runs-on-jsonrpcdispatcher-basesession-removed)**. +`Client` принимает URL (Streamable HTTP), `StdioServerParameters` (подпроцесс stdio), любой другой контекстный менеджер транспорта, например `sse_client(...)`, или — в тестах — сам объект сервера (в памяти, без транспорта). Вход в `async with` подключается и согласует версию протокола, на каком бы поколении ни говорил сервер; после этого `client.server_capabilities` и `client.protocol_version` просто доступны, как и `client.server_info`, когда сервер себя идентифицирует (теперь это `Implementation | None`, поскольку в поколении 2026 идентификация необязательна). Колбэки сэмплирования и элицитации, зарегистрированные в v1, по-прежнему работают (их тела затрагивает то же переименование атрибутов в snake_case, что и всё остальное на этой странице), теперь они ещё и отвечают на запросы внутри результатов в стиле 2026 (см. ниже) и выполняются параллельно, а не по одному. `ClientSession` по-прежнему лежит в основе для тех, кому нужна низкоуровневая поверхность, и `client.session` её отдаёт; она тоже изменилась (работает на новом движке-диспетчере, и некоторые её собственные сигнатуры поменялись), так что прежде чем спускаться на этот уровень, прочитайте **[Руководство по миграции](migration.md#clientsession-now-runs-on-jsonrpcdispatcher-basesession-removed)**. Страница **[Объект Client](client/index.md)** знакомит с ним, **[Транспорты клиента](client/transports.md)** описывает четыре формы подключения, **[Колбэки клиента](client/callbacks.md)** — сами колбэки, а **[Тестирование](get-started/testing.md)** показывает шаблон работы в памяти, который заменяет вспомогательную функцию `create_connected_server_and_client_session()` из v1. @@ -171,11 +171,15 @@ v2 реализует ревизию 2026-07-28 и обслуживает **об Замена разворачивает вызов. Инструмент, которому что-то нужно от пользователя, *возвращает* вопрос (`InputRequiredResult`), клиент отвечает на него теми же колбэками, что были всегда, и вызов повторяется с приложенными ответами. `Client` ведёт этот цикл за вас. На сервере вы редко собираете результат сами, потому что это делает **[зависимость](handlers/dependencies.md)**: аннотируйте параметр `Resolve(ask_quantity)`, где `ask_quantity` — обычная функция, которую вы пишете, и SDK спросит тем механизмом, который поддерживает подключение: живым запросом элицитации на сессии старого поколения или многораундовым запросом на 2026. Одно тело инструмента, оба поколения: -```python title="dual_era.py" hl_lines="24 37-38" +```python title="server.py" hl_lines="21" --8<-- "docs_src/legacy_clients/tutorial001.py" ``` -В этом файле вся идея собрана в одном месте: один сервер, один инструмент на `Resolve`, а также клиент старого поколения и современный клиент, оба получающие свой ответ, — всё в памяти. **[Многораундовые запросы](handlers/multi-round-trip.md)** объясняет механизм (включая `request_state`, который SDK запечатывает и проверяет за вас); **[Элицитация](handlers/elicitation.md)** описывает, как спрашивать. +```python title="client.py" hl_lines="14-15" +--8<-- "docs_src/legacy_clients/tutorial001_client.py" +``` + +В этих двух файлах вся идея целиком: один сервер, один инструмент на `Resolve`, а также клиент старого поколения и современный клиент, оба получающие свой ответ от одного и того же работающего сервера (**[Обслуживание клиентов старого поколения](run/legacy-clients.md)** разбирает их по шагам). **[Многораундовые запросы](handlers/multi-round-trip.md)** объясняет механизм (включая `request_state`, который SDK запечатывает и проверяет за вас); **[Элицитация](handlers/elicitation.md)** описывает, как спрашивать. !!! warning "Это единственное место, где перенесённый сервер v1 меняет поведение" Первыми на это натыкаются ваши собственные тесты: `Client(mcp)` по умолчанию согласует 2026-07-28 с вашим diff --git a/i18n/tr/pages/advanced/apps.md b/i18n/tr/pages/advanced/apps.md index 4f2582c9d0..dbe3380688 100644 --- a/i18n/tr/pages/advanced/apps.md +++ b/i18n/tr/pages/advanced/apps.md @@ -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 @@ SDK bunu yerleşik `Apps` uzantısı (`io.modelcontextprotocol/ui`) olarak sunar ## Yüzü olan bir saat {#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 @@ Her istemci uygulamaları çizmez. Şartname bunun sizin için ne anlama geldiğ Model `content`'i okur; iframe insanlar içindir. UI destekli bir host yine de metin sonucunu modele iletir, yalnızca metin destekleyen bir istemci ise *sadece* onu alır. Yani kanonik desen tek araç, iki yanıttır. `get_time`'a bir daha bakın: -```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)` yalnızca istemci `io.modelcontextprotocol/ui` uzantısını beyan ettiğinde **ve** `mimeTypes` ayarlarında `text/html;profile=mcp-app`'i listelediğinde `True` olur. Alan zorunludur, bu yüzden onu atlayan bir istemci sayılmaz. Aynı dosyadaki `main()` tam olarak bunu beyan eder: anlaşmanın istemci tarafı, ve zengin yanıt geri gelir. +`client_supports_apps(ctx)` yalnızca istemci `io.modelcontextprotocol/ui` uzantısını beyan ettiğinde **ve** `mimeTypes` ayarlarında `text/html;profile=mcp-app`'i listelediğinde `True` olur. Alan zorunludur, bu yüzden onu atlayan bir istemci sayılmaz. Anlaşmanın istemci tarafı şöyle: + +```python title="client.py" hl_lines="8 12" +--8<-- "docs_src/apps/tutorial001_client.py" +``` + +`server.py` dosyasını HTTP üzerinden sunun, ardından istemciyi ikinci bir terminalden çalıştırın: + +```console +uv run mcp run server.py --transport streamable-http +``` + +```console +python client.py +``` + +```text +2026-06-26T12:00:00Z +``` + +Zengin yanıt geri geldi. `Client` çağrısından `extensions=[APPS_SUPPORT]` argümanını çıkarın; aynı program bunun yerine `The time is 2026-06-26T12:00:00Z.` yazdırır. Yalnızca metin destekleyen bir istemcinin görüp göreceği de budur. !!! warning Tek içerik olarak asla `"[Rendered UI]"` gibi bir yer tutucu döndürmeyin. Yedek metin işe yaramazsa araç, yalnızca metin destekleyen her istemci için ve modelin kendisi için işe yaramaz. O cümleyi yazın. diff --git a/i18n/tr/pages/advanced/extensions.md b/i18n/tr/pages/advanced/extensions.md index 0403a29f9e..a144b0170d 100644 --- a/i18n/tr/pages/advanced/extensions.md +++ b/i18n/tr/pages/advanced/extensions.md @@ -1,6 +1,6 @@ --- translation: - sections: [05891e7cc1938a13, b3c01a6af28c51ee, 7ffc91f5e38bdfe0, 717d3f235a8333a7, f471a13b2fe5d737, ed6af2df4b656dff] + sections: [05891e7cc1938a13, b3c01a6af28c51ee, 6eba6ce094f4417e, 125f28588c85dd7b, ddd8969b698ca11c, ed6af2df4b656dff] tool: 1 --- # Uzantılar {#extensions} @@ -49,7 +49,7 @@ TypeError: Stamps.identifier must be a `vendor-prefix/name` string İşe yarar en küçük uzantı, bir araç ve bir ayarlar eşlemesidir: -```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 @@ TypeError: Stamps.identifier must be a `vendor-prefix/name` string * `settings()`, `capabilities.extensions["com.example/stamps"]` konumunda duyurulan değerdir. Uzantıyı ayarsız duyurmak için `{}` (varsayılan) döndürün. * Uzantı sunucuyu hiçbir zaman almaz. Katkıları veri olarak beyan eder; bunları `MCPServer` tüketir. Değiştirilecek bir `self.server` yoktur. -Kanıtı da `main()`: doğrudan `mcp`'ye bağlanan bellek içi bir istemci: +HTTP üzerinden sunun; kanıt da bir istemcidir: -```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" ``` +Bu sayfadaki her `server.py` bu komutla sunulur; her `client.py` de ikinci bir terminalden `python client.py` ile onun yanında çalışır. + ### Kendi metotlarınızı sunma {#serving-your-own-methods} Bir uzantı **yeni istek metotları** kaydedebilir: spesifikasyonunkilerin yanında sunulan kendi fiilleri: -```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 @@ Metotlar **yalnızca ekleme niteliğindedir**. SDK bunu çalışma zamanında de ### İstemci tarafı {#the-client-side} -Aynı dosyadaki `main()`, istemci tarafının tamamıdır; iki yarısıyla birlikte: +İstemci başlı başına bir programdır ve istemci tarafının iki yarısını da taşır: -```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)])` uzantıyı beyan eder. Beyanlar `ClientCapabilities.extensions` hâline gelir: 2026-07-28 bağlantısında eşleme istek başına `_meta` zarfında taşınır, böylece sunucu onu **her** istekte görür; eski nesil bir bağlantıda `initialize` el sıkışmasıyla taşınır. Sunucu kodu hangisi olduğuyla ilgilenmez: `require_client_extension(ctx, ...)` ve `ctx.session.check_client_capability(...)` her iki yolda da doğru kaynağı okur. * Satıcıya özgü metotlar bir katman aşağıya, `client.session.send_request(...)` düzeyine iner; `Client` yalnızca spesifikasyon fiilleri için birinci sınıf metotlar kazanır. `send_request` herhangi bir `Request` alt sınıfını kabul eder, bu yüzden satıcıya özgü istek olduğu gibi geçer. +* `SearchRequest` ve taşıdığı iki model uzantının iletim sözleşmesidir; bu yüzden istemci onları kendisi için beyan eder. Yayımlanmış bir uzantı bunları her iki tarafın da içe aktardığı bir pakette sunardı. ### `tools/call` isteğini yakalama {#intercepting-toolscall} @@ -109,12 +116,18 @@ Kanca `tools/call` isteğini sarmalar, başka hiçbir şeyi değil. Her iletiyi ## Bir istemci uzantısı kullanma {#using-a-client-extension} -**İstemci uzantısı**, aynı sözleşmenin tüketen taraftan görünüşüdür: tek bir tanımlayıcının arkasında toplanmış bir istemci tarafı davranış paketi. Örnekleri `Client(extensions=[...])` ile geçirin ve araçları normal şekilde çağırın: +**İstemci uzantısı**, aynı sözleşmenin tüketen taraftan görünüşüdür: tek bir tanımlayıcının arkasında toplanmış bir istemci tarafı davranış paketi. Buradaki sunucu `buy` çağrısını malın kendisi yerine kullanılacak bir makbuzla yanıtlar; bunu da yalnızca uzantıyı beyan etmiş bir istemci için yapar: -```python title="client.py" hl_lines="66-68" +```python title="server.py" hl_lines="22-25" --8<-- "docs_src/extensions/tutorial006.py" ``` +İstemcide örnekleri `Client(extensions=[...])` ile geçirin ve araçları normal şekilde çağırın: + +```python title="client.py" hl_lines="33-35" +--8<-- "docs_src/extensions/tutorial006_client.py" +``` + `call_tool("buy", ...)`, diğer her çağrı gibi düz bir `CallToolResult` döndürür. Uzantının değiştirdiği şu: sunucu artık `buy` çağrısını nihai bir sonuç yerine `receipt` adlı bir **sonuç biçimiyle** yanıtlayabilir ve `Receipts`, `call_tool` dönmeden önce onu tamamlar (burada makbuzu bir takip çağrısıyla kullanarak). Çağrı yerinde hiçbir şey değişmez. Uzantıyı çıkarırsanız bunların hiçbiri olmaz: sunucunun kontrol noktası onu beyan etmemiş bir istemciyi reddeder (hata -32021) ve kontrolü atlayan bir sunucudan gelen sahiplenilmiş bir biçim, spesifikasyonun tanınmayan bir `resultType` için gerektirdiği gibi doğrulamadan geçemez. Bağlantının her iki ucunda da varsayılan olarak kapalı. @@ -124,15 +137,15 @@ Uzantıyı çıkarırsanız bunların hiçbiri olmaz: sunucunun kontrol noktası ```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")]) ``` ## İstemci uzantısı yazma {#writing-a-client-extension} `ClientExtension`'dan alt sınıf türetin ve yalnızca ihtiyaç duyduklarınızı geçersiz kılın. Her birinin varsayılanı olan üç katkı türü var: `settings()`, `claims()` ve `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" ``` * Tanımlayıcı, sunucununkiyle aynı dilbilgisini izler ve sınıf tanımlandığında doğrulanır. @@ -153,12 +166,18 @@ def notifications(self) -> Sequence[NotificationBinding[Any]]: ### Uzantı fiilleri {#extension-verbs} -Bir uzantının kendi istek metotları istemci tarafında kayıt gerektirmez. Satıcıya özgü bir istek türü `mcp.types.Request`'ten türer ve [Kendi metotlarınızı sunma](#serving-your-own-methods) bölümündeki gibi `client.session.send_request` üzerinden gider. Tek bir ekleme var: bir params anahtarının `Mcp-Name` başlığında taşınması gerektiğinde (tasks gibi uzantı spesifikasyonları kendi fiilleri için bunu şart koşar) istek türü `name_param` beyan eder: +Bir uzantının kendi istek metotları istemci tarafında kayıt gerektirmez. Satıcıya özgü bir istek türü `mcp.types.Request`'ten türer ve [Kendi metotlarınızı sunma](#serving-your-own-methods) bölümündeki gibi `client.session.send_request` üzerinden gider. Uzantısı, adlandırılmış bir iş hakkında tek bir fiil sunan bir sunucu düşünün: -```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" ``` +İstemcide tek bir ekleme var: bir params anahtarının `Mcp-Name` başlığında taşınması gerektiğinde (tasks gibi uzantı spesifikasyonları kendi fiilleri için bunu şart koşar) istek türü `name_param` beyan eder: + +```python title="client.py" hl_lines="20-23 28-29" +--8<-- "docs_src/extensions/tutorial007_client.py" +``` + Oturum, `params["jobId"]` değerini her gönderim yolunda `Mcp-Name` başlığına yansıtır; eksik bir değer ise gerekli bir başlığı sessizce atlamak yerine açıkça hata verir. ## Bir uzantının yapamayacakları {#what-an-extension-cannot-do} diff --git a/i18n/tr/pages/advanced/low-level-server.md b/i18n/tr/pages/advanced/low-level-server.md index 42a7a211bc..ba9cd121a4 100644 --- a/i18n/tr/pages/advanced/low-level-server.md +++ b/i18n/tr/pages/advanced/low-level-server.md @@ -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 --- # Düşük seviyeli Server {#the-low-level-server} @@ -36,18 +36,22 @@ Bu, **[Araçlar](../servers/tools.md)** sayfasında dokuz satır `@mcp.tool()` i ### Deneyin {#try-it} -Bunun için Inspector yok: `mcp dev` ve `mcp run` yalnızca `MCPServer` kabul eder. Bellek içi `Client` bunu umursamaz; düşük seviyeli bir `Server`'ı tıpkı bir `MCPServer`'ı aldığı gibi alır: +`mcp dev` ve `mcp run` yalnızca `MCPServer` kabul eder, bu yüzden bunu kendiniz sunarsınız. `server.py` dosyasının son satırı ondan sıradan bir ASGI uygulaması oluşturur, uvicorn da onu çalıştırır: -```python title="main.py" +```console +uvicorn server:app --port 8000 +``` + +Inspector'ı ya da herhangi bir istemciyi `http://localhost:8000/mcp` adresine yönlendirin: + +```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 @@ asyncio.run(main()) * `result.structured_content` değeri `None`. Yüksek seviyeli sunucu `-> str` dönüş türünü sizin yerinize `{"result": ...}` içine sarmalar; burada sizin oluşturmadığınızı kimse oluşturmaz. * `list_tools`, **sizin** yazdığınız şemayı karakteri karakterine döndürür. Yüksek seviyeli sürümde her özellikte `"title": "Query"`, kökte de `"title": "search_booksArguments"` vardı: Pydantic'in bıraktığı izler. Burada ise ağa giden bir şey varsa onu oraya siz koymuşsunuzdur. +Testte uvicorn'u ve portu atlarsınız: `Client(server)`, düşük seviyeli bir `Server`'ı süreç içinde tıpkı bir `MCPServer`'ı aldığı gibi alır; **[Test etme](../get-started/testing.md)** sayfasının anlattığı kalıp da budur. + ## Sizin yerinize hiçbir şey denetlenmez {#nothing-is-checked-for-you} `MCPServer`, çağrıyı kendi ürettiği şemaya göre doğrulayarak hatalı bir argümanı fonksiyonunuz daha çalışmadan reddeder (**[Araçlar](../servers/tools.md)**). @@ -215,4 +221,4 @@ Bunların her biri, artık kavramlarını bildiğiniz birer fikir; her birinin k * `add_request_handler(method, params_type, handler)` her metodu sunar. `initialize` ayrılmıştır. * Bir `Server`'ın duyurduğu yetenekler, hangi işleyicileri kaydettiğinizden türetilir. -`Client(server)` iki sunucuya da aynı davrandı, çünkü ikisi aynı protokolün *ta kendisi*; bütün mesele de bu. Bir alt katman ise bir sınıf bile değil: **[Middleware](middleware.md)**. +İstemci iki sunucuya da aynı davrandı, çünkü ikisi aynı protokolün *ta kendisi*; bütün mesele de bu. Bir alt katman ise bir sınıf bile değil: **[Middleware](middleware.md)**. diff --git a/i18n/tr/pages/advanced/pagination.md b/i18n/tr/pages/advanced/pagination.md index e686ca1b67..68454ac2fe 100644 --- a/i18n/tr/pages/advanced/pagination.md +++ b/i18n/tr/pages/advanced/pagination.md @@ -1,6 +1,6 @@ --- translation: - sections: [a9aba7a026c7bd85, ed32bda7ba9ae33a, 7e64cc5646abb91f, 22a0129ee78b3c63, d875373c06d8d2f9] + sections: [a9aba7a026c7bd85, 83e2a08b9d46a398, 9fd8a0aa384b3257, 22a0129ee78b3c63, d875373c06d8d2f9] tool: 1 --- # Sayfalama {#pagination} @@ -31,9 +31,13 @@ Sayfalama, kaynak listesi aslında bir veritabanı olan sunucu içindir: tek yan ### Deneyin {#try-it} -`Client(server)`, düşük seviyeli bir `Server`'a bellek içinde, bir `MCPServer`'a bağlandığı gibi bağlanır. +`mcp run` yalnızca bir `MCPServer` kabul eder; bu yüzden bunu kendiniz sunarsınız. `server.py` dosyasının son satırı `Server`'dan sıradan bir ASGI uygulaması oluşturur, uvicorn da onu çalıştırır: -`list_resources()`'ı argümansız çağırın. `book-1`'den `book-10`'a kadar on kaynak alırsınız ve `next_cursor`, `"10"` dizgesidir. +```console +uvicorn server:app --port 8000 +``` + +Herhangi bir istemciyi (**[İstemci](../client/index.md)** ya da Inspector) `http://localhost:8000/mcp` adresine yönlendirin ve `list_resources()`'ı argümansız çağırın. `book-1`'den `book-10`'a kadar on kaynak alırsınız ve `next_cursor`, `"10"` dizgesidir. Bunu `list_resources(cursor="10")` ile geri verin; ilk kaynak `book-11`, yeni `next_cursor` ise `"20"` olur. @@ -43,7 +47,7 @@ Onuncu sayfa, `next_cursor` değeri `None` olarak döner. Bitti. `Client` üzerindeki her `list_*` metodu (`list_tools`, `list_resources`, `list_resource_templates`, `list_prompts`) bir `cursor=` anahtar kelimesi alır. Sayfalanmış bir listeyi sonuna kadar okumak tek bir `while True`'dur: -```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 @@ Onuncu sayfa, `next_cursor` değeri `None` olarak döner. Bitti. * `next_cursor`'a bakmadan **önce** listeyi genişletin: son sayfada da kaynaklar vardır. * Çıkış koşulu `next_cursor is None`'dır. Bunun dışındaki her şey, dokunulmadan doğrudan `cursor=`'a geri gider. -`main()`'ini çalıştırın; `100 resources` yazdırır: on tane onluk sayfa, on sayfa olduğundan hiç haberi olmayan bir döngü tarafından birleştirilmiş. +uvicorn hâlâ `server.py` dosyasını sunarken ikinci bir terminalde `python client.py` komutunu çalıştırın. `100 resources` yazdırır: on tane onluk sayfa; bunları, on sayfa olduğundan hiç haberi olmayan bir döngü birleştirir. Bu, **[İstemci](../client/index.md)** sayfasının her `list_*` fiili için gösterdiği döngünün aynısıdır ve sayfalamayan bir sunucuya karşı hiçbir maliyeti yoktur: ilk yanıtta `next_cursor`, `None` olur ve döngü bir kez çalışır. diff --git a/i18n/tr/pages/client/caching.md b/i18n/tr/pages/client/caching.md index 359fbe25a9..0e734d9699 100644 --- a/i18n/tr/pages/client/caching.md +++ b/i18n/tr/pages/client/caching.md @@ -1,6 +1,6 @@ --- translation: - sections: [9e7b9a1710e5aeba, b74ca4c1d2ddddee, fa8714e61bf90c5a, 04db67a886b7271c, 857690fb8f876800] + sections: [9e7b9a1710e5aeba, 66a2e9acc9101d54, d3d25aa802f5145a, 04db67a886b7271c, 857690fb8f876800] tool: 1 --- # Önbellekleme ipuçları {#caching-hints} @@ -30,7 +30,7 @@ Varsayılan olarak her sonuç `ttlMs: 0, cacheScope: "private"` der: anında bay Alt düzey `Server`'da işleyiciler sonuçlarını elle oluşturur ve `ttl_ms` / `cache_scope` sonuç modellerindeki sıradan alanlardır. Bunları açıkça ayarlayan bir işleyici, alan alan, her zaman oluşturucu eşlemesine üstün gelir: -```python title="server.py" hl_lines="10 16" +```python title="server.py" hl_lines="11 17" --8<-- "docs_src/caching/tutorial002.py" ``` @@ -44,10 +44,24 @@ Sayfalandırılmış listelerle ilgili bir uyarı: protokol bir listenin **her s 2026-07-28 oturumunda `Client` ipuçlarına sizin yerinize uyar: varsayılan olarak açık, yerleşik bir yanıt önbelleği vardır. `ttlMs` taşıyarak gelen bir sonuç saklanır ve o TTL içinde yapılan özdeş bir çağrı hiç tur atılmadan önbellekten sunulur. *Hiç* ipucu taşımayan bir sonuç önbelleğe alınmaz: ipucu taşımayan sonuçlar `CacheConfig.default_ttl_ms` değerini alır, bu da varsayılan olarak `0`'dır (anında bayat); dolayısıyla hiçbir şey beyan etmeyen bir sunucu, her zaman gördüğü çağrı başına bir istek trafiğinin aynısını görür. -```python title="client.py" hl_lines="33 35 38" +Bunu izlemek için önceki bölümdeki `server.py` dosyasını uvicorn ile sunun (son satırı ASGI uygulamasını oluşturur). İşleyici, gerçekten her çalıştığında bir satır yazdırır: + +```console +uvicorn server:app --port 8000 +``` + +```python title="client.py" hl_lines="20 23 28" --8<-- "docs_src/caching/tutorial003.py" ``` +İkinci bir terminalden `python client.py` komutunu çalıştırın. İlk sonucun taşıdığı ipuçlarını yazdırır: işleyicinin `ttlMs` değeri, eşlemenin `cacheScope`'unun yanında: + +```text +1000 public +``` + +Hikâyenin geri kalanını sunucunun terminali anlatır: uvicorn'un istek log'ları arasında `tools/list served` üç kez görünür. + Dört çağrı, üç getirme. İkinci çağrı taze bir girdi buldu ve sunucuya hiç ulaşmadı; (enjekte edilen) saati TTL'nin ötesine ilerletmek üçüncünün yeniden getirmesine yol açtı; dördüncü `cache_mode="refresh"` dedi. Bu anahtar sözcük argümanı önbellekleme yapan beş fiilde bulunur (`list_tools`, `list_prompts`, `list_resources`, `list_resource_templates`, `read_resource`): * `"use"` (varsayılan) varsa taze bir girdiyi sunar, yoksa getirilen sonucu saklar. @@ -56,7 +70,7 @@ Dört çağrı, üç getirme. İkinci çağrı taze bir girdi buldu ve sunucuya `"use"`'un üzerinde bir kural vardır: **`meta` taşıyan çağrılar her zaman sunucuya ulaşır.** `meta` ayarlanmış bir istek (bir ilerleme token'ı, izleme alanları) ağ üzerinde gerçek bir istek bekler; bu yüzden `cache_mode="use"` altında `"refresh"` gibi ele alınır: önbellek okuması atlanır ve getirilen sonuç yine de önbellekteki girdinin yerine geçer. `"bypass"` ve açık bir `"refresh"` her zamanki gibi davranır. -Önbelleklemeyi tamamen kapatmak için `Client(server, cache=None)` ile oluşturun: her çağrı yeniden bir tur olur ve `cache_mode` hâlâ kabul edilse de hiçbir şey yapmaz. +Önbelleklemeyi tamamen kapatmak için `Client`'ı oluştururken `cache=None` geçirin: her çağrı yeniden bir tur olur ve `cache_mode` hâlâ kabul edilse de hiçbir şey yapmaz. Kapsama da otomatik olarak uyulur: `"private"` girdiler önbelleğin *bölümüne* (partition, aşağıda) göre anahtarlanır, `"public"` olanlar ise daha geniş paylaşıma katılmayı seçebilir. Ayrıca **bildirimler TTL'yi yener**, ama yalnızca tam olarak adlandırdıkları girdiler için: bir `list_changed` bildirimi eşleşen önbellekteki listeyi çıkarır, `resources/updated` ise tam olarak kendi URI'si altında saklanan önbellekteki okumayı çıkarır; ne kadar taze olurlarsa olsunlar. 2026-07-28 bağlantısında bu bildirimler `client.listen(...)` ile açtığınız bir `subscriptions/listen` akışı üzerinden gelir ve çıkarma, izleyiciniz olayı görmeden önce tamamlanır; bunun sayfası **[Abonelikler](subscriptions.md)**. @@ -82,7 +96,7 @@ client = Client("https://api.example.com/mcp", cache=CacheConfig(default_ttl_ms= Bölüm ayrıca `Client`'ın ömrü boyunca sabittir. Bağlantının yetkilendirme bağlamı oturum ortasında değişirse (örneğin farklı bir principal olarak yeniden kimlik doğrulama), önbellek bunu takip etmez; yeni principal için yeni bir `Client` oluşturun. -Önbellek anahtarları ayrıca **sunucunun kimliğini** de taşır: bağlandığınız URL dizesi, varsa `user:pass@` kullanıcı bilgisi çıkarılmış ve bunun dışında bayt bayt aynı hâliyle. Büyük/küçük harf katlama yok, sorgu yeniden sıralama yok, sondaki eğik çizgi temizliği yok. Az normalleştirmek yalnızca paylaşımdan ödün verir, aşırı normalleştirmek ise iki kiracıyı birleştirebilir (`?tenant=a` ve `?tenant=b`); bu yüzden yüzeysel olarak farklı URL'ler girdi paylaşmaz, o kadar. URL olmadığında (süreç içi bir sunucu ya da bir `Transport` örneği) istemci bunun yerine örnek başına rastgele bir kimlik alır; sunucuya ad vermek için `CacheConfig.target_id`'yi ayarlayın (özel bir depoyla bu zorunludur ve oluşturma bunu söyler). Kimlik, anahtar malzemesine girmeden önce sha256 ile özetlenir; dolayısıyla sorgu dizesinde sır taşıyan bir URL depo anahtarlarında asla görünmez. Özet öncesi hâlini siz de loglamayın. +Önbellek anahtarları ayrıca **sunucunun kimliğini** de taşır: bağlandığınız URL dizesi, varsa `user:pass@` kullanıcı bilgisi çıkarılmış ve bunun dışında bayt bayt aynı hâliyle. Büyük/küçük harf katlama yok, sorgu yeniden sıralama yok, sondaki eğik çizgi temizliği yok. Az normalleştirmek yalnızca paylaşımdan ödün verir, aşırı normalleştirmek ise iki kiracıyı birleştirebilir (`?tenant=a` ve `?tenant=b`); bu yüzden yüzeysel olarak farklı URL'ler girdi paylaşmaz, o kadar. URL olmadığında (süreç içi bir sunucu ya da bir `Transport` örneği) istemci bunun yerine örnek başına rastgele bir kimlik alır; sunucuya ad vermek için `CacheConfig.target_id`'yi ayarlayın (özel bir depoyla bu zorunludur ve oluşturma bunu söyler). Kimlik, anahtar malzemesine girmeden önce sha256 ile özetlenir; dolayısıyla sorgu dizesinde sır taşıyan bir URL depo anahtarlarında asla görünmez. Özet öncesi hâlini siz de log'a yazmayın. !!! warning "`share_public` sunucuya tüm filo genelinde güvenir" Varsayılan olarak `"public"` girdiler bile kendi bölümlerinde kalır. `share_public=True`, sunucunun `cacheScope: "public"` olarak işaretlediği girdileri depoyu kullanan **her** bölüme sunar; sunucunun sınıflandırmasına hepsi adına güvenir. Kiracıya özgü veriye (hata ya da kötü niyet sonucu) `"public"` damgası vuran bir sunucu, o zaman bir kiracının yanıtını diğerlerine sızdırır. Bayrak bilerek yalnızca oluşturucu düzeyindedir: çağrı başına `cache_mode` önbelleklemeyi daraltabilir, ama çağrı başına hiçbir şey paylaşımı genişletemez. diff --git a/i18n/tr/pages/client/callbacks.md b/i18n/tr/pages/client/callbacks.md index 8cb234ac4a..36b86ce145 100644 --- a/i18n/tr/pages/client/callbacks.md +++ b/i18n/tr/pages/client/callbacks.md @@ -1,6 +1,6 @@ --- translation: - sections: [adf3c545b5be46b6, 916cd3ab1c03f461, e9be7a8d0eb0a456, 565890a636288ecf, 6af7e49db9129ec3, 06b0238c174186af, 90c6043be435fcb0] + sections: [adf3c545b5be46b6, 916cd3ab1c03f461, 32ef568335dd95a7, 565890a636288ecf, 6af7e49db9129ec3, 06b0238c174186af, 0abc5ea5cb7ff6b3] tool: 1 --- # İstemci callback'leri {#client-callbacks} @@ -63,7 +63,7 @@ Sizden tek bir `tools/call`, sunucudan geriye tek bir `elicitation/create`, onu `Client(...)` çağrısındaki `mode="legacy"` gerçekten iş yapıyor. Varsayılan olarak `Client(...)` modern protokol yolunu müzakere eder ve o yolda sunucudan istemciye gelen istekler için bir geri kanal (back-channel) yoktur: `ctx.elicit`, callback'iniz daha çalışmadan başarısız olur. Buna aktarım karar vermez; müzakere edilen - protokol karar verir, bellek içinde de bir URL üzerinden de aynı şekilde. İstemcinizin böyle bir isteği + protokol karar verir. İstemcinizin böyle bir isteği yanıtlaması gerektiğinde `mode="legacy"`'yi sabitleyin; bu sayfanın arkasındaki her test bunu yapar. Ayrıntıların tamamı **[Protokol sürümleri](../protocol-versions.md)** sayfasında. @@ -152,4 +152,4 @@ Bunları `Client(...)`'a tıpkı `elicitation_callback` gibi geçirin. * `sampling_callback` ve `list_roots_callback` aynı şekilde çalışır ama kullanım dışı özelliklere hizmet eder; modern sunucular bunun yerine çok turlu istekler (multi-round-trip) kullanır. * `logging_callback` ve `message_handler` bildirimleri alır. Hiçbir şey bildirmezler. -`Client(...)`'ın ilk argümanı bir aktarım nesnesidir. **[İstemci aktarımları](transports.md)** her türünü ele alır. +`Client(...)`'ın ilk argümanı aktarımı seçer. **[İstemci aktarımları](transports.md)** her türünü ele alır. diff --git a/i18n/tr/pages/client/index.md b/i18n/tr/pages/client/index.md index 2cc12da86a..47b945d78f 100644 --- a/i18n/tr/pages/client/index.md +++ b/i18n/tr/pages/client/index.md @@ -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 --- # İstemci {#the-client} @@ -11,13 +11,23 @@ Tek bir yaşam döngüsü olan tek bir nesnedir: oluşturun, `async with` bloğu ## İlk istemciniz {#your-first-client} -```python title="client.py" hl_lines="14-18" +Bir istemcinin konuşacağı bir sunucuya ihtiyacı vardır. Bu sayfadaki her örneğin bağlandığı sunucu aşağıdaki Bookshop. Onu `server.py` olarak kaydedin ve HTTP üzerinden çalışır durumda bırakın: + +```python title="server.py" --8<-- "docs_src/client/tutorial001.py" ``` -Üstteki sunucu yalnızca bağlanacak bir şeyiniz olsun diye orada. İstemci, vurgulanan beş satırdan ibaret. +```console +uv run mcp run server.py --transport streamable-http +``` + +Bu, sunucuyu `http://localhost:8000/mcp` adresinde sunar. İstemci ayrı bir programdır. Onu `client.py` olarak kaydedin ve ikinci bir terminalde `python client.py` komutunu çalıştırın: -* `Client(mcp)` çağrısına **sunucu nesnesinin kendisi** verilir. Bu, bellek içi aktarımdır: alt süreç yok, port yok, HTTP yok. Bu sayfadaki her örnek ve yazdığınız her test böyle bağlanır. +```python title="client.py" hl_lines="7-11" +--8<-- "docs_src/client/tutorial001_client.py" +``` + +* `Client("http://localhost:8000/mcp")` çağrısına bir **URL** verilir; bu yüzden az önce başlattığınız sunucuya Streamable HTTP üzerinden bağlanır. * `async with` **yaşam döngüsüdür**. Bloğa girdiğinizde bağlantı kurulur ve anlaşma yapılır; çıktığınızda bağlantı kesilir. `connect()` / `close()` çifti yoktur ve blok bittikten sonra bir `Client` yeniden kullanılamaz. * Bloğun içinde bağlantı bilgileri düz özellikler olarak zaten hazırdır. @@ -25,10 +35,10 @@ Tek bir yaşam döngüsü olan tek bir nesnedir: oluşturun, `async with` bloğu `Client` tek bir konumsal argüman alır ve aktarımı onun türünden çözümler: -* Bir `MCPServer` (veya düşük seviyeli `Server`) örneği: **süreç içinde** bağlanır. -* Bir URL dizesi (`Client("http://localhost:8000/mcp")`): Streamable HTTP, yani üretim yolu. -* Bir `StdioServerParameters`: **alt süreç** olarak başlatılacak komut; onunla stdin ve stdout'u üzerinden konuşulur. +* Bir URL dizesi (`Client("http://localhost:8000/mcp")`): Streamable HTTP, dağıtımda kullandığınız aktarım. +* Bir `StdioServerParameters`: yerel bir **alt süreç** olarak başlatılacak komut; onunla stdin ve stdout'u üzerinden konuşulur. * Bir **aktarım**: `async with ... as (read, write)` ile kullanabileceğiniz herhangi bir şey; örneğin kendi HTTP istemcinizi saran `streamable_http_client(url, http_client=...)`. +* Bir `MCPServer` (veya düşük seviyeli `Server`) örneği: **süreç içinde** bağlanır; alt süreç yok, port yok. Bu seçenek testler içindir ve **[Test etme](../get-started/testing.md)** sayfası onun üzerine kurulur. Bu sayfadaki geri kalan her şey dördünde de aynıdır. Başlıklar, alt süreçler, zaman aşımları ve `Transport` protokolünün kendi sayfası var: **[İstemci aktarımları](transports.md)**. @@ -49,11 +59,11 @@ Hiç protokol sürümü seçmediniz. Varsayılan olarak `Client` sunucuyu yoklar ## Araçları listeleme {#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()` bir `ListToolsResult` döndürür; araçlar `.tools` içindedir. Her biri, bir host'un modele vereceği eksiksiz tanımdır: +`list_tools()` bir `ListToolsResult` döndürür; araçlar `.tools` içindedir. Her biri, bir host'un modele vereceği eksiksiz tanımdır. İşte ilki: ```python tool.name # 'search_books' @@ -77,6 +87,8 @@ tool.description # 'Search the catalog by title or author.' Bu şema, bir arayüzün argüman formu oluşturması için gereken her şeydir; bir modelin geçerli argümanlar üretmesi için gereken her şey de odur. +İkinci araç olan `lookup_book`, `title=` olmadan kaydedildi; bu yüzden `tool.title` değeri `None`. + !!! tip `title` isteğe bağlıdır; bu yüzden araçları bir insana gösteren arayüzün seçim yapması gerekir: varsa `title`, yoksa `name`. `from mcp.shared.metadata_utils import get_display_name` tam olarak bunu yapar; @@ -86,7 +98,7 @@ Bu şema, bir arayüzün argüman formu oluşturması için gereken her şeydir; `call_tool(name, arguments)` aracı çalıştırır ve size bir `CallToolResult` geri verir. -```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 @@ Tek dönüş değeri, okunacak üç şey. Her birinin tüketicisi farklı. Kaynak fiilleri çift gelir: listelemenin iki yolu, okumanın tek yolu. -```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 @@ Bir istemciye bir kaynağın ne zaman değiştiği de bildirilebilir. 2025 nesli ## Prompt'lar {#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 @@ Host bu mesajları doğrudan modele verir. Özelliğin tamamı bu. Tamamlama işleyicisi olan bir sunucu, kullanıcı yazdıkça prompt ve kaynak şablonu argümanlarını otomatik tamamlayabilir. -```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 @@ Yanıt `result.completion.values` içindedir. `"p"` yazın, sunucu `['poetry']` Her `list_*` yöntemi bir `cursor=` anahtar sözcüğü alır ve her sonuç bir `next_cursor` taşır. `next_cursor` `None` olduğunda her şeyi almışsınız demektir. -```python title="client.py" hl_lines="22-30" +```python title="client.py" hl_lines="7-15" --8<-- "docs_src/client/tutorial007.py" ``` -Bu döngü her sunucuya karşı doğrudur. `MCPServer` her şeyi tek sayfada döndürür; bu yüzden `next_cursor` `None` olur ve döngü bir kez çalışır. Çoğu kodun bunu hiç yazmamasının nedeni budur. Gerçekten sayfalayan sunucular ve imleçlerin uyduğu kurallar **[Sayfalama](../advanced/pagination.md)** sayfasında. +`list_all_tools` her sunucuya karşı doğrudur. `MCPServer` her şeyi tek sayfada döndürür; bu yüzden `next_cursor` `None` olur ve döngü bir kez çalışır. Çoğu kodun bunu hiç yazmamasının nedeni budur. Gerçekten sayfalayan sunucular ve imleçlerin uyduğu kurallar **[Sayfalama](../advanced/pagination.md)** sayfasında. ## Testlerde {#in-tests} -Süreç ve port olmadan `Client(mcp)`, sunucunuz için zaten bir test düzeneğidir. +Bu sayfadaki her `client.py`, `server.py` dosyasına HTTP üzerinden ulaştı. Bir testte ağı atlar ve `Client`'a sunucu nesnesinin kendisini verirsiniz: `from server import mcp`, ardından `Client(mcp)`. Süreç yok, port yok; yukarıdaki her yöntem aynı şekilde çalışır. -Bunun için yapılmış tek bir kurucu bayrağı var: `Client(mcp, raise_exceptions=True)`. Yalnızca bellek içi bağlantılarda etkisi olur; onu açıklayan ve bütün kalıbı onun etrafında kuran sayfa ise **[Test etme](../get-started/testing.md)**. +Bunun için yapılmış tek bir kurucu bayrağı var: `Client(mcp, raise_exceptions=True)`. Yalnızca süreç içi bağlantılarda etkisi olur; onu açıklayan ve bütün kalıbı onun etrafında kuran sayfa ise **[Test etme](../get-started/testing.md)**. ## Özet {#recap} -* `Client(x)` bir sunucu nesnesine bellek içinden, bir URL dizesine Streamable HTTP üzerinden, geri kalan her şeye de bir aktarım aracılığıyla bağlanır. +* `Client(x)` bir URL dizesine Streamable HTTP üzerinden bağlanır, bir `StdioServerParameters` için alt süreç başlatır, bir aktarıma doğrudan girer ve testlerde sunucu nesnesinin kendisini alır. * `async with` yaşam döngüsünün tamamıdır. İçinde `server_capabilities` ve `protocol_version` zaten doludur; sunucu sağladığında `server_info` ve `instructions` da öyle. * `list_tools()` size her aracın `name`, `title`, `description` ve `input_schema` değerlerini verir. * `call_tool()` model için `content`, kodunuz için `structured_content` ve `is_error` döndürür. İstisna fırlatan bir araç istisna değil, sonuçtur. diff --git a/i18n/tr/pages/client/oauth-clients.md b/i18n/tr/pages/client/oauth-clients.md index 640f85a339..8619f430f1 100644 --- a/i18n/tr/pages/client/oauth-clients.md +++ b/i18n/tr/pages/client/oauth-clients.md @@ -1,6 +1,6 @@ --- translation: - sections: [c6899d3892bd9fa0, 79372cff3cc48a88, 63878d29e87c3e73, 13175843d3588af4, e7e2b9fd516f77de, 758f06399b513c1f, a05d7278487d610b] + sections: [c6899d3892bd9fa0, 79372cff3cc48a88, c2dae1ebe2ebd543, 13175843d3588af4, df06056fb16b3846, 758f06399b513c1f, a05d7278487d610b] tool: 1 --- # OAuth istemcileri {#oauth-clients} @@ -82,20 +82,22 @@ Gerçek bir istemci `input()` çağırmak yerine yönlendirme URI'si üzerinde k `Client` ilk kez bir istek gönderdiğinde sunucu `401` yanıtını verir. Sağlayıcı devralır: -1. **Keşif.** `WWW-Authenticate` başlığını okur, sunucunun Protected Resource Metadata belgesini `/.well-known/oauth-protected-resource` adresinden alır, bu kaynağı hangi yetkilendirme sunucusunun koruduğunu öğrenir ve *o* sunucunun metadatasını alır. +1. **Keşif.** `WWW-Authenticate` başlığını okur, sunucunun Protected Resource Metadata belgesini `/.well-known/oauth-protected-resource` adresinden alır, bu kaynağı hangi yetkilendirme sunucusunun koruduğunu öğrenir ve *o* sunucunun metadatasını alır. (Kaynak metadatası yayımlamayan daha eski bir sunucudan bunun yerine kendi kökeninde yetkilendirme sunucusu metadatası istenir.) Her iki durumda da metadata, `issuer` alanında hangi sunucu için alındıysa o sunucuyu belirtmelidir; başka her şey reddedilir. 2. **Kayıt.** Depoda bir şey yok mu? `OAuthClientMetadata`'nızla sizi dinamik olarak kaydeder ve sonucu saklar. 3. **Yetkilendirme.** PKCE çiftini ve bir `state` üretir, yetkilendirme URL'sini oluşturur, `redirect_handler`'ınızı await eder, ardından kod için `callback_handler`'ınızı await eder. 4. **Değişim.** Kodu bir `OAuthToken` ile takas eder, onu saklar ve özgün isteğinizi `Authorization: Bearer ...` ile yeniden gönderir. Bundan sonra sessizdir. Token'lar depodan gelir, süresi dolmuş bir erişim token'ı yenileme token'ıyla yenilenir ve ancak bunların hiçbiri işe yaramadığında akışı yeniden çalıştırır. +Bu isteklerin hepsi için tek bir aktarım kuralı geçerlidir: içinde çalıştıkları MCP isteği gibi, bir yönlendirmeyi yalnızca aynı kökende kalıyor ve metodu koruyorsa izlerler (örneğin sondaki eğik çizgi için bir 307/308); başka her yönlendirmeyi o URL yanıt vermiyormuş gibi ele alırlar. + Bunların hiçbirini siz yazmadınız. Geriye iki anahtar sözcük argümanı kalır (`client_metadata_url` ve `validate_resource_url`) ve bu dosyanın ikisine de ihtiyacı yoktur. Bilmeye değer olanı `client_metadata_url`'dir; aşağıda kendi bölümü var. ### Deneyin {#try-it} -Bu belgelerdeki örneklerin çoğunu bellek içi bir `Client(server)` ile sınayabilirsiniz. Bunu değil: akışın bütün amacı bir HTTP `401`'idir ve bellek içi bir istemci ile sunucusu arasında HTTP yoktur. +Testlerinizin kullandığı bellek içi `Client(server)` burada işe yaramaz: akışın bütün amacı bir HTTP `401`'idir ve bellek içi bir istemci ile sunucusu arasında HTTP yoktur. -Depo canlı sürümü içerir. `examples/servers/simple-auth/` bağımsız bir yetkilendirme sunucusu ile korumalı bir MCP sunucusu çalıştırır; `examples/clients/simple-auth-client/` ise bu sayfadaki istemcinin küçük bir CLI'a dönüşmüş hâlidir. README'sinde iki komut var: sunucuları başlatın, istemciyi onlara karşı çalıştırın ve dört adımın geçişini izleyin. +Depo canlı sürümü içerir. `examples/servers/simple-auth/` bağımsız bir yetkilendirme sunucusu ile korumalı bir MCP sunucusu çalıştırır; `examples/clients/simple-auth-client/` ise bu sayfadaki istemcinin küçük bir CLI aracına dönüşmüş hâlidir. README'sinde iki komut var: sunucuları başlatın, istemciyi onlara karşı çalıştırın ve dört adımın geçişini izleyin. ## Client ID Metadata Documents {#client-id-metadata-documents} @@ -111,13 +113,14 @@ Bir gece görevi, bir CI adımı, başka bir servis. Tarayıcı yok, "allow" dü `ClientCredentialsOAuthProvider` aynı `httpx2.Auth`'tur, insan hariç: -```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" ``` Neler değişti: * `OAuthClientMetadata` yok, işleyiciler yok. `client_id` ve `client_secret` geçirirsiniz; sağlayıcı bunların etrafında asgari bir `client_credentials` kaydı oluşturur ve dinamik kaydı tamamen atlar. +* `issuer`, bu kimlik bilgilerini veren yetkilendirme sunucusunu belirtir; onun `/.well-known/oauth-authorization-server` belgesinin döndürdüğü `issuer` değerini kullanın. Keşif yine yukarıdaki gibi çalışır, ancak token istekleri yalnızca *o* yayıncıya ait metadatadan oluşturulur; MCP sunucusu başka bir yeri gösteriyorsa akış bunun yerine bir `OAuthFlowError` ile durur. Bu parametreyi atlamak kullanım dışı bırakıldı ve 3.0 sürümünde zorunlu hâle gelir (bkz. **[Kullanım dışı özellikler](../deprecated.md#deprecated-sdk-helpers)**); o zamana kadar sağlayıcı uyarı verir ve keşif hangi yetkilendirme sunucusunu bulursa onu kullanır. * `scope`, boşlukla ayrılmış bir dizedir; OAuth'un iletilen verideki biçimi budur. * Bundan sonraki her şey aynıdır: aynı `TokenStorage`, aynı `httpx2.AsyncClient(auth=...)`, aynı `streamable_http_client`. @@ -130,7 +133,7 @@ Varsayılan olarak sır, token isteğinde HTTP Basic kimlik doğrulaması olarak `mcp.client.auth.extensions.client_credentials` içinde bir sağlayıcı daha var: paylaşılan bir sır yerine JWT ile kimlik doğrulayan istemciler için **`PrivateKeyJWTOAuthProvider`** (`private_key_jwt`; anahtar çifti ve iş yükü kimliği türü). Aynı kalıbı izler: - bir tane oluşturun, `auth=`'a koyun. Aynı modül, onun assertion'ını oluşturan iki yardımcıyı da + bir tane oluşturun (aynı isteğe bağlı `issuer` parametresini alır), `auth=`'a koyun. Aynı modül, onun assertion'ını oluşturan iki yardımcıyı da sunar: `SignedJWTParameters` ve `static_assertion_provider`. İnsansız bir durum daha var: istemci, hangi MCP sunucularına erişebileceğine kullanıcının değil kimlik sağlayıcısının karar verdiği bir kuruluşa aittir. Bu, kendi güven modeli ve kendi sayfası olan farklı bir yetkilendirme türüdür: **[Kimlik beyanı](identity-assertion.md)**. diff --git a/i18n/tr/pages/client/session-groups.md b/i18n/tr/pages/client/session-groups.md index 25e965c497..0499620d6c 100644 --- a/i18n/tr/pages/client/session-groups.md +++ b/i18n/tr/pages/client/session-groups.md @@ -1,6 +1,6 @@ --- translation: - sections: [09c857a25a9dc37a, 43bc6a76a243a50e, 0a716022a88768df, 4b7f78042bfcfff7, c112662e61b03315, 58974ba1f489a8b4, d18adbdbb835ea73] + sections: [09c857a25a9dc37a, 43bc6a76a243a50e, 0a716022a88768df, 4b7f78042bfcfff7, c112662e61b03315, 58974ba1f489a8b4, ed4d17e894864056] tool: 1 --- # Oturum grupları {#session-groups} @@ -78,7 +78,7 @@ Elinizde zaten bağlı bir `ClientSession` varsa (`Client.session` bunlardan bir ## Özet {#recap} * `ClientSessionGroup` birçok sunucu bağlantısını tutar ve bunların araçlarını, kaynaklarını ve prompt'larını birer `dict`'te birleştirir. -* Her sunucu için `connect_to_server(params)`. Aktarım parametreleri alır; bir `Client`'ın aldığı sunucu nesnesini ya da URL'yi asla almaz. +* Her sunucu için `connect_to_server(params)`. Aktarım parametreleri alır; bir `Client`'ın aldığı URL'yi ya da `Transport`'u asla almaz. * `group.call_tool(name, arguments)` çağrıyı sizin yerinize sahibi olan sunucuya yönlendirir. * Adlar grubun tamamında benzersiz olmalıdır; `search` aracı olan iki sunucu kendi hâllerine bırakılırsa bir arada bulunamaz. * `component_name_hook=` kaydedilen her adı yeniden yazar. Sözlük anahtarı değişir, ağ üzerindeki ad değişmez. diff --git a/i18n/tr/pages/client/transports.md b/i18n/tr/pages/client/transports.md index 552ede3095..bbf104ccf3 100644 --- a/i18n/tr/pages/client/transports.md +++ b/i18n/tr/pages/client/transports.md @@ -1,6 +1,6 @@ --- translation: - sections: [9cac816674181eb0, 0700f337babcd4dd, 2bde0dd58cdf00f5, 40b4916d82eaf1d4, 3d0832f39b0d7059, dfa4446556badef0, 5bd93be2ab2ecb9c] + sections: [9cac816674181eb0, 7c157764133fea1f, 40b4916d82eaf1d4, 10d151f2cc75317f, 3d0832f39b0d7059, 92742ba36533633d, 0aeca6145e7bd302] tool: 1 --- # İstemci aktarımları {#client-transports} @@ -11,30 +11,15 @@ Aktarımı hiçbir zaman ayrıca yapılandırmazsınız. `Client` tek bir konums Her birinin *sunucu* tarafı (`mcp.run()`'ın ne yaptığı ve neyi dağıttığınız) **[Sunucunuzu çalıştırma](../run/index.md)** sayfasında. -## Bellek içinde {#in-memory} - -Sunucu nesnesinin kendisini geçirin: - -```python title="client.py" hl_lines="14" ---8<-- "docs_src/client_transports/tutorial001.py" -``` - -Alt süreç yok, port yok, ağ üzerinde tek bir bayt yok. İstemci ve sunucu aynı süreçteki iki nesnedir; yine de çağrı gerçek protokol katmanından geçer: `search_books`, HTTP üzerinden nasıl olacaksa tam olarak öyle listelenir, doğrulanır ve çağrılır. - -Bu, onu aynı anda iki şey yapar: - -* **Bir test düzeneği.** Bu belgelerdeki her örnek bu şekilde çalıştırılır ve **[Test etme](../get-started/testing.md)** sayfası tüm deseni bunun üzerine kurar. -* **Bir gömme API'si.** Sunucuyu oluşturan bir uygulamanın, araçlarını çağırmak için ağ üzerinden bir sıçrama yapmasına gerek yoktur. - ## Streamable HTTP {#streamable-http} -Bir URL dizesi geçirin; arkasına dağıtım yaptığınız aktarım olan **Streamable HTTP**'yi elde edersiniz: +Bir URL dizesi geçirin; arkasına dağıtım yaptığınız ve ilk başvurmanız gereken aktarım olan **Streamable HTTP**'yi elde edersiniz: ```python title="client.py" hl_lines="5" --8<-- "docs_src/client_transports/tutorial002.py" ``` -Üretim istemcisinin tamamı bu. `Client`, URL'yi sizin için `streamable_http_client(...)` ile sarar; bunu da MCP'nin gerektirdiği şekilde yapılandırılmış bir `httpx2.AsyncClient` üzerine kurar: `follow_redirects=True`, connect/write/pool için 30 saniyelik zaman aşımı ve sunucu bir yanıt akışını açık tutabileceği için 300 saniyelik okuma zaman aşımı. +Üretim istemcisinin tamamı bu. `Client`, URL'yi sizin için `streamable_http_client(...)` ile sarar; bunu da MCP'nin gerektirdiği şekilde yapılandırılmış bir `httpx2.AsyncClient` üzerine kurar: connect/write/pool için 30 saniyelik zaman aşımı ve sunucu bir yanıt akışını açık tutabileceği için 300 saniyelik okuma zaman aşımı. !!! check Oluşturduğunuz bir `Client` bağlı **değildir**. Oluşturma yalnızca aktarımı seçer; @@ -50,7 +35,7 @@ Bir URL dizesi geçirin; arkasına dağıtım yaptığınız aktarım olan **Str Bir `Authorization` başlığına, bir çereze, bir vekil sunucuya, mTLS'e ya da farklı bir zaman aşımına ihtiyaç duyduğunuz anda `httpx2.AsyncClient`'ı kendiniz oluşturun ve `streamable_http_client`'a verin: -```python title="client.py" hl_lines="8-14" +```python title="client.py" hl_lines="8-13" --8<-- "docs_src/client_transports/tutorial003.py" ``` @@ -79,9 +64,30 @@ ortam değişkenlerini ayarlayın ya da `httpx2.AsyncClient`'ınıza açıkça b !!! info `httpx2`, tanıdık `httpx` API'sini korur; yani `httpx`'i biliyorsanız kimlik doğrulama, vekil sunucular, olay kancaları, yeniden denemeler ve bağlantı sınırlarının burada nasıl yapılacağını zaten biliyorsunuz. SDK üzerine hiçbir şey eklemez, - hiçbir şeyi de eksiltmez. OAuth'un takıldığı yer de burası: + [yönlendirme işleme](#redirects) dışında hiçbir şeyi de eksiltmez. OAuth'un takıldığı yer de burası: `httpx2.AsyncClient(auth=OAuthClientProvider(...))`. Bu akışın tamamı **[OAuth istemcileri](oauth-clients.md)** sayfasında. +### Yönlendirmeler {#redirects} + +Aktarım, verdiğiniz URL'ye ve yalnızca o kökene (origin) bağlanır. + +* Aynı şema, ana bilgisayar ve portta kalan bir `307`/`308` yönlendirmesi izlenir; aynı ana bilgisayar üzerindeki `http://` → `https://` yönlendirmesi de öyle. Bu, alışıldık `/mcp` → `/mcp/` sondaki eğik çizgi yönlendirmesini kapsar. +* Başka herhangi bir yere yapılan yönlendirme **izlenmez**. Çağrı şu hatayla başarısız olur: + + ```text + MCPError: Redirect to https://other.example.com/mcp not followed; use that URL as the endpoint if it is the intended server + ``` + + O URL kastettiğiniz sunucuysa yapılandırmanıza yazın. Değilse sunucu ya da önündeki bir vekil sunucu yanlış yapılandırılmıştır. + +Bu, geçirdiğiniz her `httpx2.AsyncClient` için geçerlidir: MCP isteklerinde `follow_redirects` ayarına hiçbir yönde bakılmaz. SDK'nın OAuth sağlayıcıları da aynı kuralı kendi isteklerine uygular. + +!!! tip + `Redirect to http://… not followed: it would downgrade this HTTPS endpoint to plain HTTP` iletisi, + sunucunun haberdar olmadığı, TLS sonlandıran bir vekil sunucunun arkasında durduğu ve `http://` yönlendirmeleri + verdiği anlamına gelir. Bu, sunucu tarafında düzeltilir (**[Dağıtım ve ölçekleme](../run/deploy.md#behind-a-tls-terminating-proxy)**) + ya da iletinin önerdiği tam `https://…/` URL'si kullanılarak. + ## stdio {#stdio} Bir **stdio** sunucusu bir alt süreçtir. İstemci onu başlatır, stdin'ine JSON-RPC yazar ve stdout'undan JSON-RPC okur. Bir masaüstü host'un makinenizde bir sunucuyu çalıştırma biçimi budur: bir host, bu kod artı bir kullanıcı arayüzü*dür* ve **[Gerçek bir host'a bağlanma](../get-started/real-host.md)**, aynı ilişkinin host'un tarafından, bir yapılandırma dosyası olarak görülen halidir. @@ -104,6 +110,18 @@ Alt sürecin stderr'i sizinkine gider. Başka bir yere göndermek için aktarım Bir API anahtarına ihtiyaç duyan bir sunucu onu orada bulamaz. `env=` ile açıkça geçirin; bu değişkenler izin listesinin üstüne birleştirilir. Yukarıda `BOOKSHOP_API_KEY`'in yaptığı budur. +## Bellek içinde {#in-memory} + +Bir testte dağıtılacak da başlatılacak da bir şey yoktur. Sunucu nesnesinin kendisini geçirin: + +```python hl_lines="14" +--8<-- "docs_src/client_transports/tutorial001.py" +``` + +Alt süreç yok, port yok, ağ üzerinde tek bir bayt yok. İstemci ve sunucu aynı süreçteki iki nesnedir; yine de çağrı gerçek protokol katmanından geçer: `search_books`, HTTP üzerinden nasıl olacaksa tam olarak öyle listelenir, doğrulanır ve çağrılır. **[Test etme](../get-started/testing.md)** sayfası tüm deseni bunun üzerine kurar. + +Aynı biçim bir gömme API'si olarak da iş görür: sunucuyu kendisi oluşturan bir uygulama, araçlarını ağ üzerinden bir sıçrama olmadan çağırabilir. + ## SSE {#sse} `mcp.client.sse` içindeki `sse_client(url)`, Streamable HTTP'nin yerini aldığı HTTP aktarımıdır. Hâlâ onu konuşan bir sunucuyla konuşmak için aynı şekilde sarın, `Client(sse_client("http://localhost:8000/sse"))`, ve üzerine yeni hiçbir şey kurmayın. @@ -112,15 +130,16 @@ Alt sürecin stderr'i sizinkine gider. Başka bir yere göndermek için aktarım `Client` için yukarıdakilerin hepsi aynı şeydir. -Bir **aktarım**, `(read, write)` mesaj akışı çifti veren herhangi bir asenkron bağlam yöneticisidir: resmi olarak `mcp.client` içindeki `Transport` protokolü. `Client`, argümanını türüne göre çözümler: bir sunucu nesnesi süreç içinde bağlanır, bir `str` `streamable_http_client(url)` olur, bir `StdioServerParameters` `stdio_client(params)` olur ve geri kalan her şeye doğrudan bir aktarım olarak girilir. `stdio_client(...)`, `streamable_http_client(...)` ve `sse_client(...)`'in hepsinin aynı yuvaya oturmasının ve kendinizinkini yazabilmenizin nedeni bu son kuraldır. +Bir **aktarım**, `(read, write)` mesaj akışı çifti veren herhangi bir asenkron bağlam yöneticisidir: resmi olarak `mcp.client` içindeki `Transport` protokolü. `Client`, argümanını türüne göre çözümler: bir `str` `streamable_http_client(url)` olur, bir `StdioServerParameters` `stdio_client(params)` olur, bir sunucu nesnesi süreç içinde bağlanır ve geri kalan her şeye doğrudan bir aktarım olarak girilir. `stdio_client(...)`, `streamable_http_client(...)` ve `sse_client(...)`'in hepsinin aynı yuvaya oturmasının ve kendinizinkini yazabilmenizin nedeni bu son kuraldır. ## Özet {#recap} -* `Client(mcp)` (sunucu nesnesi) bellek içinde bağlanır. Testler ve gömme için kullanın. * `Client("http://.../mcp")` (bir URL), üretim aktarımı olan Streamable HTTP üzerinden bağlanır. * Başlıklar, kimlik doğrulama, vekil sunucular ve zaman aşımları, `streamable_http_client(url, http_client=...)`'a geçirdiğiniz bir `httpx2.AsyncClient` üzerinde yer alır. `headers=` anahtar sözcüğü yoktur. +* Yönlendirmeler yalnızca URL'nin kendi kökeni içinde (sondaki eğik çizgi için `307`/`308`) ve aynı ana bilgisayarda `http`→`https` için izlenir. Geri kalan her şey `Redirect to … not followed` hatasıyla başarısız olur; nihai URL'yi yapılandırın. * stdio, `Client(StdioServerParameters(...))` demektir. Onu `stdio_client(...)` ile yalnızca alt sürecin stderr'ini başka yere yönlendirmek için kendiniz sarın. * Alt süreç sizinkini değil, izin listesine göre oluşturulmuş bir ortam alır; `env=` buna ekleme yapar. +* `Client(mcp)` (sunucu nesnesi) bellek içinde bağlanır. Testlerde ya da bir sunucuyu onu oluşturan uygulamaya gömmek için kullanın. * Bir aktarım, `async with x as (read, write)` yapabildiğiniz herhangi bir şeydir. `Client`, sunucu nesnesi, URL ya da `StdioServerParameters` olmayan her şeyi doğrudan bu protokole verir. * Bir `Client` oluşturmak aktarımı seçer. Onu `async with` açar. diff --git a/i18n/tr/pages/deprecated.md b/i18n/tr/pages/deprecated.md index af116483ee..945cdba622 100644 --- a/i18n/tr/pages/deprecated.md +++ b/i18n/tr/pages/deprecated.md @@ -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 --- # Kullanım dışı özellikler {#deprecated-features} -2026-07-28 spesifikasyonu beş şeyi emekliye ayırıyor. SDK hâlâ hepsini uygular ve artık her biri bir **kullanım dışı bırakma uyarısı** taşır. Bir SDK yardımcısı ise kendi gerekçesiyle kullanım dışı bırakıldı ve [sayfanın sonunda](#deprecated-sdk-helpers) listeleniyor. +2026-07-28 spesifikasyonu beş şeyi emekliye ayırıyor. SDK hâlâ hepsini uygular ve artık her biri bir **kullanım dışı bırakma uyarısı** taşır. SDK düzeyindeki birkaç kullanım dışı bırakma ise kendi gerekçesine dayanır ve [sayfanın sonunda](#deprecated-sdk-helpers) listeleniyor. Aşağıdaki tablo kullanım dışı bırakılan her özelliği, neden gittiğini ve yerine neyin üzerine inşa etmeniz gerektiğini gösterir. @@ -136,11 +136,13 @@ API'nin tamamı bu. Yöntem başına bir anahtar yok, zaten istemezsiniz de: tek ## Kullanım dışı SDK yardımcıları {#deprecated-sdk-helpers} -Bunlar spesifikasyon değişikliği değil, yalnızca daha iyi bir alternatifi olan SDK iç ayrıntılarıdır. Aynı `MCPDeprecationWarning` ile uyarırlar ve 3.0'da kaldırılacaklar. +Bunlar spesifikasyon değişikliği değil, yalnızca daha iyi bir alternatifi olan SDK kullanım biçimleridir. Aynı `MCPDeprecationWarning` ile uyarırlar ve 3.0 eski biçimi kaldırır. | Kullanım dışı | Bunun yerine ne yaparsınız | |---|---| | `FuncMetadata.call_fn_with_arg_validation()` | `FuncMetadata.validate_arguments()`, ardından `FuncMetadata.call_fn()`. Bunu yalnızca `FuncMetadata`'yı doğrudan kullanan kod (örneğin özel bir `Tool` alt sınıfı) çağırırdı. | +| `validate_token_resource=` olmadan `AuthSettings(resource_server_url=...)` | Onu ayarlayın: `True`, doğrulayıcınızın `resource_server_url` için düzenlendiğini bildirmediği bearer token'ları sunucunun reddetmesini sağlar; `False`, token'ın audience değerini doğrulayıcınızın kendisinin denetlediğini söyler (bkz. **[Yetkilendirme](run/authorization.md#a-token-verifier)**). Ayarlanmadığında `False` gibi davranır; 3.0, `resource_server_url` ayarlı olduğunda `True`'yu varsayılan yapar. | +| `issuer=` olmadan `ClientCredentialsOAuthProvider(...)` veya `PrivateKeyJWTOAuthProvider(...)` | Kimlik bilgilerini düzenleyen yetkilendirme sunucusunu belirten `issuer=` geçirin (bkz. **[OAuth istemcileri yazma](client/oauth-clients.md#machine-to-machine)**). Onsuz, kimlik bilgilerini hangi yetkilendirme sunucusunun alacağına MCP sunucusu karar verir; 3.0 bu anahtar kelimeyi zorunlu yapar. | ## Özet {#recap} @@ -149,7 +151,7 @@ Bunlar spesifikasyon değişikliği değil, yalnızca daha iyi bir alternatifi o * Kullanım dışı bırakma tavsiye niteliğindedir: iletilen veride değişiklik yok, her şey 2026 öncesi oturumlarda çalışmaya devam eder ve görünür bir `MCPDeprecationWarning` alırsınız (bir `UserWarning`, dolayısıyla varsayılan olarak açık). * Örnekleme ve kök dizinler ayrıca, 2026-07-28 oturumunda bulunmayan bir geri kanala ihtiyaç duyar. Modern bir bağlantıda önce uyarır, sonra istisna fırlatırlar. * `warnings.filterwarnings("ignore", category=MCPDeprecationWarning)` tüm kategoriyi susturur; pytest'te `"error::mcp.MCPDeprecationWarning"` bunu bir test hatasına dönüştürür. -* Bir SDK yardımcısı, `FuncMetadata.call_fn_with_arg_validation()`, 3.0'da kaldırılmak üzere ayrıca kullanım dışı bırakıldı. +* [SDK düzeyindeki kullanım dışı bırakmalar](#deprecated-sdk-helpers) da aynı kurala uyar: şimdi uyarırlar, 3.0 ise eski biçimi kaldırır. * Yeni kod bunların hiçbiri üzerine kurulmamalıdır. Bu belgelerdeki diğer tüm sayfalar güncel API'yi anlatır. diff --git a/i18n/tr/pages/get-started/first-steps.md b/i18n/tr/pages/get-started/first-steps.md index e7e9ec7495..f52e64fcef 100644 --- a/i18n/tr/pages/get-started/first-steps.md +++ b/i18n/tr/pages/get-started/first-steps.md @@ -1,6 +1,6 @@ --- translation: - sections: [0d6c05bcbf836bf3, 59a7b14eeefc68c1, 7114d8d6daba203f, e8bbb56a98ba7bc9, 5138010f6159901c, f78da7c7c363d4c6, 220a939cab348686] + sections: [0d6c05bcbf836bf3, 9a78b5f6b44b18ab, 7114d8d6daba203f, e8bbb56a98ba7bc9, bfd2fd1153e71dac, 1615a994ef071fdd, 65c599fae991f245] tool: 1 --- # İlk adımlar {#first-steps} @@ -17,7 +17,7 @@ Bundan sonraki her sayfada göreceğiniz üç sözcük: * **İstemci**, host'un içinde yaşar ve MCP konuşur. Host, bağlandığı her sunucu için bir istemci çalıştırır. * **Sunucu**, bu SDK ile sizin oluşturduğunuz şeydir. İstemcilere bir şeyler sunar. Modelle hiçbir zaman doğrudan konuşmaz. -Sunucuyu siz yazarsınız. Host'lar başkasının ürünüdür. SDK size bir de `Client` verir. Onu sunucularınızı test etmek için kullanırsınız; bu sayfanın ilerisinde karşınıza çıkar. +Sunucuyu siz yazarsınız. Host'lar başkasının ürünüdür. SDK size bir de `Client` verir: bir host'un bir sunucuya URL ile ulaşmak ya da onu alt süreç olarak başlatmak için kullanacağı sınıfın ta kendisi. Bu sayfanın ilerisinde karşınıza çıkar; sunucularınızı da onunla test edeceksiniz. ## Üç temel öğe {#the-three-primitives} @@ -83,22 +83,20 @@ Inspector'da üç sekme gördünüz. Üç tane olduğunu nereden bildi? Bir istemci bağlandığında sunucu **yeteneklerini** beyan eder: hangi istek ailelerini yanıtlayacağını. İstemci, neyi isteyeceğine karar vermek için bu beyanı kullanır. Bunu siz hiç yazmadınız; `MCPServer` sizin yerinize beyan eder. -Kendiniz bakın. SDK'nın `Client`'ı sunucu nesnesini doğrudan kabul eder ve ona **bellek içinde** bağlanır (alt süreç yok, port yok): - -```python -import asyncio - -from mcp import Client - -from server import mcp +Kendiniz bakın. Bir terminalde `server.py` dosyasını HTTP üzerinden çalışır halde bırakın: +```console +uv run mcp run server.py --transport streamable-http +``` -async def main() -> None: - async with Client(mcp) as client: - print(client.server_capabilities.model_dump(exclude_none=True)) +ve başka bir terminalden bir istemciyi ona yöneltin: +```python title="client.py" hl_lines="7-8" +--8<-- "docs_src/first_steps/tutorial001_client.py" +``` -asyncio.run(main()) +```console +python client.py ``` ```text @@ -115,11 +113,12 @@ Bu sözlük, sunucunuzun beyan ettiği **yeteneklerdir**. Bağlanan her istemcin `MCPServer` üç temel öğenin hepsini sunar; bu yüzden üçü de her zaman beyan edilir. -Orada ne olmadığına dikkat edin. `completions` (kaynak şablonları ve prompt'lar için argüman otomatik tamamlama) sizin yazacağınız bir işleyici gerektirir; bu sunucuda yok, dolayısıyla yetenek de yok ve uslu bir istemci sormaz. İsteğe bağlı her şey için kural budur: şeyi kaydedin, yetenek belirir; **[Tamamlamalar](../servers/completions.md)** bunu kanıtlar. +Orada ne olmadığına dikkat edin. `completions` (kaynak şablonları ve prompt'lar için argüman otomatik tamamlama) sizin yazacağınız bir işleyici gerektirir; bu sunucuda yok, dolayısıyla yetenek de yok ve kurallara uyan bir istemci sormaz. İsteğe bağlı her şey için kural budur: şeyi kaydedin, yetenek belirir; **[Tamamlamalar](../servers/completions.md)** bunu kanıtlar. !!! info - `Client(mcp)`, bu belgelerdeki her örneğin test edildiği aynı bellek içi istemcidir; - sizinkileri de böyle test edeceksiniz. Kendine ait koca bir sayfası var: **[Test etme](testing.md)**. + Bu `client.py` eksiksiz bir MCP istemcisidir; sayfası da **[İstemci](../client/index.md)**. + Bir testte terminali ve portu atlar, `Client`'a doğrudan sunucu nesnesinin kendisini verirsiniz: + `Client(mcp)`. Onun da kendine ait koca bir sayfası var: **[Test etme](testing.md)**. ## Yazmadıklarınız {#what-you-did-not-write} @@ -128,7 +127,7 @@ Bu sayfaya dönüp bir bakın. Üç küçük Python fonksiyonu yazdınız. Şunl * Bir JSON Schema. `a: int, b: int`, `add` şemasının *ta kendisidir*. * Bir istek işleyici. `tools/list`, `resources/read`, `prompts/get`: hepsi sizin yerinize sunulur. * Bir yetenek beyanı. `MCPServer` onu sizin yerinize yaptı. -* Tek satır protokol. Sürüm anlaşması, JSON-RPC çerçevelemesi, yetenek değiş tokuşu: hepsi `mcp dev` ve `Client(mcp)` içinde oldu ve siz hiçbirini görmediniz. +* Tek satır protokol. Sürüm anlaşması, JSON-RPC çerçevelemesi, yetenek değiş tokuşu: hepsi `mcp dev` ve `client.py` içinde oldu ve siz hiçbirini görmediniz. SDK'nın bütün meselesi bu oran. @@ -139,6 +138,6 @@ SDK'nın bütün meselesi bu oran. * Her temel öğe için bir dekoratör: `@mcp.tool()`, `@mcp.resource(uri)`, `@mcp.prompt()`. Ad, açıklama ve şema fonksiyondan gelir. * İçinde `{param}` olan bir URI, somut kaynaklardan ayrı listelenen bir kaynak **şablonu** oluşturur. * Sunucunun **yetenekleri** sizin yerinize beyan edilir ve bir istemci yalnızca sunucunun beyan ettiklerini ister. -* `Client(mcp)` sunucu nesnesine bellek içinde bağlanır: ilk günden test düzeneğiniz. +* `Client("http://localhost:8000/mcp")` çalışan sunucunuzla konuşur. Ona bunun yerine sunucu nesnesini verin, `Client(mcp)`, ve ilk günden test düzeneğiniz olur. Sırada **[Gerçek bir host'a bağlanma](real-host.md)** var: bu sunucu, gerçekten, Claude Desktop'ın ya da bir IDE'nin içinde. Ardından **[Test etme](testing.md)**: bir sayfa, bir bellek içi istemci ve çalışıp çalışmadığını bir daha asla tahmin etmek zorunda kalmazsınız. Ondan sonra her temel öğenin kendi sayfası var; modelin yönettiğiyle başlıyoruz: **[Araçlar](../servers/tools.md)**. diff --git a/i18n/tr/pages/get-started/testing.md b/i18n/tr/pages/get-started/testing.md index a5ce1e0c74..8b61d6f5e4 100644 --- a/i18n/tr/pages/get-started/testing.md +++ b/i18n/tr/pages/get-started/testing.md @@ -1,13 +1,13 @@ --- translation: - sections: ['4926721070127497', c52a1de2b6b32f40, 8e792bf8c7489ec6, 627195f7159e24ef] + sections: [5d13c2f0ba42c0d2, c52a1de2b6b32f40, 8e792bf8c7489ec6, 38552ea228b0a04f] tool: 1 --- # Test etme {#testing} -Python SDK, **bellek içi aktarıma** sahip bir `Client` sınıfıyla gelir: ona sunucu nesnenizi geçirirsiniz, o da doğrudan bağlanır. +SDK'nın `Client` sınıfı, yani bir URL'ye bağlanan ya da bir alt süreç başlatan sınıfın ta kendisi, **bellek içinde** de bağlanır: ona sunucu nesnenizi geçirin, o da sunucuyla doğrudan konuşur. -Alt süreç yok. Port yok. Hiç aktarım yok. FastAPI'nin `TestClient`'ıyla aynı fikir. +Alt süreç yok. Port yok. Ağ üzerinden giden hiçbir şey yok. FastAPI'nin `TestClient`'ıyla aynı fikir. ## Temel kullanım {#basic-usage} @@ -97,7 +97,7 @@ budur: testiniz temizlenmiş mesaj yerine gerçek mesajı görür. Testlerde açık bırakın. Üretim kodunda bir anlamı yoktur. -## Varsayılan olarak süreç içi {#in-process-by-default} +## Varsayılan olarak nesilden bağımsız {#era-neutral-by-default} !!! note `Client(mcp)` süreç içinde bağlanır ve varsayılan olarak **nesilden bağımsızdır**: sunucuyu diff --git a/i18n/tr/pages/handlers/multi-round-trip.md b/i18n/tr/pages/handlers/multi-round-trip.md index e4fe64c15c..939a100645 100644 --- a/i18n/tr/pages/handlers/multi-round-trip.md +++ b/i18n/tr/pages/handlers/multi-round-trip.md @@ -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 --- # Çok turlu istekler {#multi-round-trip-requests} @@ -164,7 +164,7 @@ Düşük seviyeli `Server` hiçbir şeyin hazır gelmediği katmandır: `MCPServ ## 2026-07-28 sürümüne özgü bir sonuç {#a-2026-07-28-result} -`InputRequiredResult` yalnızca **2026-07-28** protokol sürümünde vardır. Bellek içi `Client(server)` onu sizin yerinize anlaşarak belirler; ağ üzerinde `mode="auto"` keşfeder. Bağlandıktan sonra `client.protocol_version` size ne elde ettiğinizi söyler. +`InputRequiredResult` yalnızca **2026-07-28** protokol sürümünde vardır. `Client`'ın varsayılan `mode="auto"` ayarı onu her bağlantıda keşfeder. Bağlandıktan sonra `client.protocol_version` size ne elde ettiğinizi söyler. !!! warning 2026 öncesi bir oturumda `InputRequiredResult` koyacak bir yer yoktur. `mode="legacy"` bir diff --git a/i18n/tr/pages/handlers/progress.md b/i18n/tr/pages/handlers/progress.md index 373b63941e..98d86bb943 100644 --- a/i18n/tr/pages/handlers/progress.md +++ b/i18n/tr/pages/handlers/progress.md @@ -1,6 +1,6 @@ --- translation: - sections: [5315262fe26b33e1, 9d8e98840f1b78f0, 0284b215e85366c4, 8534d8dbb4053a70, 2966fac6fe697007] + sections: [5315262fe26b33e1, 9d8e98840f1b78f0, 52d6009a07e770ea, 8534d8dbb4053a70, 2966fac6fe697007] tool: 1 --- # İlerleme {#progress} @@ -29,19 +29,17 @@ Bir **`Context`** parametresi alın ve `report_progress`'i çağırın: İstemci, `call_tool`'a `progress_callback=` geçirerek **çağrı başına** dahil olur: -```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) Callback, sunucunun bildirdiklerini olduğu gibi alan `async` bir fonksiyondur: `progress`, `total`, `message`. !!! info - `Client(mcp)` doğrudan sunucu nesnesine, bellek içinde bağlanır; **[Test etme](../get-started/testing.md)** - sayfasının üzerine kurulduğu istemcinin aynısıdır. `Client` hangi aktarımı kullanırsa kullansın - `progress_callback` aynı parametredir; birazdan göreceğiniz *zamanlama* ise bellek içi bağlantıya - özgüdür. Bu bağlantı callback'inizi satır içinde çalıştırır, bu yüzden her bildirim `call_tool` - dönmeden önce ulaşır. Gerçek bir aktarım üzerinde bildirimler sonuçla yarışır ve yavaş bir callback, - `call_tool` döndükten sonra hâlâ çalışıyor olabilir. + `Client`'a ne verirseniz verin `progress_callback` aynı parametredir: buradaki gibi bir URL, bir + `StdioServerParameters` ya da testteki sunucu nesnesi. Yine de gerçek bir aktarım üzerinde + zamanlamaya dikkat edin. Her bildirim yanıtın yanında, kendi başına iletilir; bu yüzden yavaş bir + callback, `call_tool` döndükten sonra hâlâ çalışıyor olabilir. Yalnızca süreç içi test bağlantısı + callback'i satır içinde çalıştırır ve her bildirimin önce ulaşmasını garanti eder. ### Deneyin {#try-it} -`client.py` dosyasını `server.py` dosyasının yanına koyun ve çalıştırın: +`server.py` dosyasını HTTP üzerinden sunun, ardından istemciyi ikinci bir terminalden çalıştırın: + +```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.'} ``` -Sunucudaki her `await ctx.report_progress(...)`, istemcide sırasıyla bir `show` çağrısına dönüştü ve her iki satır da `call_tool` dönmeden **önce** yazdırıldı. İlerleme sonucun içine paketlenmez; araç hâlâ çalışırken akar. +Sunucudaki her `await ctx.report_progress(...)`, istemcide sırasıyla bir `show` çağrısına dönüştü. İlerleme sonucun içine paketlenmez. Araç hâlâ çalışırken akar. !!! warning `progress_callback` `Client`'a değil, **çağrıya** aittir. Bunun için bir kurucu argümanı yoktur, diff --git a/i18n/tr/pages/protocol-versions.md b/i18n/tr/pages/protocol-versions.md index b1fbd7664a..8705fc9299 100644 --- a/i18n/tr/pages/protocol-versions.md +++ b/i18n/tr/pages/protocol-versions.md @@ -1,6 +1,6 @@ --- translation: - sections: [478fd619e5f90ef8, aef094a00e44e248, bab8cbf3449fa7e9, df1809b15a58335b, 5f9d8c2336ed0239, f54974398e43ddef, b24443dd78584870] + sections: [d98e337bf28845e0, dd642acbba36f615, f547b3e76da19c94, 149513378588da5c, 4e149ed992046709, f54974398e43ddef, b24443dd78584870] tool: 1 --- # Protokol sürümleri {#protocol-versions} @@ -11,9 +11,17 @@ MCP'nin iki nesli var. Bununla neredeyse hiç ilgilenmeniz gerekmez, çünkü anlaşmayı sizin yerinize `Client` yapar. Bu sayfa, bunu denetleyen tek yapıcı argümanı, yani `mode=` parametresini ve onu değiştireceğiniz üç durumu anlatır. +Bu sayfadaki her kod parçası, **[İstemci](client/index.md)** sayfasındaki Bookshop `server.py` sunucusuyla konuşan bir `client.py` dosyasıdır. O sunucuyu bir terminalde başlatın: + +```console +uv run mcp run server.py --transport streamable-http +``` + +Ardından her kod parçasını ikinci bir terminalde `python client.py` ile çalıştırın. + ## `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 @@ Her iki durumda da bağlanmış olarak çıkarsınız ve hangisinin gerçekleşt Özelliğin tamamı bu. Tek bir `Client`, her nesilden sunucu, kodunuzda dallanma yok. !!! info - `MCPServer`, `server/discover` isteğini her aktarımda yanıtlar (bellek içi, stdio, Streamable - HTTP); bu yüzden kendi sunucunuza karşı `auto` her zaman `2026-07-28`'e ulaşır. Geri dönüş - yalnızca gerçek bir 2026 öncesi sunucuya karşı devreye girer, ki tam da o zaman bunu istersiniz. + `MCPServer`, `server/discover` isteğini her aktarımda yanıtlar (Streamable HTTP, stdio ve + testlerinizin kullandığı süreç içi bağlantı); bu yüzden kendi sunucunuza karşı `auto` her zaman + `2026-07-28`'e ulaşır. Geri dönüş yalnızca gerçek bir 2026 öncesi sunucuya karşı devreye girer, + ki tam da o zaman bunu istersiniz. ## `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 @@ Sunucunun başlattığı bir istek, sunucunun *sizi* çağırmasıdır: `ctx.eli `mode`, modern bir protokol sürümü dizgesini de kabul eder. Bugün bu küme tam olarak `["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 @@ Sorgu ucuzdur, ancak yine de her yeniden bağlanmada ödediğiniz bir turdur ve Öyleyse saklayın. Bir `auto` bağlantısından sonra `client.session.discover_result`, sunucunun gönderdiği `DiscoverResult`'ı olduğu gibi tutar: `supported_versions`, `capabilities`, `instructions` ve sunucunun sonucun `_meta` alanına işlediği kimlik. Bir sonraki sefer bunu `prior_discover=` olarak geri verin: -```python title="client.py" hl_lines="15 17" +```python title="client.py" hl_lines="8 10" --8<-- "docs_src/protocol_versions/tutorial004.py" ``` diff --git a/i18n/tr/pages/run/asgi.md b/i18n/tr/pages/run/asgi.md index 7ed6be8dc3..4a3676c546 100644 --- a/i18n/tr/pages/run/asgi.md +++ b/i18n/tr/pages/run/asgi.md @@ -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 --- # Mevcut bir uygulamaya ekleme {#add-to-an-existing-app} @@ -100,7 +100,7 @@ Sondaki o `/mcp`, `streamable_http_path` değeridir. Bunu `"/"` yapın, mount ö --8<-- "docs_src/asgi/tutorial004.py" ``` -Artık istemciler `/notes/mcp` yoluna değil `/notes` yoluna bağlanır. +Artık istemciler `/notes/mcp` yoluna değil `/notes/` yoluna bağlanır. ## Tarayıcı istemcileri için CORS {#cors-for-browser-clients} @@ -143,4 +143,4 @@ Tarayıcı tabanlı bir istemcinin sizden iki izne ihtiyacı vardır: MCP istek * Tarayıcı istemcilerinin CORS'a ihtiyacı vardır: `Mcp-*` istek başlıkları için `allow_headers`, yanıt için `expose_headers=["Mcp-Session-Id"]`. * `@mcp.custom_route()`, `/mcp`'nin yanına düz, kimlik doğrulaması olmayan HTTP endpoint'leri ekler. -Sunucu gerçek bir URL'den erişilebilir olduğunda **[İstemci](../client/index.md)** ona bir sunucu nesnesi yerine o URL ile bağlanır. +Sunucu gerçek bir URL'den erişilebilir olduğunda **[İstemci](../client/index.md)** ona o URL ile bağlanır. diff --git a/i18n/tr/pages/run/authorization.md b/i18n/tr/pages/run/authorization.md index c04c1eefeb..484d7b4246 100644 --- a/i18n/tr/pages/run/authorization.md +++ b/i18n/tr/pages/run/authorization.md @@ -1,6 +1,6 @@ --- translation: - sections: [d62c13457fc4a534, 80e73abaca6e0652, d1dc4c54cd00ec9c, 14ad3bc7904036bb, 5225f127bc1b9c77, fe1626fdd5aad1da, 4556cb7ea1a04a31] + sections: [d62c13457fc4a534, 80e73abaca6e0652, 128a492d18295f64, 14ad3bc7904036bb, 54a6697833fcc1d9, fe1626fdd5aad1da, 811d083c1da8bcf6] tool: 1 --- # Yetkilendirme {#authorization} @@ -23,12 +23,12 @@ Bu sayfa sunucu tarafını anlatır. Yetkilendirme sunucunuzu keşfeden ve token SDK'nın geçerli bir token'ın neye benzediği konusunda bir fikri yoktur. Bunu **`TokenVerifier`**'ı uygulayarak siz söylersiniz: -```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` tek bir asenkron metodu olan bir protokoldür. `verify_token`, `Authorization` başlığındaki ham token'ı alır; geçerliyse bir **`AccessToken`**, değilse `None` döndürür. Uygulanacak başka bir şey yok. -* Buradaki, token'ı bir tabloda arar. Gerçek bir doğrulayıcı JWT imzasını doğrular ya da yetkilendirme sunucusunun token-introspection endpoint'ini çağırır. O kod sizindir; SDK onu yalnızca çağırır. +* Buradaki, token'ı bir tabloda arar; her girdi, token'ın hangi kaynak için verildiğini kaydeder. Gerçek bir doğrulayıcı JWT imzasını doğrular ya da yetkilendirme sunucusunun token-introspection endpoint'ini çağırır ve token'ın kimin için verildiğini (`aud` değerini) `AccessToken.resource` içinde bildirir. O kod sizindir; SDK onu yalnızca çağırır. * `token_verifier=` ve `auth=` her zaman birlikte kullanılır. Birini diğeri olmadan geçirirseniz `MCPServer(...)` daha tek bir istek sunmadan `ValueError` fırlatır. `AuthSettings`, kaynak sunucunuzun dışa dönük yüzüdür: @@ -36,6 +36,10 @@ SDK'nın geçerli bir token'ın neye benzediği konusunda bir fikri yoktur. Bunu * `issuer_url`: token'larınızı veren yetkilendirme sunucusu. * `resource_server_url`: bu MCP endpoint'inin herkese açık URL'si. Bir token'ın *hangi* kaynak için olduğunu belirtir ve keşif belgesi burada bulunur. * `required_scopes`: her token bunların hepsini taşımalıdır. +* `validate_token_resource`: `AccessToken.resource` değeri `resource_server_url` olmayan her token'ı reddeder. `resource_server_url` ayarlıyken bunu ayarlamadan bırakmak uyarı verir (`MCPDeprecationWarning`) ve `False` gibi davranır; 3.0 sürümü kaynak sunucuları için varsayılanı `True` yapar. + * Yetkilendirme sunucunuz token'ları istemcinin istediği `resource` değerine bağlıyorsa açın; MCP istemcileri bu değeri her zaman gönderir. `resource_server_url` değerini istemcilerin bağlandığı URL'nin birebir aynısı olarak tutun. + * Yetkilendirme sunucunuz kendi audience tanımlayıcılarını kullanıyorsa (bir Auth0 API tanımlayıcısı, bir Entra uygulama kimliği) kapalı bırakın ve bunun yerine `aud` değerini doğrulayıcınızda denetleyin; bu sunucu için olmayan bir token'da `None` döndürün. + * `aud` bir listeyse, `resource_server_url` ile eşit olan girdiyi `resource` alanına koyun. !!! tip SDK deposundaki `examples/servers/simple-auth/` dizininde, gerçek bir yetkilendirme sunucusunun @@ -91,7 +95,7 @@ Sunucunuzu hiç duymamış bir istemci içeri giden yolu bu belgeyle bulur: `aut Herhangi bir işleyicinin içinde **`get_access_token()`**, doğrulayıcınızın geçerli istek için döndürdüğü `AccessToken`'dır: -```python title="server.py" hl_lines="4 32-35" +```python title="server.py" hl_lines="4 35-38" --8<-- "docs_src/authorization/tutorial002.py" ``` @@ -125,6 +129,6 @@ Bir yetkilendirme sunucusu, kullanıcının onay ekranından tıklayarak geçmes * `token_verifier=` ve `auth=AuthSettings(issuer_url=..., resource_server_url=..., required_scopes=[...])` her zaman birlikte kullanılır. * SDK, [RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728) Protected Resource Metadata belgesini `/.well-known/oauth-protected-resource/...` altında yayımlar ve kimliği doğrulanmamış istekleri, `WWW-Authenticate` başlığı ona işaret eden bir 401 ile yanıtlar. Keşif hikâyesinin tamamı bu. * Herhangi bir işleyicide `get_access_token()`, kimin çağırdığını söyler. -* Yetkilendirme bir HTTP meselesidir. `stdio` ve bellek içi istemci onu hiç görmez. +* Yetkilendirme bir HTTP meselesidir. `stdio` ve bellek içi test istemcisi onu hiç görmez. İstemci yarısı (yetkilendirme sunucunuzu keşfetme ve token'ı sizin yerinize alma) **[OAuth istemcileri](../client/oauth-clients.md)** sayfasında. Kullanıcıdan kimlik istemek yerine bir kimliği *beyan eden* istemci ise **[Kimlik beyanı](../client/identity-assertion.md)** sayfasında. diff --git a/i18n/tr/pages/run/deploy.md b/i18n/tr/pages/run/deploy.md index 3155935d68..29a6eaf491 100644 --- a/i18n/tr/pages/run/deploy.md +++ b/i18n/tr/pages/run/deploy.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 --- # Dağıtım ve ölçekleme {#deploy-scale} @@ -47,6 +47,22 @@ Gerçek bir ana bilgisayar adının arkasına dağıtıldığında, aynı varsay her bağlantıyı reddeden bir sunucu, aksi kanıtlanana kadar bir Host izin listesi sorunudur. **[Sorun giderme](../troubleshooting.md)** de buradan başlar. +## TLS sonlandıran bir vekil sunucunun arkasında {#behind-a-tls-terminating-proxy} + +TLS bir vekil sunucuda (bir ingress, bir yük dengeleyici, Caddy, nginx) sona eriyor ve uvicorn onun arkasında düz HTTP sunuyorsa, uvicorn'a vekil sunucunun `X-Forwarded-*` başlıklarına güvenmesini söyleyin: + +```console +uvicorn server:app --proxy-headers --forwarded-allow-ips='' +``` + +Bu olmadan uygulama `http://` üzerinden sunulduğunu sanır ve verdiği her yönlendirme (en yaygını `/mcp` → `/mcp/`) `http://…` adresini gösterir. Python istemcisi bir HTTPS endpoint'inden düz HTTP'ye giden bir yönlendirmeyi izlemeyi reddeder ve bunu açıkça söyler: + +```text +MCPError: Redirect to http://mcp.example.com/mcp/ not followed: it would downgrade this HTTPS endpoint to plain HTTP. +``` + +İstemci tarafındaki geçici çözüm, sunucunun sunduğu tam URL'yi (`https://mcp.example.com/mcp/`, sondaki eğik çizgi dahil) yapılandırmaktır; böylece hiç yönlendirme olmaz. Asıl çözüm yukarıdaki bayraktır. `FORWARDED_ALLOW_IPS` aynı ayarın ortam değişkeni yazımıdır; `*` her atlama noktasına güvenir, bu da yalnızca uvicorn'a vekil sunucudan başka hiçbir şey ulaşamıyorsa doğrudur. + ## Worker'lar ve kimin yapışkan olması gerektiği {#workers-and-who-has-to-be-sticky} Ana bilgisayar adı yanıt vermeye başladıktan sonra, arkasına birden fazla worker koyun. Bunun için SDK'da bir ayar yoktur; bir Starlette uygulamasını, herhangi bir ASGI uygulamasını ölçeklediğiniz gibi ölçeklersiniz: nesneyi, fork etmeyi bilen bir şeye verirsiniz: @@ -171,6 +187,7 @@ Bir `MCPServer` bir uygulama sunucusu değil, bir protokol gerçeklemesidir. Bun ## Özet {#recap} * Varsayılan olarak uygulama yalnızca localhost'a gönderilen istekleri yanıtlar. `transport_security=TransportSecuritySettings(allowed_hosts=[...], allowed_origins=[...])` yayına çıkış kapısıdır: onu geçirene kadar gerçek bir ana bilgisayar adının arkasındaki her istek bir `421`'dir ve nedeni yalnızca sunucunun log'undadır. +* TLS sonlandıran bir vekil sunucunun arkasında uvicorn'u `--proxy-headers --forwarded-allow-ips=...` ile çalıştırın; yoksa yönlendirmeleri `http://` adresini gösterir ve istemci onları reddeder. * 2026-07-28'de oturum yoktur ve bir yük dengeleyicinin yapışacağı hiçbir şey yoktur. `stateless_http=True` yalnızca eski nesle ait bir ayardır, çünkü modern bir istek o bayrak hiç okunmadan yönlendirilir ve yanıtlanır. * Varsayılan `requestState` anahtarı, süreç başına basılan `os.urandom(32)`'dir. Farklı bir worker'a ulaşan çok turlu bir yeniden deneme `-32602` *"Invalid or expired requestState"* ile başarısız olur. * Çözüm `RequestStateSecurity(keys=[...])` **ve** her örnekte aynı sunucu adıdır. Ad, token'ın varsayılan audience claim'idir. Aynı anahtarlar, aynı ad. diff --git a/i18n/tr/pages/run/index.md b/i18n/tr/pages/run/index.md index b4c206212c..fe5c4147b6 100644 --- a/i18n/tr/pages/run/index.md +++ b/i18n/tr/pages/run/index.md @@ -1,6 +1,6 @@ --- translation: - sections: [fea8d769ff9edeba, ce8e2ad42f29ef71, 0d705efb19cf99c2, 2fd7cf825e6d2b2c, 9adc400e8c88e854, 318893ad8e2e9924, 6b63ab96b34476c0] + sections: [fea8d769ff9edeba, ce8e2ad42f29ef71, 0d705efb19cf99c2, 9fd2357154a5b7e7, 9adc400e8c88e854, 318893ad8e2e9924, 6b63ab96b34476c0] tool: 1 --- # Sunucunuzu çalıştırma {#running-your-server} @@ -75,6 +75,11 @@ Her aktarımın kendi anahtar sözcük argümanları vardır ve hepsi `run()` ü * `max_request_body_size`: bayt cinsinden kabul edilen en büyük istek gövdesi. Varsayılan olarak 4 MiB; daha büyük istekler, ayrıştırma veya oturum oluşturma öncesinde HTTP 413 alır. Bunu yalnızca meşru MCP mesajları bu boyutu aştığında yükseltin. +* `session_idle_timeout`: eski nesil bir oturumun, sunucu onu kapatmadan önce sürmekte olan hiçbir şey + olmadan bekleyebileceği saniye sayısı. Varsayılan 1800. `None` bunu devre dışı bırakır. Bkz. + [Oturum ömrü ve sınırlar](legacy-clients.md#session-lifetime-and-limits). +* `max_sessions`: bir sürecin aynı anda kaç eski nesil oturum tuttuğu. Varsayılan 10 000. `None` + sınırı kaldırır. Aynı bölümde ele alınır. * `event_store`, `retry_interval`, `transport_security`: kaldığı yerden devam edebilme ve DNS rebinding koruması. localhost dışında bir yere dağıtım yapana kadar bekleyebilirler; `transport_security` konusunu **[Dağıtım ve ölçekleme](deploy.md)** ele alır. !!! warning diff --git a/i18n/tr/pages/run/legacy-clients.md b/i18n/tr/pages/run/legacy-clients.md index da62841d35..055a9930e5 100644 --- a/i18n/tr/pages/run/legacy-clients.md +++ b/i18n/tr/pages/run/legacy-clients.md @@ -1,6 +1,6 @@ --- translation: - sections: [3d1663c18edc824c, d4fd37009a13f03d, af9f398a5a8b679a, 470c2dd144294d69, 8e45827e6d24e8c8, 91dfd0ce98ebb03c] + sections: [3d1663c18edc824c, 90956965ae6a1ca1, af9f398a5a8b679a, 5ce83b1f9d88da62, 0d9b5d13fffc94e5, 8e45827e6d24e8c8, 91dfd0ce98ebb03c] tool: 1 --- # Eski nesil istemcilere hizmet verme {#serving-legacy-clients} @@ -22,15 +22,25 @@ Yani eski nesil istemci, *ona göre* bir şey inşa ettiğiniz bir hedef değil. ## Tek işleyici, iki nesil {#one-handler-both-eras} -İşte kullanıcıya bir şey sorması gereken bir araç ve onu çağıran her iki nesilden istemci: +İşte kullanıcıya bir şey sorması gereken bir araç: -```python title="server.py" hl_lines="24 37-38" +```python title="server.py" hl_lines="21" --8<-- "docs_src/legacy_clients/tutorial001.py" ``` `reserve`, modelin sağlamadığı tek bir şeye ihtiyaç duyar: kaç kopya. Bir araç bunu `Annotated[..., Resolve(ask_quantity)]` ile bildirir (ayrıntıların tamamı **[Bağımlılıklar](../handlers/dependencies.md)** sayfasında). `reserve` içinde hiçbir şey bir sürüm adı vermez, bir yetenek kontrol etmez ya da dallanmaz. -İki istemci **aynı anda**, aynı `mcp` nesnesi üzerinde açıktır. `mode="legacy"`, `initialize` el sıkışmasını çalıştırır: 2026 öncesi bir istemcinin açtığı bağlantının ta kendisi. Diğeri varsayılanı alır ve `2026-07-28` sürümünde karar kılar. +Onu HTTP üzerinden sunun. İşte onu çağıran her iki nesilden istemci: + +```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" +``` + +İki istemci **aynı anda**, çalışan aynı sunucuya karşı açıktır. `mode="legacy"`, `initialize` el sıkışmasını çalıştırır: 2026 öncesi bir istemcinin açtığı bağlantının ta kendisi. Diğeri varsayılanı alır ve `2026-07-28` sürümünde karar kılar. İkinci bir terminalden `python client.py` komutunu çalıştırın: ```text 2025-11-25 {'result': "Reserved 2 of 'Dune'."} @@ -64,6 +74,31 @@ Tek worker'da bu görünmez. İki worker'da ise sorunun tamamı budur: `Mcp-Sess **devam ettirilebilirliktir** (kaçırılan SSE olaylarını *aynı* oturuma yeniden bağlanan bir istemciye yeniden oynatmak). Bir oturumu asla başka bir süreçten erişilebilir kılmaz. +## Oturum ömrü ve sınırlar {#session-lifetime-and-limits} + +Eski nesil bir oturum sonsuza dek yaşamaz ve tek bir süreç bunlardan sınırsız sayıda tutmaz. Bunu iki ayar denetler. İkisi de `run()`, `streamable_http_app()` ve `Server.streamable_http_app()` üzerinde birer anahtar sözcük argümanıdır. Modern (`2026-07-28`) bağlantılarda ve `stateless_http=True` durumunda oturum yoktur; bu yüzden iki ayar da onlar için geçerli değildir. + +| Ayar | Varsayılan | Ne yapar | İstemci ne görür | Kapatmak için | +|---|---|---|---|---| +| `session_idle_timeout` | `1800` (30 dk) | Bu süre boyunca devam eden hiçbir işlemi olmayan bir oturumu kapatır. | `404 Session not found`. Yeniden `initialize` göndermesi gerekir. | `None` | +| `max_sessions` | `10_000` | Bu sayının ötesinde oturum açmayı reddeder. Mevcut oturumlara dokunulmaz ve hiçbir şey tahliye edilmez. | JSON-RPC kodu `-32603` ile `503 Too many open sessions`. | `None` | + +Neler "devam eden işlem" sayılır: + +* Açık bir `GET` akışı. SDK istemcileri bir tane açık tutar; bu yüzden bağlı bir istemcinin oturumu asla sona ermez. +* Hâlâ yanıtlanmakta olan bir istek. Zaman aşımından uzun süren bir araç çağrısı kesilmez; geri sayım ancak o bittiğinde başlar. +* Başka hiçbir şey. İstekler arasında saat işler. Oturumdaki herhangi bir istek onu yeniden başlatır, `ping` dahil. Bir oturum sona erdikten sonra onu hiçbir şey diriltmez. + +Oturumunu `DELETE` ile bitiren bir istemci onu hemen serbest bırakır. Açılış isteği reddedilen bir istemci de öyle. + +```python +mcp.run(transport="streamable-http", session_idle_timeout=None, max_sessions=50_000) +``` + +İki olay da sunucu log'unda görünür. Sona erme, `INFO` düzeyinde `Session idle timeout` olarak görünür. Reddedilen bir açılış ise `WARNING` düzeyinde `Refusing to open a new session: sessions are already open` olarak. + +Sınırlar süreç başınadır. Dört worker'la tavan, `max_sessions` değerinin dört katıdır ve her worker kendi oturumlarını kendisi sonlandırır. + ## Tek ayar düğmesi: `stateless_http` {#the-one-knob-stateless_http} Yapışkanlık ödemeyi reddettiğiniz bir bedelse, değiştirebileceğiniz tam olarak tek bir şey var. @@ -89,8 +124,7 @@ Onunla ilgili iki şey, ne yaptığından daha önemli. !!! check Yanlış olanı yapın. `reserve`, az önce iki istemciye de hizmet veren aracın ta kendisi. Onu - `stateless_http=True` ile dağıtın, aynı iki istemciyi HTTP üzerinden bağlayın ve her birinden - çağırın. + `stateless_http=True` ile dağıtın, aynı iki istemciyi bağlayın ve her birinden çağırın. Modern istemci hâlâ `Reserved 2 of 'Dune'.` alır. Modern kol değişmedi. diff --git a/i18n/tr/pages/troubleshooting.md b/i18n/tr/pages/troubleshooting.md index 3362b53054..5933595a30 100644 --- a/i18n/tr/pages/troubleshooting.md +++ b/i18n/tr/pages/troubleshooting.md @@ -1,6 +1,6 @@ --- translation: - sections: [2efaecdef109a5c5, fcacd3e66b8635a4, 25323d737dcf0261, 8a6e351ec756904d, 137454d469c867f5, 6392596bd6df54f0, 41126fa9c4fe432f, 480b6d7897e30ab4, d83bb682e708dde0, ebbed3449c499db4, 323ef84f6b4bebde, 30fd31be74169d9a, 656943c6cb567218, c2dc3b1007d2e987, 7cf5386b997d04e9, 0b59feed8384456e, 0cba47bae78d04eb, e4355f4c7cf4fb2e] + sections: [3d58228e81b99543, 170514ce901c4139, 17d61fad0a50d62b, 8a6e351ec756904d, 137454d469c867f5, 6392596bd6df54f0, 41126fa9c4fe432f, 480b6d7897e30ab4, d83bb682e708dde0, ebbed3449c499db4, 525cdf1755e29d4c, 30fd31be74169d9a, d2e88333d4f7841f, c2dc3b1007d2e987, d6eabf60cc366341, f798e815252852c2, 0cba47bae78d04eb, 2c218ba829abf74e] tool: 1 --- # Sorun giderme {#troubleshooting} @@ -13,6 +13,12 @@ Girdilerin birkaçı şu tek sunucuya karşı çalışır. Bir araç ve bir şab --8<-- "docs_src/troubleshooting/tutorial001.py" ``` +Bu girdiler ona `http://localhost:8000/mcp` adresinden ulaşır; bu yüzden onu HTTP üzerinden çalışır durumda bırakın: + +```console +uv run mcp run server.py --transport streamable-http +``` + Bu sayfanın alıntıladığı hatalar gerçektir: SDK'nın kendi test paketi her birini yeniden üretir. ## `ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception)` {#exceptiongroup-unhandled-errors-in-a-taskgroup-1-sub-exception} @@ -23,7 +29,7 @@ Bu bir MCP hatası değil. anyio gürültüsüdür ve asıl hatanız yapıştır ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: await client.read_resource("weather://Atlantis") ``` @@ -49,7 +55,7 @@ Bununla yapılacak iki şey var: ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: try: await client.read_resource("weather://Atlantis") except MCPError as e: @@ -67,7 +73,7 @@ async def main() -> None: ```python async def main() -> None: - client = Client(mcp) + client = Client("http://localhost:8000/mcp") tools = await client.list_tools() # RuntimeError ``` @@ -75,7 +81,7 @@ async def main() -> None: ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: tools = await client.list_tools() ``` @@ -252,7 +258,7 @@ Bunun sayfası, tek uygulamada birden fazla sunucu ve FastAPI dahil, **[Mevcut b ## `MCPError: Session not found` {#mcperror-session-not-found} -Sunucu, istemcinizin gönderdiği `Mcp-Session-Id`'yi tanımıyor; bunun nedeni neredeyse her zaman sunucunun **yeniden başlamış** olmasıdır (ya da farklı bir örneğe yönlendirilmişsinizdir). Oturumlar o tek sürecin belleğinde yaşar. +Sunucu, istemcinizin gönderdiği `Mcp-Session-Id`'yi tanımıyor. Ya sunucu **yeniden başladı** (ya da farklı bir örneğe yönlendirildiniz) ya da `session_idle_timeout` süresi boyunca (varsayılan olarak 30 dakika) hiçbir şey işlemde olmadığı için oturumun **süresi doldu**. [Oturum ömrü ve sınırları](run/legacy-clients.md#session-lifetime-and-limits) bölümüne bakın. Oturumlar o tek sürecin belleğinde yaşar. Bulunacak bir sunucu hatası yok. HTTP yanıtı, gövdesi JSON-RPC *olan* bir `404`'tür; bu yüzden yukarıdaki `421`'in aksine python `Client` bunu size birebir gösterir: @@ -262,9 +268,9 @@ Bulunacak bir sunucu hatası yok. HTTP yanıtı, gövdesi JSON-RPC *olan* bir `4 Çözüm yeniden bağlanmaktır: `async with Client(...)` bloğundan çıkın ve yeni bir oturum üzerinde anlaşan yeni bir bloğa girin. Uzun ömürlü bir istemci için bu, çağrılarınızın etrafında `MCPError`'ı yakalamak ve ölü bir oturumun içinde yeniden denemek yerine bu mesajda yeniden bağlanmak demektir. -Bu, yeniden başlatma *olmadan* oluyorsa, yapışkan oturumlar olmadan birden fazla worker çalıştırıyorsunuz demektir: her worker kendi oturum tablosunu tutar, bu yüzden yanlış olana yönlendirilen bir istek buraya düşer. Bu konu ve iki çözümü (yapışkan yönlendirme veya `stateless_http=True`) **[Dağıtım ve ölçekleme](run/deploy.md)** ile **[Eski nesil istemcilere hizmet verme](run/legacy-clients.md)** sayfalarında. +Bu, yeniden başlatma *olmadan* ve istemci o kadar uzun süre sessiz kalmadan oluyorsa, yapışkan oturumlar olmadan birden fazla worker çalıştırıyorsunuz demektir: her worker kendi oturum tablosunu tutar, bu yüzden yanlış olana yönlendirilen bir istek buraya düşer. Bu konu ve iki çözümü (yapışkan yönlendirme veya `stateless_http=True`) **[Dağıtım ve ölçekleme](run/deploy.md)** ile **[Eski nesil istemcilere hizmet verme](run/legacy-clients.md)** sayfalarında. -Sunucu operatörü için eşleşen log satırı `Rejected request with unknown or expired session ID: `'dir. `INFO` düzeyinde log'a yazılır; bu yüzden olağan `WARNING` eşiğinde görünmez. Bir dağıtımın hemen ardından bunu öbekler halinde görmek normaldir; bağlı her istemci yeniden bağlanıyordur. +Sunucu operatörü için eşleşen log satırı `Rejected request with unknown or expired session ID: `'dir. `INFO` düzeyinde log'a yazılır; bu yüzden olağan `WARNING` eşiğinde görünmez. Bir dağıtımın hemen ardından bunu öbekler halinde görmek normaldir; bağlı her istemci yeniden bağlanıyordur. Oturumun süresi dolduğunda ise bu satırdan önce, yine `INFO` düzeyinde, `Session idle timeout` gelir. ## `MCPError: Method not found` {#mcperror-method-not-found} @@ -276,7 +282,13 @@ Modern protokolün kaldırdığı bir istek olmasına rağmen bu hatayı **üret Sunucunuz kullanıcıya bir şey sormak istiyor ve bu istemci kendisine soru sorulabileceğini hiç söylemedi. -Bir elicitation (kullanıcıdan bilgi isteme) çözümleyicisi, bağlı istemci form elicitation'ı bildirmediğinde baştan reddeder ve `e.error.data` tam olarak neyin eksik olduğunu adlandırır: +Bu Bistro, rezervasyon yapmadan önce bir çözümleyici aracılığıyla sorar: + +```python title="server.py" hl_lines="15-17 21" +--8<-- "docs_src/troubleshooting/tutorial007.py" +``` + +Bunu Weather sunucusunun yerine sunun ve `book_table`'ı hiçbir `elicitation_callback` geçirmemiş bir istemciden çağırın. Çözümleyici baştan reddeder, çünkü bağlı istemci form elicitation'ı (kullanıcıdan bilgi isteme) hiç bildirmemiştir; `e.error.data` ise tam olarak neyin eksik olduğunu adlandırır: ```json { @@ -290,7 +302,7 @@ Bir elicitation (kullanıcıdan bilgi isteme) çözümleyicisi, bağlı istemci ```python async def main() -> None: - async with Client(mcp, elicitation_callback=handle_elicitation) as client: + async with Client("http://localhost:8000/mcp", elicitation_callback=handle_elicitation) as client: result = await client.call_tool("book_table", {"date": "Friday"}) ``` @@ -315,14 +327,14 @@ Bunu eski nesil bir bağlantıda `ctx.elicit()`'ten görürsünüz; herhangi bir İşleyiciniz, isteğin ortasında istemciye ulaşmaya çalıştı; hem de çağrısının sunucudan gelen bir isteği taşıyabilecek hiçbir kanalı olmayan bir bağlantıda. Bir çağrıyı bu duruma sokan üç sunucu yapılandırması var. -**Bir `2026-07-28` bağlantısı: her aktarımda, her zaman.** Modern protokolde sunucunun başlattığı istek diye bir şey hiç yoktur; bu yüzden sunucu daha hiçbir şey gönderilmeden reddeder. Bununla karşılaşmanın klasik yolu bir aracın içindeki `ctx.elicit()`'tir (hem de daha ilk bellek içi testte, çünkü `Client(server)` sorulmadan `2026-07-28` üzerinde anlaşır) ve `elicitation_callback=` geçirmek hiçbir şeyi değiştirmez, çünkü istemciye yanıtlayacağı bir istek hiç ulaşmaz: +**Bir `2026-07-28` bağlantısı: her aktarımda, her zaman.** Modern protokolde sunucunun başlattığı istek diye bir şey hiç yoktur; bu yüzden sunucu daha hiçbir şey gönderilmeden reddeder. Bununla karşılaşmanın klasik yolu bir aracın içindeki `ctx.elicit()`'tir; genellikle de o aracın daha ilk bellek içi **[testinde](get-started/testing.md)**, çünkü `Client(mcp)` sorulmadan `2026-07-28` üzerinde anlaşır. `elicitation_callback=` geçirmek hiçbir şeyi değiştirmez, çünkü istemciye yanıtlayacağı bir istek hiç ulaşmaz: ```python title="server.py" hl_lines="16" --8<-- "docs_src/troubleshooting/tutorial006.py" ``` ```python -async def main() -> None: +async def test_book_table() -> None: async with Client(mcp) as client: await client.call_tool("book_table", {"date": "Friday"}) ``` @@ -364,7 +376,7 @@ Sunucu, istemcinizin geri yansıttığı `requestState` token'ını doğrulayama ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: await client.call_tool("forecast", {"city": "London"}, request_state="round-1-from-worker-a") ``` @@ -417,7 +429,7 @@ mcp = MCPServer("Weather", request_state_security=RequestStateSecurity(keys=[key * Sunucu log'undaki `Tool already exists:`, aynı adlı iki aracın teke indiğinin tek işaretidir. * Tek 421, üç yazım: `Server returned an error response` (python `Client`), `421 Misdirected Request` / `Invalid Host header` (geri kalan her şey), `Invalid Host header: ` (sunucu log'u). Çözüm: `transport_security=TransportSecuritySettings(allowed_hosts=[...])`. * `Task group is not initialized` -> ana uygulamanın lifespan'i `mcp.session_manager.run()`'a hiç girmemiş, bağlanmış bir uygulama. -* `Session not found` -> sunucu yeniden başladı; yeniden bağlanın. +* `Session not found` -> sunucu yeniden başladı ya da oturumun süresi doldu (`session_idle_timeout`); yeniden bağlanın. * `Cannot send 'elicitation/create': ... no back-channel ...` -> `ctx.elicit()` sunucudan istemciye bir kanala ihtiyaç duyar: bir `2026-07-28` bağlantısında hiç yoktur, `stateless_http=True` eski nesil olanı, `json_response=True` ise istek kapsamlı olanı ortadan kaldırır. Bir çözümleyici kullanın (eski nesil bir istemci için ayrıca kanalı koruyan bir sunucu gerekir). Komşusu `Method not found`, karşı tarafın protokol sürümünde olmayan bir yöntem için yapılmış bir istektir. * `Client did not declare the form elicitation capability ...` ve `Elicitation not supported` -> istemcide `elicitation_callback=` eksik. * `Invalid or expired requestState` nedenini ağ üzerinde asla söylemez. Sunucu log'u söyler; `unknown key`, `RequestStateSecurity(keys=[...])`'i worker'lar arasında paylaşın demektir. diff --git a/i18n/tr/pages/whats-new.md b/i18n/tr/pages/whats-new.md index 6380cf3600..0429738618 100644 --- a/i18n/tr/pages/whats-new.md +++ b/i18n/tr/pages/whats-new.md @@ -1,6 +1,6 @@ --- translation: - sections: [cfe01c0c5863dfa2, 1c58c5cfcc37d455, a7392996acf1ad8f, 875eb2889263424e] + sections: [cfe01c0c5863dfa2, 0dbb68d8b210177b, 80cf193023af3ed4, 875eb2889263424e] tool: 1 --- # v2'deki yenilikler {#whats-new-in-v2} @@ -42,11 +42,11 @@ Bir aracın ihtiyaç duyduğu her şey modelden gelmek zorunda değil. v2 ile ge v1 size iç içe üç katman veriyordu: ham akışlar üreten bir aktarım bağlam yöneticisi, bunların etrafına sarılmış bir `ClientSession` ve elle çağrılan bir `await session.initialize()`. v2'de tek bir nesne var: -```python title="client.py" hl_lines="14-18" ---8<-- "docs_src/client/tutorial001.py" +```python title="client.py" hl_lines="7-11" +--8<-- "docs_src/client/tutorial001_client.py" ``` -`Client` bir sunucu nesnesi (bellek içi, aktarım yok: test senaryosu), bir URL (Streamable HTTP), bir `StdioServerParameters` (bir stdio alt süreci) ya da `sse_client(...)` gibi başka herhangi bir aktarım bağlam yöneticisi alır. `async with` bloğuna girmek bağlantıyı kurar ve sunucu hangi nesli konuşuyorsa ona göre protokol sürümünde anlaşır; ardından `client.server_capabilities` ve `client.protocol_version` hazırdır, sunucu kendini tanıttığında `client.server_info` da öyle (artık `Implementation | None` türünde, çünkü 2026 neslinde kimlik isteğe bağlı). v1'de kaydettiğiniz örnekleme ve elicitation callback'leri hâlâ çalışır (gövdeleri, bu sayfadaki her şey gibi aynı snake_case öznitelik yeniden adlandırmasını görür); artık 2026 tarzı sonuç-içinde-isteklere de (aşağıda) yanıt verirler ve teker teker değil eşzamanlı çalışırlar. Düşük düzey yüzeyi isteyenler için `ClientSession` hâlâ altta duruyor ve `client.session` onu size verir; o da taşındı (yeni dispatcher motoru üzerinde çalışır ve kendi imzalarından bazıları değişti), bu yüzden aşağı inmeden önce **[Geçiş kılavuzu](migration.md#clientsession-now-runs-on-jsonrpcdispatcher-basesession-removed)** sayfasını okuyun. +`Client` bir URL (Streamable HTTP), bir `StdioServerParameters` (bir stdio alt süreci), `sse_client(...)` gibi başka herhangi bir aktarım bağlam yöneticisi ya da testlerde sunucu nesnesinin kendisini (bellek içi, aktarım yok) alır. `async with` bloğuna girmek bağlantıyı kurar ve sunucu hangi nesli konuşuyorsa ona göre protokol sürümünde anlaşır; ardından `client.server_capabilities` ve `client.protocol_version` hazırdır, sunucu kendini tanıttığında `client.server_info` da öyle (artık `Implementation | None` türünde, çünkü 2026 neslinde kimlik isteğe bağlı). v1'de kaydettiğiniz örnekleme ve elicitation callback'leri hâlâ çalışır (gövdeleri, bu sayfadaki her şey gibi aynı snake_case öznitelik yeniden adlandırmasını görür); artık 2026 tarzı sonuç-içinde-isteklere de (aşağıda) yanıt verirler ve teker teker değil eşzamanlı çalışırlar. Düşük düzey yüzeyi isteyenler için `ClientSession` hâlâ altta duruyor ve `client.session` onu size verir; o da taşındı (yeni dispatcher motoru üzerinde çalışır ve kendi imzalarından bazıları değişti), bu yüzden aşağı inmeden önce **[Geçiş kılavuzu](migration.md#clientsession-now-runs-on-jsonrpcdispatcher-basesession-removed)** sayfasını okuyun. **[Client](client/index.md)** sayfası onu tanıtır, **[İstemci aktarımları](client/transports.md)** dört bağlantı biçimini anlatır, **[İstemci callback'leri](client/callbacks.md)** callback'lerin kendisini ele alır ve **[Test etme](get-started/testing.md)** v1'in `create_connected_server_and_client_session()` yardımcısının yerini alan bellek içi kalıbı gösterir. @@ -171,11 +171,15 @@ Bunun istemci tarafı **[Protokol sürümleri](protocol-versions.md)** sayfasın Yerine gelen çözüm çağrıyı tersine çevirir. Kullanıcıdan bir şeye ihtiyaç duyan araç soruyu *döndürür* (`InputRequiredResult`), istemci onu her zamanki callback'leriyle yanıtlar ve çağrı yanıtlar eklenmiş hâlde yeniden denenir. Bu döngüyü sizin için `Client` yürütür. Sunucuda sonucu nadiren kendiniz kurarsınız, çünkü bunu bir **[bağımlılık](handlers/dependencies.md)** yapar: bir parametreyi `Resolve(ask_quantity)` ile işaretleyin (`ask_quantity` sizin yazdığınız sıradan bir fonksiyondur), SDK de bağlantının desteklediği mekanizma hangisiyse onun üzerinden sorar: eski nesil bir oturumda canlı bir elicitation isteği, 2026'da çok turlu bir istek. Tek araç gövdesi, iki nesil birden: -```python title="dual_era.py" hl_lines="24 37-38" +```python title="server.py" hl_lines="21" --8<-- "docs_src/legacy_clients/tutorial001.py" ``` -Bu dosya tüm vaadin tek yerde özeti: bir sunucu, `Resolve` destekli bir araç ve ikisi de yanıtını bellek içinde alan bir eski nesil istemci ile bir modern istemci. **[Çok turlu istekler](handlers/multi-round-trip.md)** mekanizmayı açıklar (SDK'nın sizin için mühürleyip doğruladığı `request_state` dâhil); sorma kısmı **[Elicitation](handlers/elicitation.md)** sayfasında. +```python title="client.py" hl_lines="14-15" +--8<-- "docs_src/legacy_clients/tutorial001_client.py" +``` + +Bu iki dosya tüm vaadin özeti: bir sunucu, `Resolve` destekli bir araç ve ikisi de yanıtını çalışan aynı sunucudan alan bir eski nesil istemci ile bir modern istemci (**[Eski nesil istemcilere hizmet verme](run/legacy-clients.md)** bunları adım adım anlatır). **[Çok turlu istekler](handlers/multi-round-trip.md)** mekanizmayı açıklar (SDK'nın sizin için mühürleyip doğruladığı `request_state` dâhil); sorma kısmı **[Elicitation](handlers/elicitation.md)** sayfasında. !!! warning "Taşınmış bir v1 sunucusunun davranış değiştirdiği tek yer burası" Buna ilk sizin testleriniz çarpar: `Client(mcp)` v2 sunucunuzla varsayılan olarak 2026-07-28 diff --git a/i18n/uk/pages/advanced/apps.md b/i18n/uk/pages/advanced/apps.md index 8929b7aec2..76f5f941fe 100644 --- a/i18n/uk/pages/advanced/apps.md +++ b/i18n/uk/pages/advanced/apps.md @@ -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,13 +18,13 @@ SDK постачає це як вбудоване розширення `Apps` (` ## Годинник із циферблатом {#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" ``` Чотири кроки: -* `Apps()`: один екземпляр тримає ваші інструменти з прив'язаним UI та їхні ресурси. +* `Apps()`: один екземпляр тримає інструменти з прив'язаним UI та їхні ресурси. * `@apps.tool(resource_uri="ui://clock/app.html")`: звичайний інструмент плюс позначка `_meta.ui.resourceUri`. Усе, що приймає `@mcp.tool()` (name, title, description, ...), передається далі. * `apps.add_html_resource("ui://clock/app.html", CLOCK_HTML)`: відповідний ресурс, який віддається як `text/html;profile=mcp-app`. Саме цей MIME-тип каже хосту: «це застосунок, відобрази його». * `MCPServer("clock", extensions=[apps])`: увімкнення. Тепер сервер оголошує `io.modelcontextprotocol/ui` у `capabilities.extensions`. @@ -39,11 +39,31 @@ SDK постачає це як вбудоване розширення `Apps` (` Модель читає `content`; iframe — для людей. Хост із підтримкою UI все одно передає текстовий результат моделі, а суто текстовий клієнт отримує *лише* його. Тож канонічний шаблон — один інструмент, дві відповіді. Погляньте на `get_time` ще раз: -```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)` дорівнює `True` лише тоді, коли клієнт оголосив розширення `io.modelcontextprotocol/ui` **і** вказав `text/html;profile=mcp-app` у своїх налаштуваннях `mimeTypes`. Поле обов'язкове, тож клієнт, який його пропустив, не зараховується. Саме це й оголошує `main()` у тому самому файлі: клієнтську половину узгодження — і у відповідь приходить розширений варіант. +`client_supports_apps(ctx)` дорівнює `True` лише тоді, коли клієнт оголосив розширення `io.modelcontextprotocol/ui` **і** вказав `text/html;profile=mcp-app` у своїх налаштуваннях `mimeTypes`. Поле обов'язкове, тож клієнт, який його пропустив, не зараховується. Ось клієнтська половина узгодження: + +```python title="client.py" hl_lines="8 12" +--8<-- "docs_src/apps/tutorial001_client.py" +``` + +Віддайте `server.py` через HTTP, а потім із другого термінала запустіть клієнт: + +```console +uv run mcp run server.py --transport streamable-http +``` + +```console +python client.py +``` + +```text +2026-06-26T12:00:00Z +``` + +Повернулася розширена відповідь. Приберіть `extensions=[APPS_SUPPORT]` з виклику `Client` — і та сама програма натомість надрукує `The time is 2026-06-26T12:00:00Z.`, а це все, що коли-небудь бачить суто текстовий клієнт. !!! warning Ніколи не повертайте заглушку на кшталт `"[Rendered UI]"` як єдиний вміст. Якщо резервний текст марний, інструмент марний для кожного суто текстового клієнта й для самої моделі. Напишіть нормальне речення. diff --git a/i18n/uk/pages/advanced/extensions.md b/i18n/uk/pages/advanced/extensions.md index 9e73f6b622..1c935aecd5 100644 --- a/i18n/uk/pages/advanced/extensions.md +++ b/i18n/uk/pages/advanced/extensions.md @@ -1,6 +1,6 @@ --- translation: - sections: [05891e7cc1938a13, b3c01a6af28c51ee, 7ffc91f5e38bdfe0, 717d3f235a8333a7, f471a13b2fe5d737, ed6af2df4b656dff] + sections: [05891e7cc1938a13, b3c01a6af28c51ee, 6eba6ce094f4417e, 125f28588c85dd7b, ddd8969b698ca11c, ed6af2df4b656dff] tool: 1 --- # Розширення {#extensions} @@ -49,7 +49,7 @@ TypeError: Stamps.identifier must be a `vendor-prefix/name` string Найменше корисне розширення — один інструмент і карта налаштувань: -```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 @@ TypeError: Stamps.identifier must be a `vendor-prefix/name` string * `settings()` — це значення, що оголошується в `capabilities.extensions["com.example/stamps"]`. Поверніть `{}` (типове значення), щоб оголосити розширення без налаштувань. * Розширення ніколи не отримує сервер. Воно оголошує свій внесок як дані; `MCPServer` їх споживає. Ніякого `self.server`, який можна було б змінювати, немає. -А `main()` — доказ: клієнт у пам'яті, під'єднаний безпосередньо до `mcp`: +Запустіть його через HTTP, а доказом буде клієнт: -```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" ``` +Кожен `server.py` на цій сторінці запускається цією командою, а кожен `client.py` працює поруч із ним: `python client.py` у другому терміналі. + ### Обслуговування власних методів {#serving-your-own-methods} Розширення може реєструвати **нові методи запитів**: власні дієслова, які обслуговуються поруч із методами специфікації: -```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 @@ TypeError: Stamps.identifier must be a `vendor-prefix/name` string ### Клієнтська сторона {#the-client-side} -`main()` у тому самому файлі — це вся клієнтська частина, обидві її половини: +Клієнт — окрема програма, і в ній обидві половини клієнтської частини: -```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)])` оголошує розширення. Оголошення стають `ClientCapabilities.extensions`: на з'єднанні версії 2026-07-28 карта подорожує в конверті `_meta` кожного запиту, тож сервер бачить її в **кожному** запиті; на з'єднанні старого покоління вона передається з рукостисканням `initialize`. Серверному коду байдуже, який саме шлях: `require_client_extension(ctx, ...)` і `ctx.session.check_client_capability(...)` читають правильне джерело в обох випадках. * Вендорські методи опускаються на один шар нижче, до `client.session.send_request(...)`; повноцінні методи в `Client` з'являються лише для дієслів специфікації. `send_request` приймає будь-який підклас `Request`, тож вендорський запит проходить як є. +* `SearchRequest` і дві моделі, які він несе, — це мережевий контракт розширення, тож клієнт оголошує їх для себе сам. Опубліковане розширення постачало б їх у пакеті, який імпортують обидві сторони. ### Перехоплення `tools/call` {#intercepting-toolscall} @@ -109,12 +116,18 @@ TypeError: Stamps.identifier must be a `vendor-prefix/name` string ## Використання клієнтського розширення {#using-a-client-extension} -**Клієнтське розширення** — той самий контракт з боку споживача: набір клієнтської поведінки за одним ідентифікатором. Передайте екземпляри в `Client(extensions=[...])` і викликайте інструменти як зазвичай: +**Клієнтське розширення** — той самий контракт з боку споживача: набір клієнтської поведінки за одним ідентифікатором. Сервер тут відповідає на `buy` не товаром, а квитанцією, яку треба погасити, — і лише клієнту, що оголосив розширення: -```python title="client.py" hl_lines="66-68" +```python title="server.py" hl_lines="22-25" --8<-- "docs_src/extensions/tutorial006.py" ``` +На клієнті передайте екземпляри в `Client(extensions=[...])` і викликайте інструменти як зазвичай: + +```python title="client.py" hl_lines="33-35" +--8<-- "docs_src/extensions/tutorial006_client.py" +``` + `call_tool("buy", ...)` повертає звичайний `CallToolResult`, як і будь-який інший виклик. Що змінило розширення: тепер сервер може відповісти на `buy` **формою результату** `receipt` замість остаточного результату, а `Receipts` доводить її до кінця (тут — погашаючи квитанцію наступним викликом), перш ніж `call_tool` поверне значення. У місці виклику нічого не змінюється. Приберіть розширення — і нічого з цього не існує: шлагбаум сервера відмовляє клієнту, який його не оголосив (помилка -32021), а заявлена форма від сервера, що обходить шлагбаум, не проходить перевірку — точно так, як специфікація вимагає для нерозпізнаного `resultType`. Вимкнено за замовчуванням, з обох кінців з'єднання. @@ -124,15 +137,15 @@ TypeError: Stamps.identifier must be a `vendor-prefix/name` string ```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")]) ``` ## Написання клієнтського розширення {#writing-a-client-extension} Успадкуйте `ClientExtension` і перевизначте лише те, що потрібно. Три види внеску, кожен із типовою реалізацією: `settings()`, `claims()` і `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" ``` * Ідентифікатор підпорядковується тій самій граматиці, що й на сервері, і перевіряється під час визначення класу. @@ -153,12 +166,18 @@ def notifications(self) -> Sequence[NotificationBinding[Any]]: ### Дієслова розширень {#extension-verbs} -Власні методи запитів розширення не потребують реєстрації на клієнті. Тип вендорського запиту успадковує `mcp.types.Request` і проходить через `client.session.send_request`, як у розділі [Обслуговування власних методів](#serving-your-own-methods). Одне доповнення: коли ключ параметрів мусить передаватися в заголовку `Mcp-Name` (специфікації розширень, як-от tasks, вимагають цього для своїх дієслів), тип запиту оголошує `name_param`: +Власні методи запитів розширення не потребують реєстрації на клієнті. Тип вендорського запиту успадковує `mcp.types.Request` і проходить через `client.session.send_request`, як у розділі [Обслуговування власних методів](#serving-your-own-methods). Візьмімо сервер, розширення якого обслуговує одне дієслово про іменоване завдання: -```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" ``` +Одне доповнення на клієнті: коли ключ параметрів мусить передаватися в заголовку `Mcp-Name` (специфікації розширень, як-от tasks, вимагають цього для своїх дієслів), тип запиту оголошує `name_param`: + +```python title="client.py" hl_lines="20-23 28-29" +--8<-- "docs_src/extensions/tutorial007_client.py" +``` + Сесія дзеркалить `params["jobId"]` у `Mcp-Name` на кожному шляху надсилання, а відсутнє значення дає гучну помилку замість того, щоб мовчки пропустити обов'язковий заголовок. ## Чого розширення не може {#what-an-extension-cannot-do} diff --git a/i18n/uk/pages/advanced/low-level-server.md b/i18n/uk/pages/advanced/low-level-server.md index 98926a312b..2e9859f886 100644 --- a/i18n/uk/pages/advanced/low-level-server.md +++ b/i18n/uk/pages/advanced/low-level-server.md @@ -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 --- # Низькорівневий Server {#the-low-level-server} @@ -36,18 +36,22 @@ translation: ### Спробуйте самі {#try-it} -Inspector тут не допоможе: `mcp dev` і `mcp run` приймають лише `MCPServer`. `Client`, що працює в пам'яті, до цього байдужий: низькорівневий `Server` він приймає так само, як і `MCPServer`: +`mcp dev` і `mcp run` приймають лише `MCPServer`, тож цей сервер доведеться запускати самостійно. Останній рядок `server.py` будує з нього звичайний ASGI-застосунок, а uvicorn його запускає: -```python title="main.py" +```console +uvicorn server:app --port 8000 +``` + +Спрямуйте Inspector або будь-який клієнт на `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 @@ asyncio.run(main()) * `result.structured_content` дорівнює `None`. Високорівневий сервер загортає `-> str` у `{"result": ...}` за вас; тут ніхто не будує того, чого не побудували ви. * `list_tools` повертає схему, яку набрали **ви**, символ у символ. У високорівневій версії на кожній властивості було `"title": "Query"`, а в корені — `"title": "search_booksArguments"`: артефакти Pydantic. Тут, якщо щось є в переданих даних, — це ви його туди поклали. +У тесті uvicorn і порт не потрібні: `Client(server)` приймає низькорівневий `Server` у тому ж процесі так само, як і `MCPServer`, — саме цей підхід описано на сторінці **[Тестування](../get-started/testing.md)**. + ## Ніхто нічого не перевіряє за вас {#nothing-is-checked-for-you} `MCPServer` відхиляє хибний аргумент ще до того, як ваша функція запуститься, перевіряючи виклик за схемою, яку сам згенерував (**[Інструменти](../servers/tools.md)**). @@ -215,4 +221,4 @@ use Server.middleware to observe or wrap initialization * `add_request_handler(method, params_type, handler)` обслуговує будь-який метод. `initialize` зарезервовано. * Можливості, які оголошує `Server`, виводяться з того, які обробники ви зареєстрували. -`Client(server)` поводився з обома серверами однаково, бо вони *і є* тим самим протоколом — у цьому й увесь сенс. Наступний шар нижче — взагалі не клас: це **[Middleware](middleware.md)**. +Клієнт поводився з обома серверами однаково, бо вони *і є* тим самим протоколом — у цьому й увесь сенс. Наступний шар нижче — взагалі не клас: це **[Middleware](middleware.md)**. diff --git a/i18n/uk/pages/advanced/pagination.md b/i18n/uk/pages/advanced/pagination.md index 01888ab4d2..fc962221a9 100644 --- a/i18n/uk/pages/advanced/pagination.md +++ b/i18n/uk/pages/advanced/pagination.md @@ -1,6 +1,6 @@ --- translation: - sections: [a9aba7a026c7bd85, ed32bda7ba9ae33a, 7e64cc5646abb91f, 22a0129ee78b3c63, d875373c06d8d2f9] + sections: [a9aba7a026c7bd85, 83e2a08b9d46a398, 9fd8a0aa384b3257, 22a0129ee78b3c63, d875373c06d8d2f9] tool: 1 --- # Пагінація {#pagination} @@ -31,9 +31,13 @@ translation: ### Спробуйте самі {#try-it} -`Client(server)` під'єднується до низькорівневого `Server` у пам'яті так само, як і до `MCPServer`. +`mcp run` приймає лише `MCPServer`, тож цей сервер доведеться обслуговувати самостійно. Останній рядок `server.py` будує з `Server` звичайний ASGI-застосунок, а uvicorn його запускає: -Викличте `list_resources()` без аргументів. Повертається десять ресурсів, від `book-1` до `book-10`, а `next_cursor` — рядок `"10"`. +```console +uvicorn server:app --port 8000 +``` + +Спрямуйте будь-який клієнт (**[Клієнт](../client/index.md)** або Inspector) на `http://localhost:8000/mcp` і викличте `list_resources()` без аргументів. Повертається десять ресурсів, від `book-1` до `book-10`, а `next_cursor` — рядок `"10"`. Передайте його назад через `list_resources(cursor="10")` — і перший ресурс уже `book-11`, а новий `next_cursor` — `"20"`. @@ -43,7 +47,7 @@ translation: Кожен метод `list_*` класу `Client` (`list_tools`, `list_resources`, `list_resource_templates`, `list_prompts`) приймає іменований параметр `cursor=`. Вичерпати список зі сторінками — це один `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 @@ translation: * Розширюйте список **до** того, як дивитися на `next_cursor`: на останній сторінці теж є ресурси. * `next_cursor is None` — це вихід. Усе інше йде прямо назад у `cursor=`, без змін. -Запустіть його `main()` — і він надрукує `100 resources`: десять сторінок по десять, зшитих циклом, який так і не дізнався, що сторінок було десять. +Поки uvicorn усе ще обслуговує `server.py`, запустіть `python client.py` у другому терміналі. Він надрукує `100 resources`: десять сторінок по десять, зшитих циклом, який так і не дізнався, що сторінок було десять. Це той самий цикл, який **[Клієнт](../client/index.md)** показує для кожного дієслова `list_*`, і проти сервера без сторінок він нічого не коштує: `next_cursor` дорівнює `None` вже в першій відповіді, і цикл виконується один раз. diff --git a/i18n/uk/pages/client/caching.md b/i18n/uk/pages/client/caching.md index 95ba40ece2..055bd21d15 100644 --- a/i18n/uk/pages/client/caching.md +++ b/i18n/uk/pages/client/caching.md @@ -1,6 +1,6 @@ --- translation: - sections: [9e7b9a1710e5aeba, b74ca4c1d2ddddee, fa8714e61bf90c5a, 04db67a886b7271c, 857690fb8f876800] + sections: [9e7b9a1710e5aeba, 66a2e9acc9101d54, d3d25aa802f5145a, 04db67a886b7271c, 857690fb8f876800] tool: 1 --- # Підказки щодо кешування {#caching-hints} @@ -30,7 +30,7 @@ translation: На низькорівневому `Server` обробники будують результати вручну, а `ttl_ms` / `cache_scope` — звичайні поля моделей результатів. Обробник, який задає їх явно, завжди перемагає мапу конструктора, поле за полем: -```python title="server.py" hl_lines="10 16" +```python title="server.py" hl_lines="11 17" --8<-- "docs_src/caching/tutorial002.py" ``` @@ -44,10 +44,24 @@ translation: У сесії 2026-07-28 `Client` дотримується підказок за вас: він має вбудований кеш відповідей, увімкнений за замовчуванням. Результат, що прийшов із `ttlMs`, зберігається, і ідентичний виклик у межах цього TTL обслуговується з кешу без звернення до сервера. Результат *без* підказки не кешується: результати без підказок отримують `CacheConfig.default_ttl_ms`, типове значення якого `0` (одразу застарілий), тож сервер, який нічого не оголошує, бачить рівно той самий трафік виклик-за-викликом, що й завжди. -```python title="client.py" hl_lines="33 35 38" +Щоб побачити це на власні очі, запустіть `server.py` з попереднього розділу через uvicorn (його останній рядок будує ASGI-застосунок). Обробник друкує рядок щоразу, коли справді виконується: + +```console +uvicorn server:app --port 8000 +``` + +```python title="client.py" hl_lines="20 23 28" --8<-- "docs_src/caching/tutorial003.py" ``` +Запустіть `python client.py` у другому терміналі. Він надрукує підказки, які містив перший результат: `ttlMs` обробника поруч із `cacheScope` з мапи: + +```text +1000 public +``` + +Решту розповідає термінал сервера: між логами запитів uvicorn рядок `tools/list served` з'являється тричі. + Чотири виклики, три отримання. Другий виклик знайшов свіжий запис і не дійшов до сервера; переведення (впровадженого) годинника за межу TTL змусило третій знову звернутися до сервера; четвертий сказав `cache_mode="refresh"`. Цей іменований аргумент є в п'яти методів із кешуванням (`list_tools`, `list_prompts`, `list_resources`, `list_resource_templates`, `read_resource`): * `"use"` (за замовчуванням) віддає свіжий запис, якщо він є, а якщо ні — звертається до сервера і зберігає результат. @@ -56,7 +70,7 @@ translation: Одне правило стоїть над `"use"`: **виклики з `meta` завжди доходять до сервера.** Запит із заданим `meta` (токен перебігу виконання, поля трасування) очікує запиту мережею, тому за `cache_mode="use"` його обробляють як `"refresh"`: читання з кешу пропускається, а отриманий результат усе одно замінює закешований запис. `"bypass"` і явний `"refresh"` поводяться як завжди. -Щоб повністю вимкнути кешування, створіть клієнт як `Client(server, cache=None)`: кожен виклик знову стає зверненням до сервера, а `cache_mode`, хоч і приймається далі, нічого не робить. +Щоб повністю вимкнути кешування, передайте `cache=None` під час створення `Client`: кожен виклик знову стає зверненням до сервера, а `cache_mode`, хоч і приймається далі, нічого не робить. Область також дотримується автоматично: записи `"private"` прив'язані до *розділу* (partition) кешу (нижче), тоді як записи `"public"` можуть за бажанням ділитися ширше. А **сповіщення перемагають TTL** для конкретних записів, які вони називають: сповіщення `list_changed` витісняє відповідний закешований список, а `resources/updated` витісняє закешоване читання, збережене рівно під його URI, хоч би якими свіжими вони були. На з'єднанні 2026-07-28 ці сповіщення надходять потоком `subscriptions/listen`, який відкривають через `client.listen(...)`, і витіснення завершується до того, як ваш спостерігач побачить подію; докладніше — на сторінці **[Підписки](subscriptions.md)**. diff --git a/i18n/uk/pages/client/callbacks.md b/i18n/uk/pages/client/callbacks.md index 73dc532851..912a32a0ca 100644 --- a/i18n/uk/pages/client/callbacks.md +++ b/i18n/uk/pages/client/callbacks.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} @@ -63,7 +63,7 @@ result.content # [TextContent(type='text', text='Card issued to Ada Lovelace.') `mode="legacy"` у виклику `Client(...)` стоїть не просто так. За замовчуванням `Client(...)` узгоджує сучасний шлях протоколу, а на ньому немає зворотного каналу (back-channel) для запитів від сервера до клієнта: `ctx.elicit` завершується помилкою ще до того, як запуститься колбек. Вирішує це не транспорт, а узгоджений - протокол — однаково і в пам'яті, і за URL. Фіксуйте `mode="legacy"` щоразу, коли клієнт має + протокол. Фіксуйте `mode="legacy"` щоразу, коли клієнт має відповідати на такий запит; так робить кожен тест за цією сторінкою. Докладніше — на сторінці **[Версії протоколу](../protocol-versions.md)**. У сесії 2026-07-28 колбек не зникає, він просто отримує дані інакше: коли інструмент повертає @@ -151,4 +151,4 @@ result.structured_content # {'result': ['elicitation']} * `sampling_callback` і `list_roots_callback` працюють так само, але обслуговують застарілі можливості; сучасні сервери натомість використовують багатораундові запити. * `logging_callback` і `message_handler` отримують сповіщення. Вони нічого не оголошують. -Перший аргумент `Client(...)` — об'єкт транспорту. Усі його різновиди описано на сторінці **[Транспорти клієнта](transports.md)**. +Перший аргумент `Client(...)` визначає транспорт. Усі його різновиди описано на сторінці **[Транспорти клієнта](transports.md)**. diff --git a/i18n/uk/pages/client/index.md b/i18n/uk/pages/client/index.md index 91a410624b..4fc9f8faa6 100644 --- a/i18n/uk/pages/client/index.md +++ b/i18n/uk/pages/client/index.md @@ -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 --- # Клієнт {#the-client} @@ -11,13 +11,23 @@ translation: ## Перший клієнт {#your-first-client} -```python title="client.py" hl_lines="14-18" +Клієнтові потрібен сервер, з яким говорити. До цього Bookshop під'єднується кожен приклад на цій сторінці. Збережіть його як `server.py` і залиште працювати через HTTP: + +```python title="server.py" --8<-- "docs_src/client/tutorial001.py" ``` -Сервер угорі потрібен лише для того, щоб було до чого під'єднатися. Клієнт — це п'ять виділених рядків. +```console +uv run mcp run server.py --transport streamable-http +``` + +Тепер сервер доступний за адресою `http://localhost:8000/mcp`. Клієнт — окрема програма. Збережіть його як `client.py` і запустіть `python client.py` у другому терміналі: -* `Client(mcp)` отримує **сам об'єкт сервера**. Це транспорт у пам'яті: без підпроцесу, без порту, без HTTP. Саме так під'єднується кожен приклад на цій сторінці й кожен тест, який ви напишете. +```python title="client.py" hl_lines="7-11" +--8<-- "docs_src/client/tutorial001_client.py" +``` + +* `Client("http://localhost:8000/mcp")` отримує **URL**, тож під'єднується через Streamable HTTP до сервера, який ви щойно запустили. * `async with` — це **життєвий цикл**. Вхід у блок під'єднує й узгоджує параметри; вихід — від'єднує. Пари `connect()` / `close()` немає, а `Client` не можна використати повторно після завершення блоку. * Усередині блоку відомості про з'єднання вже доступні як звичайні властивості. @@ -25,10 +35,10 @@ translation: `Client` приймає один позиційний аргумент і визначає транспорт за його типом: -* Екземпляр `MCPServer` (або низькорівневого `Server`): під'єднання **в межах процесу**. -* Рядок з URL (`Client("http://localhost:8000/mcp")`): Streamable HTTP, шлях для робочого розгортання. -* `StdioServerParameters`: команда, яку буде запущено як **підпроцес**; спілкування з ним іде через його stdin і stdout. +* Рядок з URL (`Client("http://localhost:8000/mcp")`): Streamable HTTP, транспорт для робочого розгортання. +* `StdioServerParameters`: команда, яку буде запущено як локальний **підпроцес**; спілкування з ним іде через його stdin і stdout. * **Транспорт**: будь-що, що можна використати як `async with ... as (read, write)`, наприклад `streamable_http_client(url, http_client=...)` навколо вашого власного HTTP-клієнта. +* Екземпляр `MCPServer` (або низькорівневого `Server`): під'єднання **в межах процесу**, без підпроцесу й без порту. Це для тестів, і на ньому побудована сторінка **[Тестування](../get-started/testing.md)**. Усе інше на цій сторінці однакове для всіх чотирьох. Заголовки, підпроцеси, тайм-аути та протокол `Transport` мають власну сторінку: **[Транспорти клієнта](transports.md)**. @@ -49,11 +59,11 @@ translation: ## Перелік інструментів {#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()` повертає `ListToolsResult`; інструменти лежать у `.tools`. Кожен із них — повне означення, яке хост передав би моделі: +`list_tools()` повертає `ListToolsResult`; інструменти лежать у `.tools`. Кожен із них — повне означення, яке хост передав би моделі. Ось перший: ```python tool.name # 'search_books' @@ -77,6 +87,8 @@ tool.description # 'Search the catalog by title or author.' Ця схема — усе, що потрібно UI, щоб показати форму аргументів, і все, що потрібно моделі, щоб сформувати коректні аргументи. +Другий інструмент, `lookup_book`, зареєстровано без `title=`, тож його `tool.title` дорівнює `None`. + !!! tip `title` необов'язковий, тож UI, що показує інструменти людині, має обирати: `title`, якщо він є, і `name`, якщо немає. `from mcp.shared.metadata_utils import get_display_name` робить саме це — @@ -86,7 +98,7 @@ tool.description # 'Search the catalog by title or author.' `call_tool(name, arguments)` запускає інструмент і повертає `CallToolResult`. -```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 @@ result.is_error # False Дії з ресурсами йдуть парами: два способи перелічити, один спосіб прочитати. -```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 @@ result.is_error # False ## Промпти {#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 @@ message.content # TextContent(type='text', text='Recommend one poetry book from Сервер з обробником автодоповнення може доповнювати аргументи промптів і шаблонів ресурсів, поки користувач друкує. -```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 @@ message.content # TextContent(type='text', text='Recommend one poetry book from Кожен метод `list_*` приймає іменований аргумент `cursor=`, а кожен результат містить `next_cursor`. Коли `next_cursor` дорівнює `None`, у вас є все. -```python title="client.py" hl_lines="22-30" +```python title="client.py" hl_lines="7-15" --8<-- "docs_src/client/tutorial007.py" ``` -Цей цикл коректний для будь-якого сервера. `MCPServer` повертає все однією сторінкою, тож `next_cursor` дорівнює `None` і цикл виконується один раз — саме тому більшість коду його ніколи не пише. Сервери, що справді розбивають результати на сторінки, і правила, яким підкоряються курсори, — на сторінці **[Пагінація](../advanced/pagination.md)**. +`list_all_tools` коректна для будь-якого сервера. `MCPServer` повертає все однією сторінкою, тож `next_cursor` дорівнює `None` і цикл виконується один раз — саме тому більшість коду його ніколи не пише. Сервери, що справді розбивають результати на сторінки, і правила, яким підкоряються курсори, — на сторінці **[Пагінація](../advanced/pagination.md)**. ## У тестах {#in-tests} -`Client(mcp)` без процесу й без порту — це вже тестова обв'язка для вашого сервера. +Кожен `client.py` на цій сторінці звертався до `server.py` через HTTP. У тесті мережу оминають і передають у `Client` сам об'єкт сервера: `from server import mcp`, потім `Client(mcp)`. Без процесу, без порту, а всі методи вище працюють так само. -Саме для цього є один прапорець конструктора: `Client(mcp, raise_exceptions=True)`. Він діє лише на з'єднаннях у пам'яті, а пояснює його й будує навколо нього весь підхід сторінка **[Тестування](../get-started/testing.md)**. +Саме для цього є один прапорець конструктора: `Client(mcp, raise_exceptions=True)`. Він діє лише на з'єднаннях у межах процесу, а пояснює його й будує навколо нього весь підхід сторінка **[Тестування](../get-started/testing.md)**. ## Підсумки {#recap} -* `Client(x)` під'єднується в пам'яті до об'єкта сервера, через Streamable HTTP — до рядка з URL і через транспорт — до всього іншого. +* `Client(x)` під'єднується через Streamable HTTP до рядка з URL, запускає підпроцес для `StdioServerParameters`, входить у транспорт напряму, а в тестах приймає сам об'єкт сервера. * `async with` — це весь життєвий цикл. Усередині нього `server_capabilities` і `protocol_version` уже заповнені; `server_info` та `instructions` — теж, якщо сервер їх надає. * `list_tools()` дає `name`, `title`, `description` та `input_schema` кожного інструмента. * `call_tool()` повертає `content` для моделі, `structured_content` для вашого коду та `is_error`. Інструмент, що викидає виняток, — це результат, а не виняток. diff --git a/i18n/uk/pages/client/oauth-clients.md b/i18n/uk/pages/client/oauth-clients.md index e39d21846b..bf3e8726b3 100644 --- a/i18n/uk/pages/client/oauth-clients.md +++ b/i18n/uk/pages/client/oauth-clients.md @@ -1,6 +1,6 @@ --- translation: - sections: [c6899d3892bd9fa0, 79372cff3cc48a88, 63878d29e87c3e73, 13175843d3588af4, e7e2b9fd516f77de, 758f06399b513c1f, a05d7278487d610b] + sections: [c6899d3892bd9fa0, 79372cff3cc48a88, c2dae1ebe2ebd543, 13175843d3588af4, df06056fb16b3846, 758f06399b513c1f, a05d7278487d610b] tool: 1 --- # OAuth-клієнти {#oauth-clients} @@ -81,18 +81,20 @@ translation: Коли `Client` уперше надсилає запит, сервер відповідає `401`. Далі справу бере на себе провайдер: -1. **Виявлення.** Він читає заголовок `WWW-Authenticate`, завантажує Protected Resource Metadata сервера з `/.well-known/oauth-protected-resource`, дізнається, який сервер авторизації захищає цей ресурс, і завантажує метадані *того* сервера. +1. **Виявлення.** Він читає заголовок `WWW-Authenticate`, завантажує Protected Resource Metadata сервера з `/.well-known/oauth-protected-resource`, дізнається, який сервер авторизації захищає цей ресурс, і завантажує метадані *того* сервера. (Старіший сервер, що не публікує метаданих ресурсу, натомість запитують про метадані сервера авторизації за його власним origin.) У будь-якому разі метадані мають називати своїм `issuer` той сервер, для якого їх завантажено; усе інше відхиляється. 2. **Реєстрація.** У сховищі порожньо? Він динамічно реєструє вас із вашими `OAuthClientMetadata` і зберігає результат. 3. **Авторизація.** Він генерує пару PKCE і `state`, будує URL авторизації, очікує ваш `redirect_handler`, а потім — ваш `callback_handler`, щоб отримати код. 4. **Обмін.** Він міняє код на `OAuthToken`, зберігає його й повторює ваш початковий запит із `Authorization: Bearer ...`. Після цього все тихо. Токени беруться зі сховища, прострочений токен доступу оновлюється за допомогою токена оновлення, і лише коли ніщо з цього не спрацьовує, провайдер запускає потік знову. +Для всіх цих запитів діє одне транспортне правило: як і MCP-запит, усередині якого вони виконуються, вони переходять за перенаправленням лише тоді, коли воно лишається на тому самому origin і зберігає метод (скажімо, 307/308 на кінцеву скісну риску), а будь-яке інше перенаправлення трактують так, ніби цей URL не відповідає. + Нічого з цього ви не писали. Лишаються ще два іменовані аргументи (`client_metadata_url` і `validate_resource_url`), і цьому файлу не потрібен жоден. Знати варто про `client_metadata_url`; йому присвячено окремий розділ нижче. ### Спробуйте самі {#try-it} -Більшість прикладів у цій документації можна перевірити за допомогою `Client(server)` у пам'яті. Цей — ні: уся суть потоку в HTTP-відповіді `401`, а між клієнтом у пам'яті та його сервером HTTP немає. +`Client(server)` у пам'яті, яким користуються ваші тести, тут не допоможе: уся суть потоку в HTTP-відповіді `401`, а між клієнтом у пам'яті та його сервером HTTP немає. У репозиторії є жива версія. `examples/servers/simple-auth/` запускає окремий сервер авторизації та захищений MCP-сервер; `examples/clients/simple-auth-client/` — це клієнт із цієї сторінки, що виріс у невеликий CLI. У його README є дві команди: запустіть сервери, запустіть клієнт проти них — і спостерігайте, як проходять усі чотири кроки. @@ -110,13 +112,14 @@ URL має бути HTTPS із некореневим шляхом; усе ін `ClientCredentialsOAuthProvider` — це той самий `httpx2.Auth`, тільки без людини: -```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" ``` Що змінилося: * Немає `OAuthClientMetadata`, немає обробників. Ви передаєте `client_id` і `client_secret`; провайдер будує навколо них мінімальну реєстрацію `client_credentials` і повністю пропускає динамічну реєстрацію. +* `issuer` називає сервер авторизації, що видав ці облікові дані; використовуйте значення `issuer`, яке повертає його документ `/.well-known/oauth-authorization-server`. Виявлення й далі виконується, як описано вище, але запити на токен будуються лише з метаданих *цього* видавця; якщо MCP-сервер указує кудись інде, потік натомість зупиняється з `OAuthFlowError`. Пропускати його — застаріла практика, і у версії 3.0 він стане обов'язковим (див. **[Застарілі можливості](../deprecated.md#deprecated-sdk-helpers)**); доти провайдер попереджає й використовує той сервер авторизації, який знайде виявлення. * `scope` — це рядок, розділений пробілами: такий формат OAuth використовує в переданих даних. * Усе далі ідентичне: той самий `TokenStorage`, той самий `httpx2.AsyncClient(auth=...)`, той самий `streamable_http_client`. @@ -129,7 +132,7 @@ URL має бути HTTPS із некореневим шляхом; усе ін Ще один провайдер живе в `mcp.client.auth.extensions.client_credentials`: **`PrivateKeyJWTOAuthProvider`** — для клієнтів, які автентифікуються за допомогою JWT замість спільного секрету (`private_key_jwt`, варіант із парою ключів та workload identity). Він дотримується - тієї ж схеми: створіть екземпляр і передайте в `auth=`. У тому ж модулі є + тієї ж схеми: створіть екземпляр (він приймає той самий необов'язковий `issuer`) і передайте в `auth=`. У тому ж модулі є `SignedJWTParameters` і `static_assertion_provider` — два допоміжні засоби, що будують для нього assertion. Є ще одна ситуація без людини: клієнт належить підприємству, де провайдер ідентичності, а не користувач, вирішує, до яких MCP-серверів він може звертатися. Це інший грант із власною моделлю довіри та власною сторінкою — **[Твердження про ідентичність](identity-assertion.md)**. diff --git a/i18n/uk/pages/client/session-groups.md b/i18n/uk/pages/client/session-groups.md index db219633ce..4e4f14e93f 100644 --- a/i18n/uk/pages/client/session-groups.md +++ b/i18n/uk/pages/client/session-groups.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-groups} @@ -78,7 +78,7 @@ translation: ## Підсумки {#recap} * `ClientSessionGroup` тримає багато з'єднань із серверами й зводить їхні інструменти, ресурси й промпти в один `dict` кожного виду. -* `connect_to_server(params)` для кожного сервера. Він приймає параметри транспорту й ніколи — об'єкт сервера чи URL, як `Client`. +* `connect_to_server(params)` для кожного сервера. Він приймає параметри транспорту й ніколи — URL чи `Transport`, які приймає `Client`. * `group.call_tool(name, arguments)` сам спрямовує виклик на сервер-власник. * Імена мають бути унікальними в межах усієї групи; два сервери з інструментом `search` самі по собі співіснувати не можуть. * `component_name_hook=` переписує кожне зареєстроване ім'я. Змінюється ключ словника, а не ім'я в переданих даних. diff --git a/i18n/uk/pages/client/transports.md b/i18n/uk/pages/client/transports.md index 97ed054445..17aab3a8a3 100644 --- a/i18n/uk/pages/client/transports.md +++ b/i18n/uk/pages/client/transports.md @@ -1,6 +1,6 @@ --- translation: - sections: [9cac816674181eb0, 0700f337babcd4dd, 2bde0dd58cdf00f5, 40b4916d82eaf1d4, 3d0832f39b0d7059, dfa4446556badef0, 5bd93be2ab2ecb9c] + sections: [9cac816674181eb0, 7c157764133fea1f, 40b4916d82eaf1d4, 10d151f2cc75317f, 3d0832f39b0d7059, 92742ba36533633d, 0aeca6145e7bd302] tool: 1 --- # Транспорти клієнта {#client-transports} @@ -11,30 +11,15 @@ translation: *Серверний* бік кожного з них (що робить `mcp.run()` і що ви розгортаєте) описано на сторінці **[Запуск сервера](../run/index.md)**. -## У пам'яті {#in-memory} - -Передайте сам об'єкт сервера: - -```python title="client.py" hl_lines="14" ---8<-- "docs_src/client_transports/tutorial001.py" -``` - -Жодного підпроцесу, жодного порту, жодних байтів у мережі. Клієнт і сервер — це два об'єкти в одному процесі, а виклик усе одно проходить через справжній протокольний рівень: `search_books` перелічується, валідується й викликається точнісінько так само, як це було б через HTTP. - -Тож це одразу дві речі: - -* **Тестовий стенд.** Кожен приклад у цій документації перевіряється саме так, а сторінка **[Тестування](../get-started/testing.md)** будує довкола цього весь підхід. -* **API для вбудовування.** Застосунку, який сам створює сервер, не потрібен мережевий перехід, щоб викликати його інструменти. - ## Streamable HTTP {#streamable-http} -Передайте рядок з URL — і отримаєте **Streamable HTTP**, транспорт, за яким розгортають сервер: +Передайте рядок з URL — і отримаєте **Streamable HTTP**, транспорт, за яким розгортають сервер і до якого варто звертатися насамперед: ```python title="client.py" hl_lines="5" --8<-- "docs_src/client_transports/tutorial002.py" ``` -Оце й увесь продакшен-клієнт. `Client` сам загортає URL у `streamable_http_client(...)` поверх `httpx2.AsyncClient`, налаштованого так, як потрібно MCP: `follow_redirects=True`, 30-секундний тайм-аут на connect/write/pool і 300-секундний тайм-аут на читання, бо сервер може тримати потік відповіді відкритим. +Оце й увесь продакшен-клієнт. `Client` сам загортає URL у `streamable_http_client(...)` поверх `httpx2.AsyncClient`, налаштованого так, як потрібно MCP: 30-секундний тайм-аут на connect/write/pool і 300-секундний тайм-аут на читання, бо сервер може тримати потік відповіді відкритим. !!! check Створений `Client` ще **не** під'єднаний. Конструктор лише обирає транспорт; @@ -50,7 +35,7 @@ translation: Щойно знадобиться заголовок `Authorization`, cookie, проксі, mTLS чи інший тайм-аут — створіть `httpx2.AsyncClient` самостійно й передайте його в `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 @@ translation: !!! info `httpx2` зберігає знайомий API `httpx`, тож якщо ви знаєте `httpx`, то вже вмієте робити тут автентифікацію, проксі, хуки подій, повторні спроби й обмеження з'єднань. SDK нічого не додає зверху й нічого не - забирає. Саме сюди під'єднується й OAuth: + забирає, окрім [обробки перенаправлень](#redirects). Саме сюди під'єднується й OAuth: `httpx2.AsyncClient(auth=OAuthClientProvider(...))`. Увесь цей процес описано на сторінці **[OAuth-клієнти](oauth-clients.md)**. +### Перенаправлення {#redirects} + +Транспорт під'єднується до URL, який ви йому передали, і лише до цього origin. + +* Перенаправлення `307`/`308`, що лишається в межах тієї самої схеми, хоста й порту, виконується; так само й `http://` → `https://` на тому самому хості. Це покриває звичне перенаправлення з кінцевою скісною рискою `/mcp` → `/mcp/`. +* Перенаправлення будь-куди інде **не** виконується. Виклик завершується помилкою: + + ```text + MCPError: Redirect to https://other.example.com/mcp not followed; use that URL as the endpoint if it is the intended server + ``` + + Якщо цей URL — сервер, який ви мали на увазі, впишіть його в конфігурацію. Якщо ні — сервер або проксі перед ним налаштовано неправильно. + +Це стосується будь-якого `httpx2.AsyncClient`, який ви передаєте: його налаштування `follow_redirects` для MCP-запитів не враховується — в жоден бік. OAuth-провайдери SDK застосовують те саме правило до власних запитів. + +!!! tip + `Redirect to http://… not followed: it would downgrade this HTTPS endpoint to plain HTTP` означає, що + сервер стоїть за проксі з термінацією TLS, про який не знає, і видає перенаправлення на `http://`. + Це виправляють на сервері (**[Розгортання та масштабування](../run/deploy.md#behind-a-tls-terminating-proxy)**) + або використанням точного URL `https://…/`, який підказує повідомлення. + ## stdio {#stdio} Сервер **stdio** — це підпроцес. Клієнт запускає його, пише JSON-RPC в його stdin і читає JSON-RPC з його stdout. Саме так десктопний хост запускає сервер на вашій машині: хост і *є* цим кодом плюс UI, а сторінка **[Під'єднання до справжнього хоста](../get-started/real-host.md)** показує ті самі стосунки з боку хоста — як конфігураційний файл. @@ -105,6 +111,18 @@ stderr дочірнього процесу йде у ваш. Щоб спряму Сервер, якому потрібен ключ API, там його не знайде. Передайте його явно через `env=`; ці змінні накладаються поверх списку дозволених. Саме це й робить `BOOKSHOP_API_KEY` вище. +## У пам'яті {#in-memory} + +У тесті нема чого розгортати й нема чого запускати. Передайте сам об'єкт сервера: + +```python hl_lines="14" +--8<-- "docs_src/client_transports/tutorial001.py" +``` + +Жодного підпроцесу, жодного порту, жодних байтів у мережі. Клієнт і сервер — це два об'єкти в одному процесі, а виклик усе одно проходить через справжній протокольний рівень: `search_books` перелічується, валідується й викликається точнісінько так само, як це було б через HTTP. Сторінка **[Тестування](../get-started/testing.md)** будує довкола цього весь підхід. + +Ця сама форма водночас слугує API для вбудовування: застосунок, який сам створює сервер, може викликати його інструменти без мережевого переходу. + ## SSE {#sse} `sse_client(url)` з `mcp.client.sse` — це HTTP-транспорт, на зміну якому прийшов Streamable HTTP. Загортайте його так само, `Client(sse_client("http://localhost:8000/sse"))`, щоб говорити із сервером, який досі ним користується, — і не будуйте на ньому нічого нового. @@ -113,15 +131,16 @@ stderr дочірнього процесу йде у ваш. Щоб спряму Для `Client` усе перелічене вище — одне й те саме. -**Транспорт** — це будь-який асинхронний контекстний менеджер, що повертає пару потоків повідомлень `(read, write)`: формально — протокол `Transport` у `mcp.client`. `Client` розв'язує свій аргумент за типом: об'єкт сервера під'єднується в межах процесу, `str` стає `streamable_http_client(url)`, `StdioServerParameters` стає `stdio_client(params)`, а в будь-що інше він входить безпосередньо як у транспорт. Саме завдяки останньому правилу `stdio_client(...)`, `streamable_http_client(...)` і `sse_client(...)` стають на одне й те саме місце — і саме тому можна написати власний. +**Транспорт** — це будь-який асинхронний контекстний менеджер, що повертає пару потоків повідомлень `(read, write)`: формально — протокол `Transport` у `mcp.client`. `Client` розв'язує свій аргумент за типом: `str` стає `streamable_http_client(url)`, `StdioServerParameters` стає `stdio_client(params)`, об'єкт сервера під'єднується в межах процесу, а в будь-що інше він входить безпосередньо як у транспорт. Саме завдяки останньому правилу `stdio_client(...)`, `streamable_http_client(...)` і `sse_client(...)` стають на одне й те саме місце — і саме тому можна написати власний. ## Підсумки {#recap} -* `Client(mcp)` (об'єкт сервера) під'єднується в пам'яті. Використовуйте для тестів і для вбудовування. * `Client("http://.../mcp")` (URL) під'єднується через Streamable HTTP, продакшен-транспорт. * Заголовки, автентифікація, проксі й тайм-аути належать `httpx2.AsyncClient`, який ви передаєте в `streamable_http_client(url, http_client=...)`. Іменованого аргументу `headers=` немає. +* Перенаправлення виконуються лише в межах власного origin цього URL (`307`/`308` із кінцевою скісною рискою), плюс `http`→`https` на тому самому хості. Усе інше завершується помилкою `Redirect to … not followed`; пропишіть у конфігурації кінцевий URL. * stdio — це `Client(StdioServerParameters(...))`. Загортайте його в `stdio_client(...)` самостійно лише для того, щоб перенаправити stderr дочірнього процесу. * Підпроцес отримує середовище зі списку дозволених, а не ваше; `env=` його доповнює. +* `Client(mcp)` (об'єкт сервера) під'єднується в пам'яті. Використовуйте в тестах або щоб вбудувати сервер у застосунок, який його створив. * Транспорт — це будь-що, з чим можна зробити `async with x as (read, write)`. Усе, що не є об'єктом сервера, URL чи `StdioServerParameters`, `Client` передає прямо цьому протоколу. * Створення `Client` обирає транспорт. `async with` його відкриває. diff --git a/i18n/uk/pages/deprecated.md b/i18n/uk/pages/deprecated.md index c079c51691..08e50718e0 100644 --- a/i18n/uk/pages/deprecated.md +++ b/i18n/uk/pages/deprecated.md @@ -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 --- # Застарілі можливості {#deprecated-features} -Специфікація 2026-07-28 виводить з ужитку п'ять речей. SDK і далі реалізує кожну з них, і кожна тепер супроводжується **попередженням про застарілість**. Один допоміжний метод SDK оголошено застарілим окремо, і його наведено [наприкінці сторінки](#deprecated-sdk-helpers). +Специфікація 2026-07-28 виводить з ужитку п'ять речей. SDK і далі реалізує кожну з них, і кожна тепер супроводжується **попередженням про застарілість**. Кілька речей оголошено застарілими на рівні самого SDK, незалежно від специфікації; їх наведено [наприкінці сторінки](#deprecated-sdk-helpers). Таблиця нижче називає кожну застарілу можливість, пояснює, чому вона зникає, і вказує заміну, на яку варто спиратися. @@ -136,13 +136,15 @@ warnings.filterwarnings("ignore", category=MCPDeprecationWarning) Один рядок конфігурації pytest — і застарілий виклик більше ніколи не прокрадеться назад у вашу кодову базу, не проваливши тест. -## Застарілі допоміжні методи SDK {#deprecated-sdk-helpers} +## Застарілі допоміжні засоби SDK {#deprecated-sdk-helpers} -Це не зміни специфікації, а лише внутрішні частини SDK, для яких є краща заміна. Вони попереджають тим самим `MCPDeprecationWarning` і будуть вилучені у версії 3.0. +Це не зміни специфікації, а лише способи використання SDK, для яких є краща заміна. Вони попереджають тим самим `MCPDeprecationWarning`, а версія 3.0 вилучає стару форму. | Застаріле | Що робити натомість | |---|---| | `FuncMetadata.call_fn_with_arg_validation()` | `FuncMetadata.validate_arguments()`, а потім `FuncMetadata.call_fn()`. Його викликав лише код, що працює з `FuncMetadata` безпосередньо (скажімо, власний підклас `Tool`). | +| `AuthSettings(resource_server_url=...)` без `validate_token_resource=` | Задайте його: з `True` сервер відхиляє bearer-токени, про які ваш верифікатор не повідомляє, що їх видано для `resource_server_url`; `False` означає, що верифікатор сам перевіряє аудиторію токена (див. **[Авторизація](run/authorization.md#a-token-verifier)**). Незадане значення поводиться як `False`; у версії 3.0 `True` стане типовим значенням щоразу, коли задано `resource_server_url`. | +| `ClientCredentialsOAuthProvider(...)` або `PrivateKeyJWTOAuthProvider(...)` без `issuer=` | Передайте `issuer=` з адресою сервера авторизації, який видав облікові дані (див. **[Написання OAuth-клієнтів](client/oauth-clients.md#machine-to-machine)**). Без нього MCP-сервер вирішує, який сервер авторизації їх отримає; у версії 3.0 цей іменований аргумент стане обов'язковим. | ## Підсумки {#recap} @@ -151,7 +153,7 @@ warnings.filterwarnings("ignore", category=MCPDeprecationWarning) * Застарілість має рекомендаційний характер: жодних змін у переданих даних, усе й далі працює із сесіями до 2026, а ви отримуєте помітне попередження `MCPDeprecationWarning` (це `UserWarning`, тож воно ввімкнене за замовчуванням). * Семплювання й кореневі каталоги додатково потребують зворотного каналу, якого сесія 2026-07-28 не має. На сучасному з'єднанні вони попереджають, а потім викидають виняток. * `warnings.filterwarnings("ignore", category=MCPDeprecationWarning)` приглушує всю категорію; `"error::mcp.MCPDeprecationWarning"` у pytest перетворює її на провал тесту. -* Один допоміжний метод SDK, `FuncMetadata.call_fn_with_arg_validation()`, оголошено застарілим окремо — його вилучать у версії 3.0. +* [Застарілі засоби на рівні SDK](#deprecated-sdk-helpers) підпорядковуються тому самому правилу: зараз вони попереджають, а версія 3.0 відкидає стару форму. * Новий код не варто будувати на жодній із цих можливостей. Усі інші сторінки цієї документації навчають чинного API. diff --git a/i18n/uk/pages/get-started/first-steps.md b/i18n/uk/pages/get-started/first-steps.md index 2b711b43e7..4c4fb6fce4 100644 --- a/i18n/uk/pages/get-started/first-steps.md +++ b/i18n/uk/pages/get-started/first-steps.md @@ -1,6 +1,6 @@ --- translation: - sections: [0d6c05bcbf836bf3, 59a7b14eeefc68c1, 7114d8d6daba203f, e8bbb56a98ba7bc9, 5138010f6159901c, f78da7c7c363d4c6, 220a939cab348686] + sections: [0d6c05bcbf836bf3, 9a78b5f6b44b18ab, 7114d8d6daba203f, e8bbb56a98ba7bc9, bfd2fd1153e71dac, 1615a994ef071fdd, 65c599fae991f245] tool: 1 --- # Перші кроки {#first-steps} @@ -17,7 +17,7 @@ translation: * **Клієнт** живе всередині хоста й говорить мовою MCP. Хост запускає по одному клієнту на кожен сервер, до якого під'єднаний. * **Сервер** — це те, що ви створюєте за допомогою цього SDK. Він надає речі клієнтам. Він ніколи не говорить із моделлю напряму. -Ви пишете сервер. Хости — це чийсь інший продукт. SDK також дає вам `Client`. Ним ви тестуватимете свої сервери, і він з'явиться далі на цій сторінці. +Ви пишете сервер. Хости — це чийсь інший продукт. SDK також дає вам `Client` — той самий клас, яким хост звертався б до сервера за URL або запускав би його як підпроцес. Він з'явиться далі на цій сторінці, і саме ним ви тестуватимете свої сервери. ## Три примітиви {#the-three-primitives} @@ -83,22 +83,20 @@ Inspector запустив ваш сервер через **stdio**, один і Коли клієнт під'єднується, сервер оголошує свої **можливості**: на які сімейства запитів він відповідатиме. Клієнт використовує це оголошення, щоб вирішити, про що взагалі просити. Ви його не писали; `MCPServer` оголошує його за вас. -Подивіться самі. `Client` з SDK приймає об'єкт сервера напряму й під'єднується до нього **в пам'яті** (без підпроцесу, без порту): - -```python -import asyncio - -from mcp import Client - -from server import mcp +Подивіться самі. Залиште `server.py` працювати через HTTP в одному терміналі: +```console +uv run mcp run server.py --transport streamable-http +``` -async def main() -> None: - async with Client(mcp) as client: - print(client.server_capabilities.model_dump(exclude_none=True)) +а з іншого спрямуйте на нього клієнта: +```python title="client.py" hl_lines="7-8" +--8<-- "docs_src/first_steps/tutorial001_client.py" +``` -asyncio.run(main()) +```console +python client.py ``` ```text @@ -118,8 +116,9 @@ asyncio.run(main()) Зверніть увагу на те, чого там немає. `completions` (автодоповнення аргументів для шаблонів ресурсів і промптів) потребує обробника, який пишете ви; у цього сервера його немає, тож можливість відсутня, і коректний клієнт про неї не проситиме. Це правило для всього необов'язкового: зареєструйте річ — і можливість з'явиться; **[Автодоповнення](../servers/completions.md)** це доводить. !!! info - `Client(mcp)` — той самий клієнт у пам'яті, яким протестовано кожен приклад у цій документації, і - саме ним ви тестуватимете свої. Йому присвячено цілу сторінку: **[Тестування](testing.md)**. + Цей `client.py` — уже повноцінний MCP-клієнт, і йому присвячено сторінку **[Клієнт](../client/index.md)**. + У тесті термінал і порт не потрібні: передайте `Client` сам об'єкт сервера — + `Client(mcp)`. Цьому теж присвячено цілу сторінку: **[Тестування](testing.md)**. ## Чого ви не писали {#what-you-did-not-write} @@ -128,7 +127,7 @@ asyncio.run(main()) * JSON Schema. `a: int, b: int` *і є* схема для `add`. * Обробник запитів. `tools/list`, `resources/read`, `prompts/get`: усе обслуговується за вас. * Оголошення можливостей. `MCPServer` зробив його за вас. -* Жодного рядка протоколу. Узгодження версії, обрамлення JSON-RPC, обмін можливостями: усе це відбулося всередині `mcp dev` і `Client(mcp)`, і ви цього не бачили. +* Жодного рядка протоколу. Узгодження версії, обрамлення JSON-RPC, обмін можливостями: усе це відбулося всередині `mcp dev` і `client.py`, і ви цього не бачили. У цьому співвідношенні й увесь сенс SDK. @@ -139,6 +138,6 @@ asyncio.run(main()) * Один декоратор на примітив: `@mcp.tool()`, `@mcp.resource(uri)`, `@mcp.prompt()`. Назва, опис і схема беруться з функції. * URI з `{param}` створює **шаблон** ресурсу, який показується окремо від конкретних ресурсів. * **Можливості** сервера оголошуються за вас, а клієнт просить лише те, що сервер оголосив. -* `Client(mcp)` під'єднується до об'єкта сервера в пам'яті: ваш тестовий стенд із першого дня. +* `Client("http://localhost:8000/mcp")` говорить із вашим запущеним сервером. Передайте йому натомість об'єкт сервера — `Client(mcp)` — і це ваш тестовий стенд із першого дня. Далі — **[Під'єднання до справжнього хоста](real-host.md)**: цей сервер усередині Claude Desktop або IDE, по-справжньому. Потім **[Тестування](testing.md)**: одна сторінка, один клієнт у пам'яті — і більше ніколи не доведеться гадати, чи воно працює. Після цього кожен примітив отримує власну сторінку, починаючи з того, яким керує модель: **[Інструменти](../servers/tools.md)**. diff --git a/i18n/uk/pages/get-started/testing.md b/i18n/uk/pages/get-started/testing.md index f384dcde9f..3e82e7bdb0 100644 --- a/i18n/uk/pages/get-started/testing.md +++ b/i18n/uk/pages/get-started/testing.md @@ -1,13 +1,13 @@ --- translation: - sections: ['4926721070127497', c52a1de2b6b32f40, 8e792bf8c7489ec6, 627195f7159e24ef] + sections: [5d13c2f0ba42c0d2, c52a1de2b6b32f40, 8e792bf8c7489ec6, 38552ea228b0a04f] tool: 1 --- # Тестування {#testing} -Python SDK містить клас `Client` із **транспортом у пам'яті**: передайте йому об'єкт сервера — і він під'єднається до нього напряму. +Клас `Client` із SDK — той самий, що під'єднується за URL чи запускає підпроцес, — уміє під'єднуватися й **у пам'яті**: передайте йому об'єкт сервера, і він говоритиме з ним напряму. -Жодного підпроцесу. Жодного порту. Узагалі жодного транспорту. Та сама ідея, що й `TestClient` у FastAPI. +Жодного підпроцесу. Жодного порту. Нічого не передається мережею. Та сама ідея, що й `TestClient` у FastAPI. ## Базове використання {#basic-usage} @@ -96,12 +96,12 @@ async def test_call_add_tool(client: Client): Залишайте його ввімкненим у тестах. У робочому коді він не має сенсу. -## У тому самому процесі за замовчуванням {#in-process-by-default} +## Нейтральність щодо покоління за замовчуванням {#era-neutral-by-default} !!! note `Client(mcp)` під'єднується в межах процесу й за замовчуванням **нейтральний щодо покоління**: він зондує сервер і - обирає відповідний шлях протоколу. Зафіксуйте `mode="legacy"`, якщо тест перевіряє семантику, специфічну для - старого покоління — push семплювання (sampling) чи еліцитації (elicitation), `message_handler`, — і приберіть там `raise_exceptions=True`: + обирає відповідний шлях протоколу. Зафіксуйте `mode="legacy"`, якщо тест перевіряє семантику, властиву саме + старому поколінню — push семплювання (sampling) чи еліцитації (elicitation), `message_handler`, — і приберіть там `raise_exceptions=True`: з'єднання старого покоління взагалі нічого не узагальнює, а прапорець повторно викидає збій усередині завдання сервера, а не у вашому тесті. diff --git a/i18n/uk/pages/handlers/multi-round-trip.md b/i18n/uk/pages/handlers/multi-round-trip.md index 68c6ba20ce..d8a0cd07a1 100644 --- a/i18n/uk/pages/handlers/multi-round-trip.md +++ b/i18n/uk/pages/handlers/multi-round-trip.md @@ -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-round-trip-requests} @@ -164,7 +164,7 @@ TTL, прив'язка до принципала й прив'язка до за ## Результат версії 2026-07-28 {#a-2026-07-28-result} -`InputRequiredResult` існує лише у версії протоколу **2026-07-28**. `Client(server)` у пам'яті узгоджує її за вас; мережею її виявляє `mode="auto"`. Після під'єднання `client.protocol_version` покаже, що саме ви отримали. +`InputRequiredResult` існує лише у версії протоколу **2026-07-28**. Режим `mode="auto"`, типовий для `Client`, виявляє її на будь-якому з'єднанні. Після під'єднання `client.protocol_version` покаже, що саме ви отримали. !!! warning У сесії, старшій за 2026, `InputRequiredResult` просто нікуди покласти. Поверніть його з обробника на diff --git a/i18n/uk/pages/handlers/progress.md b/i18n/uk/pages/handlers/progress.md index e6d9f4cb19..7e4a22db55 100644 --- a/i18n/uk/pages/handlers/progress.md +++ b/i18n/uk/pages/handlers/progress.md @@ -1,6 +1,6 @@ --- translation: - sections: [5315262fe26b33e1, 9d8e98840f1b78f0, 0284b215e85366c4, 8534d8dbb4053a70, 2966fac6fe697007] + sections: [5315262fe26b33e1, 9d8e98840f1b78f0, 52d6009a07e770ea, 8534d8dbb4053a70, 2966fac6fe697007] tool: 1 --- # Перебіг виконання {#progress} @@ -29,19 +29,17 @@ translation: Клієнт підписується **окремо для кожного виклику**, передаючи `progress_callback=` у `call_tool`: -```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) Колбек — це `async`-функція, яка приймає рівно те, що повідомив сервер: `progress`, `total`, `message`. !!! info - `Client(mcp)` під'єднується безпосередньо до об'єкта сервера, у пам'яті, — це той самий клієнт, на якому - побудована сторінка **[Тестування](../get-started/testing.md)**. Параметр `progress_callback` однаковий - незалежно від транспорту, який використовує `Client`; а от *хронометраж*, який ви зараз побачите, - властивий саме з'єднанню в пам'яті. Воно запускає колбек одразу на місці, тож кожен звіт надходить до - того, як `call_tool` поверне результат. На справжньому транспорті сповіщення змагаються з результатом, - і повільний колбек може ще виконуватися після того, як `call_tool` уже повернув результат. + Параметр `progress_callback` однаковий незалежно від того, що ви передали в `Client`: URL, як тут, + `StdioServerParameters` чи об'єкт сервера в тесті. Проте зважайте на хронометраж на справжньому + транспорті. Кожне сповіщення доставляється окремо, поруч із відповіддю, тож повільний колбек може + ще виконуватися після того, як `call_tool` уже повернув результат. Лише тестове з'єднання в межах + процесу запускає колбек одразу на місці й гарантує, що кожен звіт надійде раніше. ### Спробуйте самі {#try-it} -Покладіть `client.py` поруч із `server.py` і запустіть: +Запустіть `server.py` через HTTP, а потім запустіть клієнт у другому терміналі: + +```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.'} ``` -Кожен `await ctx.report_progress(...)` на сервері перетворився на один виклик `show` на клієнті, у тому самому порядку, і обидва рядки надрукувалися **до** того, як `call_tool` повернув результат. Перебіг не пакується в результат — він надходить потоком, поки інструмент іще працює. +Кожен `await ctx.report_progress(...)` на сервері перетворився на один виклик `show` на клієнті, у тому самому порядку. Перебіг не пакується в результат. Він надходить потоком, поки інструмент іще працює. !!! warning `progress_callback` належить **виклику**, а не `Client`. Аргументу конструктора для нього немає, diff --git a/i18n/uk/pages/protocol-versions.md b/i18n/uk/pages/protocol-versions.md index fcbb234723..bd5202a263 100644 --- a/i18n/uk/pages/protocol-versions.md +++ b/i18n/uk/pages/protocol-versions.md @@ -1,6 +1,6 @@ --- translation: - sections: [478fd619e5f90ef8, aef094a00e44e248, bab8cbf3449fa7e9, df1809b15a58335b, 5f9d8c2336ed0239, f54974398e43ddef, b24443dd78584870] + sections: [d98e337bf28845e0, dd642acbba36f615, f547b3e76da19c94, 149513378588da5c, 4e149ed992046709, f54974398e43ddef, b24443dd78584870] tool: 1 --- # Версії протоколу {#protocol-versions} @@ -11,9 +11,17 @@ translation: Перейматися цим майже ніколи не доводиться, бо `Client` домовляється за вас. Ця сторінка — про єдиний аргумент конструктора, який цим керує, `mode=`, і про три випадки, коли його змінюють. +Кожен фрагмент на цій сторінці — це `client.py`, що розмовляє з `server.py` книгарні Bookshop зі сторінки **[Клієнт](client/index.md)**. Запустіть цей сервер в одному терміналі: + +```console +uv run mcp run server.py --transport streamable-http +``` + +Потім запускайте кожен фрагмент у другому терміналі командою `python client.py`. + ## `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 @@ translation: Оце й уся можливість. Один `Client`, сервер будь-якого покоління, жодних розгалужень у вашому коді. !!! info - `MCPServer` відповідає на `server/discover` на кожному транспорті — in-memory, stdio, streamable - HTTP — тож із власним сервером `auto` завжди зупиняється на `2026-07-28`. Запасний шлях - спрацьовує лише проти справжнього сервера, випущеного до 2026, — саме тоді, коли він і потрібен. + `MCPServer` відповідає на `server/discover` на кожному транспорті — Streamable HTTP, stdio та + внутрішньопроцесному з'єднанні, яким користуються ваші тести, — тож із власним сервером `auto` + завжди зупиняється на `2026-07-28`. Запасний шлях спрацьовує лише проти справжнього сервера, + випущеного до 2026, — саме тоді, коли він і потрібен. ## `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 @@ translation: `mode` також приймає рядок сучасної версії протоколу. Сьогодні ця множина — рівно `["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 @@ ValueError: mode must be 'legacy', 'auto', or one of ['2026-07-28']; got '2025-0 Тож збережіть її. Після з'єднання в режимі `auto` `client.session.discover_result` містить точний `DiscoverResult`, який надіслав сервер: його `supported_versions`, `capabilities`, `instructions` і відомості про себе, які сервер записав у `_meta` результату. Наступного разу передайте його назад як `prior_discover=`: -```python title="client.py" hl_lines="15 17" +```python title="client.py" hl_lines="8 10" --8<-- "docs_src/protocol_versions/tutorial004.py" ``` diff --git a/i18n/uk/pages/run/asgi.md b/i18n/uk/pages/run/asgi.md index 8525330fea..84e57d8707 100644 --- a/i18n/uk/pages/run/asgi.md +++ b/i18n/uk/pages/run/asgi.md @@ -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 --- # Додавання до наявного застосунку {#add-to-an-existing-app} @@ -99,7 +99,7 @@ uvicorn server:app --8<-- "docs_src/asgi/tutorial004.py" ``` -Тепер клієнти під'єднуються до `/notes`, а не до `/notes/mcp`. +Тепер клієнти під'єднуються до `/notes/`, а не до `/notes/mcp`. ## CORS для браузерних клієнтів {#cors-for-browser-clients} @@ -142,4 +142,4 @@ uvicorn server:app * Браузерним клієнтам потрібен CORS: `allow_headers` для заголовків запиту `Mcp-*`, `expose_headers=["Mcp-Session-Id"]` для відповіді. * `@mcp.custom_route()` додає звичайні HTTP-кінцеві точки без автентифікації поруч із `/mcp`. -Щойно сервер стане досяжним за справжньою URL-адресою, **[Клієнт](../client/index.md)** під'єднається до нього за цією URL-адресою, а не через об'єкт сервера. +Щойно сервер стане досяжним за справжньою URL-адресою, **[Клієнт](../client/index.md)** під'єднається до нього за цією URL-адресою. diff --git a/i18n/uk/pages/run/authorization.md b/i18n/uk/pages/run/authorization.md index d293bd321c..250704df01 100644 --- a/i18n/uk/pages/run/authorization.md +++ b/i18n/uk/pages/run/authorization.md @@ -1,60 +1,64 @@ --- translation: - sections: [d62c13457fc4a534, 80e73abaca6e0652, d1dc4c54cd00ec9c, 14ad3bc7904036bb, 5225f127bc1b9c77, fe1626fdd5aad1da, 4556cb7ea1a04a31] + sections: [d62c13457fc4a534, 80e73abaca6e0652, 128a492d18295f64, 14ad3bc7904036bb, 54a6697833fcc1d9, fe1626fdd5aad1da, 811d083c1da8bcf6] tool: 1 --- # Авторизація {#authorization} -Через Streamable HTTP ваш MCP-сервер — це звичайний вебсервіс, і захищати його слід так само, як будь-який інший вебсервіс: bearer-токенами OAuth 2.1. +Через Streamable HTTP ваш MCP-сервер — це звичайний вебсервіс, і захищати його треба так само, як будь-який вебсервіс: bearer-токенами OAuth 2.1. -У термінах OAuth ваш сервер — це **сервер ресурсів** (resource server). Він ніколи нікого не автентифікує й ніколи не видає токенів. Він робить одне: дивиться на заголовок `Authorization` у кожному запиті й вирішує, чи придатний токен у ньому. +У термінах OAuth ваш сервер — це **сервер ресурсів**. Він нікого не автентифікує і ніколи не видає токенів. Він робить одне: дивиться на заголовок `Authorization` кожного запиту й вирішує, чи дійсний токен у ньому. -Ця сторінка — про серверний бік. Клієнт, який знаходить ваш сервер авторизації й отримує токен, описано на сторінці **[Клієнти OAuth](../client/oauth-clients.md)**. +Ця сторінка — про серверну частину. Клієнт, який знаходить ваш сервер авторизації й отримує токен, описаний на сторінці **[OAuth-клієнти](../client/oauth-clients.md)**. ## Три сторони {#the-three-parties} -* **Сервер авторизації** автентифікує людей і видає токени доступу. Ви його не пишете. Це ваш постачальник ідентичності (Auth0, Keycloak, Entra, ваш власний). +* **Сервер авторизації** автентифікує людей і видає токени доступу. Його ви не пишете. Це ваш постачальник ідентичності (Auth0, Keycloak, Entra, власний). * **Сервер ресурсів** — це ваш MCP-сервер. Він перевіряє токен у кожному запиті. * **Клієнт** з'ясовує, якому серверу авторизації ви довіряєте, отримує від нього токен і надсилає його вам як `Authorization: Bearer `. -Оце й увесь трикутник. Усе на цій сторінці стосується середнього пункту. +Оце й увесь трикутник. Усе на цій сторінці — про середній пункт. ## Верифікатор токенів {#a-token-verifier} -SDK не має власної думки про те, який вигляд має дійсний токен. Це визначаєте ви, реалізувавши **`TokenVerifier`**: +SDK не має власної думки про те, який токен вважати дійсним. Це визначаєте ви, реалізуючи **`TokenVerifier`**: -```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` — це протокол з одним асинхронним методом. `verify_token` отримує сирий токен із заголовка `Authorization` і повертає **`AccessToken`**, якщо він дійсний, або `None`, якщо ні. Більше нічого реалізовувати не треба. -* Цей верифікатор шукає токен у таблиці. Справжній перевіряє підпис JWT або звертається до кінцевої точки інтроспекції токенів сервера авторизації. Цей код — ваш; SDK лише викликає його. -* `token_verifier=` і `auth=` завжди йдуть разом. Передайте одне без іншого — і `MCPServer(...)` викине `ValueError` ще до того, як обслужить хоч один запит. +* `TokenVerifier` — це протокол з одним асинхронним методом. `verify_token` отримує сирий токен із заголовка `Authorization` і повертає **`AccessToken`**, якщо токен дійсний, або `None`, якщо ні. Більше нічого реалізовувати не потрібно. +* Цей шукає токен у таблиці; кожен запис фіксує ресурс, для якого токен видано. Справжній перевіряє підпис JWT або викликає кінцеву точку інтроспекції токенів сервера авторизації і повідомляє, для кого видано токен (його `aud`), у `AccessToken.resource`. Цей код — ваш; SDK його лише викликає. +* `token_verifier=` і `auth=` завжди йдуть у парі. Передайте одне без іншого — і `MCPServer(...)` викине `ValueError`, ще не обслуживши жодного запиту. -`AuthSettings` — це публічне обличчя вашого сервера ресурсів: +`AuthSettings` — публічне обличчя вашого сервера ресурсів: * `issuer_url`: сервер авторизації, що видає ваші токени. -* `resource_server_url`: публічний URL цієї MCP-кінцевої точки. Він указує, для *якого* ресурсу призначено токен, і саме тут розміщено документ виявлення. +* `resource_server_url`: публічна URL-адреса цієї кінцевої точки MCP. Вона вказує, для *якого* ресурсу призначено токен, і саме за нею лежить документ виявлення. * `required_scopes`: кожен токен мусить містити їх усі. +* `validate_token_resource`: відхиляти будь-який токен, чий `AccessToken.resource` не дорівнює `resource_server_url`. Якщо залишити його незаданим, коли `resource_server_url` задано, з'являється попередження (`MCPDeprecationWarning`), а поведінка відповідає `False`; у 3.0 типовим значенням для серверів ресурсів стане `True`. + * Увімкніть його, якщо ваш сервер авторизації прив'язує токени до `resource`, який запросив клієнт, — а MCP-клієнти завжди його надсилають. `resource_server_url` має бути точно тією URL-адресою, до якої під'єднуються клієнти. + * Залиште вимкненим, якщо ваш сервер авторизації використовує власні ідентифікатори аудиторії (ідентифікатор API в Auth0, ідентифікатор застосунку в Entra), і натомість перевіряйте `aud` у своєму верифікаторі, повертаючи `None` для токена, що не призначений цьому серверу. + * Якщо `aud` — список, покладіть у `resource` той елемент, що дорівнює `resource_server_url`. !!! tip - `examples/servers/simple-auth/` у репозиторії SDK містить `IntrospectionTokenVerifier`, який звертається - до кінцевої точки [RFC 7662](https://datatracker.ietf.org/doc/html/rfc7662) справжнього сервера авторизації. Саме таку форму мають більшість продакшн-верифікаторів. + У `examples/servers/simple-auth/` у репозиторії SDK є `IntrospectionTokenVerifier`, який викликає + кінцеву точку [RFC 7662](https://datatracker.ietf.org/doc/html/rfc7662) справжнього сервера авторизації. Саме таку форму має більшість верифікаторів у реальних розгортаннях. -## Що з'являється через HTTP {#what-you-get-over-http} +## Що ви отримуєте через HTTP {#what-you-get-over-http} -Авторизація живе в HTTP-заголовках, тож існує лише на HTTP-транспортах. Запустіть сервер на тому, який розгортаєте: `mcp.run(transport="streamable-http")` розміщує його на `http://127.0.0.1:8000/mcp`, а решта — на сторінці **[Запуск сервера](index.md)**. Тепер застосунок має два маршрути: +Авторизація живе в HTTP-заголовках, тож існує лише на HTTP-транспортах. Запускайте її на тому, який розгортаєте: `mcp.run(transport="streamable-http")` піднімає сервер на `http://127.0.0.1:8000/mcp`, а решту описано на сторінці **[Запуск сервера](index.md)**. Тепер застосунок має два маршрути: ```text /mcp /.well-known/oauth-protected-resource/mcp ``` -Ви зареєстрували один інструмент. Другий маршрут належить SDK. +Ви зареєстрували один інструмент. Другий маршрут — від SDK. ### Виявлення {#discovery} -Зробіть `GET` на цей well-known-шлях — і отримаєте **[RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728) Protected Resource Metadata**, побудовані безпосередньо з вашого `AuthSettings`: +Зробіть `GET` на цей well-known шлях — і отримаєте **[RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728) Protected Resource Metadata**, побудовані прямо з ваших `AuthSettings`: ```json { @@ -65,7 +69,7 @@ SDK не має власної думки про те, який вигляд м } ``` -Саме завдяки цьому документу клієнт, який ніколи не чув про ваш сервер, знаходить дорогу: він читає `authorization_servers` і йде туди по токен. Ви не написали з нього жодного рядка. +Саме завдяки цьому документу клієнт, який ніколи не чув про ваш сервер, знаходить шлях усередину: він читає `authorization_servers` і йде туди по токен. Ви не написали з нього жодного рядка. !!! check Зверніться до `/mcp` без токена (або з таким, для якого ваш верифікатор повернув `None`) — і запит @@ -78,26 +82,26 @@ SDK не має власної думки про те, який вигляд м {"error": "invalid_token", "error_description": "Authentication required"} ``` - Нічого не було розібрано, і жоден інструмент не виконався. А вказівник `resource_metadata` у `WWW-Authenticate` — - саме те, що робить виявлення автоматичним: 401 -> документ метаданих -> сервер авторизації -> токен -> повторна спроба. + Нічого не розібрано, жоден інструмент не виконувався. А вказівник `resource_metadata` у `WWW-Authenticate` — + це те, що робить виявлення автоматичним: 401 -> документ метаданих -> сервер авторизації -> токен -> повторна спроба. !!! warning - Ніщо з цього не захищає `stdio`. Канал не має заголовка `Authorization`, тож `token_verifier` там ніколи - не викликається. Межа безпеки `stdio`-сервера — це процес, який його запустив. Те саме - стосується `Client(mcp)` у пам'яті, який ви використовуєте в тестах: він під'єднується безпосередньо до об'єкта сервера - й оминає HTTP-рівень, разом з авторизацією. + Нічого з цього не захищає `stdio`. Канал (pipe) не має заголовка `Authorization`, тож `token_verifier` там ніколи + не викликається. Межа безпеки `stdio`-сервера — процес, який його запустив. Те саме + стосується `Client(mcp)` у пам'яті, який використовують у тестах: він під'єднується безпосередньо до об'єкта сервера + й оминає HTTP-рівень разом з авторизацією. ## Ідентичність того, хто викликає {#the-callers-identity} Усередині будь-якого обробника **`get_access_token()`** — це `AccessToken`, який ваш верифікатор повернув для поточного запиту: -```python title="server.py" hl_lines="4 32-35" +```python title="server.py" hl_lines="4 35-38" --8<-- "docs_src/authorization/tutorial002.py" ``` -* Це працює в інструментах, ресурсах і промптах, і нічого нікуди передавати не треба: middleware авторизації зберігає його в контекстній змінній для кожного запиту. -* Повертається **той самий об'єкт, який побудував ваш верифікатор**: `client_id`, `scopes`, `subject`, `expires_at` і будь-які додаткові `claims`, які ви додали. Це й є зачіпка для правил на рівні окремих інструментів: прочитайте scopes і відмовте. -* Поза автентифікованим HTTP-запитом він повертає `None`. У пам'яті й через `stdio` це завжди `None`. +* Це працює в інструментах, ресурсах і промптах, і нічого не треба передавати вручну: middleware авторизації зберігає його в контекстній змінній для кожного запиту. +* Повертається **той самий об'єкт, що його побудував ваш верифікатор**: `client_id`, `scopes`, `subject`, `expires_at` і будь-які додаткові `claims`, які ви додали. Це і є зачіпка для правил на рівні окремих інструментів: прочитайте scopes і відмовте. +* Поза автентифікованим HTTP-запитом функція повертає `None`. У пам'яті та через `stdio` це завжди `None`. Викличте `whoami` з `Authorization: Bearer alice-token` — і модель прочитає: @@ -109,22 +113,22 @@ alice (scopes: notes:read) SDK дає вам половину сервера ресурсів: перевірити, оголосити, відмовити. Він не дає сторінки входу, екрана згоди чи токена. -Щоб побачити всі три сторони в русі, запустіть `examples/servers/simple-auth/` з репозиторію SDK (невеликий сервер авторизації та сервер ресурсів, налаштований точно як на цій сторінці), а потім спрямуйте на нього `examples/clients/simple-auth-client/`, щоб пройти повний шлях виявлення й отримання токена. +Щоб побачити всі три сторони в русі, запустіть `examples/servers/simple-auth/` з репозиторію SDK (невеликий сервер авторизації та сервер ресурсів, налаштований точно як на цій сторінці), а потім спрямуйте на нього `examples/clients/simple-auth-client/` — і пройдете весь шлях від виявлення до токена. !!! info - Є другий аргумент конструктора, `auth_server_provider=`, який вбудовує повноцінний сервер - авторизації всередину вашого MCP-сервера. Він з'явився ще до розділення AS/RS, навколо якого - побудовано специфікацію авторизації MCP. Новим серверам не слід до нього вдаватися. + Є ще другий аргумент конструктора, `auth_server_provider=`, який вбудовує повноцінний сервер авторизації + всередину вашого MCP-сервера. Він з'явився раніше за розділення AS/RS, навколо якого побудована специфікація + авторизації MCP. У нових серверах його використовувати не слід. -Сервер авторизації також може прийняти підписане твердження корпоративного постачальника ідентичності замість того, щоб користувач проходив екран згоди, і SDK підтримує обидва боки цього обміну. Про цей grant і клієнта, що його пред'являє, — на сторінці **[Твердження ідентичності](../client/identity-assertion.md)**. +Сервер авторизації може також прийняти підписане твердження від корпоративного постачальника ідентичності замість того, щоб користувач проклацував екран згоди, і SDK підтримує обидві сторони цього обміну. Цей грант і клієнт, що його пред'являє, описані на сторінці **[Твердження ідентичності](../client/identity-assertion.md)**. ## Підсумки {#recap} -* Через Streamable HTTP ваш сервер — це **сервер ресурсів** OAuth 2.1: він перевіряє токени й ніколи їх не видає. -* `TokenVerifier` — це вся поверхня інтеграції: один асинхронний метод, на вході токен, на виході `AccessToken | None`. -* `token_verifier=` і `auth=AuthSettings(issuer_url=..., resource_server_url=..., required_scopes=[...])` завжди йдуть разом. +* Через Streamable HTTP ваш сервер — **сервер ресурсів** OAuth 2.1: він перевіряє токени й ніколи їх не видає. +* `TokenVerifier` — уся поверхня інтеграції: один асинхронний метод, токен на вході, `AccessToken | None` на виході. +* `token_verifier=` і `auth=AuthSettings(issuer_url=..., resource_server_url=..., required_scopes=[...])` завжди йдуть у парі. * SDK публікує [RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728) Protected Resource Metadata за адресою `/.well-known/oauth-protected-resource/...` і відповідає на неавтентифіковані запити кодом 401, чий заголовок `WWW-Authenticate` вказує на них. Оце й уся історія виявлення. * `get_access_token()` у будь-якому обробнику — це той, хто викликає. -* Авторизація — справа HTTP. `stdio` та клієнт у пам'яті ніколи її не бачать. +* Авторизація — справа HTTP. `stdio` і тестовий клієнт у пам'яті ніколи її не бачать. -Клієнтська половина (виявлення вашого сервера авторизації й отримання токена за вас) — на сторінці **[Клієнти OAuth](../client/oauth-clients.md)**. А клієнт, який *стверджує* ідентичність замість того, щоб запитувати її в користувача, — на сторінці **[Твердження ідентичності](../client/identity-assertion.md)**. +Клієнтська половина (виявлення сервера авторизації й отримання токена за вас) — на сторінці **[OAuth-клієнти](../client/oauth-clients.md)**. А клієнт, який *стверджує* ідентичність замість того, щоб просити її в користувача, — на сторінці **[Твердження ідентичності](../client/identity-assertion.md)**. diff --git a/i18n/uk/pages/run/deploy.md b/i18n/uk/pages/run/deploy.md index fe83a33e31..a09c492800 100644 --- a/i18n/uk/pages/run/deploy.md +++ b/i18n/uk/pages/run/deploy.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 --- # Розгортання та масштабування {#deploy-scale} @@ -47,6 +47,22 @@ translation: розгорнутий сервер, який відхиляє кожне з'єднання, — це список дозволених Host, доки не доведено протилежне. **[Усунення несправностей](../troubleshooting.md)** теж починається звідси. +## За проксі, що завершує TLS {#behind-a-tls-terminating-proxy} + +Якщо TLS завершується на проксі (ingress, балансувальник навантаження, Caddy, nginx), а uvicorn за ним обслуговує звичайний HTTP, скажіть uvicorn довіряти заголовкам `X-Forwarded-*` від проксі: + +```console +uvicorn server:app --proxy-headers --forwarded-allow-ips='' +``` + +Без цього застосунок вважає, що його обслуговують через `http://`, і будь-яке перенаправлення, яке він видає (зазвичай це `/mcp` → `/mcp/`), веде на `http://…`. Python-клієнт відмовляється переходити з HTTPS-ендпоінта на звичайний HTTP і прямо про це каже: + +```text +MCPError: Redirect to http://mcp.example.com/mcp/ not followed: it would downgrade this HTTPS endpoint to plain HTTP. +``` + +Тимчасовий обхід на боці клієнта — вказати точний URL, який обслуговує сервер (`https://mcp.example.com/mcp/`, зі скісною рискою наприкінці), щоб перенаправлення не було взагалі. Справжнє виправлення — прапорець вище. `FORWARDED_ALLOW_IPS` — той самий параметр у вигляді змінної середовища; `*` довіряє кожному проміжному вузлу, що правильно лише тоді, коли до uvicorn не може дістатися ніхто, крім проксі. + ## Робочі процеси, і для кого потрібні липкі сесії {#workers-and-who-has-to-be-sticky} Щойно ім'я хоста відповідає, поставте за ним більше ніж один робочий процес. Для цього в SDK немає жодного перемикача; Starlette-застосунок масштабують так само, як і будь-який ASGI-застосунок, — передають об'єкт чомусь, що вміє створювати дочірні процеси: @@ -170,6 +186,7 @@ python -c "import secrets; print(secrets.token_hex(32))" ## Підсумки {#recap} * За замовчуванням застосунок відповідає лише на запити, адресовані localhost. `transport_security=TransportSecuritySettings(allowed_hosts=[...], allowed_origins=[...])` — це ворота виходу в світ: доки його не передати, кожен запит за справжнім іменем хоста — це `421`, а причина є лише в лозі сервера. +* За проксі, що завершує TLS, запускайте uvicorn з `--proxy-headers --forwarded-allow-ips=...`, інакше його перенаправлення ведуть на `http://`, і клієнт їх відхиляє. * На 2026-07-28 немає сесії і нічого, до чого балансувальник навантаження мав би прив'язуватися. `stateless_http=True` — перемикач лише для старого покоління, бо сучасний запит маршрутизується й отримує відповідь ще до того, як цей прапорець узагалі прочитають. * Типовий ключ `requestState` — це `os.urandom(32)`, випущений окремо в кожному процесі. Багатораундова повторна спроба, що потрапила до іншого робочого процесу, збоїть з `-32602` *«Invalid or expired requestState»*. * Виправлення — `RequestStateSecurity(keys=[...])` **і** те саме ім'я сервера на кожному екземплярі. Ім'я — типове твердження про аудиторію токена. Ті самі ключі, те саме ім'я. diff --git a/i18n/uk/pages/run/index.md b/i18n/uk/pages/run/index.md index d5f388f36c..5d4f90c89b 100644 --- a/i18n/uk/pages/run/index.md +++ b/i18n/uk/pages/run/index.md @@ -1,6 +1,6 @@ --- translation: - sections: [fea8d769ff9edeba, ce8e2ad42f29ef71, 0d705efb19cf99c2, 2fd7cf825e6d2b2c, 9adc400e8c88e854, 318893ad8e2e9924, 6b63ab96b34476c0] + sections: [fea8d769ff9edeba, ce8e2ad42f29ef71, 0d705efb19cf99c2, 9fd2357154a5b7e7, 9adc400e8c88e854, 318893ad8e2e9924, 6b63ab96b34476c0] tool: 1 --- # Запуск сервера {#running-your-server} @@ -75,6 +75,11 @@ Inspector робить рівно те саме, що й справжній хо * `max_request_body_size`: найбільший прийнятний розмір тіла запиту в байтах. За замовчуванням 4 МіБ; більші запити отримують HTTP 413 ще до розбору чи створення сесії. Збільшуйте його лише тоді, коли легітимні MCP-повідомлення перевищують цей розмір. +* `session_idle_timeout`: скільки секунд сесія старого покоління може простоювати без жодного запиту в роботі, + перш ніж сервер її закриє. За замовчуванням 1800. `None` вимикає це. Див. + [Час життя сесії та обмеження](legacy-clients.md#session-lifetime-and-limits). +* `max_sessions`: скільки сесій старого покоління один процес утримує одночасно. За замовчуванням 10 000. `None` + знімає обмеження. Описано в тому самому розділі. * `event_store`, `retry_interval`, `transport_security`: відновлюваність і захист від DNS-rebinding. Вони можуть зачекати, доки ви не розгорнете сервер деінде, крім localhost; `transport_security` описано на сторінці **[Розгортання та масштабування](deploy.md)**. !!! warning diff --git a/i18n/uk/pages/run/legacy-clients.md b/i18n/uk/pages/run/legacy-clients.md index 099aa9d32c..3f8c96611f 100644 --- a/i18n/uk/pages/run/legacy-clients.md +++ b/i18n/uk/pages/run/legacy-clients.md @@ -1,6 +1,6 @@ --- translation: - sections: [3d1663c18edc824c, d4fd37009a13f03d, af9f398a5a8b679a, 470c2dd144294d69, 8e45827e6d24e8c8, 91dfd0ce98ebb03c] + sections: [3d1663c18edc824c, 90956965ae6a1ca1, af9f398a5a8b679a, 5ce83b1f9d88da62, 0d9b5d13fffc94e5, 8e45827e6d24e8c8, 91dfd0ce98ebb03c] tool: 1 --- # Обслуговування клієнтів старого покоління {#serving-legacy-clients} @@ -21,15 +21,25 @@ SDK маршрутизує кожен запит за його заголовк ## Один обробник, обидва покоління {#one-handler-both-eras} -Ось інструмент, якому треба дещо запитати в користувача, і клієнти обох поколінь, що його викликають: +Ось інструмент, якому треба дещо запитати в користувача: -```python title="server.py" hl_lines="24 37-38" +```python title="server.py" hl_lines="21" --8<-- "docs_src/legacy_clients/tutorial001.py" ``` `reserve` потребує однієї речі, якої модель не надала: скільки примірників. Через `Annotated[..., Resolve(ask_quantity)]` інструмент це й оголошує (докладніше — на сторінці **[Залежності](../handlers/dependencies.md)**). Ніщо в `reserve` не називає версію, не перевіряє можливість і не розгалужується. -Обидва клієнти відкриті **одночасно**, на тому самому об'єкті `mcp`. `mode="legacy"` виконує рукостискання `initialize` — саме те з'єднання, яке відкриває клієнт до 2026 року. Другий клієнт бере значення за замовчуванням і потрапляє на `2026-07-28`. +Запустіть його через HTTP — і ось клієнти обох поколінь, що його викликають: + +```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" +``` + +Обидва клієнти відкриті **одночасно**, до того самого сервера, що працює. `mode="legacy"` виконує рукостискання `initialize` — саме те з'єднання, яке відкриває клієнт до 2026 року. Другий клієнт бере значення за замовчуванням і потрапляє на `2026-07-28`. Запустіть `python client.py` з другого термінала: ```text 2025-11-25 {'result': "Reserved 2 of 'Dune'."} @@ -62,6 +72,41 @@ SDK маршрутизує кожен запит за його заголовк надсилання пропущених SSE-подій клієнту, що перепід'єднується до *тієї самої* сесії), а не сховище сесій. Він ніколи не робить сесію досяжною з іншого процесу. +## Час життя сесій і обмеження {#session-lifetime-and-limits} + +Сесія старого покоління не живе вічно, і один процес не тримає їх необмежену кількість. +Цим керують два налаштування. Обидва — іменовані аргументи `run()`, `streamable_http_app()` +і `Server.streamable_http_app()`. Сучасні з'єднання (`2026-07-28`) і `stateless_http=True` +сесій не мають, тож жодне з налаштувань їх не стосується. + +| Налаштування | За замовчуванням | Що робить | Що бачить клієнт | Як вимкнути | +|---|---|---|---|---| +| `session_idle_timeout` | `1800` (30 хв) | Закриває сесію, у якій стільки часу нічого не було в роботі. | `404 Session not found`. Доведеться знову виконати `initialize`. | `None` | +| `max_sessions` | `10_000` | Відмовляється відкривати сесію понад цю кількість. Наявні сесії не зачіпаються, і нічого не витісняється. | `503 Too many open sessions` з кодом JSON-RPC `-32603`. | `None` | + +Що вважається «в роботі»: + +* Відкритий потік `GET`. Клієнти SDK тримають один відкритим, тож сесія під'єднаного + клієнта ніколи не спливає. +* Запит, на який досі відповідають. Виклик інструмента, що триває довше за тайм-аут, не + переривається, а відлік починається лише після його завершення. +* Більше нічого. Між запитами годинник іде. Будь-який запит у сесії перезапускає його, + зокрема й `ping`. Щойно сесія спливла, ніщо її не оживить. + +Клієнт, що завершує свою сесію через `DELETE`, звільняє її негайно. Так само й клієнт, чий +початковий запит відхилено. + +```python +mcp.run(transport="streamable-http", session_idle_timeout=None, max_sessions=50_000) +``` + +Обидві події потрапляють у лог сервера. Спливання — це `Session idle timeout` на рівні +`INFO`. Відмова у відкритті — `Refusing to open a new session: sessions are already open` +на рівні `WARNING`. + +Обмеження діють на процес. Із чотирма робочими процесами стеля — учетверо `max_sessions`, +і кожен робочий процес сам завершує свої сесії за тайм-аутом. + ## Єдиний перемикач: `stateless_http` {#the-one-knob-stateless_http} Якщо липкість — ціна, яку ви платити не готові, змінити можна рівно одну річ. @@ -87,8 +132,8 @@ SDK маршрутизує кожен запит за його заголовк !!! check Зробіть неправильно. `reserve` — той самий інструмент, що щойно обслужив обох клієнтів. - Розгорніть його зі `stateless_http=True`, під'єднайте тих самих двох клієнтів через HTTP - і викличте його з кожного. + Розгорніть його зі `stateless_http=True`, під'єднайте тих самих двох клієнтів і викличте + його з кожного. Сучасний клієнт, як і раніше, отримує `Reserved 2 of 'Dune'.` Сучасна гілка не змінилася. diff --git a/i18n/uk/pages/troubleshooting.md b/i18n/uk/pages/troubleshooting.md index 2f5df7a21d..1b631ed4a1 100644 --- a/i18n/uk/pages/troubleshooting.md +++ b/i18n/uk/pages/troubleshooting.md @@ -1,6 +1,6 @@ --- translation: - sections: [2efaecdef109a5c5, fcacd3e66b8635a4, 25323d737dcf0261, 8a6e351ec756904d, 137454d469c867f5, 6392596bd6df54f0, 41126fa9c4fe432f, 480b6d7897e30ab4, d83bb682e708dde0, ebbed3449c499db4, 323ef84f6b4bebde, 30fd31be74169d9a, 656943c6cb567218, c2dc3b1007d2e987, 7cf5386b997d04e9, 0b59feed8384456e, 0cba47bae78d04eb, e4355f4c7cf4fb2e] + sections: [3d58228e81b99543, 170514ce901c4139, 17d61fad0a50d62b, 8a6e351ec756904d, 137454d469c867f5, 6392596bd6df54f0, 41126fa9c4fe432f, 480b6d7897e30ab4, d83bb682e708dde0, ebbed3449c499db4, 525cdf1755e29d4c, 30fd31be74169d9a, d2e88333d4f7841f, c2dc3b1007d2e987, d6eabf60cc366341, f798e815252852c2, 0cba47bae78d04eb, 2c218ba829abf74e] tool: 1 --- # Усунення несправностей {#troubleshooting} @@ -13,6 +13,12 @@ translation: --8<-- "docs_src/troubleshooting/tutorial001.py" ``` +Ці пункти звертаються до нього за адресою `http://localhost:8000/mcp`, тож залиште його працювати через HTTP: + +```console +uv run mcp run server.py --transport streamable-http +``` + Помилки, які цитує ця сторінка, справжні: власний набір тестів SDK відтворює кожну з них. ## `ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception)` {#exceptiongroup-unhandled-errors-in-a-taskgroup-1-sub-exception} @@ -23,7 +29,7 @@ translation: ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: await client.read_resource("weather://Atlantis") ``` @@ -49,7 +55,7 @@ async def main() -> None: ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: try: await client.read_resource("weather://Atlantis") except MCPError as e: @@ -67,7 +73,7 @@ async def main() -> None: ```python async def main() -> None: - client = Client(mcp) + client = Client("http://localhost:8000/mcp") tools = await client.list_tools() # RuntimeError ``` @@ -75,7 +81,7 @@ async def main() -> None: ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: tools = await client.list_tools() ``` @@ -252,7 +258,7 @@ app = Starlette(routes=[Mount("/", app=mcp.streamable_http_app())], lifespan=lif ## `MCPError: Session not found` {#mcperror-session-not-found} -Сервер не впізнає `Mcp-Session-Id`, який надіслав ваш клієнт, майже завжди тому, що сервер **перезапустився** (або вас спрямували на інший екземпляр). Сесії живуть у пам'яті того одного процесу. +Сервер не впізнає `Mcp-Session-Id`, який надіслав ваш клієнт. Або сервер **перезапустився** (чи вас спрямували на інший екземпляр), або **сплив строк** сесії, бо протягом `session_idle_timeout` (за замовчуванням це 30 хвилин) у ній не було жодного запиту в роботі. Див. [Час життя сесії та обмеження](run/legacy-clients.md#session-lifetime-and-limits). Сесії живуть у пам'яті того одного процесу. Помилки в сервері тут немає. HTTP-відповідь — це `404`, тіло якого *є* JSON-RPC, тож, на відміну від `421` вище, python `Client` показує це повідомлення дослівно: @@ -262,9 +268,9 @@ app = Starlette(routes=[Mount("/", app=mcp.streamable_http_app())], lifespan=lif Виправлення — перепід'єднатися: вийдіть із блоку `async with Client(...)` і ввійдіть у новий, який узгодить свіжу сесію. Для довгоживучого клієнта це означає перехоплювати `MCPError` навколо викликів і перепід'єднуватися на це повідомлення, а не повторювати спроби всередині мертвої сесії. -Якщо це трапляється *без* перезапуску, у вас працює більше одного робочого процесу без липких сесій: кожен робочий процес тримає власну таблицю сесій, тож запит, спрямований не на той, опиняється тут. Про це та про два виправлення (липка маршрутизація або `stateless_http=True`) — сторінки **[Розгортання й масштабування](run/deploy.md)** і **[Обслуговування клієнтів старого покоління](run/legacy-clients.md)**. +Якщо це трапляється *без* перезапуску і клієнт не мовчав так довго, у вас працює більше одного робочого процесу без липких сесій: кожен робочий процес тримає власну таблицю сесій, тож запит, спрямований не на той, опиняється тут. Про це та про два виправлення (липка маршрутизація або `stateless_http=True`) — сторінки **[Розгортання й масштабування](run/deploy.md)** і **[Обслуговування клієнтів старого покоління](run/legacy-clients.md)**. -Для оператора сервера відповідний рядок у лозі — `Rejected request with unknown or expired session ID: `. Він пишеться на рівні `INFO`, тож за звичного порога `WARNING` його не видно. Бачити його сплесками одразу після розгортання — нормально: кожен під'єднаний клієнт перепід'єднується. +Для оператора сервера відповідний рядок у лозі — `Rejected request with unknown or expired session ID: `. Він пишеться на рівні `INFO`, тож за звичного порога `WARNING` його не видно. Бачити його сплесками одразу після розгортання — нормально: кожен під'єднаний клієнт перепід'єднується. Якщо ж сплив строк сесії, перед цим рядком іде `Session idle timeout`, теж на рівні `INFO`. ## `MCPError: Method not found` {#mcperror-method-not-found} @@ -276,7 +282,13 @@ app = Starlette(routes=[Mount("/", app=mcp.streamable_http_app())], lifespan=lif Ваш сервер хоче про щось запитати користувача, а цей клієнт ніколи не казав, що його можна питати. -Резолвер еліцитації (elicitation) відмовляє одразу, якщо під'єднаний клієнт не оголосив еліцитацію через форму, а `e.error.data` називає, чого саме бракує: +Це Bistro питає, перш ніж забронювати, — через резолвер: + +```python title="server.py" hl_lines="15-17 21" +--8<-- "docs_src/troubleshooting/tutorial007.py" +``` + +Запустіть його замість сервера Weather і викличте `book_table` із клієнта, якому не передали `elicitation_callback`. Резолвер відмовляє одразу, бо під'єднаний клієнт так і не оголосив еліцитацію (elicitation) через форму, а `e.error.data` називає, чого саме бракує: ```json { @@ -290,7 +302,7 @@ app = Starlette(routes=[Mount("/", app=mcp.streamable_http_app())], lifespan=lif ```python async def main() -> None: - async with Client(mcp, elicitation_callback=handle_elicitation) as client: + async with Client("http://localhost:8000/mcp", elicitation_callback=handle_elicitation) as client: result = await client.call_tool("book_table", {"date": "Friday"}) ``` @@ -315,14 +327,14 @@ async def main() -> None: Ваш обробник спробував звернутися до клієнта посеред запиту на з'єднанні, де виклик не має каналу, здатного нести запит від сервера. Є три конфігурації сервера, за яких виклик опиняється в такому становищі. -**З'єднання `2026-07-28`: будь-який транспорт, завжди.** Сучасний протокол узагалі не має запитів, ініційованих сервером, тож сервер відмовляє ще до того, як щось надіслано. `ctx.elicit()` усередині інструмента — класичний спосіб на це натрапити (у найпершому ж тесті в пам'яті, бо `Client(server)` узгоджує `2026-07-28`, навіть якщо його про це не просили), і передавання `elicitation_callback=` нічого не змінює, бо до клієнта ніколи не доходить запит, на який він міг би відповісти: +**З'єднання `2026-07-28`: будь-який транспорт, завжди.** Сучасний протокол узагалі не має запитів, ініційованих сервером, тож сервер відмовляє ще до того, як щось надіслано. `ctx.elicit()` усередині інструмента — класичний спосіб на це натрапити, зазвичай у найпершому ж **[тесті](get-started/testing.md)** цього інструмента в пам'яті, бо `Client(mcp)` узгоджує `2026-07-28`, навіть якщо його про це не просили. Передавання `elicitation_callback=` нічого не змінює, бо до клієнта ніколи не доходить запит, на який він міг би відповісти: ```python title="server.py" hl_lines="16" --8<-- "docs_src/troubleshooting/tutorial006.py" ``` ```python -async def main() -> None: +async def test_book_table() -> None: async with Client(mcp) as client: await client.call_tool("book_table", {"date": "Friday"}) ``` @@ -364,7 +376,7 @@ mcp.shared.exceptions.MCPError: Cannot send 'elicitation/create': this transport ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: await client.call_tool("forecast", {"city": "London"}, request_state="round-1-from-worker-a") ``` @@ -417,7 +429,7 @@ mcp = MCPServer("Weather", request_state_security=RequestStateSecurity(keys=[key * `Tool already exists:` у лозі сервера — єдина ознака того, що два однойменні інструменти злилися в один. * Один 421, три написання: `Server returned an error response` (python `Client`), `421 Misdirected Request` / `Invalid Host header` (усе інше), `Invalid Host header: ` (лог сервера). Виправлення: `transport_security=TransportSecuritySettings(allowed_hosts=[...])`. * `Task group is not initialized` -> змонтований застосунок, чий хост-застосунок у своєму життєвому циклі так і не ввійшов у `mcp.session_manager.run()`. -* `Session not found` -> сервер перезапустився; перепід'єднайтеся. +* `Session not found` -> сервер перезапустився або сплив строк сесії (`session_idle_timeout`); перепід'єднайтеся. * `Cannot send 'elicitation/create': ... no back-channel ...` -> `ctx.elicit()` потребує каналу від сервера до клієнта: з'єднання `2026-07-28` його ніколи не має, `stateless_http=True` забирає канал старого покоління, а `json_response=True` — канал у межах запиту. Використовуйте резолвер (клієнту старого покоління також потрібен сервер, що зберігає канал). Сусідня помилка `Method not found` — це запит методу, якого немає в ревізії протоколу іншої сторони. * `Client did not declare the form elicitation capability ...` і `Elicitation not supported` -> клієнту бракує `elicitation_callback=`. * `Invalid or expired requestState` ніколи не пояснює причину в переданих даних. Лог сервера — пояснює; `unknown key` означає, що треба зробити `RequestStateSecurity(keys=[...])` спільним для всіх робочих процесів. diff --git a/i18n/uk/pages/whats-new.md b/i18n/uk/pages/whats-new.md index 02d1a39e71..132967224b 100644 --- a/i18n/uk/pages/whats-new.md +++ b/i18n/uk/pages/whats-new.md @@ -1,6 +1,6 @@ --- translation: - sections: [cfe01c0c5863dfa2, 1c58c5cfcc37d455, a7392996acf1ad8f, 875eb2889263424e] + sections: [cfe01c0c5863dfa2, 0dbb68d8b210177b, 80cf193023af3ed4, 875eb2889263424e] tool: 1 --- # Що нового у v2 {#whats-new-in-v2} @@ -42,11 +42,11 @@ mcp = MCPServer("Demo") # v1: FastMCP("Demo") v1 давав три вкладені шари: контекстний менеджер транспорту, що видає сирі потоки, обгорнуту навколо них `ClientSession` і викликаний вручну `await session.initialize()`. У v2 є один об'єкт: -```python title="client.py" hl_lines="14-18" ---8<-- "docs_src/client/tutorial001.py" +```python title="client.py" hl_lines="7-11" +--8<-- "docs_src/client/tutorial001_client.py" ``` -`Client` приймає об'єкт сервера (у пам'яті, без транспорту: це сценарій тестування), URL (Streamable HTTP), `StdioServerParameters` (підпроцес stdio) або будь-який інший контекстний менеджер транспорту, як-от `sse_client(...)`. Вхід в `async with` під'єднує та узгоджує версію протоколу, хай яким поколінням говорить сервер; після цього `client.server_capabilities` і `client.protocol_version` просто є, як і `client.server_info`, коли сервер себе ідентифікує (тепер це `Implementation | None`, бо ідентичність у поколінні 2026 необов'язкова). Колбеки семплювання й еліцитації, зареєстровані у v1, і далі працюють (їхні тіла зазнають того самого перейменування атрибутів у snake_case, що й усе інше на цій сторінці), тепер вони також відповідають на запити всередині результатів у стилі 2026 (нижче) і виконуються паралельно, а не по одному. `ClientSession` досі лежить під сподом для тих, кому потрібна низькорівнева поверхня, і `client.session` її віддає; вона теж змінилася (працює на новому рушії диспетчера, і деякі її власні сигнатури змінилися), тож прочитайте **[Посібник з міграції](migration.md#clientsession-now-runs-on-jsonrpcdispatcher-basesession-removed)**, перш ніж спускатися нижче. +`Client` приймає URL (Streamable HTTP), `StdioServerParameters` (підпроцес stdio), будь-який інший контекстний менеджер транспорту, як-от `sse_client(...)`, або, у тестах, сам об'єкт сервера (у пам'яті, без транспорту). Вхід в `async with` під'єднує та узгоджує версію протоколу, хай яким поколінням говорить сервер; після цього `client.server_capabilities` і `client.protocol_version` просто є, як і `client.server_info`, коли сервер себе ідентифікує (тепер це `Implementation | None`, бо ідентичність у поколінні 2026 необов'язкова). Колбеки семплювання й еліцитації, зареєстровані у v1, і далі працюють (їхні тіла зазнають того самого перейменування атрибутів у snake_case, що й усе інше на цій сторінці), тепер вони також відповідають на запити всередині результатів у стилі 2026 (нижче) і виконуються паралельно, а не по одному. `ClientSession` досі лежить під сподом для тих, кому потрібна низькорівнева поверхня, і `client.session` її віддає; вона теж змінилася (працює на новому рушії диспетчера, і деякі її власні сигнатури змінилися), тож прочитайте **[Посібник з міграції](migration.md#clientsession-now-runs-on-jsonrpcdispatcher-basesession-removed)**, перш ніж спускатися нижче. **[Клієнт](client/index.md)** знайомить із ним, **[Транспорти клієнта](client/transports.md)** описує чотири форми під'єднання, **[Колбеки клієнта](client/callbacks.md)** — самі колбеки, а **[Тестування](get-started/testing.md)** показує шаблон роботи в пам'яті, що замінює допоміжну функцію `create_connected_server_and_client_session()` з v1. @@ -171,11 +171,15 @@ v2 реалізує редакцію 2026-07-28 і обслуговує **оби Заміна розвертає виклик у зворотний бік. Інструмент, якому щось потрібно від користувача, *повертає* запитання (`InputRequiredResult`), клієнт відповідає на нього тими самими колбеками, які мав завжди, і виклик повторюється з прикріпленими відповідями. `Client` веде цей цикл за вас. На сервері ви рідко будуєте результат самі, бо це робить **[залежність](handlers/dependencies.md)**: анотуйте параметр `Resolve(ask_quantity)`, де `ask_quantity` — звичайна функція, яку ви пишете, і SDK запитає тим механізмом, який підтримує з'єднання: живим запитом еліцитації на сесії старого покоління або багатораундовим обміном на 2026. Одне тіло інструмента, обидва покоління: -```python title="dual_era.py" hl_lines="24 37-38" +```python title="server.py" hl_lines="21" --8<-- "docs_src/legacy_clients/tutorial001.py" ``` -Цей файл — уся ідея в одному місці: один сервер, один інструмент на основі `Resolve`, і клієнт старого покоління разом із сучасним клієнтом, які обидва отримують свою відповідь, у пам'яті. **[Багатораундові запити](handlers/multi-round-trip.md)** пояснює механізм (зокрема `request_state`, який SDK запечатує й перевіряє за вас); **[Еліцитація](handlers/elicitation.md)** — саме запитування. +```python title="client.py" hl_lines="14-15" +--8<-- "docs_src/legacy_clients/tutorial001_client.py" +``` + +Ці два файли — уся ідея в одному місці: один сервер, один інструмент на основі `Resolve`, і клієнт старого покоління разом із сучасним клієнтом, які обидва отримують свою відповідь від того самого запущеного сервера (**[Обслуговування клієнтів старого покоління](run/legacy-clients.md)** розбирає їх покроково). **[Багатораундові запити](handlers/multi-round-trip.md)** пояснює механізм (зокрема `request_state`, який SDK запечатує й перевіряє за вас); **[Еліцитація](handlers/elicitation.md)** — саме запитування. !!! warning "Це єдине місце, де перенесений сервер v1 змінює поведінку" Першими на це натрапляють ваші власні тести: `Client(mcp)` за замовчуванням узгоджує з вашим сервером v2 diff --git a/i18n/zh-hant/pages/advanced/apps.md b/i18n/zh-hant/pages/advanced/apps.md index 42fe76af67..73884b17ec 100644 --- a/i18n/zh-hant/pages/advanced/apps.md +++ b/i18n/zh-hant/pages/advanced/apps.md @@ -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 @@ SDK 以內建的 `Apps` 擴充功能(`io.modelcontextprotocol/ui`)提供這 ## 有錶面的時鐘 {#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 @@ HTML 本身會監聽主機的 `postMessage` 並顯示結果。真正的 app 請 模型讀的是 `content`;iframe 是給人看的。支援 UI 的主機照樣會把文字結果餵給模型,而純文字用戶端**只**會拿到那個。所以標準做法是一個工具,兩種答案。再看一次 `get_time`: -```python title="server.py" hl_lines="23-27" +```python title="server.py" hl_lines="21-25" --8<-- "docs_src/apps/tutorial001.py" ``` -只有當用戶端宣告了 `io.modelcontextprotocol/ui` 擴充功能,**而且**在它的 `mimeTypes` 設定裡列出 `text/html;profile=mcp-app` 時,`client_supports_apps(ctx)` 才會是 `True`。這個欄位是必填的,所以省略它的用戶端不算數。同一個檔案裡的 `main()` 宣告的正是這些:協商的用戶端那一半,於是回來的是豐富版的答案。 +只有當用戶端宣告了 `io.modelcontextprotocol/ui` 擴充功能,**而且**在它的 `mimeTypes` 設定裡列出 `text/html;profile=mcp-app` 時,`client_supports_apps(ctx)` 才會是 `True`。這個欄位是必填的,所以省略它的用戶端不算數。以下是協商中用戶端的那一半: + +```python title="client.py" hl_lines="8 12" +--8<-- "docs_src/apps/tutorial001_client.py" +``` + +透過 HTTP 提供 `server.py`,然後在第二個終端機執行用戶端: + +```console +uv run mcp run server.py --transport streamable-http +``` + +```console +python client.py +``` + +```text +2026-06-26T12:00:00Z +``` + +回來的是豐富版的答案。把 `extensions=[APPS_SUPPORT]` 從 `Client` 呼叫裡拿掉,同一個程式就會改印出 `The time is 2026-06-26T12:00:00Z.`,而這就是純文字用戶端所能看到的全部。 !!! warning 絕對不要把 `"[Rendered UI]"` 這類佔位文字當成唯一的內容回傳。如果後備文字沒有用,這個工具對每個純文字用戶端、對模型本身就都沒有用。好好寫那句話。 diff --git a/i18n/zh-hant/pages/advanced/extensions.md b/i18n/zh-hant/pages/advanced/extensions.md index d90e61c289..37c6e5fa96 100644 --- a/i18n/zh-hant/pages/advanced/extensions.md +++ b/i18n/zh-hant/pages/advanced/extensions.md @@ -1,6 +1,6 @@ --- translation: - sections: [05891e7cc1938a13, b3c01a6af28c51ee, 7ffc91f5e38bdfe0, 717d3f235a8333a7, f471a13b2fe5d737, ed6af2df4b656dff] + sections: [05891e7cc1938a13, b3c01a6af28c51ee, 6eba6ce094f4417e, 125f28588c85dd7b, ddd8969b698ca11c, ed6af2df4b656dff] tool: 1 --- # 擴充功能 {#extensions} @@ -49,7 +49,7 @@ TypeError: Stamps.identifier must be a `vendor-prefix/name` string 最小的有用擴充功能是一個工具加上一個設定對映表: -```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 @@ TypeError: Stamps.identifier must be a `vendor-prefix/name` string * `settings()` 是在 `capabilities.extensions["com.example/stamps"]` 宣告的值。回傳 `{}`(預設值)表示宣告這個擴充功能但不帶任何設定。 * 擴充功能永遠不會拿到伺服器。它以資料的形式宣告貢獻,由 `MCPServer` 取用。沒有 `self.server` 可以修改。 -而 `main()` 就是證明,一個記憶體內用戶端直接連上 `mcp`: +透過 HTTP 提供服務,用戶端就是證明: -```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" ``` +本頁每個 `server.py` 都用這個指令提供服務,每個 `client.py` 則在第二個終端機用 `python client.py` 與它並行執行。 + ### 提供自己的方法 {#serving-your-own-methods} 擴充功能可以註冊**新的請求方法**:屬於它自己的動詞,和規格定義的方法並列提供: -```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 @@ TypeError: Stamps.identifier must be a `vendor-prefix/name` string ### 用戶端這一側 {#the-client-side} -同一個檔案的 `main()` 就是完整的用戶端故事,兩半都在裡面: +用戶端是獨立的程式,用戶端故事的兩半都在裡面: -```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)])` 宣告這個擴充功能。這些宣告會變成 `ClientCapabilities.extensions`:在 2026-07-28 連線上,這個對映表隨著每個請求的 `_meta` 信封傳送,所以伺服器在**每一個**請求上都看得到它;在舊版連線上,它則搭著 `initialize` 交握傳送。伺服器程式碼不用在意是哪一種:`require_client_extension(ctx, ...)` 和 `ctx.session.check_client_capability(...)` 在兩條路徑上都會讀取正確的來源。 * 廠商方法要往下一層用 `client.session.send_request(...)`;`Client` 只會為規格動詞長出一級方法。`send_request` 接受任何 `Request` 子類別,所以廠商請求原樣傳遞即可。 +* `SearchRequest` 和它攜帶的兩個模型是這個擴充功能的線路契約,所以用戶端自己宣告一份。正式發布的擴充功能會把它們放進一個套件,讓兩端都匯入。 ### 攔截 `tools/call` {#intercepting-toolscall} @@ -109,12 +116,18 @@ TypeError: Stamps.identifier must be a `vendor-prefix/name` string ## 使用用戶端擴充功能 {#using-a-client-extension} -**用戶端擴充功能**是從使用端看的同一份契約:掛在單一識別碼之下的一組用戶端行為。把實例傳給 `Client(extensions=[...])`,然後照常呼叫工具: +**用戶端擴充功能**是從使用端看的同一份契約:掛在單一識別碼之下的一組用戶端行為。這裡的伺服器回應 `buy` 時給的不是貨品,而是一張待兌換的收據,而且只對宣告了這個擴充功能的用戶端這樣做: -```python title="client.py" hl_lines="66-68" +```python title="server.py" hl_lines="22-25" --8<-- "docs_src/extensions/tutorial006.py" ``` +在用戶端,把實例傳給 `Client(extensions=[...])`,然後照常呼叫工具: + +```python title="client.py" hl_lines="33-35" +--8<-- "docs_src/extensions/tutorial006_client.py" +``` + `call_tool("buy", ...)` 回傳普通的 `CallToolResult`,和其他所有呼叫一樣。擴充功能改變的是:伺服器現在可以用 `receipt` **結果形狀**來回應 `buy`,而不是最終結果,而 `Receipts` 會在 `call_tool` 回傳之前把它完成(這裡是透過後續呼叫兌換收據)。呼叫端的程式碼完全不用動。 拿掉這個擴充功能,這一切就不存在:伺服器的關卡會拒絕沒有宣告它的用戶端(錯誤 -32021),而來自跳過關卡的伺服器的認領形狀會驗證失敗,完全符合規格對無法辨識的 `resultType` 的要求。預設關閉,線路的兩端都是。 @@ -124,15 +137,15 @@ TypeError: Stamps.identifier must be a `vendor-prefix/name` string ```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")]) ``` ## 撰寫用戶端擴充功能 {#writing-a-client-extension} 繼承 `ClientExtension`,只覆寫需要的部分。三種貢獻類型,各有預設實作:`settings()`、`claims()` 和 `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" ``` * 識別碼遵循和伺服器相同的語法,在類別定義時驗證。 @@ -153,12 +166,18 @@ def notifications(self) -> Sequence[NotificationBinding[Any]]: ### 擴充功能動詞 {#extension-verbs} -擴充功能自己的請求方法不需要用戶端註冊。廠商請求型別繼承 `mcp.types.Request`,並透過 `client.session.send_request` 送出,如[提供自己的方法](#serving-your-own-methods)所示。多一件事:當某個參數鍵必須搭上 `Mcp-Name` 標頭時(像 tasks 這類擴充功能規格對它們的動詞有此要求),請求型別要宣告 `name_param`: +擴充功能自己的請求方法不需要用戶端註冊。廠商請求型別繼承 `mcp.types.Request`,並透過 `client.session.send_request` 送出,如[提供自己的方法](#serving-your-own-methods)所示。以一個伺服器為例,它的擴充功能提供一個和具名工作有關的動詞: -```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" ``` +用戶端多一件事:當某個參數鍵必須搭上 `Mcp-Name` 標頭時(像 tasks 這類擴充功能規格對它們的動詞有此要求),請求型別要宣告 `name_param`: + +```python title="client.py" hl_lines="20-23 28-29" +--8<-- "docs_src/extensions/tutorial007_client.py" +``` + 工作階段會在每一條送出路徑上把 `params["jobId"]` 鏡射到 `Mcp-Name`,而缺少值時會明確失敗,而不是默默省略必要的標頭。 ## 擴充功能不能做的事 {#what-an-extension-cannot-do} diff --git a/i18n/zh-hant/pages/advanced/low-level-server.md b/i18n/zh-hant/pages/advanced/low-level-server.md index 9e05b31442..62bcfd4d36 100644 --- a/i18n/zh-hant/pages/advanced/low-level-server.md +++ b/i18n/zh-hant/pages/advanced/low-level-server.md @@ -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 --- # 低階 Server {#the-low-level-server} @@ -36,18 +36,22 @@ translation: ### 試試看 {#try-it} -這個沒有 Inspector 可用:`mcp dev` 和 `mcp run` 只接受 `MCPServer`。記憶體內的 `Client` 則不在乎;它接收低階 `Server` 的方式和接收 `MCPServer` 完全一樣: +`mcp dev` 和 `mcp run` 只接受 `MCPServer`,所以這個得自己提供服務。`server.py` 的最後一行用它建立一個普通的 ASGI 應用程式,再交給 uvicorn 執行: -```python title="main.py" +```console +uvicorn server:app --port 8000 +``` + +把 Inspector 或任何用戶端指向 `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 @@ asyncio.run(main()) * `result.structured_content` 是 `None`。高階伺服器會幫你把 `-> str` 包成 `{"result": ...}`;在這裡,你沒建的東西,沒有人會替你建。 * `list_tools` 回傳的是**你**打出來的 schema,一字不差。高階版本每個屬性上都有 `"title": "Query"`,根部還有一個 `"title": "search_booksArguments"`:那是 Pydantic 的產物。在這一層,線路上有的東西,都是你放上去的。 +在測試中可以跳過 uvicorn 和連接埠:`Client(server)` 在處理程序內接收低階 `Server` 的方式和接收 `MCPServer` 完全一樣,**[測試](../get-started/testing.md)** 講的就是這個模式。 + ## 沒有人替你檢查 {#nothing-is-checked-for-you} `MCPServer` 會在函式執行之前就拒絕錯誤的引數,依照它產生的 schema 驗證這次呼叫(**[工具](../servers/tools.md)**)。 @@ -214,4 +220,4 @@ use Server.middleware to observe or wrap initialization * `add_request_handler(method, params_type, handler)` 可以服務任何方法。`initialize` 被保留。 * `Server` 公告的能力,由你註冊了哪些處理函式推導而來。 -`Client(server)` 對兩種伺服器一視同仁,因為它們**就是**同一個協定,這正是重點所在。再往下一層根本不是類別:是 **[中介軟體](middleware.md)**。 +用戶端對兩種伺服器一視同仁,因為它們**就是**同一個協定,這正是重點所在。再往下一層根本不是類別:是 **[中介軟體](middleware.md)**。 diff --git a/i18n/zh-hant/pages/advanced/pagination.md b/i18n/zh-hant/pages/advanced/pagination.md index aa46b278c3..b0b0bd1bf0 100644 --- a/i18n/zh-hant/pages/advanced/pagination.md +++ b/i18n/zh-hant/pages/advanced/pagination.md @@ -1,6 +1,6 @@ --- translation: - sections: [a9aba7a026c7bd85, ed32bda7ba9ae33a, 7e64cc5646abb91f, 22a0129ee78b3c63, d875373c06d8d2f9] + sections: [a9aba7a026c7bd85, 83e2a08b9d46a398, 9fd8a0aa384b3257, 22a0129ee78b3c63, d875373c06d8d2f9] tool: 1 --- # 分頁 {#pagination} @@ -29,9 +29,13 @@ translation: ### 試試看 {#try-it} -`Client(server)` 在記憶體內連線到低階 `Server` 的方式,和連到 `MCPServer` 完全一樣。 +`mcp run` 只接受 `MCPServer`,所以這個得自己提供服務。`server.py` 的最後一行從 `Server` 建出一個普通的 ASGI 應用程式,交給 uvicorn 執行: -不帶引數呼叫 `list_resources()`。會拿到十個資源,`book-1` 到 `book-10`,而 `next_cursor` 是字串 `"10"`。 +```console +uvicorn server:app --port 8000 +``` + +把任何用戶端(**[用戶端](../client/index.md)**,或 Inspector)指向 `http://localhost:8000/mcp`,不帶引數呼叫 `list_resources()`。會拿到十個資源,`book-1` 到 `book-10`,而 `next_cursor` 是字串 `"10"`。 用 `list_resources(cursor="10")` 把它交回去,第一個資源就是 `book-11`,新的 `next_cursor` 是 `"20"`。 @@ -41,7 +45,7 @@ translation: `Client` 上的每個 `list_*` 方法(`list_tools`、`list_resources`、`list_resource_templates`、`list_prompts`)都接受 `cursor=` 關鍵字引數。把分頁清單抓完只要一個 `while True`: -```python title="client.py" hl_lines="26-32" +```python title="client.py" hl_lines="9-15" --8<-- "docs_src/pagination/tutorial002.py" ``` @@ -49,7 +53,7 @@ translation: * 先 extend,**再**看 `next_cursor`:最後一頁也有資源。 * `next_cursor is None` 就是出口。其他任何值都原封不動直接放回 `cursor=`。 -執行它的 `main()`,會印出 `100 resources`:十頁、每頁十個,由一個從頭到尾不知道有十頁的迴圈接起來。 +讓 uvicorn 繼續提供 `server.py`,在第二個終端機執行 `python client.py`。會印出 `100 resources`:十頁、每頁十個,由一個從頭到尾不知道有十頁的迴圈接起來。 這和 **[用戶端](../client/index.md)** 為每個 `list_*` 動詞示範的迴圈是同一個,而且對不分頁的伺服器也沒有任何代價:第一個回應的 `next_cursor` 就是 `None`,迴圈只執行一次。 diff --git a/i18n/zh-hant/pages/client/caching.md b/i18n/zh-hant/pages/client/caching.md index bdd63b73ab..03ef804e15 100644 --- a/i18n/zh-hant/pages/client/caching.md +++ b/i18n/zh-hant/pages/client/caching.md @@ -1,6 +1,6 @@ --- translation: - sections: [9e7b9a1710e5aeba, b74ca4c1d2ddddee, fa8714e61bf90c5a, 04db67a886b7271c, 857690fb8f876800] + sections: [9e7b9a1710e5aeba, 66a2e9acc9101d54, d3d25aa802f5145a, 04db67a886b7271c, 857690fb8f876800] tool: 1 --- # 快取提示 {#caching-hints} @@ -27,7 +27,7 @@ translation: 在低階的 `Server` 上,處理函式自己手動組出結果,而 `ttl_ms` / `cache_scope` 只是結果模型上的欄位。明確設定這些欄位的處理函式,永遠勝過建構子的對應表,而且是逐欄位比較: -```python title="server.py" hl_lines="10 16" +```python title="server.py" hl_lines="11 17" --8<-- "docs_src/caching/tutorial002.py" ``` @@ -41,10 +41,24 @@ translation: 在 2026-07-28 的工作階段(session)上,`Client` 會替你遵守這些提示:它內建一個回應快取,預設開啟。帶著 `ttlMs` 抵達的結果會被存起來,在 TTL 內完全相同的呼叫會直接由快取提供,不需要往返。**沒有**帶提示的結果不會被快取:沒有提示的結果會套用 `CacheConfig.default_ttl_ms`,它預設為 `0`(立刻過期),所以什麼都沒宣告的伺服器,看到的流量和以往一模一樣,一次呼叫就一次請求。 -```python title="client.py" hl_lines="33 35 38" +要親眼看看這個過程,就用 uvicorn 提供前一節的 `server.py`(它的最後一行會建立 ASGI 應用程式)。處理函式每次真正執行時都會印出一行: + +```console +uvicorn server:app --port 8000 +``` + +```python title="client.py" hl_lines="20 23 28" --8<-- "docs_src/caching/tutorial003.py" ``` +在第二個終端機執行 `python client.py`。它會印出第一個結果帶著的提示,處理函式的 `ttlMs` 和對應表的 `cacheScope` 並排: + +```text +1000 public +``` + +伺服器的終端機交代了剩下的部分:在 uvicorn 的請求記錄之間,`tools/list served` 出現了三次。 + 四次呼叫,三次抓取。第二次呼叫找到新鮮的項目,根本沒送到伺服器;把(注入的)時鐘撥過 TTL 之後,第三次又重新抓取;第四次則指定了 `cache_mode="refresh"`。這個關鍵字引數存在於五個會快取的動詞上(`list_tools`、`list_prompts`、`list_resources`、`list_resource_templates`、`read_resource`): * `"use"`(預設)如果有新鮮的項目就直接提供,沒有的話就抓取並存起來。 @@ -53,7 +67,7 @@ translation: 有一條規則凌駕於 `"use"` 之上:**帶有 `meta` 的呼叫一定會送到伺服器。**設定了 `meta` 的請求(進度 token、追蹤欄位)期待的是一個實際送上線路的請求,所以在 `cache_mode="use"` 下會被當成 `"refresh"` 處理:跳過快取讀取,而抓取回來的結果仍然會取代快取中的項目。`"bypass"` 和明確指定的 `"refresh"` 行為照舊。 -要完全關掉快取,就用 `Client(server, cache=None)` 建構:每次呼叫又都變回一次往返,而 `cache_mode` 雖然仍可接受,但不會有任何作用。 +要完全關掉快取,就在建構 `Client` 時傳入 `cache=None`:每次呼叫又都變回一次往返,而 `cache_mode` 雖然仍可接受,但不會有任何作用。 範圍也會自動遵守:`"private"` 項目綁定在快取的**分區(partition)**上(見下文),而 `"public"` 項目則可以選擇更廣的共用。此外,對通知點名的那些項目來說,**通知勝過 TTL**:`list_changed` 通知會逐出對應的快取清單,`resources/updated` 則會逐出恰好存在該 URI 下的快取讀取結果,不管它們有多新鮮。在 2026-07-28 連線上,這些通知是透過你用 `client.listen(...)` 開啟的 `subscriptions/listen` 串流送達的,而且逐出會在你的監看程式看到事件之前完成;詳情請見 **[訂閱](subscriptions.md)**。 diff --git a/i18n/zh-hant/pages/client/callbacks.md b/i18n/zh-hant/pages/client/callbacks.md index db0ad2f5a2..e121e76368 100644 --- a/i18n/zh-hant/pages/client/callbacks.md +++ b/i18n/zh-hant/pages/client/callbacks.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} @@ -58,7 +58,7 @@ result.content # [TextContent(type='text', text='Card issued to Ada Lovelace.') 你送出一個 `tools/call`,伺服器回送一個 `elicitation/create`,由你的函式回答,全都發生在同一次工具呼叫之內。 !!! info - `Client(...)` 呼叫上的 `mode="legacy"` 是真的有作用。預設情況下 `Client(...)` 會協商出現代的協定路徑,而這條路徑沒有讓伺服器向用戶端發請求的反向通道(back-channel):`ctx.elicit` 在你的回呼有機會執行之前就失敗了。決定這件事的不是傳輸方式,而是協商出來的協定,記憶體內和透過 URL 連線都一樣。只要用戶端必須回應這類請求,就固定用 `mode="legacy"`;這一頁背後的每個測試都是這樣做的。完整說明請見 **[協定版本](../protocol-versions.md)**。 + `Client(...)` 呼叫上的 `mode="legacy"` 是真的有作用。預設情況下 `Client(...)` 會協商出現代的協定路徑,而這條路徑沒有讓伺服器向用戶端發請求的反向通道(back-channel):`ctx.elicit` 在你的回呼有機會執行之前就失敗了。決定這件事的不是傳輸方式,而是協商出來的協定。只要用戶端必須回應這類請求,就固定用 `mode="legacy"`;這一頁背後的每個測試都是這樣做的。完整說明請見 **[協定版本](../protocol-versions.md)**。 在 2026-07-28 的工作階段(session)上,回呼並沒有失效,只是餵給它的方式不同:當工具回傳帶有 `ElicitRequest` 的 `InputRequiredResult` 時,`Client` 會把那個項目分派給同一個 `elicitation_callback`,並替你重試這次呼叫。這個流程就是 **[多輪往返(multi-round-trip)請求](../handlers/multi-round-trip.md)**。 @@ -139,4 +139,4 @@ result.structured_content # {'result': ['elicitation']} * `sampling_callback` 和 `list_roots_callback` 的運作方式相同,但服務的是已棄用的功能;現代的伺服器改用多輪往返請求。 * `logging_callback` 和 `message_handler` 接收通知。它們不宣告任何東西。 -`Client(...)` 的第一個引數是一個傳輸物件。**[用戶端傳輸方式](transports.md)** 涵蓋了每一種。 +`Client(...)` 的第一個引數決定傳輸方式。**[用戶端傳輸方式](transports.md)** 涵蓋了每一種。 diff --git a/i18n/zh-hant/pages/client/index.md b/i18n/zh-hant/pages/client/index.md index 8bd6edeea1..8e88f950ca 100644 --- a/i18n/zh-hant/pages/client/index.md +++ b/i18n/zh-hant/pages/client/index.md @@ -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 --- # 用戶端 {#the-client} @@ -11,13 +11,23 @@ Python 程式要和 MCP 伺服器對話,靠的就是 **`Client`**。 ## 你的第一個用戶端 {#your-first-client} -```python title="client.py" hl_lines="14-18" +用戶端需要有伺服器可以對話。這一頁的每段程式碼連的都是這間 Bookshop。把它存成 `server.py`,讓它透過 HTTP 持續執行: + +```python title="server.py" --8<-- "docs_src/client/tutorial001.py" ``` -最上面的伺服器只是讓你有東西可以連而已。用戶端就是標示出來的那五行。 +```console +uv run mcp run server.py --transport streamable-http +``` + +這樣伺服器就在 `http://localhost:8000/mcp` 提供服務。用戶端是另一個獨立的程式。把它存成 `client.py`,在第二個終端機執行 `python client.py`: -* `Client(mcp)` 拿到的是**伺服器物件本身**。這就是記憶體內傳輸:沒有子處理程序、沒有連接埠、沒有 HTTP。這一頁的每個範例,以及你寫的每個測試,都是這樣連線的。 +```python title="client.py" hl_lines="7-11" +--8<-- "docs_src/client/tutorial001_client.py" +``` + +* `Client("http://localhost:8000/mcp")` 拿到的是一個 **URL**,所以它透過 Streamable HTTP 連到你剛啟動的伺服器。 * `async with` 就是**生命週期**。進入時連線並協商;離開時斷線。沒有 `connect()` / `close()` 這種成對的方法,而且區塊結束後 `Client` 不能再重複使用。 * 在區塊內,連線的各項資訊已經以普通屬性的形式準備好了。 @@ -25,10 +35,10 @@ Python 程式要和 MCP 伺服器對話,靠的就是 **`Client`**。 `Client` 接受一個位置引數,並依它的型別決定傳輸方式: -* `MCPServer`(或低階的 `Server`)實例:在**同一個處理程序內**連線。 -* URL 字串(`Client("http://localhost:8000/mcp")`):Streamable HTTP,也就是正式環境的路徑。 -* `StdioServerParameters`:要當作**子處理程序**啟動的命令,透過它的 stdin 和 stdout 溝通。 +* URL 字串(`Client("http://localhost:8000/mcp")`):Streamable HTTP,也就是部署時使用的傳輸方式。 +* `StdioServerParameters`:要當作本機**子處理程序**啟動的命令,透過它的 stdin 和 stdout 溝通。 * 一個**傳輸**:任何可以 `async with ... as (read, write)` 的東西,例如用 `streamable_http_client(url, http_client=...)` 包住你自己的 HTTP 用戶端。 +* `MCPServer`(或低階的 `Server`)實例:在**同一個處理程序內**連線,沒有子處理程序,也沒有連接埠。這一種是給測試用的,**[測試](../get-started/testing.md)** 就是以它為基礎。 這一頁其餘的內容在這四種情況下完全相同。標頭、子處理程序、逾時,以及 `Transport` 協定另外有專屬的頁面:**[用戶端傳輸方式](transports.md)**。 @@ -48,11 +58,11 @@ Python 程式要和 MCP 伺服器對話,靠的就是 **`Client`**。 ## 列出工具 {#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()` 回傳一個 `ListToolsResult`;工具在 `.tools` 裡。每一個都是 MCP 主機(host)會交給模型的完整定義: +`list_tools()` 回傳一個 `ListToolsResult`;工具在 `.tools` 裡。每一個都是 MCP 主機(host)會交給模型的完整定義。第一個是這樣: ```python tool.name # 'search_books' @@ -76,6 +86,8 @@ tool.description # 'Search the catalog by title or author.' 這份 schema 就是 UI 算繪引數表單所需的一切,也是模型產生合法引數所需的一切。 +第二個工具 `lookup_book` 註冊時沒有給 `title=`,所以它的 `tool.title` 是 `None`。 + !!! tip `title` 是選填的,所以把工具顯示給人看的 UI 得自己挑:有的話就用 `title`,沒有就用 `name`。`from mcp.shared.metadata_utils import get_display_name` 做的正是這件事,適用於工具、資源、資源範本和提示詞。 @@ -83,7 +95,7 @@ tool.description # 'Search the catalog by title or author.' `call_tool(name, arguments)` 會執行工具,並回傳一個 `CallToolResult`。 -```python title="client.py" hl_lines="27-34" +```python title="client.py" hl_lines="9-16" --8<-- "docs_src/client/tutorial003.py" ``` @@ -131,7 +143,7 @@ result.is_error # False 資源的動作是成組的:兩種列出的方式,一種讀取的方式。 -```python title="client.py" hl_lines="22-31" +```python title="client.py" hl_lines="9-18" --8<-- "docs_src/client/tutorial004.py" ``` @@ -145,7 +157,7 @@ result.is_error # False ## 提示詞 {#prompts} -```python title="client.py" hl_lines="15-20" +```python title="client.py" hl_lines="8-13" --8<-- "docs_src/client/tutorial005.py" ``` @@ -170,7 +182,7 @@ message.content # TextContent(type='text', text='Recommend one poetry book from 有自動完成處理函式的伺服器,可以在使用者輸入時自動完成提示詞和資源範本的引數。 -```python title="client.py" hl_lines="27-31" +```python title="client.py" hl_lines="9-13" --8<-- "docs_src/client/tutorial006.py" ``` @@ -183,21 +195,21 @@ message.content # TextContent(type='text', text='Recommend one poetry book from 每個 `list_*` 方法都接受 `cursor=` 關鍵字引數,每個結果都帶有 `next_cursor`。當 `next_cursor` 是 `None`,表示全部拿到了。 -```python title="client.py" hl_lines="22-30" +```python title="client.py" hl_lines="7-15" --8<-- "docs_src/client/tutorial007.py" ``` -這個迴圈對任何伺服器都正確。`MCPServer` 會一頁回傳全部,所以 `next_cursor` 是 `None`,迴圈只跑一次,這也是為什麼大部分程式碼從來不寫它。真正會分頁的伺服器,以及游標遵守的規則,請見 **[分頁](../advanced/pagination.md)**。 +`list_all_tools` 對任何伺服器都正確。`MCPServer` 會一頁回傳全部,所以 `next_cursor` 是 `None`,迴圈只跑一次,這也是為什麼大部分程式碼從來不寫它。真正會分頁的伺服器,以及游標遵守的規則,請見 **[分頁](../advanced/pagination.md)**。 ## 在測試中 {#in-tests} -不需要處理程序、不需要連接埠的 `Client(mcp)`,本身就已經是伺服器的測試工具了。 +這一頁的每個 `client.py` 都是透過 HTTP 連到 `server.py`。在測試裡可以跳過網路,直接把伺服器物件本身交給 `Client`:`from server import mcp`,然後 `Client(mcp)`。沒有處理程序、沒有連接埠,而上面的每個方法用起來都一樣。 -有一個建構子旗標是專為此設計的:`Client(mcp, raise_exceptions=True)`。它只對記憶體內連線有作用,而 **[測試](../get-started/testing.md)** 頁面會解釋它,並圍繞它建立整套模式。 +有一個建構子旗標是專為此設計的:`Client(mcp, raise_exceptions=True)`。它只對同一處理程序內的連線有作用,而 **[測試](../get-started/testing.md)** 頁面會解釋它,並圍繞它建立整套模式。 ## 重點回顧 {#recap} -* `Client(x)` 對伺服器物件以記憶體內方式連線,對 URL 字串透過 Streamable HTTP 連線,其他情況則透過傳輸連線。 +* `Client(x)` 對 URL 字串透過 Streamable HTTP 連線,對 `StdioServerParameters` 啟動子處理程序,對傳輸則直接進入,在測試中則接受伺服器物件本身。 * `async with` 就是整個生命週期。在裡面,`server_capabilities` 和 `protocol_version` 已經填好;伺服器有提供時,`server_info` 和 `instructions` 也是。 * `list_tools()` 給你每個工具的 `name`、`title`、`description` 和 `input_schema`。 * `call_tool()` 回傳給模型的 `content`、給程式碼的 `structured_content`,以及 `is_error`。會引發例外的工具是一個結果,不是例外。 diff --git a/i18n/zh-hant/pages/client/oauth-clients.md b/i18n/zh-hant/pages/client/oauth-clients.md index fd0cb76aa6..6e36906095 100644 --- a/i18n/zh-hant/pages/client/oauth-clients.md +++ b/i18n/zh-hant/pages/client/oauth-clients.md @@ -1,6 +1,6 @@ --- translation: - sections: [c6899d3892bd9fa0, 79372cff3cc48a88, 63878d29e87c3e73, 13175843d3588af4, e7e2b9fd516f77de, 758f06399b513c1f, a05d7278487d610b] + sections: [c6899d3892bd9fa0, 79372cff3cc48a88, c2dae1ebe2ebd543, 13175843d3588af4, df06056fb16b3846, 758f06399b513c1f, a05d7278487d610b] tool: 1 --- # OAuth 用戶端 {#oauth-clients} @@ -76,18 +76,20 @@ translation: `Client` 第一次送出請求時,伺服器回應 `401`。provider 接手: -1. **探索。** 讀取 `WWW-Authenticate` 標頭,從 `/.well-known/oauth-protected-resource` 抓取伺服器的 Protected Resource Metadata,得知是哪個授權伺服器在保護這個資源,再去抓取**那個**伺服器的中繼資料。 +1. **探索。** 讀取 `WWW-Authenticate` 標頭,從 `/.well-known/oauth-protected-resource` 抓取伺服器的 Protected Resource Metadata,得知是哪個授權伺服器在保護這個資源,再去抓取**那個**伺服器的中繼資料。(較舊、沒有發布資源中繼資料的伺服器,則改向它自己的 origin 索取授權伺服器中繼資料。)無論哪種情況,中繼資料列出的 `issuer` 都必須就是它所屬的那個伺服器;不是的話一律拒絕。 2. **註冊。** 儲存庫裡什麼都沒有?它會用你的 `OAuthClientMetadata` 動態註冊,並把結果存起來。 3. **授權。** 產生 PKCE 配對和一個 `state`,組出授權 URL,await 你的 `redirect_handler`,接著 await 你的 `callback_handler` 取得授權碼。 4. **交換。** 拿授權碼換得 `OAuthToken`,存起來,然後帶著 `Authorization: Bearer ...` 重送你原本的請求。 之後它就很安靜。權杖從儲存庫拿出來用,過期的存取權杖用重新整理權杖更新,只有這些都行不通時才會重跑整個流程。 +有一條傳輸規則適用於所有這些請求:和它們所在的那個 MCP 請求一樣,重新導向只有在留在同一個 origin 且保持相同方法時才會跟隨(例如補上結尾斜線的 307/308),其他任何重新導向都視為那個 URL 沒有回應。 + 這些你一行都沒寫。還剩兩個關鍵字引數(`client_metadata_url` 和 `validate_resource_url`),這個檔案兩個都用不到。值得認識的是 `client_metadata_url`,下面有它專屬的一節。 ### 試試看 {#try-it} -這份文件裡的大多數範例都能用記憶體內的 `Client(server)` 檢驗。這個不行:整個流程的重點就是一個 HTTP `401`,而記憶體內的用戶端和它的伺服器之間根本沒有 HTTP。 +測試裡用的記憶體內 `Client(server)` 在這裡幫不上忙:整個流程的重點就是一個 HTTP `401`,而記憶體內的用戶端和它的伺服器之間根本沒有 HTTP。 儲存庫裡附有實際運作的版本。`examples/servers/simple-auth/` 會執行一個獨立的授權伺服器和一個受保護的 MCP 伺服器;`examples/clients/simple-auth-client/` 則是這一頁的用戶端長成的一個小型 CLI。它的 README 有那兩個指令:啟動伺服器、對著它們執行用戶端,就能看著上面四個步驟依序發生。 @@ -105,13 +107,14 @@ URL 必須是 HTTPS 且路徑不能是根路徑;否則在建構時就會引發 `ClientCredentialsOAuthProvider` 是同一個 `httpx2.Auth`,只是少了人: -```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" ``` 改變的地方: * 沒有 `OAuthClientMetadata`,沒有處理函式。傳入 `client_id` 和 `client_secret`;provider 會圍繞它們建出一筆最精簡的 `client_credentials` 註冊,並完全跳過動態註冊。 +* `issuer` 指明發出這組憑證的授權伺服器;用它的 `/.well-known/oauth-authorization-server` 文件回傳的 `issuer` 值。探索仍照上面那樣執行,但權杖請求永遠只會用**那個** issuer 的中繼資料來組裝;如果 MCP 伺服器指向別的地方,流程會停下並引發 `OAuthFlowError`。省略它已棄用,3.0 起會變成必填(見 **[已棄用的功能](../deprecated.md#deprecated-sdk-helpers)**);在那之前,provider 會發出警告,並使用探索找到的那個授權伺服器。 * `scope` 是以空格分隔的字串,也就是 OAuth 的線路格式。 * 下游的一切完全相同:同樣的 `TokenStorage`、同樣的 `httpx2.AsyncClient(auth=...)`、同樣的 `streamable_http_client`。 @@ -121,7 +124,7 @@ URL 必須是 HTTPS 且路徑不能是根路徑;否則在建構時就會引發 `client_secret` 要從環境變數或祕密管理工具讀取,絕對不要放進版本控制。 !!! info - `mcp.client.auth.extensions.client_credentials` 裡還有一個 provider:**`PrivateKeyJWTOAuthProvider`**,給用 JWT 而非共用 secret 來驗證的用戶端使用(`private_key_jwt`,也就是金鑰對與工作負載身分那一類)。它遵循同樣的模式:建構一個,放到 `auth=` 上。同一個模組還附了 `SignedJWTParameters` 和 `static_assertion_provider`,兩個用來建出其 assertion 的輔助工具。 + `mcp.client.auth.extensions.client_credentials` 裡還有一個 provider:**`PrivateKeyJWTOAuthProvider`**,給用 JWT 而非共用 secret 來驗證的用戶端使用(`private_key_jwt`,也就是金鑰對與工作負載身分那一類)。它遵循同樣的模式:建構一個(它接受同樣選用的 `issuer`),放到 `auth=` 上。同一個模組還附了 `SignedJWTParameters` 和 `static_assertion_provider`,兩個用來建出其 assertion 的輔助工具。 還有一種無人介入的情境:用戶端屬於某個企業,由企業的身分提供者(而非使用者)決定它可以連到哪些 MCP 伺服器。那是另一種授權類型,有自己的信任模型,也有自己的頁面:**[身分斷言](identity-assertion.md)**。 diff --git a/i18n/zh-hant/pages/client/session-groups.md b/i18n/zh-hant/pages/client/session-groups.md index 504fd1c1ab..d8ec72fde4 100644 --- a/i18n/zh-hant/pages/client/session-groups.md +++ b/i18n/zh-hant/pages/client/session-groups.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-groups} @@ -76,7 +76,7 @@ translation: ## 重點回顧 {#recap} * `ClientSessionGroup` 握有多條伺服器連線,並把它們的工具、資源和提示詞各自合併成一個 `dict`。 -* 每台伺服器呼叫一次 `connect_to_server(params)`。它接受傳輸參數,絕不是 `Client` 接受的伺服器物件或 URL。 +* 每台伺服器呼叫一次 `connect_to_server(params)`。它接受傳輸參數,絕不是 `Client` 接受的 URL 或 `Transport`。 * `group.call_tool(name, arguments)` 會替你轉送到擁有該工具的伺服器。 * 名稱在整個群組內必須唯一;兩台都有 `search` 工具的伺服器無法原樣共存。 * `component_name_hook=` 會改寫每個登記的名稱。dict 的鍵會變,線路上的名稱不變。 diff --git a/i18n/zh-hant/pages/client/transports.md b/i18n/zh-hant/pages/client/transports.md index a7f839d428..5d626ba6b0 100644 --- a/i18n/zh-hant/pages/client/transports.md +++ b/i18n/zh-hant/pages/client/transports.md @@ -1,6 +1,6 @@ --- translation: - sections: [9cac816674181eb0, 0700f337babcd4dd, 2bde0dd58cdf00f5, 40b4916d82eaf1d4, 3d0832f39b0d7059, dfa4446556badef0, 5bd93be2ab2ecb9c] + sections: [9cac816674181eb0, 7c157764133fea1f, 40b4916d82eaf1d4, 10d151f2cc75317f, 3d0832f39b0d7059, 92742ba36533633d, 0aeca6145e7bd302] tool: 1 --- # 用戶端傳輸方式 {#client-transports} @@ -11,30 +11,15 @@ translation: 每種傳輸方式的**伺服器**端(`mcp.run()` 做的事,以及你部署的東西)請見 **[執行伺服器](../run/index.md)**。 -## 記憶體內 {#in-memory} - -直接傳入伺服器物件本身: - -```python title="client.py" hl_lines="14" ---8<-- "docs_src/client_transports/tutorial001.py" -``` - -沒有子處理程序,沒有連接埠,線路上也沒有任何位元組。用戶端和伺服器是同一個處理程序裡的兩個物件,但呼叫仍然會經過真正的協定層:`search_books` 被列出、驗證、呼叫的方式,和透過 HTTP 時完全一樣。 - -所以它同時是兩樣東西: - -* **測試工具。** 這份說明文件裡的每個範例都是這樣跑過的,而 **[測試](../get-started/testing.md)** 那一頁整個模式就是圍繞它建立的。 -* **嵌入用的 API。** 自己建立伺服器的應用程式,不需要繞一圈網路就能呼叫它的工具。 - ## Streamable HTTP {#streamable-http} -傳入一個 URL 字串,得到的就是 **Streamable HTTP**,也就是部署時使用的那種傳輸方式: +傳入一個 URL 字串,得到的就是 **Streamable HTTP**,也就是部署時使用、也該優先選用的傳輸方式: ```python title="client.py" hl_lines="5" --8<-- "docs_src/client_transports/tutorial002.py" ``` -這就是完整的正式環境用戶端。`Client` 會替你把 URL 包進 `streamable_http_client(...)`,底下是一個依 MCP 需求設定好的 `httpx2.AsyncClient`:`follow_redirects=True`、connect/write/pool 的逾時為 30 秒,read 逾時則是 300 秒,因為伺服器可能會讓回應串流一直開著。 +這就是完整的正式環境用戶端。`Client` 會替你把 URL 包進 `streamable_http_client(...)`,底下是一個依 MCP 需求設定好的 `httpx2.AsyncClient`:connect/write/pool 的逾時為 30 秒,read 逾時則是 300 秒,因為伺服器可能會讓回應串流一直開著。 !!! check 建立好的 `Client` **還沒有**連線。建立只是選定傳輸方式;真正開啟它的是 `async with`。在進入之前就去拿連線,SDK 會直接告訴你: @@ -49,7 +34,7 @@ translation: 一旦需要 `Authorization` 標頭、cookie、proxy、mTLS,或不同的逾時,就自己建立 `httpx2.AsyncClient`,再交給 `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" ``` @@ -70,7 +55,25 @@ translation: 所有跟 HTTP 有關的設定,現在都放在你傳入的那一個 `httpx2.AsyncClient` 上。 !!! info - `httpx2` 保留了熟悉的 `httpx` API,所以只要會用 `httpx`,就已經知道這裡的驗證、proxy、事件掛鉤、重試和連線數限制該怎麼做。SDK 沒有在上面加任何東西,也沒有拿掉任何東西。OAuth 也是從這裡接上的:`httpx2.AsyncClient(auth=OAuthClientProvider(...))`。整個流程請見 **[OAuth 用戶端](oauth-clients.md)**。 + `httpx2` 保留了熟悉的 `httpx` API,所以只要會用 `httpx`,就已經知道這裡的驗證、proxy、事件掛鉤、重試和連線數限制該怎麼做。SDK 沒有在上面加任何東西,也沒有拿掉任何東西,[重新導向的處理](#redirects)除外。OAuth 也是從這裡接上的:`httpx2.AsyncClient(auth=OAuthClientProvider(...))`。整個流程請見 **[OAuth 用戶端](oauth-clients.md)**。 + +### 重新導向 {#redirects} + +傳輸只會連到你給它的那個 URL,而且只限那個來源(origin)。 + +* 留在同一個 scheme、主機和連接埠上的 `307`/`308` 重新導向會跟隨,同一台主機上的 `http://` → `https://` 也會。這涵蓋了常見的 `/mcp` → `/mcp/` 結尾斜線重新導向。 +* 導向其他任何地方的重新導向則**不會**跟隨。呼叫會失敗,並出現: + + ```text + MCPError: Redirect to https://other.example.com/mcp not followed; use that URL as the endpoint if it is the intended server + ``` + + 如果那個 URL 就是你要的伺服器,把它寫進設定裡。如果不是,那就是伺服器或它前面的 proxy 設定有誤。 + +不管傳入哪個 `httpx2.AsyncClient` 都一樣:MCP 請求不會參考它的 `follow_redirects` 設定,不論設成哪個方向。SDK 的 OAuth provider 對自己發出的請求也套用同一條規則。 + +!!! tip + `Redirect to http://… not followed: it would downgrade this HTTPS endpoint to plain HTTP` 表示伺服器前面有一個它不知道的、負責終結 TLS 的 proxy,所以它發出的是 `http://` 重新導向。這要在伺服器端修正(**[部署與擴展](../run/deploy.md#behind-a-tls-terminating-proxy)**),或者改用訊息裡建議的那個確切的 `https://…/` URL。 ## stdio {#stdio} @@ -91,6 +94,18 @@ translation: 需要 API 金鑰的伺服器在那裡是找不到的。請用 `env=` 明確傳入;這些變數會疊加在允許清單之上。上面的 `BOOKSHOP_API_KEY` 做的就是這件事。 +## 記憶體內 {#in-memory} + +在測試裡沒有東西要部署,也沒有東西要啟動。直接傳入伺服器物件本身: + +```python hl_lines="14" +--8<-- "docs_src/client_transports/tutorial001.py" +``` + +沒有子處理程序,沒有連接埠,線路上也沒有任何位元組。用戶端和伺服器是同一個處理程序裡的兩個物件,但呼叫仍然會經過真正的協定層:`search_books` 被列出、驗證、呼叫的方式,和透過 HTTP 時完全一樣。**[測試](../get-started/testing.md)** 那一頁整個模式就是圍繞它建立的。 + +同樣的寫法也可以當成嵌入用的 API:自己建立伺服器的應用程式,不需要繞一圈網路就能呼叫它的工具。 + ## SSE {#sse} `mcp.client.sse` 裡的 `sse_client(url)` 是被 Streamable HTTP 取代的那個 HTTP 傳輸。要和還在講它的伺服器溝通,用同樣的方式包起來即可:`Client(sse_client("http://localhost:8000/sse"))`,但不要在它上面蓋任何新東西。 @@ -99,15 +114,16 @@ translation: 對 `Client` 來說,上面這些全都是同一種東西。 -**傳輸**是任何會產出一對 `(read, write)` 訊息串流的非同步 context manager:正式地說,就是 `mcp.client` 裡的 `Transport` 協定。`Client` 依型別解析它的引數:伺服器物件就在處理程序內連線,`str` 會變成 `streamable_http_client(url)`,`StdioServerParameters` 會變成 `stdio_client(params)`,其他任何東西則直接當成傳輸進入。最後這條規則就是為什麼 `stdio_client(...)`、`streamable_http_client(...)` 和 `sse_client(...)` 都能放進同一個位置,也是為什麼你可以自己寫一個。 +**傳輸**是任何會產出一對 `(read, write)` 訊息串流的非同步 context manager:正式地說,就是 `mcp.client` 裡的 `Transport` 協定。`Client` 依型別解析它的引數:`str` 會變成 `streamable_http_client(url)`,`StdioServerParameters` 會變成 `stdio_client(params)`,伺服器物件就在處理程序內連線,其他任何東西則直接當成傳輸進入。最後這條規則就是為什麼 `stdio_client(...)`、`streamable_http_client(...)` 和 `sse_client(...)` 都能放進同一個位置,也是為什麼你可以自己寫一個。 ## 重點回顧 {#recap} -* `Client(mcp)`(伺服器物件)在記憶體內連線。用在測試和嵌入。 * `Client("http://.../mcp")`(URL)透過 Streamable HTTP 連線,也就是正式環境用的傳輸方式。 * 標頭、驗證、proxy 和逾時都放在你傳給 `streamable_http_client(url, http_client=...)` 的 `httpx2.AsyncClient` 上。沒有 `headers=` 這個關鍵字引數。 +* 重新導向只在 URL 自己的來源內跟隨(結尾斜線的 `307`/`308`),外加同一台主機上的 `http`→`https`。其他的一律失敗並出現 `Redirect to … not followed`;把最終的 URL 寫進設定即可。 * stdio 是 `Client(StdioServerParameters(...))`。只有要把子處理程序的 stderr 導到別處時,才需要自己用 `stdio_client(...)` 包起來。 * 子處理程序拿到的是允許清單上的環境,不是你的環境;`env=` 會往上加。 +* `Client(mcp)`(伺服器物件)在記憶體內連線。用在測試裡,或是把伺服器嵌入建立它的那個應用程式。 * 傳輸就是任何可以 `async with x as (read, write)` 的東西。只要不是伺服器物件、URL 或 `StdioServerParameters`,`Client` 就會直接交給那個協定處理。 * 建立 `Client` 是選定傳輸方式。`async with` 才是開啟它。 diff --git a/i18n/zh-hant/pages/deprecated.md b/i18n/zh-hant/pages/deprecated.md index ca8137da84..ad5540b8bd 100644 --- a/i18n/zh-hant/pages/deprecated.md +++ b/i18n/zh-hant/pages/deprecated.md @@ -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 --- # 已棄用的功能 {#deprecated-features} -2026-07-28 規格讓五樣東西退場。SDK 仍然實作了其中每一項,而每一項現在都帶有**棄用警告**。另外有一個 SDK 輔助函式是因為自身的原因棄用,列在[最後](#deprecated-sdk-helpers)。 +2026-07-28 規格讓五樣東西退場。SDK 仍然實作了其中每一項,而每一項現在都帶有**棄用警告**。另外有幾項 SDK 層級的棄用是出於自身的原因,列在[最後](#deprecated-sdk-helpers)。 下表列出每一項已棄用的功能、它為什麼要退場,以及應該改用的替代做法。 @@ -125,11 +125,13 @@ warnings.filterwarnings("ignore", category=MCPDeprecationWarning) ## 已棄用的 SDK 輔助函式 {#deprecated-sdk-helpers} -這些不是規格變更,只是有了更好替代做法的 SDK 內部實作。它們用同樣的 `MCPDeprecationWarning` 發出警告,並會在 3.0 移除。 +這些不是規格變更,只是有了更好替代做法的 SDK 用法。它們用同樣的 `MCPDeprecationWarning` 發出警告,舊的寫法會在 3.0 移除。 | 已棄用項目 | 替代做法 | |---|---| | `FuncMetadata.call_fn_with_arg_validation()` | 先 `FuncMetadata.validate_arguments()`,再 `FuncMetadata.call_fn()`。只有直接操作 `FuncMetadata` 的程式碼(例如自訂的 `Tool` 子類別)才會呼叫過它。 | +| 沒有設定 `validate_token_resource=` 的 `AuthSettings(resource_server_url=...)` | 把它設好:`True` 會讓伺服器拒絕驗證器沒有回報為核發給 `resource_server_url` 的 bearer 權杖;`False` 表示你的驗證器會自己檢查權杖的 audience(請見 **[授權](run/authorization.md#a-token-verifier)**)。不設定時的行為等同 `False`;3.0 起,只要設了 `resource_server_url`,預設就是 `True`。 | +| 沒有傳入 `issuer=` 的 `ClientCredentialsOAuthProvider(...)` 或 `PrivateKeyJWTOAuthProvider(...)` | 傳入 `issuer=`,指明核發這組憑證的授權伺服器(請見 **[撰寫 OAuth 用戶端](client/oauth-clients.md#machine-to-machine)**)。少了它,就變成由 MCP 伺服器決定哪個授權伺服器會收到這組憑證;3.0 會把這個關鍵字引數改為必填。 | ## 重點回顧 {#recap} @@ -138,7 +140,7 @@ warnings.filterwarnings("ignore", category=MCPDeprecationWarning) * 棄用只是勸告性質:線路沒有變更,一切在 2026 之前的工作階段上都能繼續運作,而且你會看到明顯的 `MCPDeprecationWarning`(它是 `UserWarning`,所以預設就會顯示)。 * 取樣和根目錄還額外需要一條反向通道,而 2026-07-28 的工作階段沒有。在現代連線上,它們會先警告,再引發例外。 * `warnings.filterwarnings("ignore", category=MCPDeprecationWarning)` 會讓整個類別靜音;pytest 裡的 `"error::mcp.MCPDeprecationWarning"` 則把它變成測試失敗。 -* 有一個 SDK 輔助函式 `FuncMetadata.call_fn_with_arg_validation()` 另外單獨棄用,預計在 3.0 移除。 +* [SDK 層級的棄用](#deprecated-sdk-helpers)遵循同樣的規則:現在發出警告,3.0 移除舊的寫法。 * 新程式碼不應該建立在這些東西之上。 這份說明文件的其他每一頁教的都是目前的 API。 diff --git a/i18n/zh-hant/pages/get-started/first-steps.md b/i18n/zh-hant/pages/get-started/first-steps.md index af48ee7a48..49a3fb44ed 100644 --- a/i18n/zh-hant/pages/get-started/first-steps.md +++ b/i18n/zh-hant/pages/get-started/first-steps.md @@ -1,6 +1,6 @@ --- translation: - sections: [0d6c05bcbf836bf3, 59a7b14eeefc68c1, 7114d8d6daba203f, e8bbb56a98ba7bc9, 5138010f6159901c, f78da7c7c363d4c6, 220a939cab348686] + sections: [0d6c05bcbf836bf3, 9a78b5f6b44b18ab, 7114d8d6daba203f, e8bbb56a98ba7bc9, bfd2fd1153e71dac, 1615a994ef071fdd, 65c599fae991f245] tool: 1 --- # 第一步 {#first-steps} @@ -17,7 +17,7 @@ translation: * **用戶端**位於主機內部,負責講 MCP。主機每連上一個伺服器,就執行一個用戶端。 * **伺服器**是你用這個 SDK 打造的東西。它把東西公開給用戶端,從不直接和模型溝通。 -伺服器由你來寫,主機是別人的產品。SDK 也提供一個 `Client`,用來測試你的伺服器,這一頁稍後就會出現。 +伺服器由你來寫,主機是別人的產品。SDK 也提供一個 `Client`,主機要透過 URL 連上伺服器、或把它當成子處理程序啟動,用的就是同一個類別。它在這一頁稍後就會出現,也是你測試伺服器的方式。 ## 三種基本元件 {#the-three-primitives} @@ -79,22 +79,20 @@ Inspector 透過 **stdio** 執行你的伺服器,這是 MCP 伺服器能使用 用戶端連線時,伺服器會宣告自己的**能力**:它會回應哪幾類請求。用戶端據此決定究竟該要求什麼。這份宣告不是你寫的,`MCPServer` 替你宣告好了。 -自己看看吧。SDK 的 `Client` 直接接受伺服器物件,並在**記憶體內**與它連線(沒有子處理程序,沒有連接埠): - -```python -import asyncio - -from mcp import Client - -from server import mcp +自己看看吧。在一個終端機裡讓 `server.py` 透過 HTTP 持續執行: +```console +uv run mcp run server.py --transport streamable-http +``` -async def main() -> None: - async with Client(mcp) as client: - print(client.server_capabilities.model_dump(exclude_none=True)) +再從另一個終端機把用戶端指向它: +```python title="client.py" hl_lines="7-8" +--8<-- "docs_src/first_steps/tutorial001_client.py" +``` -asyncio.run(main()) +```console +python client.py ``` ```text @@ -114,7 +112,7 @@ asyncio.run(main()) 注意少了什麼。`completions`(資源範本和提示詞的引數自動完成)需要你寫一個處理函式,這個伺服器沒有,所以這項能力不存在,守規矩的用戶端也不會去問。所有選用的東西都照這條規則:註冊了,能力就出現;**[自動完成](../servers/completions.md)** 會證明這一點。 !!! info - `Client(mcp)` 就是這份文件裡每個範例測試時用的同一個記憶體內用戶端,你也會用它來測試自己的。它有專屬的一整頁:**[測試](testing.md)**。 + 那個 `client.py` 是一個完整的 MCP 用戶端,它的專屬頁面是 **[用戶端](../client/index.md)**。測試時可以跳過終端機和連接埠,把伺服器物件本身直接交給 `Client`,也就是 `Client(mcp)`。那也有專屬的一整頁:**[測試](testing.md)**。 ## 你沒寫的東西 {#what-you-did-not-write} @@ -123,7 +121,7 @@ asyncio.run(main()) * JSON Schema。`a: int, b: int` **就是** `add` 的 schema。 * 請求處理函式。`tools/list`、`resources/read`、`prompts/get`:全都替你處理好了。 * 能力宣告。`MCPServer` 替你做了。 -* 任何一行協定。版本協商、JSON-RPC 訊框、能力交換:全都發生在 `mcp dev` 和 `Client(mcp)` 裡面,你完全沒看到。 +* 任何一行協定。版本協商、JSON-RPC 訊框、能力交換:全都發生在 `mcp dev` 和 `client.py` 裡面,你完全沒看到。 這個比例正是 SDK 的意義所在。 @@ -134,6 +132,6 @@ asyncio.run(main()) * 每種基本元件一個裝飾器:`@mcp.tool()`、`@mcp.resource(uri)`、`@mcp.prompt()`。名稱、描述和 schema 都來自函式。 * 帶 `{param}` 的 URI 會產生資源**範本**,和具體資源分開列出。 * 伺服器的**能力**會替你宣告好,而用戶端只會要求伺服器宣告過的東西。 -* `Client(mcp)` 在記憶體內連上伺服器物件:從第一天起就是你的測試工具。 +* `Client("http://localhost:8000/mcp")` 會和執行中的伺服器對話。改成把伺服器物件交給它,也就是 `Client(mcp)`,從第一天起它就是你的測試工具。 接下來是 **[連接真正的主機](real-host.md)**:把這個伺服器真的放進 Claude Desktop 或 IDE 裡。然後是 **[測試](testing.md)**:一頁、一個記憶體內用戶端,從此不用猜它到底能不能動。再之後,每種基本元件各有自己的一頁,從模型主導的那個開始:**[工具](../servers/tools.md)**。 diff --git a/i18n/zh-hant/pages/get-started/testing.md b/i18n/zh-hant/pages/get-started/testing.md index c37bac9d9c..f8a5d60b87 100644 --- a/i18n/zh-hant/pages/get-started/testing.md +++ b/i18n/zh-hant/pages/get-started/testing.md @@ -1,13 +1,13 @@ --- translation: - sections: ['4926721070127497', c52a1de2b6b32f40, 8e792bf8c7489ec6, 627195f7159e24ef] + sections: [5d13c2f0ba42c0d2, c52a1de2b6b32f40, 8e792bf8c7489ec6, 38552ea228b0a04f] tool: 1 --- # 測試 {#testing} -Python SDK 附帶一個 `Client` 類別,內建**記憶體內傳輸**:把伺服器物件傳給它,它就會直接連上去。 +SDK 的 `Client` 類別,也就是連到 URL 或啟動子處理程序的同一個類別,也能在**記憶體內**連線:把伺服器物件傳給它,它就會直接和伺服器對話。 -沒有子處理程序,沒有連接埠,根本沒有傳輸層。概念和 FastAPI 的 `TestClient` 一樣。 +沒有子處理程序,沒有連接埠,線路上什麼都沒有。概念和 FastAPI 的 `TestClient` 一樣。 ## 基本用法 {#basic-usage} @@ -86,7 +86,7 @@ async def test_call_add_tool(client: Client): 測試裡就開著它。在正式環境的程式碼裡它沒有任何意義。 -## 預設為處理程序內連線 {#in-process-by-default} +## 預設為世代中立 {#era-neutral-by-default} !!! note `Client(mcp)` 以處理程序內的方式連線,而且預設是**世代中立**的:它會探測伺服器,選出合適的協定路徑。如果測試要驗證舊版特有的語意(取樣(sampling)或徵詢(elicitation)的推送、`message_handler`),就固定用 `mode="legacy"`,並且在那裡拿掉 `raise_exceptions=True`:舊版連線本來就不會淨化,而這個旗標會讓失敗在伺服器任務裡重新引發,而不是在你的測試裡。 diff --git a/i18n/zh-hant/pages/handlers/multi-round-trip.md b/i18n/zh-hant/pages/handlers/multi-round-trip.md index 2d9c5a094d..4d97c8ccdd 100644 --- a/i18n/zh-hant/pages/handlers/multi-round-trip.md +++ b/i18n/zh-hant/pages/handlers/multi-round-trip.md @@ -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-round-trip-requests} @@ -162,7 +162,7 @@ TTL、主體綁定和請求綁定都**不是** codec 的工作:不論哪個 co ## 2026-07-28 的結果型別 {#a-2026-07-28-result} -`InputRequiredResult` 只存在於協定版本 **2026-07-28**。記憶體內的 `Client(server)` 會替你協商;走線路時,`mode="auto"` 會探知它。連線之後,`client.protocol_version` 會告訴你拿到的是什麼。 +`InputRequiredResult` 只存在於協定版本 **2026-07-28**。`Client` 預設的 `mode="auto"` 在任何連線上都會探知它。連線之後,`client.protocol_version` 會告訴你拿到的是什麼。 !!! warning 2026 之前的工作階段沒有地方放 `InputRequiredResult`。在 `mode="legacy"` 的連線上從處理函式回傳一個,runner 無法把它序列化成協商好的版本;用戶端會拿回 `-32603`「Handler returned an invalid result」錯誤。同時服務兩個世代的伺服器,必須先檢查 `ctx.protocol_version` 再動用它。 diff --git a/i18n/zh-hant/pages/handlers/progress.md b/i18n/zh-hant/pages/handlers/progress.md index de2ba68825..7f1d96d509 100644 --- a/i18n/zh-hant/pages/handlers/progress.md +++ b/i18n/zh-hant/pages/handlers/progress.md @@ -1,6 +1,6 @@ --- translation: - sections: [5315262fe26b33e1, 9d8e98840f1b78f0, 0284b215e85366c4, 8534d8dbb4053a70, 2966fac6fe697007] + sections: [5315262fe26b33e1, 9d8e98840f1b78f0, 52d6009a07e770ea, 8534d8dbb4053a70, 2966fac6fe697007] tool: 1 --- # 進度 {#progress} @@ -29,19 +29,17 @@ translation: 用戶端是**逐次呼叫**選擇加入的,做法是把 `progress_callback=` 傳給 `call_tool`: -```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,23 +54,27 @@ anyio.run(main) 回呼是一個 `async` 函式,接收的正是伺服器回報的內容:`progress`、`total`、`message`。 !!! info - `Client(mcp)` 在記憶體內直接連上伺服器物件,和 **[測試](../get-started/testing.md)** 那一頁用的是同一個用戶端。不管 `Client` 用哪種傳輸方式,`progress_callback` 都是同一個參數;接下來看到的**時序**則是記憶體內連線的。它會就地執行回呼,所以每一筆回報都會在 `call_tool` 回傳之前送達。換成真正的傳輸方式,通知會和結果競速,一個慢的回呼在 `call_tool` 回傳之後可能還在執行。 + 不管交給 `Client` 的是什麼,`progress_callback` 都是同一個參數:像這裡的 URL、一個 `StdioServerParameters`,或測試裡的伺服器物件。不過走真正的傳輸方式時要留意時序。每個通知都是在回應之外單獨送達的,所以一個慢的回呼在 `call_tool` 回傳之後可能還在執行。只有處理程序內的測試連線會就地執行回呼,並保證每一筆回報都先送達。 ### 試試看 {#try-it} -把 `client.py` 放在 `server.py` 旁邊,然後執行: +用 HTTP 提供 `server.py`,然後在第二個終端機執行用戶端: + +```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.'} ``` -伺服器上的每一個 `await ctx.report_progress(...)` 都變成用戶端上對 `show` 的一次呼叫,依序發生,而且兩行都在 `call_tool` 回傳**之前**印出。進度不會打包進結果裡;它在工具還在執行時就持續串流過來。 +伺服器上的每一個 `await ctx.report_progress(...)` 都變成用戶端上對 `show` 的一次呼叫,依序發生。進度不會打包進結果裡;它在工具還在執行時就持續串流過來。 !!! warning `progress_callback` 屬於那一次**呼叫**,不屬於 `Client`。沒有對應的建構子引數,因為不同的呼叫想要不同的回呼:這一次驅動下載進度條,下一次是一行記錄。 diff --git a/i18n/zh-hant/pages/protocol-versions.md b/i18n/zh-hant/pages/protocol-versions.md index 459a6b4102..993fce457a 100644 --- a/i18n/zh-hant/pages/protocol-versions.md +++ b/i18n/zh-hant/pages/protocol-versions.md @@ -1,6 +1,6 @@ --- translation: - sections: [478fd619e5f90ef8, aef094a00e44e248, bab8cbf3449fa7e9, df1809b15a58335b, 5f9d8c2336ed0239, f54974398e43ddef, b24443dd78584870] + sections: [d98e337bf28845e0, dd642acbba36f615, f547b3e76da19c94, 149513378588da5c, 4e149ed992046709, f54974398e43ddef, b24443dd78584870] tool: 1 --- # 協定版本 {#protocol-versions} @@ -11,9 +11,17 @@ MCP 有兩個世代。 你幾乎不需要在意這件事,因為 `Client` 會替你協商。這一頁談的是控制這件事的那一個建構子引數 `mode=`,以及需要改動它的三種情況。 +這一頁的每段程式碼都是一個 `client.py`,連線到 **[用戶端](client/index.md)** 那頁的 Bookshop `server.py`。先在一個終端機裡啟動那個伺服器: + +```console +uv run mcp run server.py --transport streamable-http +``` + +接著在第二個終端機裡用 `python client.py` 執行每一段程式碼。 + ## `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,11 +39,11 @@ MCP 有兩個世代。 整個功能就這樣。一個 `Client`,任何世代的伺服器,程式碼裡不用分支。 !!! info - `MCPServer` 在每一種傳輸方式上都會回答 `server/discover`(記憶體內、stdio、Streamable HTTP),所以對你自己的伺服器,`auto` 永遠會落在 `2026-07-28`。退回機制只會在面對真正的 2026 之前的伺服器時觸發,而那正是你希望它觸發的時候。 + `MCPServer` 在每一種傳輸方式上都會回答 `server/discover`(Streamable HTTP、stdio,以及測試時用的處理程序內連線),所以對你自己的伺服器,`auto` 永遠會落在 `2026-07-28`。退回機制只會在面對真正的 2026 之前的伺服器時觸發,而那正是你希望它觸發的時候。 ## `mode="legacy"` {#modelegacy} -```python title="client.py" hl_lines="14" +```python title="client.py" hl_lines="7" --8<-- "docs_src/protocol_versions/tutorial002.py" ``` @@ -59,7 +67,7 @@ MCP 有兩個世代。 `mode` 也接受新世代的協定版本字串。目前這個集合剛好就是 `["2026-07-28"]`。 -```python title="client.py" hl_lines="14" +```python title="client.py" hl_lines="7" --8<-- "docs_src/protocol_versions/tutorial003.py" ``` @@ -90,7 +98,7 @@ ValueError: mode must be 'legacy', 'auto', or one of ['2026-07-28']; got '2025-0 所以把它留下來。`auto` 連線之後,`client.session.discover_result` 保存著伺服器送來的那份 `DiscoverResult`:它的 `supported_versions`、`capabilities`、`instructions`,以及伺服器蓋進結果 `_meta` 裡的身分。下次把它作為 `prior_discover=` 交回去: -```python title="client.py" hl_lines="15 17" +```python title="client.py" hl_lines="8 10" --8<-- "docs_src/protocol_versions/tutorial004.py" ``` diff --git a/i18n/zh-hant/pages/run/asgi.md b/i18n/zh-hant/pages/run/asgi.md index 32c34aeb0a..dc7eb2174c 100644 --- a/i18n/zh-hant/pages/run/asgi.md +++ b/i18n/zh-hant/pages/run/asgi.md @@ -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 --- # 加到現有的應用程式中 {#add-to-an-existing-app} @@ -86,7 +86,7 @@ Starlette 的 `Host` 路由用法相同:把 `Mount("/", ...)` 換成 `Host("mc --8<-- "docs_src/asgi/tutorial004.py" ``` -現在用戶端連到 `/notes`,而不是 `/notes/mcp`。 +現在用戶端連到 `/notes/`,而不是 `/notes/mcp`。 ## 給瀏覽器用戶端的 CORS {#cors-for-browser-clients} @@ -127,4 +127,4 @@ Starlette 的 `Host` 路由用法相同:把 `Mount("/", ...)` 換成 `Host("mc * 瀏覽器用戶端需要 CORS:`allow_headers` 給 `Mcp-*` 請求標頭用,`expose_headers=["Mcp-Session-Id"]` 給回應用。 * `@mcp.custom_route()` 在 `/mcp` 旁邊加上普通、不經驗證的 HTTP 端點。 -一旦伺服器能透過真正的 URL 連到,**[用戶端](../client/index.md)** 就會用那個 URL 而不是伺服器物件來連線。 +一旦伺服器能透過真正的 URL 連到,**[用戶端](../client/index.md)** 就會用那個 URL 來連線。 diff --git a/i18n/zh-hant/pages/run/authorization.md b/i18n/zh-hant/pages/run/authorization.md index 1618cddc3c..52b0ce73b7 100644 --- a/i18n/zh-hant/pages/run/authorization.md +++ b/i18n/zh-hant/pages/run/authorization.md @@ -1,6 +1,6 @@ --- translation: - sections: [d62c13457fc4a534, 80e73abaca6e0652, d1dc4c54cd00ec9c, 14ad3bc7904036bb, 5225f127bc1b9c77, fe1626fdd5aad1da, 4556cb7ea1a04a31] + sections: [d62c13457fc4a534, 80e73abaca6e0652, 128a492d18295f64, 14ad3bc7904036bb, 54a6697833fcc1d9, fe1626fdd5aad1da, 811d083c1da8bcf6] tool: 1 --- # 授權 {#authorization} @@ -23,12 +23,12 @@ translation: 有效的權杖長什麼樣子,SDK 沒有任何預設立場。由你來告訴它,方法是實作 **`TokenVerifier`**: -```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` 是只有一個非同步方法的 protocol。`verify_token` 會拿到 `Authorization` 標頭裡的原始權杖,有效就回傳 **`AccessToken`**,無效就回傳 `None`。沒有別的需要實作。 -* 這個範例是在一張表裡查權杖。真實的實作會驗證 JWT 簽章,或呼叫授權伺服器的權杖內省(token introspection)端點。那段程式碼是你的,SDK 只負責呼叫它。 +* 這個範例是在一張表裡查權杖;每一筆都記錄了權杖是為哪個資源發出的。真實的實作會驗證 JWT 簽章,或呼叫授權伺服器的權杖內省(token introspection)端點,並在 `AccessToken.resource` 裡回報權杖是發給誰的(它的 `aud`)。那段程式碼是你的,SDK 只負責呼叫它。 * `token_verifier=` 和 `auth=` 永遠成對出現。只傳其中一個,`MCPServer(...)` 在服務任何請求之前就會引發 `ValueError`。 `AuthSettings` 是資源伺服器對外的門面: @@ -36,6 +36,10 @@ translation: * `issuer_url`:發出權杖的授權伺服器。 * `resource_server_url`:這個 MCP 端點的公開 URL。它指明權杖是給**哪一個**資源用的,也是探索文件所在的位置。 * `required_scopes`:每個權杖都必須帶有全部這些 scope。 +* `validate_token_resource`:拒絕任何 `AccessToken.resource` 不等於 `resource_server_url` 的權杖。設定了 `resource_server_url` 卻沒設定它,會發出警告(`MCPDeprecationWarning`)並視同 `False`;3.0 起,資源伺服器的預設值會改為 `True`。 + * 如果你的授權伺服器會把權杖綁定到用戶端請求的 `resource`(MCP 用戶端一定會送),就把它打開。讓 `resource_server_url` 與用戶端實際連線的 URL 完全一致。 + * 如果你的授權伺服器使用自己的 audience 識別碼(Auth0 的 API identifier、Entra 的應用程式 ID),就保持關閉,改在驗證器裡檢查 `aud`,遇到不是給這個伺服器的權杖就回傳 `None`。 + * 如果 `aud` 是一個清單,把等於 `resource_server_url` 的那一項放進 `resource`。 !!! tip SDK 儲存庫裡的 `examples/servers/simple-auth/` 有一個 `IntrospectionTokenVerifier`,會呼叫真實授權伺服器的 [RFC 7662](https://datatracker.ietf.org/doc/html/rfc7662) 端點。大多數正式環境的驗證器都是這個樣子。 @@ -85,7 +89,7 @@ translation: 在任何處理函式內,**`get_access_token()`** 就是驗證器為目前請求回傳的那個 `AccessToken`: -```python title="server.py" hl_lines="4 32-35" +```python title="server.py" hl_lines="4 35-38" --8<-- "docs_src/authorization/tutorial002.py" ``` @@ -117,6 +121,6 @@ SDK 給你的是資源伺服器這一半:驗證、公告、拒絕。它不提 * `token_verifier=` 和 `auth=AuthSettings(issuer_url=..., resource_server_url=..., required_scopes=[...])` 永遠成對出現。 * SDK 會在 `/.well-known/oauth-protected-resource/...` 發布 [RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728) Protected Resource Metadata,並以 401 回應未驗證的請求,其 `WWW-Authenticate` 標頭會指向這份文件。整個探索機制就這樣。 * 在任何處理函式裡,`get_access_token()` 就是誰在呼叫。 -* 授權是 HTTP 層的事。`stdio` 和記憶體內用戶端永遠看不到它。 +* 授權是 HTTP 層的事。`stdio` 和記憶體內測試用戶端永遠看不到它。 用戶端那一半(探索你的授權伺服器並替你取得權杖)請見 **[OAuth 用戶端](../client/oauth-clients.md)**。至於不問使用者、而是直接**斷言**身分的用戶端,請見 **[身分斷言](../client/identity-assertion.md)**。 diff --git a/i18n/zh-hant/pages/run/deploy.md b/i18n/zh-hant/pages/run/deploy.md index 01f1ed3ad9..c500dcaa08 100644 --- a/i18n/zh-hant/pages/run/deploy.md +++ b/i18n/zh-hant/pages/run/deploy.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 --- # 部署與擴展 {#deploy-scale} @@ -42,6 +42,22 @@ translation: 在用戶端找不到這幾個字。`421` 是純文字的 HTTP 回應,不是 JSON-RPC 錯誤,所以 MCP 用戶端只會引發一個籠統的傳輸錯誤;它不喜歡的主機名稱只會出現在**伺服器**的記錄裡,就一則警告。剛部署好卻拒絕所有連線的伺服器,在證明是別的原因之前,就是 Host 允許清單的問題。**[疑難排解](../troubleshooting.md)** 也從這裡開始。 +## 在終止 TLS 的代理後面 {#behind-a-tls-terminating-proxy} + +如果 TLS 在代理(ingress、負載平衡器、Caddy、nginx)就結束,uvicorn 在它後面提供純 HTTP,就告訴 uvicorn 信任代理的 `X-Forwarded-*` 標頭: + +```console +uvicorn server:app --proxy-headers --forwarded-allow-ips='' +``` + +不這麼做的話,應用程式會以為自己是透過 `http://` 提供服務,它發出的任何重新導向(常見的是 `/mcp` → `/mcp/`)都會指向 `http://…`。Python 用戶端拒絕從 HTTPS 端點跟到純 HTTP,而且會明說: + +```text +MCPError: Redirect to http://mcp.example.com/mcp/ not followed: it would downgrade this HTTPS endpoint to plain HTTP. +``` + +用戶端的權宜之計是設定伺服器實際提供服務的精確 URL(`https://mcp.example.com/mcp/`,含結尾斜線),讓重新導向根本不發生。正解是上面那個旗標。`FORWARDED_ALLOW_IPS` 是環境變數的寫法;`*` 會信任每一跳,只有在除了代理之外沒有東西碰得到 uvicorn 時才正確。 + ## Worker,以及誰需要黏性 {#workers-and-who-has-to-be-sticky} 主機名稱能回應之後,就在後面放不只一個 worker。SDK 沒有這方面的設定;擴展 Starlette 應用程式的方式跟擴展任何 ASGI 應用程式一樣,把物件交給懂得 fork 的東西: @@ -154,6 +170,7 @@ python -c "import secrets; print(secrets.token_hex(32))" ## 重點回顧 {#recap} * 預設情況下,這個應用程式只回應送往 localhost 的請求。`transport_security=TransportSecuritySettings(allowed_hosts=[...], allowed_origins=[...])` 是上線的關卡:在傳入它之前,真正主機名稱後面的每個請求都是 `421`,原因只在伺服器記錄裡。 +* 在終止 TLS 的代理後面,用 `--proxy-headers --forwarded-allow-ips=...` 執行 uvicorn,否則它的重新導向會指向 `http://`,用戶端會拒絕跟隨。 * 在 2026-07-28 上沒有工作階段,負載平衡器也沒有東西可黏。`stateless_http=True` 是只給舊版用的開關,因為現代請求在那個旗標被讀到之前就已經分流並回應了。 * 預設的 `requestState` 金鑰是 `os.urandom(32)`,每個處理程序各自鑄造。送到不同 worker 的多輪往返重試會以 `-32602`「Invalid or expired requestState」失敗。 * 解法是 `RequestStateSecurity(keys=[...])` **加上**每個執行個體相同的伺服器名稱。名稱是權杖預設的 audience 宣告。相同的金鑰,相同的名稱。 diff --git a/i18n/zh-hant/pages/run/index.md b/i18n/zh-hant/pages/run/index.md index a0a3960b38..2d4afdafd6 100644 --- a/i18n/zh-hant/pages/run/index.md +++ b/i18n/zh-hant/pages/run/index.md @@ -1,6 +1,6 @@ --- translation: - sections: [fea8d769ff9edeba, ce8e2ad42f29ef71, 0d705efb19cf99c2, 2fd7cf825e6d2b2c, 9adc400e8c88e854, 318893ad8e2e9924, 6b63ab96b34476c0] + sections: [fea8d769ff9edeba, ce8e2ad42f29ef71, 0d705efb19cf99c2, 9fd2357154a5b7e7, 9adc400e8c88e854, 318893ad8e2e9924, 6b63ab96b34476c0] tool: 1 --- # 執行伺服器 {#running-your-server} @@ -71,6 +71,8 @@ Inspector 做的事和真正的主機一模一樣:把 `server.py` 當成子處 * `json_response=True`:每個 POST 都用單一 JSON 本體回應,而不是 SSE 串流。那個本體只裝得下回應本身,別的都沒有,所以在請求中途回頭呼叫用戶端的工具(`ctx.elicit()`、取樣(sampling))在這一段會引發 `NoBackChannelError`,而綁在進行中呼叫上的通知(`ctx.report_progress()` 的進度、每次呼叫的記錄訊息)會被丟棄;獨立的 `GET` 串流仍會承載不相關的那些。 * `stateless_http=True`:每個請求一個全新的傳輸,不追蹤工作階段(session)。 * `max_request_body_size`:可接受的最大請求本體,以位元組計。預設為 4 MiB;更大的請求在解析或建立工作階段之前就會收到 HTTP 413。只有在合法的 MCP 訊息超過這個大小時才調高它。 +* `session_idle_timeout`:舊版工作階段在沒有任何進行中的請求時可以閒置的秒數,超過後伺服器就把它關掉。預設為 1800。`None` 會停用它。請見 [工作階段存留時間與限制](legacy-clients.md#session-lifetime-and-limits)。 +* `max_sessions`:一個處理程序同時能持有多少個舊版工作階段。預設為 10 000。`None` 會移除這個限制。在同一節有說明。 * `event_store`、`retry_interval`、`transport_security`:可續傳性與 DNS 重新綁定防護。這些可以先放著,等到部署到 localhost 以外的地方再說;`transport_security` 在 **[部署與擴展](deploy.md)** 有說明。 !!! warning diff --git a/i18n/zh-hant/pages/run/legacy-clients.md b/i18n/zh-hant/pages/run/legacy-clients.md index edfbce63ec..cce6d2974b 100644 --- a/i18n/zh-hant/pages/run/legacy-clients.md +++ b/i18n/zh-hant/pages/run/legacy-clients.md @@ -1,6 +1,6 @@ --- translation: - sections: [3d1663c18edc824c, d4fd37009a13f03d, af9f398a5a8b679a, 470c2dd144294d69, 8e45827e6d24e8c8, 91dfd0ce98ebb03c] + sections: [3d1663c18edc824c, 90956965ae6a1ca1, af9f398a5a8b679a, 5ce83b1f9d88da62, 0d9b5d13fffc94e5, 8e45827e6d24e8c8, 91dfd0ce98ebb03c] tool: 1 --- # 服務舊版用戶端 {#serving-legacy-clients} @@ -18,15 +18,25 @@ SDK 依每個請求的 `MCP-Protocol-Version` 標頭來路由。標明 `2026-07- ## 一個處理函式,兩個世代 {#one-handler-both-eras} -下面是一個必須問使用者問題的工具,以及兩個世代的用戶端呼叫它: +下面是一個必須問使用者問題的工具: -```python title="server.py" hl_lines="24 37-38" +```python title="server.py" hl_lines="21" --8<-- "docs_src/legacy_clients/tutorial001.py" ``` `reserve` 需要一樣模型沒有提供的東西:要幾本。`Annotated[..., Resolve(ask_quantity)]` 就是工具宣告這件事的方式(完整說明請見 **[相依性](../handlers/dependencies.md)**)。`reserve` 裡沒有任何地方指名版本、檢查能力或做分支。 -兩個用戶端**同時**開著,連到同一個 `mcp` 物件。`mode="legacy"` 會執行 `initialize` 交握:正是 2026 之前的用戶端會開啟的那種連線。另一個用預設值,落在 `2026-07-28`。 +透過 HTTP 提供服務,下面是兩個世代的用戶端呼叫它: + +```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" +``` + +兩個用戶端**同時**開著,連到同一個正在執行的伺服器。`mode="legacy"` 會執行 `initialize` 交握:正是 2026 之前的用戶端會開啟的那種連線。另一個用預設值,落在 `2026-07-28`。從第二個終端機執行 `python client.py`: ```text 2025-11-25 {'result': "Reserved 2 of 'Dune'."} @@ -53,6 +63,31 @@ SDK 依每個請求的 `MCP-Protocol-Version` 標頭來路由。標明 `2026-07- !!! warning `event_store=` 看起來像解法,但不是。它是**可恢復性**(把漏掉的 SSE 事件重播給重新連回**同一個**工作階段的用戶端),不是工作階段儲存區。它永遠不會讓工作階段能從另一個處理程序存取到。 +## 工作階段的存活時間與上限 {#session-lifetime-and-limits} + +舊版工作階段不會永遠活著,單一處理程序也不會持有無限多個。有兩個設定控制這件事,兩者都是 `run()`、`streamable_http_app()` 和 `Server.streamable_http_app()` 上的關鍵字引數。現代(`2026-07-28`)連線和 `stateless_http=True` 都沒有工作階段,所以這兩個設定對它們都不適用。 + +| 設定 | 預設值 | 作用 | 用戶端看到什麼 | 關閉方式 | +|---|---|---|---|---| +| `session_idle_timeout` | `1800`(30 分鐘) | 關閉一個已經那麼久沒有任何進行中事項的工作階段。 | `404 Session not found`。必須重新 `initialize`。 | `None` | +| `max_sessions` | `10_000` | 超過這個數量就拒絕再開新的工作階段。既有的工作階段不受影響,也不會驅逐任何一個。 | `503 Too many open sessions`,JSON-RPC 錯誤碼 `-32603`。 | `None` | + +什麼算「進行中」: + +* 一條開著的 `GET` 串流。SDK 的用戶端會保持一條開著,所以已連線的用戶端,它的工作階段永遠不會過期。 +* 還在回應中的請求。執行時間超過逾時的工具呼叫不會被中斷,倒數要等它結束後才開始。 +* 沒有別的了。請求與請求之間,時鐘照跑。工作階段上的任何請求都會讓它重新起算,`ping` 也算。工作階段一旦過期,沒有任何東西能讓它復活。 + +用 `DELETE` 結束工作階段的用戶端會立刻釋放它。開頭請求被拒絕的用戶端也是。 + +```python +mcp.run(transport="streamable-http", session_idle_timeout=None, max_sessions=50_000) +``` + +兩種事件都會出現在伺服器記錄裡。過期是 `INFO` 層級的 `Session idle timeout`。拒絕開啟是 `WARNING` 層級的 `Refusing to open a new session: sessions are already open`。 + +上限是以處理程序為單位。四個 worker 時,上限是 `max_sessions` 的四倍,每個 worker 各自讓自己的工作階段過期。 + ## 唯一的開關:`stateless_http` {#the-one-knob-stateless_http} 如果黏性是你不願付的代價,能改的東西剛好只有一樣。 @@ -73,7 +108,7 @@ SDK 依每個請求的 `MCP-Protocol-Version` 標頭來路由。標明 `2026-07- `json_response=True` 不是那個開關,但它在**每個**舊版工作階段上都會付出一半同樣的代價:用單一 JSON 本體回應的 `POST` 沒有串流可供請求範圍的通道使用,所以請求途中的 `ctx.elicit()` 會引發同樣的 `NoBackChannelError`,綁在該請求上的通知則被丟掉。工作階段的獨立串流不受影響:不相關的通知照樣送達。 !!! check - 故意做錯一次。`reserve` 正是剛剛服務了兩個用戶端的那個工具。用 `stateless_http=True` 部署它,透過 HTTP 連上同樣的兩個用戶端,從各自呼叫它。 + 故意做錯一次。`reserve` 正是剛剛服務了兩個用戶端的那個工具。用 `stateless_http=True` 部署它,連上同樣的兩個用戶端,從各自呼叫它。 現代用戶端還是得到 `Reserved 2 of 'Dune'.`,現代路徑沒變。 diff --git a/i18n/zh-hant/pages/troubleshooting.md b/i18n/zh-hant/pages/troubleshooting.md index fb91a04667..e7bc47178a 100644 --- a/i18n/zh-hant/pages/troubleshooting.md +++ b/i18n/zh-hant/pages/troubleshooting.md @@ -1,6 +1,6 @@ --- translation: - sections: [2efaecdef109a5c5, fcacd3e66b8635a4, 25323d737dcf0261, 8a6e351ec756904d, 137454d469c867f5, 6392596bd6df54f0, 41126fa9c4fe432f, 480b6d7897e30ab4, d83bb682e708dde0, ebbed3449c499db4, 323ef84f6b4bebde, 30fd31be74169d9a, 656943c6cb567218, c2dc3b1007d2e987, 7cf5386b997d04e9, 0b59feed8384456e, 0cba47bae78d04eb, e4355f4c7cf4fb2e] + sections: [3d58228e81b99543, 170514ce901c4139, 17d61fad0a50d62b, 8a6e351ec756904d, 137454d469c867f5, 6392596bd6df54f0, 41126fa9c4fe432f, 480b6d7897e30ab4, d83bb682e708dde0, ebbed3449c499db4, 525cdf1755e29d4c, 30fd31be74169d9a, d2e88333d4f7841f, c2dc3b1007d2e987, d6eabf60cc366341, f798e815252852c2, 0cba47bae78d04eb, 2c218ba829abf74e] tool: 1 --- # 疑難排解 {#troubleshooting} @@ -13,6 +13,12 @@ translation: --8<-- "docs_src/troubleshooting/tutorial001.py" ``` +那幾則會從 `http://localhost:8000/mcp` 連上它,所以讓它以 HTTP 持續執行著: + +```console +uv run mcp run server.py --transport streamable-http +``` + 這一頁引用的錯誤都是真的:SDK 自己的測試套件會重現每一個。 ## `ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception)` {#exceptiongroup-unhandled-errors-in-a-taskgroup-1-sub-exception} @@ -23,7 +29,7 @@ translation: ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: await client.read_resource("weather://Atlantis") ``` @@ -49,7 +55,7 @@ async def main() -> None: ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: try: await client.read_resource("weather://Atlantis") except MCPError as e: @@ -65,7 +71,7 @@ async def main() -> None: ```python async def main() -> None: - client = Client(mcp) + client = Client("http://localhost:8000/mcp") tools = await client.list_tools() # RuntimeError ``` @@ -73,7 +79,7 @@ async def main() -> None: ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: tools = await client.list_tools() ``` @@ -245,7 +251,7 @@ app = Starlette(routes=[Mount("/", app=mcp.streamable_http_app())], lifespan=lif ## `MCPError: Session not found` {#mcperror-session-not-found} -伺服器不認得用戶端送來的 `Mcp-Session-Id`,幾乎都是因為伺服器**重新啟動了**(或是你被導到另一個實例)。工作階段存在那一個處理程序的記憶體內。 +伺服器不認得用戶端送來的 `Mcp-Session-Id`。要嘛伺服器**重新啟動了**(或是你被導到另一個實例),要嘛工作階段**過期了**,因為在 `session_idle_timeout`(預設 30 分鐘)這段時間內沒有任何進行中的請求。請見 [工作階段的存活時間與限制](run/legacy-clients.md#session-lifetime-and-limits)。工作階段存在那一個處理程序的記憶體內。 沒有伺服器的 bug 可找。HTTP 回應是 `404`,而它的本文**就是** JSON-RPC,所以和上面的 `421` 不同,python `Client` 會原封不動地把這個顯示給你: @@ -255,9 +261,9 @@ app = Starlette(routes=[Mount("/", app=mcp.streamable_http_app())], lifespan=lif 修正是重新連線:離開 `async with Client(...)` 區塊,進入一個新的,它會協商出新的工作階段。對於長時間存活的用戶端,這表示在呼叫外面攔截 `MCPError`,遇到這個訊息就重新連線,而不是在已經死掉的工作階段裡重試。 -如果**沒有**重新啟動也發生,代表你跑了不只一個 worker 卻沒有黏性工作階段(sticky session):每個 worker 都有自己的工作階段表,所以導到錯誤 worker 的請求就會落到這裡。這件事和它的兩種修正(黏性路由,或 `stateless_http=True`)請見 **[部署與擴展](run/deploy.md)** 和 **[服務舊版用戶端](run/legacy-clients.md)**。 +如果**沒有**重新啟動、用戶端也沒有安靜那麼久卻還是發生,代表你跑了不只一個 worker 卻沒有黏性工作階段(sticky session):每個 worker 都有自己的工作階段表,所以導到錯誤 worker 的請求就會落到這裡。這件事和它的兩種修正(黏性路由,或 `stateless_http=True`)請見 **[部署與擴展](run/deploy.md)** 和 **[服務舊版用戶端](run/legacy-clients.md)**。 -對伺服器維運人員來說,對應的記錄行是 `Rejected request with unknown or expired session ID: `。它以 `INFO` 層級記錄,所以在常用的 `WARNING` 門檻下看不到。剛部署完看到它一陣陣冒出來是正常的;每個已連線的用戶端都在重新連線。 +對伺服器維運人員來說,對應的記錄行是 `Rejected request with unknown or expired session ID: `。它以 `INFO` 層級記錄,所以在常用的 `WARNING` 門檻下看不到。剛部署完看到它一陣陣冒出來是正常的;每個已連線的用戶端都在重新連線。如果是工作階段過期,那一行前面會先出現 `Session idle timeout`,同樣是 `INFO` 層級。 ## `MCPError: Method not found` {#mcperror-method-not-found} @@ -269,7 +275,13 @@ app = Starlette(routes=[Mount("/", app=mcp.streamable_http_app())], lifespan=lif 伺服器想問使用者一件事,而這個用戶端從沒說過它可以被問。 -徵詢(elicitation)解析器在已連線的用戶端沒有宣告表單徵詢時,會一開始就拒絕,而 `e.error.data` 會精確寫出缺了什麼: +這間 Bistro 在訂位前會透過解析器先問一聲: + +```python title="server.py" hl_lines="15-17 21" +--8<-- "docs_src/troubleshooting/tutorial007.py" +``` + +用它取代 Weather 伺服器來服務,再從一個沒有傳 `elicitation_callback` 的用戶端呼叫 `book_table`。解析器會一開始就拒絕,因為已連線的用戶端從沒宣告表單徵詢(elicitation),而 `e.error.data` 會精確寫出缺了什麼: ```json { @@ -283,7 +295,7 @@ app = Starlette(routes=[Mount("/", app=mcp.streamable_http_app())], lifespan=lif ```python async def main() -> None: - async with Client(mcp, elicitation_callback=handle_elicitation) as client: + async with Client("http://localhost:8000/mcp", elicitation_callback=handle_elicitation) as client: result = await client.call_tool("book_table", {"date": "Friday"}) ``` @@ -302,14 +314,14 @@ async def main() -> None: 處理函式試圖在請求途中聯繫用戶端,但這條連線上的這次呼叫沒有能承載伺服器發出請求的通道。有三種伺服器設定會讓呼叫落到這種處境。 -**`2026-07-28` 連線:任何傳輸方式,一律如此。** 現代協定完全沒有伺服器發起的請求,所以伺服器在送出任何東西之前就拒絕。工具裡的 `ctx.elicit()` 是遇到這個的典型方式(就在第一次記憶體內測試時,因為 `Client(server)` 不用交代就會協商出 `2026-07-28`),而傳入 `elicitation_callback=` 什麼都不會改變,因為根本沒有請求送到用戶端讓它回答: +**`2026-07-28` 連線:任何傳輸方式,一律如此。** 現代協定完全沒有伺服器發起的請求,所以伺服器在送出任何東西之前就拒絕。工具裡的 `ctx.elicit()` 是遇到這個的典型方式,通常是在那個工具的第一個記憶體內 **[測試](get-started/testing.md)** 裡,因為 `Client(mcp)` 不用交代就會協商出 `2026-07-28`。傳入 `elicitation_callback=` 什麼都不會改變,因為根本沒有請求送到用戶端讓它回答: ```python title="server.py" hl_lines="16" --8<-- "docs_src/troubleshooting/tutorial006.py" ``` ```python -async def main() -> None: +async def test_book_table() -> None: async with Client(mcp) as client: await client.call_tool("book_table", {"date": "Friday"}) ``` @@ -347,7 +359,7 @@ mcp.shared.exceptions.MCPError: Cannot send 'elicitation/create': this transport ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: await client.call_tool("forecast", {"city": "London"}, request_state="round-1-from-worker-a") ``` @@ -400,7 +412,7 @@ mcp = MCPServer("Weather", request_state_security=RequestStateSecurity(keys=[key * 伺服器記錄裡的 `Tool already exists:` 是兩個同名工具合併成一個的唯一跡象。 * 一個 421,三種寫法:`Server returned an error response`(python `Client`)、`421 Misdirected Request` / `Invalid Host header`(其他所有東西)、`Invalid Host header: `(伺服器記錄)。修正:`transport_security=TransportSecuritySettings(allowed_hosts=[...])`。 * `Task group is not initialized` -> 掛載的應用程式,其外層生命週期從未進入 `mcp.session_manager.run()`。 -* `Session not found` -> 伺服器重新啟動了;重新連線。 +* `Session not found` -> 伺服器重新啟動了,或工作階段過期了(`session_idle_timeout`);重新連線。 * `Cannot send 'elicitation/create': ... no back-channel ...` -> `ctx.elicit()` 需要伺服器到用戶端的通道:`2026-07-28` 連線從來沒有,`stateless_http=True` 拿走了舊版的那條,`json_response=True` 拿走了請求範圍的那條。改用解析器(舊版用戶端還需要一個保留通道的伺服器)。它的鄰居 `Method not found` 則是請求了一個對方的協定修訂版沒有的方法。 * `Client did not declare the form elicitation capability ...` 和 `Elicitation not supported` -> 用戶端少了 `elicitation_callback=`。 * `Invalid or expired requestState` 在線路上從不說原因。伺服器記錄會說;`unknown key` 表示要在各 worker 之間共用 `RequestStateSecurity(keys=[...])`。 diff --git a/i18n/zh-hant/pages/whats-new.md b/i18n/zh-hant/pages/whats-new.md index 50a0766532..bcdfc7feb8 100644 --- a/i18n/zh-hant/pages/whats-new.md +++ b/i18n/zh-hant/pages/whats-new.md @@ -1,6 +1,6 @@ --- translation: - sections: [cfe01c0c5863dfa2, 1c58c5cfcc37d455, a7392996acf1ad8f, 875eb2889263424e] + sections: [cfe01c0c5863dfa2, 0dbb68d8b210177b, 80cf193023af3ed4, 875eb2889263424e] tool: 1 --- # v2 的新功能 {#whats-new-in-v2} @@ -37,11 +37,11 @@ mcp = MCPServer("Demo") # v1: FastMCP("Demo") v1 交給你的是三層巢狀結構:一個產出原始串流的傳輸 context manager、包在外面的 `ClientSession`,再加上手動呼叫的 `await session.initialize()`。v2 只有一個物件: -```python title="client.py" hl_lines="14-18" ---8<-- "docs_src/client/tutorial001.py" +```python title="client.py" hl_lines="7-11" +--8<-- "docs_src/client/tutorial001_client.py" ``` -`Client` 接受一個伺服器物件(記憶體內、沒有傳輸,也就是測試的做法)、一個 URL(Streamable HTTP)、一個 `StdioServerParameters`(stdio 子處理程序),或任何其他傳輸 context manager,例如 `sse_client(...)`。進入 `async with` 就會連線並協商協定版本,不管伺服器講的是哪個世代;之後 `client.server_capabilities` 和 `client.protocol_version` 就直接在那裡,伺服器有表明身分時 `client.server_info` 也在(它現在是 `Implementation | None`,因為 2026 世代的身分是選用的)。在 v1 註冊的取樣和徵詢回呼仍然有效(回呼本體會遇到跟本頁其他地方一樣的 snake_case 屬性改名),現在也會回應 2026 風格的「結果中夾帶請求」(見下文),而且是並行執行,不再一次一個。想要低階介面的人,`ClientSession` 仍在底下,`client.session` 會把它交給你;它也有變動(跑在新的分派器引擎上,自己的部分簽章也改了),所以往下鑽之前先讀 **[遷移指南](migration.md#clientsession-now-runs-on-jsonrpcdispatcher-basesession-removed)**。 +`Client` 接受一個 URL(Streamable HTTP)、一個 `StdioServerParameters`(stdio 子處理程序)、任何其他傳輸 context manager(例如 `sse_client(...)`),或者在測試裡直接接受伺服器物件本身(記憶體內、沒有傳輸)。進入 `async with` 就會連線並協商協定版本,不管伺服器講的是哪個世代;之後 `client.server_capabilities` 和 `client.protocol_version` 就直接在那裡,伺服器有表明身分時 `client.server_info` 也在(它現在是 `Implementation | None`,因為 2026 世代的身分是選用的)。在 v1 註冊的取樣和徵詢回呼仍然有效(回呼本體會遇到跟本頁其他地方一樣的 snake_case 屬性改名),現在也會回應 2026 風格的「結果中夾帶請求」(見下文),而且是並行執行,不再一次一個。想要低階介面的人,`ClientSession` 仍在底下,`client.session` 會把它交給你;它也有變動(跑在新的分派器引擎上,自己的部分簽章也改了),所以往下鑽之前先讀 **[遷移指南](migration.md#clientsession-now-runs-on-jsonrpcdispatcher-basesession-removed)**。 **[用戶端](client/index.md)** 介紹它,**[用戶端傳輸方式](client/transports.md)** 說明四種連線形式,**[用戶端回呼](client/callbacks.md)** 說明回呼本身,**[測試](get-started/testing.md)** 示範取代 v1 `create_connected_server_and_client_session()` 輔助函式的記憶體內模式。 @@ -166,11 +166,15 @@ v2 實作 2026-07-28 修訂版,而且**兩個**修訂版同時服務:同一 替代方案把呼叫反過來。需要向使用者要東西的工具**回傳**那個問題(`InputRequiredResult`),用戶端用一直都有的那些回呼回答它,然後帶著答案重試這次呼叫。那個迴圈 `Client` 會替你驅動。在伺服器上很少需要自己建那個結果,因為 **[相依性](handlers/dependencies.md)** 會做:用 `Resolve(ask_quantity)` 標註一個參數,其中 `ask_quantity` 是你寫的普通函式,SDK 就會用連線支援的機制去問,在舊版工作階段上是即時的徵詢請求,在 2026 上是多輪往返。一個工具本體,兩個世代: -```python title="dual_era.py" hl_lines="24 37-38" +```python title="server.py" hl_lines="21" --8<-- "docs_src/legacy_clients/tutorial001.py" ``` -這個檔案把整個賣點集中在一處:一個伺服器、一個以 `Resolve` 為後盾的工具,以及一個舊版用戶端加一個現代用戶端都拿到答案,全在記憶體內。**[多輪往返請求](handlers/multi-round-trip.md)** 解釋機制(包括 SDK 替你密封和驗證的 `request_state`);**[徵詢](handlers/elicitation.md)** 說明怎麼問。 +```python title="client.py" hl_lines="14-15" +--8<-- "docs_src/legacy_clients/tutorial001_client.py" +``` + +這兩個檔案就是整個賣點:一個伺服器、一個以 `Resolve` 為後盾的工具,以及一個舊版用戶端加一個現代用戶端,都從同一個執行中的伺服器拿到答案(**[服務舊版用戶端](run/legacy-clients.md)** 會帶你走過這兩個檔案)。**[多輪往返請求](handlers/multi-round-trip.md)** 解釋機制(包括 SDK 替你密封和驗證的 `request_state`);**[徵詢](handlers/elicitation.md)** 說明怎麼問。 !!! warning "這是移植後的 v1 伺服器唯一會改變行為的地方" 你自己的測試最先碰到:`Client(mcp)` 對 v2 伺服器預設協商 2026-07-28,所以呼叫 `ctx.elicit()` 的工具在 v1 通過的測試裡會失敗。把問題搬進 `Resolve(...)` 參數(跨世代可攜),或者如果真的想要推送行為,就把測試用戶端釘在 `mode="legacy"`。 @@ -197,7 +201,7 @@ v2 實作 2026-07-28 修訂版,而且**兩個**修訂版同時服務:同一 * **擴充功能是一等公民。** 伺服器和用戶端在反向 DNS 識別碼底下宣告選用的能力組合([SEP-2133](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2133));內建的 `Apps` 擴充功能(MCP Apps)是參考範例。**[擴充功能](advanced/extensions.md)** 和 **[MCP Apps](advanced/apps.md)**。 * **錯誤碼標準化了。** 找不到的資源是 `-32602`,URI 放在 `error.data`,新的規格保留碼則是 `-32020`(標頭不符)、`-32021`(缺少必要能力)和 `-32022`(不支援的協定版本)。**[疑難排解](troubleshooting.md)** 以確切的訊息為索引。 * **授權更不容易用錯了。** 用戶端會驗證隨授權碼回傳的 `iss`([RFC 9207](https://datatracker.ietf.org/doc/html/rfc9207);`callback_handler` 現在回傳 `AuthorizationCodeResult`),註冊時送出 `application_type`,而且絕不會對不同的授權伺服器重送憑證。企業那一角的新東西:[SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990) 身分斷言流程。**[遷移指南](migration.md)** 列出每一項 OAuth 變更;完整說明請見 **[用戶端的 OAuth](client/oauth-clients.md)** 和 **[身分斷言](client/identity-assertion.md)**。 -* **每個伺服器都可追蹤。** OpenTelemetry 以中介軟體的形式預設啟用:每個請求都有一個伺服器 span,在處理程序設定 exporter 之前完全沒有成本。兩端都跑 SDK 時,用戶端還會在 `_meta` 裡傳播 W3C trace context,所以追蹤會接起來。**[OpenTelemetry](run/opentelemetry.md)**。 +* **每個伺服器都可追蹤。** OpenTelemetry 以中介軟體的形式預設啟用:每個請求都有一個伺服器 span,在處理程序設定 exporter 之前完全沒有成本。兩端都使用 SDK 時,用戶端還會在 `_meta` 裡傳播 W3C trace context,所以追蹤會接起來。**[OpenTelemetry](run/opentelemetry.md)**。 ## 從 v1 升級? {#upgrading-from-v1} diff --git a/i18n/zh/pages/advanced/apps.md b/i18n/zh/pages/advanced/apps.md index f3bb1d4f9a..1cc37465a5 100644 --- a/i18n/zh/pages/advanced/apps.md +++ b/i18n/zh/pages/advanced/apps.md @@ -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 @@ SDK 把它作为内置的 `Apps` 扩展(`io.modelcontextprotocol/ui`)提供 ## 一个带界面的时钟 {#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 @@ HTML 本身监听宿主的 `postMessage` 并显示结果。真正的应用请在 模型读的是 `content`;iframe 是给人看的。支持 UI 的宿主照样会把文本结果交给模型,而纯文本客户端**只**拿到那部分。所以标准模式是一个工具、两种回答。再看一遍 `get_time`: -```python title="server.py" hl_lines="23-27" +```python title="server.py" hl_lines="21-25" --8<-- "docs_src/apps/tutorial001.py" ``` -只有当客户端声明了 `io.modelcontextprotocol/ui` 扩展,**并且**在其 `mimeTypes` 设置里列出了 `text/html;profile=mcp-app` 时,`client_supports_apps(ctx)` 才为 `True`。这个字段是必填的,省略它的客户端不算数。同一文件里的 `main()` 声明的正是这些:协商中客户端的那一半,于是富结果就返回了。 +只有当客户端声明了 `io.modelcontextprotocol/ui` 扩展,**并且**在其 `mimeTypes` 设置里列出了 `text/html;profile=mcp-app` 时,`client_supports_apps(ctx)` 才为 `True`。这个字段是必填的,省略它的客户端不算数。下面是协商中客户端的那一半: + +```python title="client.py" hl_lines="8 12" +--8<-- "docs_src/apps/tutorial001_client.py" +``` + +通过 HTTP 提供 `server.py`,然后在另一个终端运行客户端: + +```console +uv run mcp run server.py --transport streamable-http +``` + +```console +python client.py +``` + +```text +2026-06-26T12:00:00Z +``` + +返回的是富结果。从 `Client` 调用里去掉 `extensions=[APPS_SUPPORT]`,同一个程序就会改为打印 `The time is 2026-06-26T12:00:00Z.`,这也是纯文本客户端所能看到的全部。 !!! warning 绝不要把 `"[Rendered UI]"` 这样的占位符当作唯一的内容返回。如果回退文本没用,这个工具对所有纯文本客户端乃至模型本身就都没用。把那句话写出来。 diff --git a/i18n/zh/pages/advanced/extensions.md b/i18n/zh/pages/advanced/extensions.md index 13be45f45d..118616bfdc 100644 --- a/i18n/zh/pages/advanced/extensions.md +++ b/i18n/zh/pages/advanced/extensions.md @@ -1,6 +1,6 @@ --- translation: - sections: [05891e7cc1938a13, b3c01a6af28c51ee, 7ffc91f5e38bdfe0, 717d3f235a8333a7, f471a13b2fe5d737, ed6af2df4b656dff] + sections: [05891e7cc1938a13, b3c01a6af28c51ee, 6eba6ce094f4417e, 125f28588c85dd7b, ddd8969b698ca11c, ed6af2df4b656dff] tool: 1 --- # 扩展 {#extensions} @@ -49,7 +49,7 @@ TypeError: Stamps.identifier must be a `vendor-prefix/name` string 最小的有用扩展就是一个工具加一份设置映射: -```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 @@ TypeError: Stamps.identifier must be a `vendor-prefix/name` string * `settings()` 是在 `capabilities.extensions["com.example/stamps"]` 处声明的值。返回 `{}`(默认值)表示声明该扩展但不带任何设置。 * 扩展永远拿不到服务器。它以数据的形式声明贡献,由 `MCPServer` 消费。没有可供修改的 `self.server`。 -`main()` 就是证明:一个直接对着 `mcp` 的内存客户端: +用 HTTP 提供服务,一个客户端就是证明: -```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" ``` +本页每个 `server.py` 都用这条命令启动,每个 `client.py` 都在另一个终端里用 `python client.py` 与它并行运行。 + ### 提供自己的方法 {#serving-your-own-methods} 扩展可以注册**新的请求方法**:它自己的动词,与规范定义的方法并列提供: -```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 @@ TypeError: Stamps.identifier must be a `vendor-prefix/name` string ### 客户端一侧 {#the-client-side} -同一个文件的 `main()` 就是客户端的全部内容,两半都在: +客户端是一个独立的程序,客户端这一侧的两半都在里面: -```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)])` 声明该扩展。这些声明会变成 `ClientCapabilities.extensions`:在 2026-07-28 连接上,该映射随每个请求的 `_meta` 信封传递,所以服务器在**每个**请求上都能看到它;在旧版连接上,它随 `initialize` 握手传递。服务器代码不用关心是哪一种:`require_client_extension(ctx, ...)` 和 `ctx.session.check_client_capability(...)` 在两条路径上都会读取正确的来源。 * 厂商方法要往下一层,用 `client.session.send_request(...)`;`Client` 只为规范动词提供一等方法。`send_request` 接受任何 `Request` 子类,所以厂商请求原样传入即可。 +* `SearchRequest` 和它携带的两个模型是这个扩展的线路约定,所以客户端自己也声明一份。公开发布的扩展会把它们放进一个双方都导入的包里。 ### 拦截 `tools/call` {#intercepting-toolscall} @@ -109,12 +116,18 @@ TypeError: Stamps.identifier must be a `vendor-prefix/name` string ## 使用客户端扩展 {#using-a-client-extension} -**客户端扩展**是从消费一侧看的同一份约定:一组归在同一个标识符之下的客户端行为。把实例传给 `Client(extensions=[...])`,然后照常调用工具: +**客户端扩展**是从消费一侧看的同一份约定:一组归在同一个标识符之下的客户端行为。这里的服务器回答 `buy` 时给的不是货物,而是一张待兑换的收据,而且只对声明了该扩展的客户端这样做: -```python title="client.py" hl_lines="66-68" +```python title="server.py" hl_lines="22-25" --8<-- "docs_src/extensions/tutorial006.py" ``` +在客户端,把实例传给 `Client(extensions=[...])`,然后照常调用工具: + +```python title="client.py" hl_lines="33-35" +--8<-- "docs_src/extensions/tutorial006_client.py" +``` + `call_tool("buy", ...)` 返回一个普通的 `CallToolResult`,和其他任何调用一样。扩展改变的是:服务器现在可以用 `receipt` **结果形态**而不是最终结果来回答 `buy`,`Receipts` 会在 `call_tool` 返回之前把它完成(这里是用一次后续调用兑换收据)。调用处什么都不用动。 去掉这个扩展,这一切就都不存在:服务器的门槛会拒绝没有声明它的客户端(错误 -32021),而跳过门槛的服务器发来的被认领形态会校验失败,正如规范对无法识别的 `resultType` 所要求的那样。默认关闭,线路两端都是。 @@ -124,15 +137,15 @@ TypeError: Stamps.identifier must be a `vendor-prefix/name` string ```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")]) ``` ## 编写客户端扩展 {#writing-a-client-extension} 继承 `ClientExtension`,只重写需要的部分。贡献分三类,各有默认实现:`settings()`、`claims()` 和 `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" ``` * 标识符遵循与服务器端相同的语法,在类定义时校验。 @@ -153,12 +166,18 @@ def notifications(self) -> Sequence[NotificationBinding[Any]]: ### 扩展动词 {#extension-verbs} -扩展自己的请求方法不需要在客户端注册。厂商请求类型继承 `mcp.types.Request`,通过 `client.session.send_request` 发送,如[提供自己的方法](#serving-your-own-methods)所示。补充一点:当某个参数键必须放进 `Mcp-Name` 头(tasks 之类的扩展规范对其动词有此要求)时,请求类型要声明 `name_param`: +扩展自己的请求方法不需要在客户端注册。厂商请求类型继承 `mcp.types.Request`,通过 `client.session.send_request` 发送,如[提供自己的方法](#serving-your-own-methods)所示。以这样一个服务器为例,它的扩展提供一个与某个具名作业相关的动词: -```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" ``` +客户端上补充一点:当某个参数键必须放进 `Mcp-Name` 头(tasks 之类的扩展规范对其动词有此要求)时,请求类型要声明 `name_param`: + +```python title="client.py" hl_lines="20-23 28-29" +--8<-- "docs_src/extensions/tutorial007_client.py" +``` + 会话在每条发送路径上都会把 `params["jobId"]` 镜像到 `Mcp-Name` 中,值缺失时会明确报错,而不是悄悄漏掉一个必需的头。 ## 扩展不能做什么 {#what-an-extension-cannot-do} diff --git a/i18n/zh/pages/advanced/low-level-server.md b/i18n/zh/pages/advanced/low-level-server.md index c4200b0f6b..437905d376 100644 --- a/i18n/zh/pages/advanced/low-level-server.md +++ b/i18n/zh/pages/advanced/low-level-server.md @@ -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 --- # 底层 Server {#the-low-level-server} @@ -36,18 +36,22 @@ translation: ### 试一试 {#try-it} -这个没有 Inspector 可用:`mcp dev` 和 `mcp run` 只接受 `MCPServer`。内存中的 `Client` 不在乎;它接收底层 `Server` 的方式和接收 `MCPServer` 完全一样: +`mcp dev` 和 `mcp run` 只接受 `MCPServer`,所以这个服务器要自己来跑。`server.py` 的最后一行用它构建了一个普通的 ASGI 应用,交给 uvicorn 运行: -```python title="main.py" +```console +uvicorn server:app --port 8000 +``` + +把 Inspector 或任何客户端指向 `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 @@ asyncio.run(main()) * `result.structured_content` 是 `None`。高层服务器会替你把 `-> str` 包装成 `{"result": ...}`;在这里,你没构建的东西没人替你构建。 * `list_tools` 返回的是**你**敲进去的模式,一字不差。高层版本在每个属性上都有 `"title": "Query"`,根上还有一个 `"title": "search_booksArguments"`:Pydantic 的产物。在这一层,线路上有什么,都是你放上去的。 +在测试里可以跳过 uvicorn 和端口:`Client(server)` 在进程内接收底层 `Server` 的方式和接收 `MCPServer` 完全一样,**[测试](../get-started/testing.md)** 讲的就是这个模式。 + ## 没有替你做任何检查 {#nothing-is-checked-for-you} `MCPServer` 会在你的函数运行之前拒绝错误的参数,按它生成的模式校验调用(**[工具](../servers/tools.md)**)。 @@ -214,4 +220,4 @@ use Server.middleware to observe or wrap initialization * `add_request_handler(method, params_type, handler)` 提供任意方法。`initialize` 是保留的。 * `Server` 公布的能力由你注册了哪些处理函数推导而来。 -`Client(server)` 对两种服务器一视同仁,因为它们**就是**同一个协议,这正是关键所在。再往下一层根本不是一个类:它是 **[中间件](middleware.md)**。 +客户端对两种服务器一视同仁,因为它们**就是**同一个协议,这正是关键所在。再往下一层根本不是一个类:它是 **[中间件](middleware.md)**。 diff --git a/i18n/zh/pages/advanced/pagination.md b/i18n/zh/pages/advanced/pagination.md index b15bf41159..3fd55ac3f6 100644 --- a/i18n/zh/pages/advanced/pagination.md +++ b/i18n/zh/pages/advanced/pagination.md @@ -1,6 +1,6 @@ --- translation: - sections: [a9aba7a026c7bd85, ed32bda7ba9ae33a, 7e64cc5646abb91f, 22a0129ee78b3c63, d875373c06d8d2f9] + sections: [a9aba7a026c7bd85, 83e2a08b9d46a398, 9fd8a0aa384b3257, 22a0129ee78b3c63, d875373c06d8d2f9] tool: 1 --- # 分页 {#pagination} @@ -29,9 +29,13 @@ translation: ### 试一试 {#try-it} -`Client(server)` 在内存中连接底层 `Server` 的方式,和连接 `MCPServer` 完全一样。 +`mcp run` 只接受 `MCPServer`,所以这个服务器得自己启动。`server.py` 的最后一行用这个 `Server` 构建出一个普通的 ASGI 应用,交给 uvicorn 运行: -不带参数调用 `list_resources()`。得到十个资源,从 `book-1` 到 `book-10`,`next_cursor` 是字符串 `"10"`。 +```console +uvicorn server:app --port 8000 +``` + +把任意客户端(**[客户端](../client/index.md)**,或者 Inspector)指向 `http://localhost:8000/mcp`,不带参数调用 `list_resources()`。得到十个资源,从 `book-1` 到 `book-10`,`next_cursor` 是字符串 `"10"`。 用 `list_resources(cursor="10")` 把它交回去,第一个资源就是 `book-11`,新的 `next_cursor` 是 `"20"`。 @@ -41,7 +45,7 @@ translation: `Client` 上的每个 `list_*` 方法(`list_tools`、`list_resources`、`list_resource_templates`、`list_prompts`)都接受一个 `cursor=` 关键字参数。取完一个分页列表只需要一个 `while True`: -```python title="client.py" hl_lines="26-32" +```python title="client.py" hl_lines="9-15" --8<-- "docs_src/pagination/tutorial002.py" ``` @@ -49,7 +53,7 @@ translation: * 先 extend,**再**看 `next_cursor`:最后一页也有资源。 * `next_cursor is None` 是出口。其他任何值都原封不动地直接塞回 `cursor=`。 -运行它的 `main()`,会打印 `100 resources`:十页、每页十个,由一个从头到尾都不知道有十页的循环拼在一起。 +让 uvicorn 继续运行 `server.py`,在第二个终端里运行 `python client.py`。它会打印 `100 resources`:十页、每页十个,由一个从头到尾都不知道有十页的循环拼在一起。 这和 **[客户端](../client/index.md)** 为每个 `list_*` 动词展示的是同一个循环,而且面对不分页的服务器也没有任何代价:第一个响应里 `next_cursor` 就是 `None`,循环只跑一次。 diff --git a/i18n/zh/pages/client/caching.md b/i18n/zh/pages/client/caching.md index 07c40df6cf..85f42d00c6 100644 --- a/i18n/zh/pages/client/caching.md +++ b/i18n/zh/pages/client/caching.md @@ -1,6 +1,6 @@ --- translation: - sections: [9e7b9a1710e5aeba, b74ca4c1d2ddddee, fa8714e61bf90c5a, 04db67a886b7271c, 857690fb8f876800] + sections: [9e7b9a1710e5aeba, 66a2e9acc9101d54, d3d25aa802f5145a, 04db67a886b7271c, 857690fb8f876800] tool: 1 --- # 缓存提示 {#caching-hints} @@ -27,13 +27,13 @@ translation: 在底层 `Server` 上,处理函数手动构建结果,`ttl_ms` / `cache_scope` 只是结果模型上的字段。显式设置了它们的处理函数总是逐字段地优先于构造函数映射: -```python title="server.py" hl_lines="10 16" +```python title="server.py" hl_lines="11 17" --8<-- "docs_src/caching/tutorial002.py" ``` 处理函数指定了 `ttl_ms=1_000`,对作用域只字未提。线路上是:`ttlMs: 1000`(处理函数的值,而不是映射里的 `60_000`)和 `cacheScope: "public"`(映射的值,因为处理函数没有设置)。显式优先于配置,配置优先于默认。这条规则按字段生效,所以处理函数可以固定一个字段,把另一个留给服务器范围的策略。 -这也是构造函数无法预知的动态情况的出口:一个按用户过滤 `resources/read` 的处理函数,可以在其他方面都是 public 的服务器上为某个 URI 返回 `cache_scope="private"`。 +这也是应对构造函数无法预知的动态情况的出口:一个按用户过滤 `resources/read` 的处理函数,可以在其他方面都是 public 的服务器上为某个 URI 返回 `cache_scope="private"`。 关于分页列表有一点要注意:协议要求同一列表的**每一页 `cacheScope` 相同**。构造函数映射天然满足这一点,因为它按方法而不是按页作键。但自行覆盖作用域的处理函数要自己负责这种一致性:在**每一**页都覆盖,绝不要只在有游标时覆盖,否则第一页和第二页会不一致。 @@ -41,10 +41,24 @@ translation: 在 2026-07-28 会话上,`Client` 替你遵从这些提示:它内置了响应缓存,默认开启。带着 `ttlMs` 到达的结果会被存起来,在 TTL 内的相同调用直接由缓存提供,不发生往返。**不**带提示的结果不会被缓存:无提示的结果使用 `CacheConfig.default_ttl_ms`,它默认为 `0`(立即过期),所以什么都没声明的服务器看到的流量和以前一模一样,一次调用对应一次请求。 -```python title="client.py" hl_lines="33 35 38" +想亲眼看看,就用 uvicorn 运行上一节的 `server.py`(它的最后一行构建了 ASGI 应用)。处理函数每次真正执行时都会打印一行: + +```console +uvicorn server:app --port 8000 +``` + +```python title="client.py" hl_lines="20 23 28" --8<-- "docs_src/caching/tutorial003.py" ``` +在另一个终端运行 `python client.py`。它打印出第一个结果携带的提示,处理函数的 `ttlMs` 挨着映射的 `cacheScope`: + +```text +1000 public +``` + +剩下的情况看服务器那边的终端:在 uvicorn 的请求日志之间,`tools/list served` 出现了三次。 + 四次调用,三次抓取。第二次调用找到了新鲜条目,根本没到服务器;把(注入的)时钟拨过 TTL 让第三次重新抓取;第四次指定了 `cache_mode="refresh"`。这个关键字参数存在于五个缓存动词上(`list_tools`、`list_prompts`、`list_resources`、`list_resource_templates`、`read_resource`): * `"use"`(默认)有新鲜条目就返回它,没有就抓取并存储。 @@ -53,7 +67,7 @@ translation: 有一条规则凌驾于 `"use"` 之上:**带 `meta` 的调用总会到达服务器。** 设置了 `meta`(进度令牌、追踪字段)的请求期望产生一次线路请求,所以在 `cache_mode="use"` 下它被当作 `"refresh"` 处理:跳过缓存读取,抓取到的结果仍会替换缓存条目。`"bypass"` 和显式的 `"refresh"` 行为照旧。 -要完全关闭缓存,用 `Client(server, cache=None)` 构造:每次调用重新变成一次往返,`cache_mode` 虽然仍被接受,但不起作用。 +要完全关闭缓存,构造 `Client` 时传入 `cache=None`:每次调用重新变成一次往返,`cache_mode` 虽然仍被接受,但不起作用。 作用域同样自动遵从:`"private"` 条目按缓存的**分区**(见下文)作键,而 `"public"` 条目可以选择更大范围的共享。并且对于通知点名的那些条目,**通知优先于 TTL**:`list_changed` 通知会驱逐对应的已缓存列表,`resources/updated` 会驱逐恰好存在其 URI 下的已缓存读取结果,不管它们多新鲜。在 2026-07-28 连接上,这些通知通过你用 `client.listen(...)` 打开的 `subscriptions/listen` 流到达,驱逐会在你的观察者看到事件之前完成;详见 **[订阅](subscriptions.md)**。 diff --git a/i18n/zh/pages/client/callbacks.md b/i18n/zh/pages/client/callbacks.md index 201c937c08..35c6d13751 100644 --- a/i18n/zh/pages/client/callbacks.md +++ b/i18n/zh/pages/client/callbacks.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} @@ -58,7 +58,7 @@ result.content # [TextContent(type='text', text='Card issued to Ada Lovelace.') 你发出一个 `tools/call`,服务器回过来一个 `elicitation/create`,由你的函数作答——全部发生在一次工具调用之内。 !!! info - `Client(...)` 调用里的 `mode="legacy"` 是真正起作用的。默认情况下 `Client(...)` 协商的是现代协议路径,而那条路径没有供服务器向客户端发请求的反向通道(back-channel):`ctx.elicit` 会在你的回调运行之前就失败。决定这一点的不是传输方式,而是协商出的协议,内存传输和 URL 传输都一样。只要你的客户端需要回答这类请求,就固定使用 `mode="legacy"`;本页背后的每个测试都是这么做的。详见 **[协议版本](../protocol-versions.md)**。 + `Client(...)` 调用里的 `mode="legacy"` 是真正起作用的。默认情况下 `Client(...)` 协商的是现代协议路径,而那条路径没有供服务器向客户端发请求的反向通道(back-channel):`ctx.elicit` 会在你的回调运行之前就失败。决定这一点的不是传输方式,而是协商出的协议。只要你的客户端需要回答这类请求,就固定使用 `mode="legacy"`;本页背后的每个测试都是这么做的。详见 **[协议版本](../protocol-versions.md)**。 在 2026-07-28 会话上,这个回调并没有失效,只是触发方式不同:当工具返回一个携带 `ElicitRequest` 的 `InputRequiredResult` 时,`Client` 会把该条目分派给同一个 `elicitation_callback`,并替你重试这次调用。那个流程见 **[多轮往返请求](../handlers/multi-round-trip.md)**。 @@ -139,4 +139,4 @@ result.structured_content # {'result': ['elicitation']} * `sampling_callback` 和 `list_roots_callback` 的工作方式相同,但服务的是已弃用的功能;现代服务器改用多轮往返请求。 * `logging_callback` 和 `message_handler` 接收通知。它们不声明任何东西。 -`Client(...)` 的第一个参数是传输对象。**[客户端传输](transports.md)** 涵盖了每一种。 +`Client(...)` 的第一个参数决定用哪种传输方式。**[客户端传输](transports.md)** 涵盖了每一种。 diff --git a/i18n/zh/pages/client/index.md b/i18n/zh/pages/client/index.md index f12c633430..aa421db5cb 100644 --- a/i18n/zh/pages/client/index.md +++ b/i18n/zh/pages/client/index.md @@ -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 --- # Client {#the-client} @@ -11,13 +11,23 @@ translation: ## 你的第一个客户端 {#your-first-client} -```python title="client.py" hl_lines="14-18" +客户端需要一个可以对话的服务器。本页的每段代码连接的都是这个 Bookshop。把它保存为 `server.py`,让它通过 HTTP 一直运行着: + +```python title="server.py" --8<-- "docs_src/client/tutorial001.py" ``` -顶部的服务器只是为了让你有东西可连。客户端就是高亮的那五行。 +```console +uv run mcp run server.py --transport streamable-http +``` + +这样它就在 `http://localhost:8000/mcp` 上提供服务。客户端是一个独立的程序。把它保存为 `client.py`,在另一个终端里运行 `python client.py`: -* `Client(mcp)` 接收的是**服务器对象本身**。这是内存传输:没有子进程,没有端口,没有 HTTP。本页的每个示例,以及你写的每个测试,都是这样连接的。 +```python title="client.py" hl_lines="7-11" +--8<-- "docs_src/client/tutorial001_client.py" +``` + +* `Client("http://localhost:8000/mcp")` 接收的是一个 **URL**,所以它通过 Streamable HTTP 连接到你刚启动的服务器。 * `async with` 就是**生命周期**。进入时连接并协商;离开时断开。没有 `connect()` / `close()` 这样的配对方法,而且 `Client` 在代码块结束后不能复用。 * 在代码块内部,连接相关的信息已经作为普通属性摆在那里了。 @@ -25,10 +35,10 @@ translation: `Client` 接收一个位置参数,并根据它的类型确定传输方式: -* `MCPServer`(或低层 `Server`)实例:**进程内**连接。 -* URL 字符串(`Client("http://localhost:8000/mcp")`):Streamable HTTP,生产环境的路径。 -* `StdioServerParameters`:要作为**子进程**启动的命令,通过它的 stdin 和 stdout 通信。 +* URL 字符串(`Client("http://localhost:8000/mcp")`):Streamable HTTP,部署时所用的传输方式。 +* `StdioServerParameters`:要作为本地**子进程**启动的命令,通过它的 stdin 和 stdout 通信。 * **传输**:任何可以 `async with ... as (read, write)` 的对象,比如用 `streamable_http_client(url, http_client=...)` 包装你自己的 HTTP 客户端。 +* `MCPServer`(或低层 `Server`)实例:**进程内**连接,没有子进程,也没有端口。这一种是给测试用的,**[测试](../get-started/testing.md)** 就建立在它之上。 本页其余内容在这四种方式下完全相同。请求头、子进程、超时以及 `Transport` 协议另有专页:**[客户端传输](transports.md)**。 @@ -48,11 +58,11 @@ translation: ## 列出工具 {#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()` 返回 `ListToolsResult`;工具在 `.tools` 里。每一个都是宿主会交给模型的完整定义: +`list_tools()` 返回 `ListToolsResult`;工具在 `.tools` 里。每一个都是宿主会交给模型的完整定义。下面是第一个: ```python tool.name # 'search_books' @@ -76,6 +86,8 @@ tool.description # 'Search the catalog by title or author.' UI 渲染参数表单所需的一切,以及模型生成合法参数所需的一切,都在这个模式里。 +第二个工具 `lookup_book` 注册时没有传 `title=`,所以它的 `tool.title` 是 `None`。 + !!! tip `title` 是可选的,所以把工具展示给人看的 UI 必须做选择:有 `title` 就用它,没有就用 `name`。`from mcp.shared.metadata_utils import get_display_name` 做的正是这件事,适用于工具、资源、资源模板和提示词。 @@ -83,7 +95,7 @@ UI 渲染参数表单所需的一切,以及模型生成合法参数所需的 `call_tool(name, arguments)` 运行工具,返回 `CallToolResult`。 -```python title="client.py" hl_lines="27-34" +```python title="client.py" hl_lines="9-16" --8<-- "docs_src/client/tutorial003.py" ``` @@ -131,7 +143,7 @@ result.is_error # False 资源动词成对出现:两种列出方式,一种读取方式。 -```python title="client.py" hl_lines="22-31" +```python title="client.py" hl_lines="9-18" --8<-- "docs_src/client/tutorial004.py" ``` @@ -145,7 +157,7 @@ result.is_error # False ## 提示词 {#prompts} -```python title="client.py" hl_lines="15-20" +```python title="client.py" hl_lines="8-13" --8<-- "docs_src/client/tutorial005.py" ``` @@ -170,7 +182,7 @@ message.content # TextContent(type='text', text='Recommend one poetry book from 带有补全处理函数的服务器可以在用户输入时自动补全提示词和资源模板的参数。 -```python title="client.py" hl_lines="27-31" +```python title="client.py" hl_lines="9-13" --8<-- "docs_src/client/tutorial006.py" ``` @@ -183,21 +195,21 @@ message.content # TextContent(type='text', text='Recommend one poetry book from 每个 `list_*` 方法都接收 `cursor=` 关键字参数,每个结果都带 `next_cursor`。`next_cursor` 为 `None` 时,说明已经拿全了。 -```python title="client.py" hl_lines="22-30" +```python title="client.py" hl_lines="7-15" --8<-- "docs_src/client/tutorial007.py" ``` -这个循环对任何服务器都正确。`MCPServer` 一页返回全部内容,所以 `next_cursor` 是 `None`,循环只跑一次,这也是为什么大多数代码从来不写它。真正分页的服务器,以及游标遵守的规则,见 **[分页](../advanced/pagination.md)**。 +`list_all_tools` 对任何服务器都正确。`MCPServer` 一页返回全部内容,所以 `next_cursor` 是 `None`,循环只跑一次,这也是为什么大多数代码从来不写它。真正分页的服务器,以及游标遵守的规则,见 **[分页](../advanced/pagination.md)**。 ## 在测试中 {#in-tests} -没有进程、没有端口的 `Client(mcp)`,本身就是服务器的测试工具。 +本页的每个 `client.py` 都是通过 HTTP 访问 `server.py` 的。在测试里可以跳过网络,把服务器对象本身交给 `Client`:先 `from server import mcp`,再 `Client(mcp)`。没有进程,没有端口,上面的每个方法用法都一样。 -有一个构造参数专为此而设:`Client(mcp, raise_exceptions=True)`。它只对内存连接生效,**[测试](../get-started/testing.md)** 页面会解释它,并围绕它搭建完整的模式。 +有一个构造参数专为此而设:`Client(mcp, raise_exceptions=True)`。它只对进程内连接生效,**[测试](../get-started/testing.md)** 页面会解释它,并围绕它搭建完整的模式。 ## 回顾 {#recap} -* `Client(x)` 传入服务器对象时走内存连接,传入 URL 字符串时走 Streamable HTTP,其他情况通过传输连接。 +* `Client(x)` 传入 URL 字符串时走 Streamable HTTP,传入 `StdioServerParameters` 时启动子进程,传入传输时直接进入它,在测试里则接收服务器对象本身。 * `async with` 就是全部生命周期。在它内部,`server_capabilities` 和 `protocol_version` 已经填好;服务器提供时,`server_info` 和 `instructions` 也已填好。 * `list_tools()` 给出每个工具的 `name`、`title`、`description` 和 `input_schema`。 * `call_tool()` 返回给模型的 `content`、给代码的 `structured_content`,以及 `is_error`。抛异常的工具是一个结果,不是异常。 diff --git a/i18n/zh/pages/client/oauth-clients.md b/i18n/zh/pages/client/oauth-clients.md index c57558d345..3efc007548 100644 --- a/i18n/zh/pages/client/oauth-clients.md +++ b/i18n/zh/pages/client/oauth-clients.md @@ -1,6 +1,6 @@ --- translation: - sections: [c6899d3892bd9fa0, 79372cff3cc48a88, 63878d29e87c3e73, 13175843d3588af4, e7e2b9fd516f77de, 758f06399b513c1f, a05d7278487d610b] + sections: [c6899d3892bd9fa0, 79372cff3cc48a88, c2dae1ebe2ebd543, 13175843d3588af4, df06056fb16b3846, 758f06399b513c1f, a05d7278487d610b] tool: 1 --- # OAuth 客户端 {#oauth-clients} @@ -76,18 +76,20 @@ translation: `Client` 第一次发送请求时,服务器回答 `401`。提供者接手: -1. **发现。** 它读取 `WWW-Authenticate` 头,从 `/.well-known/oauth-protected-resource` 获取服务器的受保护资源元数据,得知是哪个授权服务器在保护这个资源,再去获取**那个**服务器的元数据。 +1. **发现。** 它读取 `WWW-Authenticate` 头,从 `/.well-known/oauth-protected-resource` 获取服务器的受保护资源元数据,得知是哪个授权服务器在保护这个资源,再去获取**那个**服务器的元数据。(较旧的服务器如果没有发布资源元数据,则改为在它自己的源上请求授权服务器元数据。)无论哪种方式,元数据里的 `issuer` 都必须正是获取它所针对的那个服务器;其他任何值一律拒绝。 2. **注册。** 存储里什么都没有?它用你的 `OAuthClientMetadata` 动态注册,并把结果存起来。 3. **授权。** 它生成 PKCE 对和一个 `state`,构建授权 URL,await 你的 `redirect_handler`,然后 await 你的 `callback_handler` 拿到授权码。 4. **交换。** 它用授权码换来 `OAuthToken`,存起来,再带上 `Authorization: Bearer ...` 重放你最初的请求。 之后它就安静了。令牌从存储里取出,过期的访问令牌用刷新令牌刷新,只有这些都行不通时才会重新跑一遍流程。 +有一条传输规则适用于所有这些请求:和它们所嵌套的那个 MCP 请求一样,只有当重定向留在同一个源并且保持请求方法不变时(比如补尾部斜杠的 307/308)才会跟随,其他任何重定向都视为该 URL 没有应答。 + 这些你一行都没写。还剩两个关键字参数(`client_metadata_url` 和 `validate_resource_url`),这个文件都用不到。值得了解的是 `client_metadata_url`,下面单独有一节讲它。 ### 试一试 {#try-it} -这份文档里的大多数示例都可以用内存中的 `Client(server)` 验证。这个不行:整个流程的核心就是一个 HTTP `401`,而内存中的客户端和它的服务器之间没有 HTTP。 +测试里用的内存中的 `Client(server)` 在这里帮不上忙:整个流程的核心就是一个 HTTP `401`,而内存中的客户端和它的服务器之间没有 HTTP。 仓库里附带了可实际运行的版本。`examples/servers/simple-auth/` 运行一个独立的授权服务器和一个受保护的 MCP 服务器;`examples/clients/simple-auth-client/` 是本页的客户端扩展成的一个小 CLI。它的 README 里有两条命令:启动服务器,对着它们运行客户端,就能看到这四个步骤依次走过。 @@ -105,13 +107,14 @@ URL 必须是 HTTPS 且路径不能是根路径;否则在构造时就是 `Valu `ClientCredentialsOAuthProvider` 是同一个 `httpx2.Auth`,只是去掉了人: -```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" ``` 变了什么: * 没有 `OAuthClientMetadata`,没有处理函数。传入 `client_id` 和 `client_secret`;提供者围绕它们构建一个最小的 `client_credentials` 注册,完全跳过动态注册。 +* `issuer` 指明颁发这些凭据的授权服务器;使用它的 `/.well-known/oauth-authorization-server` 文档返回的 `issuer` 值。发现仍按上面的方式进行,但令牌请求只会基于**那个**颁发者的元数据构建;如果 MCP 服务器指向别处,流程会以 `OAuthFlowError` 中止。省略它已弃用,到 3.0 会变为必填(见 **[已弃用的功能](../deprecated.md#deprecated-sdk-helpers)**);在此之前,提供者会发出警告,并使用发现找到的那个授权服务器。 * `scope` 是空格分隔的字符串,即 OAuth 的线路格式。 * 下游的一切完全相同:同样的 `TokenStorage`、同样的 `httpx2.AsyncClient(auth=...)`、同样的 `streamable_http_client`。 @@ -121,7 +124,7 @@ URL 必须是 HTTPS 且路径不能是根路径;否则在构造时就是 `Valu 从环境变量或密钥管理器读取 `client_secret`,绝不要从源码版本控制里读。 !!! info - `mcp.client.auth.extensions.client_credentials` 里还有一个提供者:**`PrivateKeyJWTOAuthProvider`**,用于以 JWT 而非共享密钥进行认证的客户端(`private_key_jwt`,即密钥对和工作负载身份那一类)。它遵循同样的模式:构造一个,放到 `auth=` 上。同一个模块还附带 `SignedJWTParameters` 和 `static_assertion_provider`,两个用来构建其断言的辅助工具。 + `mcp.client.auth.extensions.client_credentials` 里还有一个提供者:**`PrivateKeyJWTOAuthProvider`**,用于以 JWT 而非共享密钥进行认证的客户端(`private_key_jwt`,即密钥对和工作负载身份那一类)。它遵循同样的模式:构造一个(它接受同样可选的 `issuer`),放到 `auth=` 上。同一个模块还附带 `SignedJWTParameters` 和 `static_assertion_provider`,两个用来构建其断言的辅助工具。 还有一种没有人参与的情形:客户端属于某个企业,由企业的身份提供者而不是用户来决定它可以访问哪些 MCP 服务器。那是另一种授权方式,有自己的信任模型和自己的页面,**[身份断言](identity-assertion.md)**。 diff --git a/i18n/zh/pages/client/session-groups.md b/i18n/zh/pages/client/session-groups.md index 1f68793f41..1a5ecb7c2e 100644 --- a/i18n/zh/pages/client/session-groups.md +++ b/i18n/zh/pages/client/session-groups.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-groups} @@ -76,7 +76,7 @@ translation: ## 回顾 {#recap} * `ClientSessionGroup` 持有多条服务器连接,并把它们的工具、资源和提示词各自合并成一个 `dict`。 -* 每个服务器调用一次 `connect_to_server(params)`。它接受传输参数,从不接受 `Client` 所接受的服务器对象或 URL。 +* 每个服务器调用一次 `connect_to_server(params)`。它接受的是传输参数,从不接受 `Client` 所接受的 URL 或 `Transport`。 * `group.call_tool(name, arguments)` 替你路由到拥有该工具的服务器。 * 名称必须在整个组内唯一;两个都有 `search` 工具的服务器无法直接共存。 * `component_name_hook=` 改写每个注册的名称。改变的是 dict 的键,线路上的名称不变。 diff --git a/i18n/zh/pages/client/transports.md b/i18n/zh/pages/client/transports.md index ecfd8ac867..d505e94503 100644 --- a/i18n/zh/pages/client/transports.md +++ b/i18n/zh/pages/client/transports.md @@ -1,6 +1,6 @@ --- translation: - sections: [9cac816674181eb0, 0700f337babcd4dd, 2bde0dd58cdf00f5, 40b4916d82eaf1d4, 3d0832f39b0d7059, dfa4446556badef0, 5bd93be2ab2ecb9c] + sections: [9cac816674181eb0, 7c157764133fea1f, 40b4916d82eaf1d4, 10d151f2cc75317f, 3d0832f39b0d7059, 92742ba36533633d, 0aeca6145e7bd302] tool: 1 --- # 客户端传输 {#client-transports} @@ -11,30 +11,15 @@ translation: 每种传输的**服务器**一侧(`mcp.run()` 做什么、你部署什么)见 **[运行你的服务器](../run/index.md)**。 -## 内存中 {#in-memory} - -传入服务器对象本身: - -```python title="client.py" hl_lines="14" ---8<-- "docs_src/client_transports/tutorial001.py" -``` - -没有子进程,没有端口,线路上没有任何字节。客户端和服务器是同一个进程里的两个对象,而调用仍然走真实的协议层:`search_books` 的列出、校验和调用,和走 HTTP 时完全一样。 - -这让它同时具有两种用途: - -* **测试支架。** 本文档中的每个示例都是这样跑通的,**[测试](../get-started/testing.md)** 页面围绕它构建了整套模式。 -* **嵌入 API。** 自己构造服务器的应用不需要经过网络就能调用它的工具。 - ## Streamable HTTP {#streamable-http} -传入一个 URL 字符串,得到的就是 **Streamable HTTP**,也就是部署时用的传输方式: +传入一个 URL 字符串,得到的就是 **Streamable HTTP**:部署时用的传输方式,也是应该首选的那一种: ```python title="client.py" hl_lines="5" --8<-- "docs_src/client_transports/tutorial002.py" ``` -这就是完整的生产环境客户端。`Client` 替你把 URL 包进 `streamable_http_client(...)`,底层是一个按 MCP 的需要配置好的 `httpx2.AsyncClient`:`follow_redirects=True`,connect/write/pool 超时 30 秒,读超时 300 秒,因为服务器可能会一直保持响应流打开。 +这就是完整的生产环境客户端。`Client` 替你把 URL 包进 `streamable_http_client(...)`,底层是一个按 MCP 的需要配置好的 `httpx2.AsyncClient`:connect/write/pool 超时 30 秒,读超时 300 秒,因为服务器可能会一直保持响应流打开。 !!! check 构造出来的 `Client` **并未**连接。构造只是选定传输方式;打开它的是 `async with`。在进入之前就去取连接,SDK 会明确告诉你: @@ -49,7 +34,7 @@ translation: 一旦需要 `Authorization` 头、cookie、代理、mTLS 或不同的超时,就自己构建 `httpx2.AsyncClient`,再把它交给 `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" ``` @@ -58,10 +43,7 @@ translation: * `httpx2.AsyncClient` 归你所有,所以由**你**进入和退出它。SDK 从不关闭不是它自己创建的客户端。 * `streamable_http_client(url, http_client=...)` 返回一个传输,`Client(transport)` 像接受其他任何东西一样接受它。 -关于 TLS 的一点说明:`httpx2` 依据操作系统的信任库(通过 -[`truststore`](https://pypi.org/project/truststore/))校验证书,而不是自带的 CA 列表。在没有可用系统 CA 库的环境(某些精简容器)中,设置标准的 `SSL_CERT_FILE`/`SSL_CERT_DIR` -环境变量,或者给你的 `httpx2.AsyncClient` 显式传入 `verify=ssl_context`(背景见 -[`httpx` 和 `httpx-sse` 被 `httpx2` 取代](../migration.md#httpx-and-httpx-sse-replaced-by-httpx2))。 +关于 TLS 的一点说明:`httpx2` 依据操作系统的信任库(通过 [`truststore`](https://pypi.org/project/truststore/))校验证书,而不是自带的 CA 列表。在没有可用系统 CA 库的环境(某些精简容器)中,设置标准的 `SSL_CERT_FILE`/`SSL_CERT_DIR` 环境变量,或者给你的 `httpx2.AsyncClient` 显式传入 `verify=ssl_context`(背景见 [`httpx` 和 `httpx-sse` 被 `httpx2` 取代](../migration.md#httpx-and-httpx-sse-replaced-by-httpx2))。 !!! warning `streamable_http_client` 过去可以直接接受 `headers=` 和 `timeout=`。现在不行了:它只有 `url`、`http_client` 和 `terminate_on_close` 三个参数。习惯性地去用 `headers=`,会得到: @@ -73,7 +55,25 @@ translation: 所有 HTTP 层面的东西现在都放在你传入的那一个 `httpx2.AsyncClient` 上。 !!! info - `httpx2` 保留了熟悉的 `httpx` API,所以只要会 `httpx`,就已经知道在这里怎么做认证、代理、事件钩子、重试和连接限制。SDK 既不在上面加东西,也不拿走什么。OAuth 也是在这里接入的:`httpx2.AsyncClient(auth=OAuthClientProvider(...))`。整个流程见 **[OAuth 客户端](oauth-clients.md)**。 + `httpx2` 保留了熟悉的 `httpx` API,所以只要会 `httpx`,就已经知道在这里怎么做认证、代理、事件钩子、重试和连接限制。SDK 既不在上面加东西,也不拿走什么,[重定向处理](#redirects)除外。OAuth 也是在这里接入的:`httpx2.AsyncClient(auth=OAuthClientProvider(...))`。整个流程见 **[OAuth 客户端](oauth-clients.md)**。 + +### 重定向 {#redirects} + +传输只连接你给它的那个 URL,而且只连接那个源(origin)。 + +* 停留在同一 scheme、主机和端口上的 `307`/`308` 重定向会被跟随,同一主机上的 `http://` → `https://` 也会。这覆盖了常见的 `/mcp` → `/mcp/` 尾部斜杠重定向。 +* 指向其他任何地方的重定向都**不会**被跟随。调用会失败并报错: + + ```text + MCPError: Redirect to https://other.example.com/mcp not followed; use that URL as the endpoint if it is the intended server + ``` + + 如果那个 URL 就是你要连的服务器,把它写进配置。如果不是,说明服务器或它前面的代理配置有误。 + +这对你传入的任何 `httpx2.AsyncClient` 都成立:MCP 请求不会参考它的 `follow_redirects` 设置,无论设成哪一种。SDK 的 OAuth provider 对它们自己的请求也应用同样的规则。 + +!!! tip + `Redirect to http://… not followed: it would downgrade this HTTPS endpoint to plain HTTP` 的意思是:服务器前面有一个它不知道的 TLS 终止代理,于是它发出的是 `http://` 重定向。这要在服务器端修复(**[部署与扩展](../run/deploy.md#behind-a-tls-terminating-proxy)**),或者直接使用报错信息建议的那个精确的 `https://…/` URL。 ## stdio {#stdio} @@ -94,6 +94,18 @@ translation: 需要 API key 的服务器在那里找不到它。用 `env=` 显式传入;这些变量会合并到允许列表之上。上面的 `BOOKSHOP_API_KEY` 做的就是这件事。 +## 内存中 {#in-memory} + +在测试里没有什么要部署,也没有什么要启动。传入服务器对象本身: + +```python hl_lines="14" +--8<-- "docs_src/client_transports/tutorial001.py" +``` + +没有子进程,没有端口,线路上没有任何字节。客户端和服务器是同一个进程里的两个对象,而调用仍然走真实的协议层:`search_books` 的列出、校验和调用,和走 HTTP 时完全一样。**[测试](../get-started/testing.md)** 页面围绕它构建了整套模式。 + +同样的形式也兼作嵌入 API:自己构造服务器的应用不需要经过网络就能调用它的工具。 + ## SSE {#sse} `sse_client(url)` 来自 `mcp.client.sse`,是被 Streamable HTTP 取代的那个 HTTP 传输。用同样的方式包一层,`Client(sse_client("http://localhost:8000/sse"))`,就能和仍在使用它的服务器通信;不要在它之上构建任何新东西。 @@ -102,15 +114,16 @@ translation: 对 `Client` 来说,上面这些都是同一种东西。 -**传输**是任何能产出一对 `(read, write)` 消息流的异步上下文管理器:正式地说,就是 `mcp.client` 中的 `Transport` 协议。`Client` 按类型解析它的参数:服务器对象在进程内连接,`str` 变成 `streamable_http_client(url)`,`StdioServerParameters` 变成 `stdio_client(params)`,其他任何东西都直接作为传输进入。正是最后这条规则让 `stdio_client(...)`、`streamable_http_client(...)` 和 `sse_client(...)` 都能放进同一个位置,也让你可以自己写一个。 +**传输**是任何能产出一对 `(read, write)` 消息流的异步上下文管理器:正式地说,就是 `mcp.client` 中的 `Transport` 协议。`Client` 按类型解析它的参数:`str` 变成 `streamable_http_client(url)`,`StdioServerParameters` 变成 `stdio_client(params)`,服务器对象在进程内连接,其他任何东西都直接作为传输进入。正是最后这条规则让 `stdio_client(...)`、`streamable_http_client(...)` 和 `sse_client(...)` 都能放进同一个位置,也让你可以自己写一个。 ## 回顾 {#recap} -* `Client(mcp)`(服务器对象)在内存中连接。用于测试和嵌入。 * `Client("http://.../mcp")`(URL)通过 Streamable HTTP 连接,即生产环境的传输方式。 * 请求头、认证、代理和超时应放在 `httpx2.AsyncClient` 上,再传给 `streamable_http_client(url, http_client=...)`。没有 `headers=` 关键字参数。 +* 重定向只在 URL 自己的源之内被跟随(尾部斜杠的 `307`/`308`),外加同一主机上的 `http`→`https`。其他情况都会以 `Redirect to … not followed` 失败;把最终的 URL 写进配置。 * stdio 是 `Client(StdioServerParameters(...))`。只有在需要重定向子进程的 stderr 时,才自己用 `stdio_client(...)` 包一层。 * 子进程拿到的是允许列表里的环境,不是你的环境;`env=` 往里添加。 +* `Client(mcp)`(服务器对象)在内存中连接。用于测试,或者把服务器嵌入构建它的那个应用。 * 传输就是任何可以 `async with x as (read, write)` 的东西。凡不是服务器对象、URL 或 `StdioServerParameters` 的参数,`Client` 都直接交给这个协议。 * 构造 `Client` 选定传输方式。`async with` 打开它。 diff --git a/i18n/zh/pages/deprecated.md b/i18n/zh/pages/deprecated.md index 9785fdf205..7d01c3efe8 100644 --- a/i18n/zh/pages/deprecated.md +++ b/i18n/zh/pages/deprecated.md @@ -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 --- # 已弃用的功能 {#deprecated-features} -2026-07-28 规范让五项内容退役。SDK 仍然实现了其中每一项,而且每一项现在都带有**弃用警告**。另有一个 SDK 辅助函数因自身原因被弃用,列在[本页末尾](#deprecated-sdk-helpers)。 +2026-07-28 规范让五项内容退役。SDK 仍然实现了其中每一项,而且每一项现在都带有**弃用警告**。另有几项 SDK 层面的弃用出于自身原因,列在[本页末尾](#deprecated-sdk-helpers)。 下表列出了每一项已弃用的功能、它为什么要退场,以及应该改用的替代方案。 @@ -125,11 +125,13 @@ warnings.filterwarnings("ignore", category=MCPDeprecationWarning) ## 已弃用的 SDK 辅助函数 {#deprecated-sdk-helpers} -这些不是规范变更,只是有了更好替代的 SDK 内部实现。它们用同一个 `MCPDeprecationWarning` 发出警告,并将在 3.0 中移除。 +这些不是规范变更,只是有了更好替代的 SDK 用法。它们用同一个 `MCPDeprecationWarning` 发出警告,3.0 会移除旧形式。 | 已弃用 | 替代做法 | |---|---| | `FuncMetadata.call_fn_with_arg_validation()` | 先调用 `FuncMetadata.validate_arguments()`,再调用 `FuncMetadata.call_fn()`。只有直接驱动 `FuncMetadata` 的代码(比如自定义的 `Tool` 子类)才调用过它。 | +| 不带 `validate_token_resource=` 的 `AuthSettings(resource_server_url=...)` | 设置它:`True` 让服务器拒绝你的验证器未报告为针对 `resource_server_url` 签发的 bearer 令牌,`False` 表示你的验证器自己检查令牌的受众(见 **[授权](run/authorization.md#a-token-verifier)**)。不设置时行为等同于 `False`;3.0 起,只要设置了 `resource_server_url`,默认值就是 `True`。 | +| 不带 `issuer=` 的 `ClientCredentialsOAuthProvider(...)` 或 `PrivateKeyJWTOAuthProvider(...)` | 传入 `issuer=`,指明签发这些凭据的授权服务器(见 **[编写 OAuth 客户端](client/oauth-clients.md#machine-to-machine)**)。不传的话,由 MCP 服务器决定哪个授权服务器收到这些凭据;3.0 起这个关键字参数必填。 | ## 回顾 {#recap} @@ -138,7 +140,7 @@ warnings.filterwarnings("ignore", category=MCPDeprecationWarning) * 弃用只是建议性的:线路上没有变化,在 2026 之前的会话上一切照常工作,你会看到一条醒目的 `MCPDeprecationWarning`(它是 `UserWarning`,所以默认开启)。 * 采样和根目录还需要一条反向通道,而 2026-07-28 会话没有。在现代连接上,它们先警告,然后抛出异常。 * `warnings.filterwarnings("ignore", category=MCPDeprecationWarning)` 让整个类别静音;pytest 中的 `"error::mcp.MCPDeprecationWarning"` 把它变成测试失败。 -* 一个 SDK 辅助函数 `FuncMetadata.call_fn_with_arg_validation()` 单独被弃用,将在 3.0 中移除。 +* [SDK 层面的弃用](#deprecated-sdk-helpers)遵循同样的规则:现在发出警告,3.0 移除旧形式。 * 新代码不应建立在其中任何一项之上。 本文档的其他每一页讲的都是当前的 API。 diff --git a/i18n/zh/pages/get-started/first-steps.md b/i18n/zh/pages/get-started/first-steps.md index a93641a426..7cb46def77 100644 --- a/i18n/zh/pages/get-started/first-steps.md +++ b/i18n/zh/pages/get-started/first-steps.md @@ -1,6 +1,6 @@ --- translation: - sections: [0d6c05bcbf836bf3, 59a7b14eeefc68c1, 7114d8d6daba203f, e8bbb56a98ba7bc9, 5138010f6159901c, f78da7c7c363d4c6, 220a939cab348686] + sections: [0d6c05bcbf836bf3, 9a78b5f6b44b18ab, 7114d8d6daba203f, e8bbb56a98ba7bc9, bfd2fd1153e71dac, 1615a994ef071fdd, 65c599fae991f245] tool: 1 --- # 第一步 {#first-steps} @@ -17,7 +17,7 @@ translation: * **客户端** 位于宿主内部,讲 MCP。宿主每连接一个服务器,就运行一个客户端。 * **服务器** 是你用这个 SDK 构建的东西。它向客户端暴露内容,从不直接和模型对话。 -你写的是服务器。宿主是别人的产品。SDK 还提供了一个 `Client`,你会用它来测试自己的服务器,本页后面就会用到。 +你写的是服务器。宿主是别人的产品。SDK 还提供了一个 `Client`,宿主通过 URL 连接服务器或把服务器作为子进程启动时用的就是这个类。本页后面会用到它,测试自己的服务器也靠它。 ## 三种原语 {#the-three-primitives} @@ -79,22 +79,20 @@ Inspector 是通过 **stdio** 运行你的服务器的,这是 MCP 服务器可 客户端连接时,服务器会声明自己的 **能力**:它会响应哪几类请求。客户端根据这份声明来决定该请求什么。这份声明你从没写过;是 `MCPServer` 替你声明的。 -自己看一下。SDK 的 `Client` 可以直接接受服务器对象,并在 **内存中** 与之连接(没有子进程,没有端口): - -```python -import asyncio - -from mcp import Client - -from server import mcp +自己看一下。在一个终端里让 `server.py` 通过 HTTP 运行着: +```console +uv run mcp run server.py --transport streamable-http +``` -async def main() -> None: - async with Client(mcp) as client: - print(client.server_capabilities.model_dump(exclude_none=True)) +然后在另一个终端里用客户端连上它: +```python title="client.py" hl_lines="7-8" +--8<-- "docs_src/first_steps/tutorial001_client.py" +``` -asyncio.run(main()) +```console +python client.py ``` ```text @@ -114,7 +112,7 @@ asyncio.run(main()) 注意这里缺了什么。`completions`(资源模板和提示词的参数自动补全)需要一个由你编写的处理函数,而这个服务器没有,所以这项能力不会出现,行为规范的客户端也就不会去问。所有可选项都遵循这条规则:注册了对应的东西,能力就出现;**[补全](../servers/completions.md)** 会证明这一点。 !!! info - `Client(mcp)` 正是这些文档里每个示例测试时所用的那个内存客户端,你测试自己的服务器也会用它。它有整整一页:**[测试](testing.md)**。 + 这个 `client.py` 就是一个完整的 MCP 客户端,**[客户端](../client/index.md)** 是它的专页。测试时可以省掉终端和端口,把服务器对象本身交给 `Client`,即 `Client(mcp)`。它也有整整一页:**[测试](testing.md)**。 ## 你没有写的东西 {#what-you-did-not-write} @@ -123,7 +121,7 @@ asyncio.run(main()) * JSON Schema。`a: int, b: int` **就是** `add` 的模式。 * 请求处理函数。`tools/list`、`resources/read`、`prompts/get`:全都替你处理好了。 * 能力声明。`MCPServer` 替你生成了。 -* 一行协议代码。版本协商、JSON-RPC 分帧、能力交换:全都发生在 `mcp dev` 和 `Client(mcp)` 内部,你一眼都没见到。 +* 一行协议代码。版本协商、JSON-RPC 分帧、能力交换:全都发生在 `mcp dev` 和 `client.py` 内部,你一眼都没见到。 这个比例,正是这个 SDK 的意义所在。 @@ -134,6 +132,6 @@ asyncio.run(main()) * 每种原语一个装饰器:`@mcp.tool()`、`@mcp.resource(uri)`、`@mcp.prompt()`。名称、描述和模式都来自函数本身。 * 带 `{param}` 的 URI 生成的是资源 **模板**,与具体资源分开列出。 * 服务器的 **能力** 会替你声明好,而客户端只会请求服务器声明过的内容。 -* `Client(mcp)` 在内存中连接服务器对象:从第一天起,它就是你的测试工具。 +* `Client("http://localhost:8000/mcp")` 与正在运行的服务器对话。改为把服务器对象交给它,即 `Client(mcp)`,它从第一天起就是你的测试工具。 接下来是 **[连接到真实宿主](real-host.md)**:把这个服务器真正放进 Claude Desktop 或 IDE 里。然后是 **[测试](testing.md)**:一页内容,一个内存客户端,从此不用再猜它到底能不能用。再之后,每种原语各有自己的一页,从模型驱动的那一种开始:**[工具](../servers/tools.md)**。 diff --git a/i18n/zh/pages/get-started/testing.md b/i18n/zh/pages/get-started/testing.md index 234d378b81..6e83904482 100644 --- a/i18n/zh/pages/get-started/testing.md +++ b/i18n/zh/pages/get-started/testing.md @@ -1,13 +1,13 @@ --- translation: - sections: ['4926721070127497', c52a1de2b6b32f40, 8e792bf8c7489ec6, 627195f7159e24ef] + sections: [5d13c2f0ba42c0d2, c52a1de2b6b32f40, 8e792bf8c7489ec6, 38552ea228b0a04f] tool: 1 --- # 测试 {#testing} -Python SDK 提供了一个带**内存传输**的 `Client` 类:把服务器对象传给它,它就会直接连接上去。 +SDK 的 `Client` 类,也就是连接 URL 或启动子进程的那个类,还能在**内存中**连接:把服务器对象传给它,它就直接和服务器对话。 -不用子进程,不占端口,根本不走任何传输。思路和 FastAPI 的 `TestClient` 一样。 +不用子进程,不占端口,线路上什么都没有。思路和 FastAPI 的 `TestClient` 一样。 ## 基本用法 {#basic-usage} @@ -86,7 +86,7 @@ async def test_call_add_tool(client: Client): 测试里就让它开着。它在生产代码中没有意义。 -## 默认在进程内 {#in-process-by-default} +## 默认不区分协议时代 {#era-neutral-by-default} !!! note `Client(mcp)` 在进程内连接,默认**不区分协议时代**:它会先探测服务器,再选择合适的协议路径。如果测试要验证旧版(legacy)特有的语义(采样(sampling)或征询(elicitation)的推送、`message_handler`),就固定使用 `mode="legacy"`,并在这种情况下去掉 `raise_exceptions=True`:旧版连接本来就不做脱敏,而这个标志会让失败在服务器任务内部重新抛出,而不是抛到你的测试里。 diff --git a/i18n/zh/pages/handlers/multi-round-trip.md b/i18n/zh/pages/handlers/multi-round-trip.md index 2c5b0e7f14..8f64990d28 100644 --- a/i18n/zh/pages/handlers/multi-round-trip.md +++ b/i18n/zh/pages/handlers/multi-round-trip.md @@ -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-round-trip)请求 {#multi-round-trip-requests} @@ -162,7 +162,7 @@ TTL、主体绑定和请求绑定**不是** codec 的工作:对每个 codec, ## 一个 2026-07-28 的结果 {#a-2026-07-28-result} -`InputRequiredResult` 只存在于协议版本 **2026-07-28**。内存中的 `Client(server)` 替你协商它;走线路时,`mode="auto"` 会发现它。连接之后,`client.protocol_version` 告诉你拿到的是什么。 +`InputRequiredResult` 只存在于协议版本 **2026-07-28**。`Client` 默认的 `mode="auto"` 在任何连接上都会发现它。连接之后,`client.protocol_version` 告诉你拿到的是什么。 !!! warning 2026 之前的会话没有地方放 `InputRequiredResult`。在 `mode="legacy"` 连接上从处理函数返回一个,运行器无法把它序列化到协商好的版本;客户端收到的是 `-32603`“Handler returned an invalid result”错误。同时服务两个时代的服务器在用它之前必须检查 `ctx.protocol_version`。 diff --git a/i18n/zh/pages/handlers/progress.md b/i18n/zh/pages/handlers/progress.md index a39d3be2e0..1461f326af 100644 --- a/i18n/zh/pages/handlers/progress.md +++ b/i18n/zh/pages/handlers/progress.md @@ -1,6 +1,6 @@ --- translation: - sections: [5315262fe26b33e1, 9d8e98840f1b78f0, 0284b215e85366c4, 8534d8dbb4053a70, 2966fac6fe697007] + sections: [5315262fe26b33e1, 9d8e98840f1b78f0, 52d6009a07e770ea, 8534d8dbb4053a70, 2966fac6fe697007] tool: 1 --- # 进度 {#progress} @@ -29,19 +29,17 @@ translation: 客户端**按调用**选择接收,方法是给 `call_tool` 传 `progress_callback=`: -```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,23 +54,27 @@ anyio.run(main) 回调是一个 `async` 函数,接收的正是服务器报告的内容:`progress`、`total`、`message`。 !!! info - `Client(mcp)` 直接在内存中连接到服务器对象,和 **[测试](../get-started/testing.md)** 页面所用的是同一个客户端。无论 `Client` 用哪种传输方式,`progress_callback` 都是同一个参数;接下来看到的**时序**则是内存连接特有的。它以内联方式运行你的回调,所以每条报告都在 `call_tool` 返回之前送达。换成真实的传输方式,通知会和结果竞速,`call_tool` 已经返回之后,一个慢的回调可能还在运行。 + 无论交给 `Client` 的是什么——像这里这样的 URL、`StdioServerParameters`,还是测试里的服务器对象——`progress_callback` 都是同一个参数。不过,走真实传输方式时要留意时序。每条通知都是单独送达的,和响应各走各的,所以 `call_tool` 已经返回之后,一个慢的回调可能还在运行。只有进程内的测试连接会以内联方式运行回调,并保证每条报告都先送达。 ### 试一试 {#try-it} -把 `client.py` 放在 `server.py` 旁边,然后运行: +通过 HTTP 启动 `server.py`,然后在另一个终端运行客户端: + +```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.'} ``` -服务器上的每一次 `await ctx.report_progress(...)` 都变成了客户端上对 `show` 的一次调用,顺序不变,而且两行都在 `call_tool` 返回**之前**打印了出来。进度不会打包进结果里;它在工具还在干活的时候就流式送出。 +服务器上的每一次 `await ctx.report_progress(...)` 都变成了客户端上对 `show` 的一次调用,顺序不变。进度不会打包进结果里,而是在工具还在干活的时候就流式送出。 !!! warning `progress_callback` 属于**调用**,而不是 `Client`。没有对应的构造函数参数,因为不同的调用想要不同的回调:这一次驱动下载进度条,下一次是一行日志。 diff --git a/i18n/zh/pages/protocol-versions.md b/i18n/zh/pages/protocol-versions.md index c2be647799..8f9a128c57 100644 --- a/i18n/zh/pages/protocol-versions.md +++ b/i18n/zh/pages/protocol-versions.md @@ -1,6 +1,6 @@ --- translation: - sections: [478fd619e5f90ef8, aef094a00e44e248, bab8cbf3449fa7e9, df1809b15a58335b, 5f9d8c2336ed0239, f54974398e43ddef, b24443dd78584870] + sections: [d98e337bf28845e0, dd642acbba36f615, f547b3e76da19c94, 149513378588da5c, 4e149ed992046709, f54974398e43ddef, b24443dd78584870] tool: 1 --- # 协议版本 {#protocol-versions} @@ -11,9 +11,17 @@ MCP 有两个时代。 你几乎不需要关心这些,因为 `Client` 会替你协商。本页讲的是控制这一行为的唯一一个构造参数 `mode=`,以及需要改动它的三种情形。 +本页的每段代码都是一个 `client.py`,连接的是 **[客户端](client/index.md)** 里那个 Bookshop 的 `server.py`。先在一个终端里启动那个服务器: + +```console +uv run mcp run server.py --transport streamable-http +``` + +然后在另一个终端里用 `python client.py` 运行每段代码。 + ## `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,11 +39,11 @@ MCP 有两个时代。 整个功能就这些。一个 `Client`,任意时代的服务器,代码里不需要分支。 !!! info - `MCPServer` 在每种传输方式上都会回答 `server/discover`——内存、stdio、Streamable HTTP——所以连接你自己的服务器时,`auto` 总是落在 `2026-07-28`。回退只会在面对真正的 2026 年之前的服务器时触发,而那正是你需要它的时候。 + `MCPServer` 在每种传输方式上都会回答 `server/discover`——Streamable HTTP、stdio,以及测试用的进程内连接——所以连接你自己的服务器时,`auto` 总是落在 `2026-07-28`。回退只会在面对真正的 2026 年之前的服务器时触发,而那正是你需要它的时候。 ## `mode="legacy"` {#modelegacy} -```python title="client.py" hl_lines="14" +```python title="client.py" hl_lines="7" --8<-- "docs_src/protocol_versions/tutorial002.py" ``` @@ -59,7 +67,7 @@ MCP 有两个时代。 `mode` 也接受一个新版协议版本字符串。目前这个集合正好是 `["2026-07-28"]`。 -```python title="client.py" hl_lines="14" +```python title="client.py" hl_lines="7" --8<-- "docs_src/protocol_versions/tutorial003.py" ``` @@ -90,7 +98,7 @@ ValueError: mode must be 'legacy', 'auto', or one of ['2026-07-28']; got '2025-0 所以把它存下来。一次 `auto` 连接之后,`client.session.discover_result` 保存着服务器发来的那个 `DiscoverResult` 原样:它的 `supported_versions`、它的 `capabilities`、它的 `instructions`,以及服务器写进结果 `_meta` 里的身份信息。下次把它作为 `prior_discover=` 传回去: -```python title="client.py" hl_lines="15 17" +```python title="client.py" hl_lines="8 10" --8<-- "docs_src/protocol_versions/tutorial004.py" ``` diff --git a/i18n/zh/pages/run/asgi.md b/i18n/zh/pages/run/asgi.md index 40d01513df..a83315046a 100644 --- a/i18n/zh/pages/run/asgi.md +++ b/i18n/zh/pages/run/asgi.md @@ -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 --- # 添加到现有应用 {#add-to-an-existing-app} @@ -86,7 +86,7 @@ Starlette 的 `Host` 路由用法相同:把 `Mount("/", ...)` 换成 `Host("mc --8<-- "docs_src/asgi/tutorial004.py" ``` -现在客户端连接 `/notes`,而不是 `/notes/mcp`。 +现在客户端连接 `/notes/`,而不是 `/notes/mcp`。 ## 面向浏览器客户端的 CORS {#cors-for-browser-clients} @@ -127,4 +127,4 @@ Starlette 的 `Host` 路由用法相同:把 `Mount("/", ...)` 换成 `Host("mc * 浏览器客户端需要 CORS:`allow_headers` 放行 `Mcp-*` 请求头,`expose_headers=["Mcp-Session-Id"]` 公开响应头。 * `@mcp.custom_route()` 在 `/mcp` 旁边添加普通的、不做认证的 HTTP 端点。 -服务器一旦能通过真实 URL 访问,**[客户端](../client/index.md)** 就可以用这个 URL 而不是服务器对象来连接它。 +服务器一旦能通过真实 URL 访问,**[客户端](../client/index.md)** 就用这个 URL 来连接它。 diff --git a/i18n/zh/pages/run/authorization.md b/i18n/zh/pages/run/authorization.md index 5b7de60d04..f3d2a3de51 100644 --- a/i18n/zh/pages/run/authorization.md +++ b/i18n/zh/pages/run/authorization.md @@ -1,6 +1,6 @@ --- translation: - sections: [d62c13457fc4a534, 80e73abaca6e0652, d1dc4c54cd00ec9c, 14ad3bc7904036bb, 5225f127bc1b9c77, fe1626fdd5aad1da, 4556cb7ea1a04a31] + sections: [d62c13457fc4a534, 80e73abaca6e0652, 128a492d18295f64, 14ad3bc7904036bb, 54a6697833fcc1d9, fe1626fdd5aad1da, 811d083c1da8bcf6] tool: 1 --- # 授权 {#authorization} @@ -23,12 +23,12 @@ translation: 有效的 token 长什么样,SDK 没有任何预设。这由你来决定,方式是实现 **`TokenVerifier`**: -```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` 是一个只有一个异步方法的协议。`verify_token` 接收 `Authorization` 头里的原始 token,有效时返回一个 **`AccessToken`**,无效时返回 `None`。除此之外没有别的要实现。 -* 这个例子是在一张表里查找 token。真实的实现会验证 JWT 签名,或者调用授权服务器的 token 自省端点。那部分代码是你的,SDK 只负责调用它。 +* 这个例子是在一张表里查找 token,表中每一项都记录了它是为哪个资源签发的。真实的实现会验证 JWT 签名,或者调用授权服务器的 token 自省端点,并在 `AccessToken.resource` 里报告 token 是签发给谁的(它的 `aud`)。那部分代码是你的,SDK 只负责调用它。 * `token_verifier=` 和 `auth=` 永远成对出现。只传其中一个,`MCPServer(...)` 会在处理任何请求之前就抛出 `ValueError`。 `AuthSettings` 是你的资源服务器对外的门面: @@ -36,6 +36,10 @@ translation: * `issuer_url`:签发你的 token 的授权服务器。 * `resource_server_url`:这个 MCP 端点的公开 URL。它指明 token 是针对**哪一个**资源的,发现文档也位于这里。 * `required_scopes`:每个 token 都必须携带其中全部 scope。 +* `validate_token_resource`:拒绝任何 `AccessToken.resource` 不等于 `resource_server_url` 的 token。设置了 `resource_server_url` 却不设置它,会发出警告(`MCPDeprecationWarning`),行为等同于 `False`;3.0 起资源服务器的默认值会变成 `True`。 + * 如果你的授权服务器把 token 绑定到客户端请求的 `resource`(MCP 客户端总是会发送它),就打开它。让 `resource_server_url` 与客户端连接的 URL 完全一致。 + * 如果你的授权服务器使用自己的受众标识符(Auth0 的 API 标识符、Entra 的应用 ID),就保持关闭,改为在验证器里检查 `aud`,对不属于本服务器的 token 返回 `None`。 + * 如果 `aud` 是列表,把其中等于 `resource_server_url` 的那一项放进 `resource`。 !!! tip "提示" SDK 仓库中的 `examples/servers/simple-auth/` 有一个 `IntrospectionTokenVerifier`,它会调用真实授权服务器的 [RFC 7662](https://datatracker.ietf.org/doc/html/rfc7662) 端点。大多数生产环境的验证器都是这个样子。 @@ -85,7 +89,7 @@ translation: 在任何处理函数内部,**`get_access_token()`** 就是你的验证器为当前请求返回的那个 `AccessToken`: -```python title="server.py" hl_lines="4 32-35" +```python title="server.py" hl_lines="4 35-38" --8<-- "docs_src/authorization/tutorial002.py" ``` @@ -117,6 +121,6 @@ SDK 给你的是资源服务器这一半:验证、公布、拒绝。它不提 * `token_verifier=` 和 `auth=AuthSettings(issuer_url=..., resource_server_url=..., required_scopes=[...])` 永远成对出现。 * SDK 在 `/.well-known/oauth-protected-resource/...` 发布 [RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728) Protected Resource Metadata,并对未认证的请求回应 401,其 `WWW-Authenticate` 头指向该文档。整个发现过程就是这样。 * 在任何处理函数里,`get_access_token()` 就是调用者是谁。 -* 授权是 HTTP 层面的事。`stdio` 和内存内客户端永远看不到它。 +* 授权是 HTTP 层面的事。`stdio` 和内存内测试客户端永远看不到它。 客户端那一半(发现你的授权服务器并替你获取 token)见 **[OAuth 客户端](../client/oauth-clients.md)**。而一个**断言**身份、而不是向用户索要身份的客户端,见 **[身份断言](../client/identity-assertion.md)**。 diff --git a/i18n/zh/pages/run/deploy.md b/i18n/zh/pages/run/deploy.md index fedd4a5d15..d6dc95d0dd 100644 --- a/i18n/zh/pages/run/deploy.md +++ b/i18n/zh/pages/run/deploy.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 --- # 部署与扩展 {#deploy-scale} @@ -42,6 +42,22 @@ translation: 在客户端那边你找不到这几个字。`421` 是纯文本的 HTTP 响应,不是 JSON-RPC 错误,所以 MCP 客户端抛出的是一个泛泛的传输错误;它不认可的那个主机名只出现在**服务器**的日志里,是一条警告。一个刚部署好、拒绝所有连接的服务器,在证明是别的原因之前,就是 Host 白名单的问题。**[故障排查](../troubleshooting.md)** 也从这里讲起。 +## 在终止 TLS 的代理后面 {#behind-a-tls-terminating-proxy} + +如果 TLS 在代理处终止(ingress、负载均衡器、Caddy、nginx),uvicorn 在它后面提供明文 HTTP,就要让 uvicorn 信任代理的 `X-Forwarded-*` 头: + +```console +uvicorn server:app --proxy-headers --forwarded-allow-ips='' +``` + +不这样做,应用会以为自己是通过 `http://` 提供服务的,它发出的任何重定向(最常见的是 `/mcp` → `/mcp/`)都指向 `http://…`。Python 客户端拒绝从 HTTPS 端点跟随到明文 HTTP,并明确报错: + +```text +MCPError: Redirect to http://mcp.example.com/mcp/ not followed: it would downgrade this HTTPS endpoint to plain HTTP. +``` + +客户端一侧的权宜之计是配置服务器实际提供的确切 URL(`https://mcp.example.com/mcp/`,包含结尾斜杠),这样就不会发生重定向。真正的修复是上面的标志。`FORWARDED_ALLOW_IPS` 是它的环境变量写法;`*` 信任每一跳,只有在除了代理之外没有别的东西能访问到 uvicorn 时才合适。 + ## Worker,以及谁需要粘性 {#workers-and-who-has-to-be-sticky} 主机名能响应之后,就在后面放不止一个 worker。SDK 没有这方面的开关;扩展一个 Starlette 应用和扩展任何 ASGI 应用一样,把对象交给一个会 fork 的东西: @@ -154,6 +170,7 @@ python -c "import secrets; print(secrets.token_hex(32))" ## 回顾 {#recap} * 默认情况下,这个应用只响应发往 localhost 的请求。`transport_security=TransportSecuritySettings(allowed_hosts=[...], allowed_origins=[...])` 是上线的关卡:在你传入它之前,真实主机名后面的每个请求都是 `421`,原因只在服务器日志里。 +* 在终止 TLS 的代理后面,用 `--proxy-headers --forwarded-allow-ips=...` 运行 uvicorn,否则它的重定向指向 `http://`,客户端会拒绝跟随。 * 在 2026-07-28 上没有会话,负载均衡器没有什么可粘的。`stateless_http=True` 是只对旧版有效的开关,因为现代请求在读到这个标志之前就已经被路由并响应了。 * 默认的 `requestState` 密钥是 `os.urandom(32)`,按进程铸造。到达另一个 worker 的多轮往返重试会以 `-32602` “Invalid or expired requestState” 失败。 * 修复办法是 `RequestStateSecurity(keys=[...])` **并且**每个实例使用相同的服务器名字。名字是令牌默认的 audience 声明。相同的密钥,相同的名字。 diff --git a/i18n/zh/pages/run/index.md b/i18n/zh/pages/run/index.md index fb8425f581..b2914c111c 100644 --- a/i18n/zh/pages/run/index.md +++ b/i18n/zh/pages/run/index.md @@ -1,6 +1,6 @@ --- translation: - sections: [fea8d769ff9edeba, ce8e2ad42f29ef71, 0d705efb19cf99c2, 2fd7cf825e6d2b2c, 9adc400e8c88e854, 318893ad8e2e9924, 6b63ab96b34476c0] + sections: [fea8d769ff9edeba, ce8e2ad42f29ef71, 0d705efb19cf99c2, 9fd2357154a5b7e7, 9adc400e8c88e854, 318893ad8e2e9924, 6b63ab96b34476c0] tool: 1 --- # 运行服务器 {#running-your-server} @@ -71,6 +71,8 @@ Inspector 做的事和真实宿主完全一样:它把 `server.py` 作为子进 * `json_response=True`:用单个 JSON 正文回应每个 POST,而不是 SSE 流。这个正文只容得下响应本身,别的什么都放不下,所以在请求中途回调客户端的工具(`ctx.elicit()`、采样(sampling))会在这一段抛出 `NoBackChannelError`;与进行中的调用绑定的通知(`ctx.report_progress()` 的进度、每次调用的日志消息)会被丢弃;独立的 `GET` 流仍然承载与之无关的通知。 * `stateless_http=True`:每个请求一个全新的传输,不跟踪会话。 * `max_request_body_size`:接受的最大请求正文大小,单位为字节。默认 4 MiB;更大的请求在解析或创建会话之前就会收到 HTTP 413。只有当合法的 MCP 消息确实超过这个大小时才调高它。 +* `session_idle_timeout`:旧版会话在没有任何进行中请求的情况下可以闲置多少秒,之后服务器会关闭它。默认 1800。`None` 表示禁用。见[会话生存期与限制](legacy-clients.md#session-lifetime-and-limits)。 +* `max_sessions`:一个进程同时持有多少个旧版会话。默认 10 000。`None` 表示不设上限。同一节里有介绍。 * `event_store`、`retry_interval`、`transport_security`:可恢复性和 DNS 重绑定防护。它们可以先放一放,等部署到 localhost 以外的地方再说;**[部署与扩展](deploy.md)** 介绍了 `transport_security`。 !!! warning diff --git a/i18n/zh/pages/run/legacy-clients.md b/i18n/zh/pages/run/legacy-clients.md index 2fe246dcb5..ce821f75f4 100644 --- a/i18n/zh/pages/run/legacy-clients.md +++ b/i18n/zh/pages/run/legacy-clients.md @@ -1,6 +1,6 @@ --- translation: - sections: [3d1663c18edc824c, d4fd37009a13f03d, af9f398a5a8b679a, 470c2dd144294d69, 8e45827e6d24e8c8, 91dfd0ce98ebb03c] + sections: [3d1663c18edc824c, 90956965ae6a1ca1, af9f398a5a8b679a, 5ce83b1f9d88da62, 0d9b5d13fffc94e5, 8e45827e6d24e8c8, 91dfd0ce98ebb03c] tool: 1 --- # 服务旧版客户端 {#serving-legacy-clients} @@ -18,15 +18,25 @@ SDK 按 `MCP-Protocol-Version` 头路由每个请求。声明 `2026-07-28` 的 ## 一个处理函数,两个时代 {#one-handler-both-eras} -下面是一个需要向用户提问的工具,以及两个时代的客户端分别调用它: +下面是一个需要向用户提问的工具: -```python title="server.py" hl_lines="24 37-38" +```python title="server.py" hl_lines="21" --8<-- "docs_src/legacy_clients/tutorial001.py" ``` `reserve` 需要一样模型没有提供的东西:要几本。工具用 `Annotated[..., Resolve(ask_quantity)]` 来声明这一点(详见 **[依赖](../handlers/dependencies.md)**)。`reserve` 里没有任何地方提到版本、检查能力或做分支。 -两个客户端**同时**打开,连的是同一个 `mcp` 对象。`mode="legacy"` 会执行 `initialize` 握手:这正是 2026 之前的客户端打开的那种连接。另一个取默认值,落在 `2026-07-28` 上。 +通过 HTTP 提供服务,下面是两个时代的客户端分别调用它: + +```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" +``` + +两个客户端**同时**打开,连的是同一个正在运行的服务器。`mode="legacy"` 会执行 `initialize` 握手:这正是 2026 之前的客户端打开的那种连接。另一个取默认值,落在 `2026-07-28` 上。在第二个终端运行 `python client.py`: ```text 2025-11-25 {'result': "Reserved 2 of 'Dune'."} @@ -53,6 +63,31 @@ SDK 按 `MCP-Protocol-Version` 头路由每个请求。声明 `2026-07-28` 的 !!! warning `event_store=` 看起来像是解决办法,其实不是。它是**可恢复性**(向重连到**同一个**会话的客户端重放错过的 SSE 事件),不是会话存储。它永远不会让一个会话能从另一个进程访问到。 +## 会话生存期与上限 {#session-lifetime-and-limits} + +旧版会话不会永远存活,一个进程也不会持有无限多个会话。有两个设置控制这一点。两者都是 `run()`、`streamable_http_app()` 和 `Server.streamable_http_app()` 上的关键字参数。现代(`2026-07-28`)连接和 `stateless_http=True` 没有会话,所以这两个设置对它们都不适用。 + +| 设置 | 默认值 | 作用 | 客户端看到什么 | 关闭方式 | +|---|---|---|---|---| +| `session_idle_timeout` | `1800`(30 分钟) | 关闭在这么长时间里没有任何进行中事务的会话。 | `404 Session not found`。它必须重新 `initialize`。 | `None` | +| `max_sessions` | `10_000` | 超过这个数量就拒绝再打开会话。现有会话不受影响,也不会驱逐任何会话。 | `503 Too many open sessions`,JSON-RPC 代码为 `-32603`。 | `None` | + +什么算“进行中”: + +* 一个打开的 `GET` 流。SDK 客户端会保持一个打开,所以已连接客户端的会话永远不会过期。 +* 一个仍在应答中的请求。运行时间超过超时的工具调用不会被中断,倒计时在它完成之后才开始。 +* 没有别的了。请求之间时钟照走。会话上的任何请求都会重置它,`ping` 也算。会话一旦过期,什么都救不回来。 + +用 `DELETE` 结束会话的客户端会立即释放它。开场请求被拒绝的客户端也是如此。 + +```python +mcp.run(transport="streamable-http", session_idle_timeout=None, max_sessions=50_000) +``` + +两种事件都会出现在服务器日志里。过期是 `INFO` 级别的 `Session idle timeout`。拒绝打开是 `WARNING` 级别的 `Refusing to open a new session: sessions are already open`。 + +这些上限按进程计。有四个 worker 时上限是 `max_sessions` 的四倍,每个 worker 各自让自己的会话过期。 + ## 唯一的开关:`stateless_http` {#the-one-knob-stateless_http} 如果粘性是你不愿付的代价,那么恰好有一样东西可以改。 @@ -73,7 +108,7 @@ SDK 按 `MCP-Protocol-Version` 头路由每个请求。声明 `2026-07-28` 的 `json_response=True` 不是那个开关,但它在**每一个**旧版会话上都要付一半同样的代价:用一个 JSON 正文回答的 `POST` 没有供请求范围通道使用的流,所以请求中途的 `ctx.elicit()` 会抛出同样的 `NoBackChannelError`,与该请求绑定的通知会被丢弃。会话的独立流不受影响:无关的通知仍然能到达。 !!! check - 故意做错一次。`reserve` 就是刚才同时服务两个客户端的那个工具。用 `stateless_http=True` 部署它,通过 HTTP 连上同样的两个客户端,分别调用它。 + 故意做错一次。`reserve` 就是刚才同时服务两个客户端的那个工具。用 `stateless_http=True` 部署它,连上同样的两个客户端,分别调用它。 现代客户端仍然收到 `Reserved 2 of 'Dune'.`,现代这一路没变。 diff --git a/i18n/zh/pages/troubleshooting.md b/i18n/zh/pages/troubleshooting.md index ca4cfc78dd..ee6f7a6b0f 100644 --- a/i18n/zh/pages/troubleshooting.md +++ b/i18n/zh/pages/troubleshooting.md @@ -1,6 +1,6 @@ --- translation: - sections: [2efaecdef109a5c5, fcacd3e66b8635a4, 25323d737dcf0261, 8a6e351ec756904d, 137454d469c867f5, 6392596bd6df54f0, 41126fa9c4fe432f, 480b6d7897e30ab4, d83bb682e708dde0, ebbed3449c499db4, 323ef84f6b4bebde, 30fd31be74169d9a, 656943c6cb567218, c2dc3b1007d2e987, 7cf5386b997d04e9, 0b59feed8384456e, 0cba47bae78d04eb, e4355f4c7cf4fb2e] + sections: [3d58228e81b99543, 170514ce901c4139, 17d61fad0a50d62b, 8a6e351ec756904d, 137454d469c867f5, 6392596bd6df54f0, 41126fa9c4fe432f, 480b6d7897e30ab4, d83bb682e708dde0, ebbed3449c499db4, 525cdf1755e29d4c, 30fd31be74169d9a, d2e88333d4f7841f, c2dc3b1007d2e987, d6eabf60cc366341, f798e815252852c2, 0cba47bae78d04eb, 2c218ba829abf74e] tool: 1 --- # 故障排查 {#troubleshooting} @@ -13,6 +13,12 @@ translation: --8<-- "docs_src/troubleshooting/tutorial001.py" ``` +这些条目通过 `http://localhost:8000/mcp` 访问它,所以让它以 HTTP 方式保持运行: + +```console +uv run mcp run server.py --transport streamable-http +``` + 本页引用的错误都是真实的:SDK 自己的测试套件复现了其中每一条。 ## `ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception)` {#exceptiongroup-unhandled-errors-in-a-taskgroup-1-sub-exception} @@ -23,7 +29,7 @@ translation: ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: await client.read_resource("weather://Atlantis") ``` @@ -49,7 +55,7 @@ async def main() -> None: ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: try: await client.read_resource("weather://Atlantis") except MCPError as e: @@ -65,7 +71,7 @@ async def main() -> None: ```python async def main() -> None: - client = Client(mcp) + client = Client("http://localhost:8000/mcp") tools = await client.list_tools() # RuntimeError ``` @@ -73,7 +79,7 @@ async def main() -> None: ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: tools = await client.list_tools() ``` @@ -245,7 +251,7 @@ app = Starlette(routes=[Mount("/", app=mcp.streamable_http_app())], lifespan=lif ## `MCPError: Session not found` {#mcperror-session-not-found} -服务器不认识客户端发来的 `Mcp-Session-Id`,几乎总是因为服务器**重启了**(或者你被路由到了另一个实例)。会话存活在那一个进程的内存里。 +服务器不认识客户端发来的 `Mcp-Session-Id`。要么服务器**重启了**(或者你被路由到了另一个实例),要么会话**过期了**——在 `session_idle_timeout`(默认 30 分钟)内没有任何进行中的请求。见[会话生存期与限制](run/legacy-clients.md#session-lifetime-and-limits)。会话存活在那一个进程的内存里。 没有服务器 bug 可找。HTTP 响应是 `404`,它的响应体**是** JSON-RPC,所以和上面的 `421` 不同,python `Client` 会把这一条原样展示出来: @@ -255,9 +261,9 @@ app = Starlette(routes=[Mount("/", app=mcp.streamable_http_app())], lifespan=lif 修复是重连:离开 `async with Client(...)` 块,进入一个新的,它会协商一个全新的会话。对于长时间运行的客户端,这意味着在调用外面捕获 `MCPError`,遇到这条消息就重连,而不是在死掉的会话里重试。 -如果**没有**重启也发生,说明你跑了不止一个 worker 却没有粘性会话:每个 worker 持有自己的会话表,所以路由到错误 worker 的请求就落到这里。这件事以及它的两种修复(粘性路由,或 `stateless_http=True`)归 **[部署与扩展](run/deploy.md)** 和 **[服务旧版客户端](run/legacy-clients.md)** 管。 +如果**没有**重启、客户端也没有沉默那么久却仍然发生,说明你跑了不止一个 worker 却没有粘性会话:每个 worker 持有自己的会话表,所以路由到错误 worker 的请求就落到这里。这件事以及它的两种修复(粘性路由,或 `stateless_http=True`)归 **[部署与扩展](run/deploy.md)** 和 **[服务旧版客户端](run/legacy-clients.md)** 管。 -对服务器运维方来说,对应的日志行是 `Rejected request with unknown or expired session ID: `。它以 `INFO` 级别记录,所以在常用的 `WARNING` 阈值下看不到。部署后马上成批出现是正常的;每个已连接的客户端都在重连。 +对服务器运维方来说,对应的日志行是 `Rejected request with unknown or expired session ID: `。它以 `INFO` 级别记录,所以在常用的 `WARNING` 阈值下看不到。部署后马上成批出现是正常的;每个已连接的客户端都在重连。如果是会话过期,那一行前面会先有一条 `Session idle timeout`,同样是 `INFO` 级别。 ## `MCPError: Method not found` {#mcperror-method-not-found} @@ -269,7 +275,13 @@ app = Starlette(routes=[Mount("/", app=mcp.streamable_http_app())], lifespan=lif 服务器想问用户点什么,而这个客户端从没说过自己可以被问。 -征询(elicitation)解析器在已连接的客户端没有声明表单征询时会一开始就拒绝,`e.error.data` 会准确指出缺了什么: +这家 Bistro 在订位之前会通过一个解析器先问一句: + +```python title="server.py" hl_lines="15-17 21" +--8<-- "docs_src/troubleshooting/tutorial007.py" +``` + +用它替换 Weather 服务器来运行,然后从一个没有传 `elicitation_callback` 的客户端调用 `book_table`。解析器会一开始就拒绝,因为已连接的客户端从没声明过表单征询(elicitation),`e.error.data` 会准确指出缺了什么: ```json { @@ -283,7 +295,7 @@ app = Starlette(routes=[Mount("/", app=mcp.streamable_http_app())], lifespan=lif ```python async def main() -> None: - async with Client(mcp, elicitation_callback=handle_elicitation) as client: + async with Client("http://localhost:8000/mcp", elicitation_callback=handle_elicitation) as client: result = await client.call_tool("book_table", {"date": "Friday"}) ``` @@ -302,14 +314,14 @@ async def main() -> None: 处理函数试图在请求中途联系客户端,而在这条连接上,这次调用没有任何能承载服务器发出请求的通道。有三种服务器配置会把调用置于这种境地。 -**`2026-07-28` 连接:任何传输方式,永远如此。** 现代协议根本没有服务器发起的请求,所以服务器在发送任何东西之前就拒绝了。工具里的 `ctx.elicit()` 是遇到它的经典方式(就在第一次内存测试里,因为 `Client(server)` 不用要求就会协商 `2026-07-28`),而传入 `elicitation_callback=` 什么也改变不了,因为根本没有请求到达客户端让它去回答: +**`2026-07-28` 连接:任何传输方式,永远如此。** 现代协议根本没有服务器发起的请求,所以服务器在发送任何东西之前就拒绝了。工具里的 `ctx.elicit()` 是遇到它的经典方式,通常就在这个工具的第一个内存 **[测试](get-started/testing.md)** 里,因为 `Client(mcp)` 不用要求就会协商 `2026-07-28`。传入 `elicitation_callback=` 什么也改变不了,因为根本没有请求到达客户端让它去回答: ```python title="server.py" hl_lines="16" --8<-- "docs_src/troubleshooting/tutorial006.py" ``` ```python -async def main() -> None: +async def test_book_table() -> None: async with Client(mcp) as client: await client.call_tool("book_table", {"date": "Friday"}) ``` @@ -347,7 +359,7 @@ mcp.shared.exceptions.MCPError: Cannot send 'elicitation/create': this transport ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: await client.call_tool("forecast", {"city": "London"}, request_state="round-1-from-worker-a") ``` @@ -400,7 +412,7 @@ mcp = MCPServer("Weather", request_state_security=RequestStateSecurity(keys=[key * 服务器日志里的 `Tool already exists:` 是两个同名工具合并成一个的唯一迹象。 * 一个 421,三种写法:`Server returned an error response`(python `Client`)、`421 Misdirected Request` / `Invalid Host header`(其他所有地方)、`Invalid Host header: `(服务器日志)。修复:`transport_security=TransportSecuritySettings(allowed_hosts=[...])`。 * `Task group is not initialized` -> 被挂载的应用,其宿主生命周期从未进入 `mcp.session_manager.run()`。 -* `Session not found` -> 服务器重启了;重连。 +* `Session not found` -> 服务器重启了或会话过期了(`session_idle_timeout`);重连。 * `Cannot send 'elicitation/create': ... no back-channel ...` -> `ctx.elicit()` 需要一条服务器到客户端的通道:`2026-07-28` 连接永远没有,`stateless_http=True` 拿走了旧版的那条,`json_response=True` 拿走了请求级的那条。用解析器(旧版客户端还需要一个保留该通道的服务器)。它的邻居 `Method not found` 是请求了对方协议修订版里没有的方法。 * `Client did not declare the form elicitation capability ...` 和 `Elicitation not supported` -> 客户端缺少 `elicitation_callback=`。 * `Invalid or expired requestState` 在线路上从不说明原因。服务器日志会说;`unknown key` 意味着要在各 worker 间共享 `RequestStateSecurity(keys=[...])`。 diff --git a/i18n/zh/pages/whats-new.md b/i18n/zh/pages/whats-new.md index fa2be14ce4..d3445d690a 100644 --- a/i18n/zh/pages/whats-new.md +++ b/i18n/zh/pages/whats-new.md @@ -1,6 +1,6 @@ --- translation: - sections: [cfe01c0c5863dfa2, 1c58c5cfcc37d455, a7392996acf1ad8f, 875eb2889263424e] + sections: [cfe01c0c5863dfa2, 0dbb68d8b210177b, 80cf193023af3ed4, 875eb2889263424e] tool: 1 --- # v2 的新变化 {#whats-new-in-v2} @@ -37,11 +37,11 @@ mcp = MCPServer("Demo") # v1: FastMCP("Demo") v1 交给你的是三层嵌套:一个产出原始流的传输上下文管理器,包在外面的 `ClientSession`,再加上手动调用的 `await session.initialize()`。v2 只有一个对象: -```python title="client.py" hl_lines="14-18" ---8<-- "docs_src/client/tutorial001.py" +```python title="client.py" hl_lines="7-11" +--8<-- "docs_src/client/tutorial001_client.py" ``` -`Client` 接受一个服务器对象(内存直连,没有传输:这就是测试的做法)、一个 URL(Streamable HTTP)、一个 `StdioServerParameters`(stdio 子进程),或者其他任意传输上下文管理器,比如 `sse_client(...)`。进入 `async with` 就会建立连接并协商协议版本,不管服务器讲的是哪一代协议;之后 `client.server_capabilities` 和 `client.protocol_version` 直接就在那里,服务器表明身份时 `client.server_info` 也一样(它现在是 `Implementation | None`,因为 2026 版的身份信息是可选的)。你在 v1 注册的采样和征询回调仍然能用(它们的函数体会看到和本页其他地方一样的 snake_case 属性重命名),现在还会回答 2026 风格的、嵌在结果里的请求(见下文),并且是并发运行而不是一次一个。想要底层接口的人仍然可以用底下的 `ClientSession`,`client.session` 会把它交给你;它也变了(运行在新的调度器引擎上,自身的一些签名也改了),所以下探之前先读 **[迁移指南](migration.md#clientsession-now-runs-on-jsonrpcdispatcher-basesession-removed)**。 +`Client` 接受一个 URL(Streamable HTTP)、一个 `StdioServerParameters`(stdio 子进程)、其他任意传输上下文管理器(比如 `sse_client(...)`),或者在测试里直接接受服务器对象本身(内存直连,没有传输)。进入 `async with` 就会建立连接并协商协议版本,不管服务器讲的是哪一代协议;之后 `client.server_capabilities` 和 `client.protocol_version` 直接就在那里,服务器表明身份时 `client.server_info` 也一样(它现在是 `Implementation | None`,因为 2026 版的身份信息是可选的)。你在 v1 注册的采样和征询回调仍然能用(它们的函数体会看到和本页其他地方一样的 snake_case 属性重命名),现在还会回答 2026 风格的、嵌在结果里的请求(见下文),并且是并发运行而不是一次一个。想要底层接口的人仍然可以用底下的 `ClientSession`,`client.session` 会把它交给你;它也变了(运行在新的调度器引擎上,自身的一些签名也改了),所以下探之前先读 **[迁移指南](migration.md#clientsession-now-runs-on-jsonrpcdispatcher-basesession-removed)**。 **[Client](client/index.md)** 介绍它,**[客户端传输](client/transports.md)** 讲四种连接形式,**[客户端回调](client/callbacks.md)** 讲回调本身,**[测试](get-started/testing.md)** 展示取代 v1 `create_connected_server_and_client_session()` 辅助函数的内存模式。 @@ -166,11 +166,15 @@ v2 实现了 2026-07-28 修订版,并且同时服务 **两个** 修订版: 替代方案把调用反了过来。需要从用户那里拿东西的工具把问题 **返回** 出去(`InputRequiredResult`),客户端用它一直都有的那些回调来回答,然后调用会带着答案重试。`Client` 替你驱动这个循环。在服务器上你很少自己构建这个结果,因为 **[依赖](handlers/dependencies.md)** 会做这件事:用 `Resolve(ask_quantity)` 注解一个参数,其中 `ask_quantity` 是你写的普通函数,SDK 就会通过连接所支持的机制去问——在旧版会话上是实时的征询请求,在 2026 上是多轮往返。一个工具函数体,两代协议: -```python title="dual_era.py" hl_lines="24 37-38" +```python title="server.py" hl_lines="21" --8<-- "docs_src/legacy_clients/tutorial001.py" ``` -这个文件把卖点集中在了一处:一个服务器,一个由 `Resolve` 支撑的工具,一个旧版客户端加一个新版客户端都拿到了各自的答案,全在内存里。**[多轮往返请求](handlers/multi-round-trip.md)** 解释这个机制(包括 `request_state`,SDK 会替你密封并验证它);**[征询](handlers/elicitation.md)** 讲提问的部分。 +```python title="client.py" hl_lines="14-15" +--8<-- "docs_src/legacy_clients/tutorial001_client.py" +``` + +这两个文件就是全部卖点:一个服务器,一个由 `Resolve` 支撑的工具,一个旧版客户端加一个新版客户端都从同一个正在运行的服务器拿到了各自的答案(**[服务旧版客户端](run/legacy-clients.md)** 会逐步讲解它们)。**[多轮往返请求](handlers/multi-round-trip.md)** 解释这个机制(包括 `request_state`,SDK 会替你密封并验证它);**[征询](handlers/elicitation.md)** 讲提问的部分。 !!! warning "这是移植过来的 v1 服务器唯一会改变行为的地方" 你自己的测试会最先碰到它:`Client(mcp)` 默认会和你的 v2 服务器协商出 2026-07-28,所以调用 `ctx.elicit()` 的工具会在一个 v1 上能通过的测试里失败。把问题挪进一个 `Resolve(...)` 参数(两代通用),或者如果你确实想要推送行为,就把测试客户端固定为 `mode="legacy"`。 diff --git a/scripts/docs/translations.py b/scripts/docs/translations.py index 4dd4cfd02a..59d454e1ff 100644 --- a/scripts/docs/translations.py +++ b/scripts/docs/translations.py @@ -13,12 +13,13 @@ Usage (from the repository root): python scripts/docs/translations.py status [--lang CODE] - python scripts/docs/translations.py translate --lang CODE [--pages PATH ...] + python scripts/docs/translations.py translate [--lang CODE ...] [--pages PATH ...] [--jobs N] python scripts/docs/translations.py stage [--lang CODE] Only `translate` calls the model (credentials come from the environment, e.g. -`ANTHROPIC_API_KEY`) and needs the `translate` dependency group; -`DOCS_TRANSLATE_MODEL`, if set, replaces the registry's `model` for that run. +`ANTHROPIC_API_KEY`) and needs the `translate` dependency group; it keeps +`--jobs` pages in flight at once, and `DOCS_TRANSLATE_MODEL`, if set, replaces +the registry's `model` for that run. Exit codes: 0 done, 1 some page failed, 2 configuration or credential error. """ @@ -31,9 +32,12 @@ import re import shutil import sys -from collections import Counter +import threading +from collections import Counter, deque from collections.abc import Callable, Iterator, Sequence +from concurrent.futures import FIRST_COMPLETED, Future, ThreadPoolExecutor, wait from dataclasses import dataclass, field +from itertools import chain, zip_longest from pathlib import Path from typing import Any, Literal, Protocol, cast, get_args @@ -54,6 +58,12 @@ OUTPUT_TOKEN_BUDGET = 64_000 # Repair turns fed back to the model after the first reply before a page fails. MAX_REPAIRS = 2 +# Pages in flight at once unless `--jobs` says otherwise: each page is its own +# conversation, so concurrency changes nothing the model sees, only wall-clock time. +DEFAULT_JOBS = 8 +# Retries (with backoff) the API client makes on rate limits and overloads before +# a request fails its page; generous, since many pages share one rate limit. +API_RETRIES = 6 NOTICES_PAGE = "i18n/notices.md" # The nav page the notices link to for how the translations are made. TRANSLATIONS_DOC = "translations.md" @@ -385,6 +395,8 @@ class Repo: prose_pages: list[str] translatable: list[str] renderer: markdown.Markdown + # python-markdown instances are not thread-safe; pages render one at a time. + _render_lock: threading.Lock = field(default_factory=threading.Lock, repr=False) def language(self, code: str) -> Language: for language in self.registry.languages: @@ -413,10 +425,12 @@ def heading_ids(self, body: str) -> list[str]: Raises: PageError: The renderer sees headings the source scan does not (setext, indented, HTML). """ - self.renderer.reset() - self.renderer.convert(body) - tokens = cast("list[dict[str, Any]]", getattr(self.renderer, "toc_tokens", [])) - ids, found = [str(token["id"]) for token in _flatten(tokens)], parse_headings(body) + with self._render_lock: + self.renderer.reset() + self.renderer.convert(body) + tokens = cast("list[dict[str, Any]]", getattr(self.renderer, "toc_tokens", [])) + ids = [str(token["id"]) for token in _flatten(tokens)] + found = parse_headings(body) if len(ids) != len(found): raise PageError(f"the page renders {len(ids)} headings but {len(found)} are ATX headings at column 0") return ids @@ -579,7 +593,10 @@ class Completion: class Translator(Protocol): - """Anything that answers a conversation (`ConfigError`: credentials rejected; `PageError`: request failed).""" + """Anything that answers a conversation (`ConfigError`: credentials rejected; `PageError`: request failed). + + `complete` is called from several threads at once when pages run in parallel. + """ def complete(self, *, model: str, system: str, messages: Sequence[Message], max_tokens: int) -> Completion: ... @@ -587,14 +604,22 @@ def complete(self, *, model: str, system: str, messages: Sequence[Message], max_ def anthropic_translator() -> Translator: """The Claude Messages API client, streaming, with the system prompt as one cached block. - `anthropic` lives in the non-default `translate` dependency group, so it is - imported here, by name: offline commands and type checking never need it. + One client serves every thread. The system prompt is everything a language's + pages share (rules, instructions, glossary), so it is the cacheable prefix: + the first request of a language writes it and the rest read it. A page that + starts before that first reply has begun streaming writes it again instead; + `command_translate` orders the work so that is rare, and nothing waits on it. + + `anthropic` (and `httpx`, its transport, whose errors can escape it) lives in + the non-default `translate` dependency group, so both are imported here, by + name: offline commands and type checking never need them. Raises: ConfigError: The `translate` dependency group is not installed, or no credentials are configured. """ try: sdk = importlib.import_module("anthropic") + httpx = importlib.import_module("httpx") except ImportError as exc: raise ConfigError( "the anthropic package is not installed; run with `uv run --frozen --group translate`" @@ -602,7 +627,7 @@ def anthropic_translator() -> Translator: # The SDK resolves every credential source it knows at construction; fail # here, before any page work, rather than on the first request. try: - client = sdk.Anthropic() + client = sdk.Anthropic(max_retries=API_RETRIES) except sdk.AnthropicError as exc: # e.g. a credential profile it was pointed at is unreadable raise ConfigError(f"cannot set up the API client: {exc}") from exc if not (client.api_key or client.auth_token or client.credentials): @@ -619,6 +644,8 @@ def complete(self, *, model: str, system: str, messages: Sequence[Message], max_ raise ConfigError(f"the API rejected the credentials: {exc.message}") from exc except sdk.APIError as exc: raise PageError(f"API request failed: {exc.message}") from exc + except httpx.HTTPError as exc: # the connection failing mid-reply is not wrapped by the SDK + raise PageError(f"API connection failed: {exc!r}") from exc usage = Usage( reply.usage.input_tokens, reply.usage.output_tokens, @@ -926,30 +953,78 @@ def translate_page(repo: Repo, inputs: Inputs, job: Job, translator: Translator, def command_translate(repo: Repo, args: argparse.Namespace, translator: Translator | None) -> int: - language = repo.language(args.lang) - inputs = repo.inputs(language) - jobs = select_jobs([classify(page) for page in repo.pages(language)], args.pages) - if not jobs: - print(f"{language.code}: nothing to translate") + codes = list(dict.fromkeys(args.lang)) # each language once, in the order given + languages = [repo.language(code) for code in codes] if codes else repo.registry.languages + per_language: list[list[tuple[Inputs, Job]]] = [] + for language in languages: + inputs = repo.inputs(language) + jobs = select_jobs([classify(page) for page in repo.pages(language)], args.pages) + if not jobs: + print(f"{language.code}: nothing to translate") + per_language.append([(inputs, job) for job in jobs]) + # Page-major across languages (every language's first page, then every second page, ...), + # so each language's first request is under way, and its cached prefix written, before + # its next page goes out, however many pages are in flight. + work = [item for item in chain.from_iterable(zip_longest(*per_language)) if item is not None] + if not work: return 0 model = os.environ.get("DOCS_TRANSLATE_MODEL") or repo.registry.model # never recorded in the generated files # Only a job with open sections calls the model; a run without one needs no client and no # credentials. Otherwise both are set up here, so bad credentials fail before any page work. - if translator is None and any(job.open for job in jobs): + if translator is None and any(job.open for _, job in work): translator = anthropic_translator() - usage, failed = Usage(), False - for job in jobs: - page = job.state.page + + def produce(inputs: Inputs, job: Job) -> tuple[str | PageError | ConfigError, Usage]: + """The page body, or why it failed or must stop the run, with the tokens it cost; runs on a pool thread.""" + spent = Usage() try: - body = translate_page(repo, inputs, job, translator, model, usage) if translator else reassemble(repo, job) - except PageError as exc: - failed = True - print(f"error: {page.key}: {exc}", file=sys.stderr) - continue - page.target.parent.mkdir(parents=True, exist_ok=True) - page.target.write_text(with_provenance(body, job.state.hashes), encoding="utf-8", newline="\n") - print(f"translated: {page.key} ({len(job.open)} of {len(job.state.hashes)} sections)", flush=True) + body = translate_page(repo, inputs, job, translator, model, spent) if translator else reassemble(repo, job) + except (PageError, ConfigError) as exc: + return exc, spent + return body, spent + + active = sorted({inputs.language.code for inputs, _ in work}, key=[lang.code for lang in languages].index) + print(f"translating {len(work)} pages ({', '.join(active)}), {args.jobs} at a time, with {model}", flush=True) + usage, failed = Usage(), False + queue = deque(work) + running: dict[Future[tuple[str | PageError | ConfigError, Usage]], tuple[Language, Job]] = {} + stopped: ConfigError | None = None # rejected credentials: start nothing more, keep what still lands + pool = ThreadPoolExecutor(max_workers=args.jobs) + try: + # The window is refilled here rather than handing the pool every page up front, so + # that once the credentials are rejected no further page starts; pages already in + # flight are still collected, and written if they made it, before the run stops. + while (queue and stopped is None) or running: + while queue and stopped is None and len(running) < args.jobs: + inputs, job = queue.popleft() + running[pool.submit(produce, inputs, job)] = (inputs.language, job) + for future in wait(running, return_when=FIRST_COMPLETED).done: + language, job = running.pop(future) + result, spent = future.result() + usage.add(spent) + if isinstance(result, ConfigError): + stopped = stopped or result + continue + page = job.state.page + if isinstance(result, PageError): + failed = True + print(f"{language.code}: error: {page.key}: {result}", file=sys.stderr) + continue + page.target.parent.mkdir(parents=True, exist_ok=True) + page.target.write_text(with_provenance(result, job.state.hashes), encoding="utf-8", newline="\n") + done = f"{len(job.open)} of {len(job.state.hashes)} sections" + print(f"{language.code}: translated {page.key} ({done})", flush=True) + except KeyboardInterrupt: + # The pool's threads are not daemons: left to them, the process would sit until every + # page in flight finished (and was thrown away). Leave now instead. + print(f"interrupted: {len(running)} pages in flight abandoned, {len(queue)} not started", file=sys.stderr) + print(f"usage: {usage}", flush=True) + os._exit(130) + finally: + pool.shutdown(wait=False) print(f"usage: {usage}") + if stopped is not None: + raise stopped return 1 if failed else 0 @@ -1072,6 +1147,12 @@ def command_status(repo: Repo, args: argparse.Namespace) -> int: # ---- Command line ---- +def _positive(text: str) -> int: + if (value := int(text)) < 1: + raise argparse.ArgumentTypeError("must be at least 1") + return value + + def _parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( prog="translations.py", description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter @@ -1080,9 +1161,14 @@ def _parser() -> argparse.ArgumentParser: status = commands.add_parser("status", help="what each language is missing") status.add_argument("--lang", metavar="CODE") translate = commands.add_parser("translate", help="translate missing and outdated pages (calls the model)") - translate.add_argument("--lang", metavar="CODE", required=True) translate.add_argument( - "--pages", nargs="+", metavar="PATH", default=[], help="re-translate exactly these pages from scratch" + "--lang", nargs="+", action="extend", metavar="CODE", default=[], help="only these (default: every language)" + ) + translate.add_argument( + "--pages", nargs="+", action="extend", metavar="PATH", default=[], help="re-translate exactly these, afresh" + ) + translate.add_argument( + "--jobs", type=_positive, metavar="N", default=DEFAULT_JOBS, help=f"pages in flight (default {DEFAULT_JOBS})" ) staged = commands.add_parser("stage", help="assemble .build/i18n/CODE/docs for the site build") staged.add_argument("--lang", metavar="CODE", help="stage this language only (default: every language)") diff --git a/tests/docs/test_translations.py b/tests/docs/test_translations.py index 81ba1cb8d3..b5eddae072 100644 --- a/tests/docs/test_translations.py +++ b/tests/docs/test_translations.py @@ -9,6 +9,7 @@ """ import json +import threading from collections.abc import Sequence from pathlib import Path @@ -197,15 +198,24 @@ def complete(self, *, model: str, system: str, messages: Sequence[t.Message], ma def run( capsys: pytest.CaptureFixture[str], root: Path, *argv: str, translator: t.Translator | None = None ) -> tuple[int, str, str]: + """`(exit code, stdout, stderr)` of one command line.""" code = t.main(list(argv), root=root, translator=translator) captured = capsys.readouterr() return code, captured.out, captured.err +def translate( + capsys: pytest.CaptureFixture[str], root: Path, *argv: str, jobs: int = 1, translator: t.Translator | None = None +) -> tuple[int, str, str]: + """`translate ARGV --jobs JOBS`: one page at a time by default, so scripted replies pair with pages in + nav order and the log has one possible order.""" + return run(capsys, root, "translate", *argv, "--jobs", str(jobs), translator=translator) + + def translate_all(capsys: pytest.CaptureFixture[str], root: Path) -> None: """Publish faithful translations of the three pages and the notices through the real command.""" fake = FakeTranslator([INDEX_JA, TOOLS_JA, TRANSLATIONS_JA, NOTICES_JA]) - assert run(capsys, root, "translate", "--lang", "ja", translator=fake)[0] == 0 + assert translate(capsys, root, "--lang", "ja", translator=fake)[0] == 0 def test_sections_tile_the_page_and_blank_lines_belong_to_the_heading_after_them() -> None: @@ -552,7 +562,7 @@ def test_glossary_of_the_wrong_shape_stops_translate_with_exit_2_but_never_break path = root / "i18n" / "ja" / "glossary.json" write(path, json.dumps(glossary)) - code, out, err = run(capsys, root, "translate", "--lang", "ja", translator=FakeTranslator([])) + code, out, err = translate(capsys, root, "--lang", "ja", translator=FakeTranslator([])) assert (code, out) == (2, "") assert err.startswith(f"translations: {path}: {message}") # an exception's own text is the interpreter's @@ -567,14 +577,15 @@ def test_translate_writes_pages_with_provenance_and_a_second_run_makes_no_calls( root = make_repo(tmp_path) fake = FakeTranslator([INDEX_JA, TOOLS_JA, TRANSLATIONS_JA, NOTICES_JA]) - code, out, err = run(capsys, root, "translate", "--lang", "ja", translator=fake) + code, out, err = translate(capsys, root, "--lang", "ja", translator=fake) assert (code, err) == (0, "") assert out == snapshot("""\ -translated: index.md (2 of 2 sections) -translated: tools.md (3 of 3 sections) -translated: translations.md (1 of 1 sections) -translated: i18n/notices.md (4 of 4 sections) +translating 4 pages (ja), 1 at a time, with test-model +ja: translated index.md (2 of 2 sections) +ja: translated tools.md (3 of 3 sections) +ja: translated translations.md (1 of 1 sections) +ja: translated i18n/notices.md (4 of 4 sections) usage: 4000 input / 1600 output / 3600 cache-write / 400 cache-read tokens """) assert (root / "i18n" / "ja" / "pages" / "tools.md").read_text(encoding="utf-8") == snapshot("""\ @@ -621,7 +632,7 @@ def test_translate_writes_pages_with_provenance_and_a_second_run_makes_no_calls( - server → サーバー. Katakana, long vowel kept.\ """) - code, out, err = run(capsys, root, "translate", "--lang", "ja") + code, out, err = translate(capsys, root, "--lang", "ja") assert (code, out, err) == (0, "ja: nothing to translate\n", "") assert run(capsys, root, "status") == snapshot( @@ -637,10 +648,10 @@ def test_docs_translate_model_overrides_the_registry_model_for_the_run( root = make_repo(tmp_path) monkeypatch.delenv("DOCS_TRANSLATE_MODEL", raising=False) fake = FakeTranslator([INDEX_JA, INDEX_JA]) - assert run(capsys, root, "translate", "--lang", "ja", "--pages", "index.md", translator=fake)[0] == 0 + assert translate(capsys, root, "--lang", "ja", "--pages", "index.md", translator=fake)[0] == 0 monkeypatch.setenv("DOCS_TRANSLATE_MODEL", "trial-model") - code, _, _ = run(capsys, root, "translate", "--lang", "ja", "--pages", "index.md", translator=fake) + code, _, _ = translate(capsys, root, "--lang", "ja", "--pages", "index.md", translator=fake) assert (code, fake.models) == (0, ["test-model", "trial-model"]) assert "model" not in (root / "i18n" / "ja" / "pages" / "index.md").read_text(encoding="utf-8") @@ -656,14 +667,14 @@ def test_translate_pages_retranslates_the_named_pages_from_scratch_even_when_a_t translate_all(capsys, root) fake = FakeTranslator([INDEX_JA.replace("へようこそ", "へようこそ!")]) - code, out, _ = run(capsys, root, "translate", "--lang", "ja", "--pages", "index.md", translator=fake) + code, out, _ = translate(capsys, root, "--lang", "ja", "--pages", "index.md", translator=fake) - assert (code, out.split("\n")[0]) == (0, "translated: index.md (2 of 2 sections)") + assert (code, out.split("\n")[1]) == (0, "ja: translated index.md (2 of 2 sections)") assert fake.conversations[0] == [t.Message("user", t.translate_request(INDEX))] assert "MCP へようこそ!" in (root / "i18n" / "ja" / "pages" / "index.md").read_text(encoding="utf-8") assert run(capsys, root, "status")[1] == snapshot("ja (日本語): 0 missing, 0 outdated, 4 current, 0 removable\n") - code, _, err = run(capsys, root, "translate", "--lang", "ja", "--pages", "nope.md", translator=fake) + code, _, err = translate(capsys, root, "--lang", "ja", "--pages", "nope.md", translator=fake) assert (code, err) == snapshot( (2, "translations: not translatable pages (nav paths such as servers/tools.md): ['nope.md']\n") @@ -684,9 +695,9 @@ def test_outdated_page_retranslates_the_changed_section_and_carries_the_rest_for ) fake = FakeTranslator([reply]) - code, out, _ = run(capsys, root, "translate", "--lang", "ja", translator=fake) + code, out, _ = translate(capsys, root, "--lang", "ja", translator=fake) - assert (code, out.split("\n")[0]) == (0, "translated: tools.md (1 of 3 sections)") + assert (code, out.split("\n")[1]) == (0, "ja: translated tools.md (1 of 3 sections)") assert fake.conversations[0][0].content == snapshot("""\ This page was translated before. Retranslate it: translate the sections listed below afresh from the current English, applying the current language instructions and glossary @@ -765,9 +776,9 @@ def test_banned_rendering_is_a_finding_in_a_retranslated_section_but_not_in_a_ca repaired = TOOLS_JA.replace("失敗を伝えるには例外を送出します。", "ファンクションから送出します。") fake = FakeTranslator([slipped, repaired]) - code, out, err = run(capsys, root, "translate", "--lang", "ja", translator=fake) + code, out, err = translate(capsys, root, "--lang", "ja", translator=fake) - assert (code, err, fake.replies, out.split("\n")[0]) == (0, "", [], "translated: tools.md (1 of 3 sections)") + assert (code, err, fake.replies, out.split("\n")[1]) == (0, "", [], "ja: translated tools.md (1 of 3 sections)") assert fake.conversations[1][2].content == snapshot("""\ Your translation broke the following structural rules. Fix each problem and return the full corrected page, changing nothing else: @@ -793,9 +804,9 @@ def test_link_dropped_in_a_carried_section_costs_no_repair_turn_and_the_stored_s ) fake = FakeTranslator([reply]) - code, out, err = run(capsys, root, "translate", "--lang", "ja", translator=fake) + code, out, err = translate(capsys, root, "--lang", "ja", translator=fake) - assert (code, err, out.split("\n")[0]) == (0, "", "translated: tools.md (1 of 3 sections)") + assert (code, err, out.split("\n")[1]) == (0, "", "ja: translated tools.md (1 of 3 sections)") assert [len(conversation) for conversation in fake.conversations] == [1] # one call, no repair turn body = t.split_front_matter((root / "i18n" / "ja" / "pages" / "tools.md").read_text(encoding="utf-8"))[1] assert t.sections(body)[0] == t.sections(TOOLS_JA)[0].replace("# ツール\n", "# ツール {#tools}\n") @@ -819,9 +830,9 @@ def no_credentials() -> t.Translator: monkeypatch.setattr(t, "anthropic_translator", no_credentials) write(root / "docs" / "tools.md", TOOLS.split("## Errors")[0].rstrip("\n") + "\n") - code, out, _ = run(capsys, root, "translate", "--lang", "ja") + code, out, _ = translate(capsys, root, "--lang", "ja") - assert (code, out.split("\n")[0]) == (0, "translated: tools.md (0 of 2 sections)") + assert (code, out.split("\n")[1]) == (0, "ja: translated tools.md (0 of 2 sections)") body = t.split_front_matter((root / "i18n" / "ja" / "pages" / "tools.md").read_text(encoding="utf-8"))[1] previous = TOOLS_JA.split("## エラー")[0].rstrip("\n") + "\n" assert body == previous.replace("# ツール\n", "# ツール {#tools}\n").replace( @@ -831,7 +842,7 @@ def no_credentials() -> t.Translator: write(root / "docs" / "tools.md", TOOLS.split("## Your first tool")[0].rstrip("\n") + "\n") write(root / "docs" / "index.md", INDEX.replace("Welcome to MCP.", "Welcome!")) - code, out, err = run(capsys, root, "translate", "--lang", "ja") + code, out, err = translate(capsys, root, "--lang", "ja") assert (code, out, err) == snapshot((2, "", "translations: no API credentials: set ANTHROPIC_API_KEY\n")) assert run(capsys, root, "status", "--lang", "ja")[1] == snapshot("""\ @@ -841,10 +852,10 @@ def no_credentials() -> t.Translator: """) fake = FakeTranslator([INDEX_JA.replace("MCP へようこそ。", "ようこそ!")]) - code, out, err = run(capsys, root, "translate", "--lang", "ja", translator=fake) + code, out, err = translate(capsys, root, "--lang", "ja", translator=fake) - assert (code, err, fake.replies, out.split("\n")[:2]) == snapshot( - (0, "", [], ["translated: index.md (1 of 2 sections)", "translated: tools.md (0 of 1 sections)"]) + assert (code, err, fake.replies, out.split("\n")[1:3]) == snapshot( + (0, "", [], ["ja: translated index.md (1 of 2 sections)", "ja: translated tools.md (0 of 1 sections)"]) ) @@ -858,19 +869,20 @@ def test_a_failing_page_does_not_stop_the_run_and_the_exit_code_is_1( truncated = t.Completion(TOOLS_JA[:40], t.Usage(10, 64_000, 0, 0), "max_tokens") fake = FakeTranslator([t.PageError("API request failed: overloaded"), truncated, refusal, NOTICES_JA]) - code, out, err = run(capsys, root, "translate", "--lang", "ja", translator=fake) + code, out, err = translate(capsys, root, "--lang", "ja", translator=fake) assert (code, out, err) == snapshot( ( 1, """\ -translated: i18n/notices.md (4 of 4 sections) +translating 4 pages (ja), 1 at a time, with test-model +ja: translated i18n/notices.md (4 of 4 sections) usage: 1020 input / 64400 output / 900 cache-write / 100 cache-read tokens """, """\ -error: index.md: API request failed: overloaded -error: tools.md: the reply was cut off at 64000 output tokens -error: translations.md: the model declined to translate this page +ja: error: index.md: API request failed: overloaded +ja: error: tools.md: the reply was cut off at 64000 output tokens +ja: error: translations.md: the model declined to translate this page """, ) ) @@ -883,12 +895,185 @@ def test_rejected_credentials_stop_the_run_with_exit_2(tmp_path: Path, capsys: p root = make_repo(tmp_path) fake = FakeTranslator([t.ConfigError("the API rejected the credentials: invalid x-api-key"), INDEX_JA]) - code, out, err = run(capsys, root, "translate", "--lang", "ja", translator=fake) + code, out, err = translate(capsys, root, "--lang", "ja", translator=fake) - assert (code, out, err) == snapshot((2, "", "translations: the API rejected the credentials: invalid x-api-key\n")) + assert (code, out, err) == snapshot( + ( + 2, + """\ +translating 4 pages (ja), 1 at a time, with test-model +usage: 0 input / 0 output / 0 cache-write / 0 cache-read tokens +""", + "translations: the API rejected the credentials: invalid x-api-key\n", + ) + ) assert fake.replies == [INDEX_JA] +def test_jobs_keeps_that_many_pages_in_flight_and_publishes_each_before_starting_another( + tmp_path: Path, capsys: pytest.CaptureFixture[str] +) -> None: + """Tool-defined: with `--jobs 2` over four pages, two requests are always waiting on the model together + (neither of a pair is answered until both wait, from two threads), never three, and by the time the + third and fourth requests are made an earlier page is already on disk: a finished page is written + when it lands, and only then does the next one start.""" + root = make_repo(tmp_path) + generated = root / "i18n" / "ja" / "pages" + replies = {"# Home": INDEX_JA, "# Tools": TOOLS_JA, "# Translations": TRANSLATIONS_JA, "# Notices": NOTICES_JA} + + class Pairs: + """Answers requests two at a time, noting how many were in flight and what was published by then.""" + + def __init__(self) -> None: + self.both, self.lock = threading.Barrier(2, timeout=5), threading.Lock() + self.in_flight = self.most = 0 + self.published: list[list[str]] = [] + + def complete(self, *, model: str, system: str, messages: Sequence[t.Message], max_tokens: int) -> t.Completion: + with self.lock: + self.in_flight += 1 + self.most = max(self.most, self.in_flight) + self.published.append(sorted(path.name for path in generated.glob("*.md"))) + try: + self.both.wait() + title = next(title for title in replies if title in messages[0].content) + return t.Completion(replies[title], t.Usage(10, 4, 0, 9)) + finally: + with self.lock: + self.in_flight -= 1 + + fake = Pairs() + + code, out, err = translate(capsys, root, "--lang", "ja", jobs=2, translator=fake) + + assert (code, err, fake.most) == (0, "", 2) + assert fake.published[:2] == [[], []] + assert all(fake.published[2:]) and len(fake.published) == 4 + assert sorted(out.splitlines()) == [ + "ja: translated i18n/notices.md (4 of 4 sections)", + "ja: translated index.md (2 of 2 sections)", + "ja: translated tools.md (3 of 3 sections)", + "ja: translated translations.md (1 of 1 sections)", + "translating 4 pages (ja), 2 at a time, with test-model", + "usage: 40 input / 16 output / 0 cache-write / 36 cache-read tokens", + ] + assert run(capsys, root, "status") == (0, "ja (日本語): 0 missing, 0 outdated, 4 current, 0 removable\n", "") + + +class RejectedTogether: + """Lets two requests in together, then rejects the credentials of those whose page title is in `rejected`.""" + + def __init__(self, rejected: Sequence[str]) -> None: + self.rejected, self.calls = rejected, 0 + self.both, self.lock = threading.Barrier(2, timeout=5), threading.Lock() + + def complete(self, *, model: str, system: str, messages: Sequence[t.Message], max_tokens: int) -> t.Completion: + with self.lock: + self.calls += 1 + self.both.wait() + if any(title in messages[0].content for title in self.rejected): + raise t.ConfigError("the API rejected the credentials: invalid bearer token") + return t.Completion(TOOLS_JA, t.Usage(10, 4, 0, 9)) + + +def test_rejected_credentials_keep_a_page_that_lands_from_the_same_flight( + tmp_path: Path, capsys: pytest.CaptureFixture[str] +) -> None: + """Tool-defined: with two pages in flight together, the credentials being rejected for one does not throw + away the other: whichever lands first, the good page is written and usage reported before exit 2.""" + root = make_repo(tmp_path) + fake = RejectedTogether(["# Home"]) + + code, out, err = translate(capsys, root, "--lang", "ja", "--pages", "index.md", "tools.md", jobs=2, translator=fake) + + assert (code, fake.calls, err) == (2, 2, "translations: the API rejected the credentials: invalid bearer token\n") + assert out == snapshot("""\ +translating 2 pages (ja), 2 at a time, with test-model +ja: translated tools.md (3 of 3 sections) +usage: 10 input / 4 output / 0 cache-write / 9 cache-read tokens +""") + assert sorted(path.name for path in (root / "i18n" / "ja" / "pages").glob("*.md")) == ["tools.md"] + + +def test_rejected_credentials_with_pages_in_flight_start_no_further_page( + tmp_path: Path, capsys: pytest.CaptureFixture[str] +) -> None: + """Tool-defined: with `--jobs 2` over four pages, once the credentials are rejected (here for both pages in + flight) the two remaining pages are never requested and nothing is written.""" + root = make_repo(tmp_path) + fake = RejectedTogether(["# Home", "# Tools"]) + + code, out, err = translate(capsys, root, "--lang", "ja", jobs=2, translator=fake) + + assert (code, fake.calls, err) == (2, 2, "translations: the API rejected the credentials: invalid bearer token\n") + assert out == snapshot("""\ +translating 4 pages (ja), 2 at a time, with test-model +usage: 0 input / 0 output / 0 cache-write / 0 cache-read tokens +""") + assert not (root / "i18n" / "ja" / "pages").exists() + + +def test_translate_without_lang_works_through_every_language_with_that_languages_own_prompt( + tmp_path: Path, capsys: pytest.CaptureFixture[str] +) -> None: + """Tool-defined: no `--lang` means every language in the registry, page by page across languages + (each language's first page before any second page), each page sent with its own language's system + prompt and reported under its code, with one usage total; `--lang` may be repeated and names a + language once however often it is given.""" + root = make_repo(tmp_path) + write( + root / "i18n" / "languages.yml", LANGUAGES + " - code: ko\n name: 한국어\n theme: ko\n hreflang: ko\n" + ) + write(root / "i18n" / "ko" / "instructions.md", "Korean rules.\n") + write(root / "i18n" / "ko" / "glossary.json", json.dumps({"keep": ["MCP"], "terms": []})) + fake = FakeTranslator( + [INDEX_JA, INDEX_JA, TOOLS_JA, TOOLS_JA, TRANSLATIONS_JA, TRANSLATIONS_JA, NOTICES_JA, NOTICES_JA] + ) + + code, out, err = translate(capsys, root, translator=fake) + + assert (code, err) == (0, "") + assert out == snapshot("""\ +translating 8 pages (ja, ko), 1 at a time, with test-model +ja: translated index.md (2 of 2 sections) +ko: translated index.md (2 of 2 sections) +ja: translated tools.md (3 of 3 sections) +ko: translated tools.md (3 of 3 sections) +ja: translated translations.md (1 of 1 sections) +ko: translated translations.md (1 of 1 sections) +ja: translated i18n/notices.md (4 of 4 sections) +ko: translated i18n/notices.md (4 of 4 sections) +usage: 8000 input / 3200 output / 7200 cache-write / 800 cache-read tokens +""") + assert [system.split("\n")[2] for system in fake.systems] == snapshot( + [ + "# Target language: 日本語 (`ja`)", + "# Target language: 한국어 (`ko`)", + "# Target language: 日本語 (`ja`)", + "# Target language: 한국어 (`ko`)", + "# Target language: 日本語 (`ja`)", + "# Target language: 한국어 (`ko`)", + "# Target language: 日本語 (`ja`)", + "# Target language: 한국어 (`ko`)", + ] + ) + assert fake.systems[1] == snapshot("""\ +General rules. + +# Target language: 한국어 (`ko`) + +Korean rules. + +## Glossary + +These terms always stay in English, spelled exactly like this: + +- MCP\ +""") + again = translate(capsys, root, "--lang", "ko", "--lang", "ja", "ko") + assert again == (0, "ko: nothing to translate\nja: nothing to translate\n", "") + + def test_repair_turn_feeds_the_findings_back_and_accepts_the_fixed_reply( tmp_path: Path, capsys: pytest.CaptureFixture[str] ) -> None: @@ -898,7 +1083,7 @@ def test_repair_turn_feeds_the_findings_back_and_accepts_the_fixed_reply( broken = TOOLS_JA.replace("`async` に", "非同期に").replace("!!! note", "!!! warning") fake = FakeTranslator([INDEX_JA, broken, TOOLS_JA, TRANSLATIONS_JA, NOTICES_JA]) - code, _, err = run(capsys, root, "translate", "--lang", "ja", translator=fake) + code, _, err = translate(capsys, root, "--lang", "ja", translator=fake) assert (code, err, fake.replies) == (0, "", []) assert [message.role for message in fake.conversations[2]] == ["user", "assistant", "user"] @@ -926,7 +1111,7 @@ def test_code_block_moved_into_another_section_is_repaired_not_published( root = make_repo(tmp_path) fake = FakeTranslator([FENCE_MOVED_JA, TOOLS_JA]) - code, _, err = run(capsys, root, "translate", "--lang", "ja", "--pages", "tools.md", translator=fake) + code, _, err = translate(capsys, root, "--lang", "ja", "--pages", "tools.md", translator=fake) assert (code, err, fake.replies) == (0, "", []) assert fake.conversations[1][2].content == snapshot("""\ @@ -954,9 +1139,9 @@ def test_code_block_moved_out_of_a_retranslated_section_gets_repair_turns_like_a moved = FENCE_MOVED_JA.replace("失敗を伝えるには例外を送出します。", "失敗するには `ToolError` を送出します。") fake = FakeTranslator([moved, fixed]) - code, out, err = run(capsys, root, "translate", "--lang", "ja", translator=fake) + code, out, err = translate(capsys, root, "--lang", "ja", translator=fake) - assert (code, err, fake.replies, out.split("\n")[0]) == (0, "", [], "translated: tools.md (1 of 3 sections)") + assert (code, err, fake.replies, out.split("\n")[1]) == (0, "", [], "ja: translated tools.md (1 of 3 sections)") assert fake.conversations[1][2].content == snapshot("""\ Your translation broke the following structural rules. Fix each problem and return the full corrected page, changing nothing else: @@ -983,7 +1168,7 @@ def test_shortened_list_and_table_are_fed_back_for_repair_before_the_page_is_pub shortened = LISTED_JA.replace(" 1. 入れ子\n* さん\n", "(以下同様)\n").replace("| c | d |\n", "") fake = FakeTranslator([shortened, LISTED_JA]) - code, _, err = run(capsys, root, "translate", "--lang", "ja", "--pages", "translations.md", translator=fake) + code, _, err = translate(capsys, root, "--lang", "ja", "--pages", "translations.md", translator=fake) assert (code, err, fake.replies) == (0, "", []) assert fake.conversations[1][2].content == snapshot("""\ @@ -1008,11 +1193,11 @@ def test_page_still_broken_after_two_repair_turns_fails_and_keeps_the_previous_t missing_fence = TOOLS_JA.replace('```python title="server.py"\n--8<-- "docs_src/server.py"\n```\n\n', "") fake = FakeTranslator([missing_fence] * 3) - code, _, err = run(capsys, root, "translate", "--lang", "ja", "--pages", "tools.md", translator=fake) + code, _, err = translate(capsys, root, "--lang", "ja", "--pages", "tools.md", translator=fake) assert (code, fake.replies) == (1, []) assert err == snapshot( - "error: tools.md: unfixed after 2 repairs: ## Your first tool: 0 code fences vs 1 in the English: keep each where it is, add none\n" + "ja: error: tools.md: unfixed after 2 repairs: ## Your first tool: 0 code fences vs 1 in the English: keep each where it is, add none\n" ) assert (root / "i18n" / "ja" / "pages" / "tools.md").read_text(encoding="utf-8") == before @@ -1058,7 +1243,7 @@ def test_stage_overlays_translations_injects_notices_and_rewrites_api_links( notice links so they hold under any path prefix, and assets ride along.""" root = make_repo(tmp_path) fake = FakeTranslator([INDEX_JA]) - assert run(capsys, root, "translate", "--lang", "ja", "--pages", "index.md", translator=fake)[0] == 0 + assert translate(capsys, root, "--lang", "ja", "--pages", "index.md", translator=fake)[0] == 0 write(root / "docs" / "api" / "mcp" / "index.md", "# API stub\n") code, out, err = run(capsys, root, "stage", "--lang", "ja") @@ -1185,7 +1370,7 @@ def test_stage_keeps_showing_the_generated_code_block_after_its_english_changes_ assert (code, err, staged.split("\n")[2]) == (0, "", '!!! note "英語版より古い翻訳"') assert ('title="server.py"' in staged, 'title="app.py"' in staged) == (True, False) - assert run(capsys, root, "translate", "--lang", "ja", translator=FakeTranslator([TOOLS_JA]))[0] == 0 + assert translate(capsys, root, "--lang", "ja", translator=FakeTranslator([TOOLS_JA]))[0] == 0 code, _, err = run(capsys, root, "stage", "--lang", "ja") staged = staged_page(root, "tools.md")