From e31e1a1e362cd086361d3214fa4c2b92394ce437 Mon Sep 17 00:00:00 2001 From: Max Isbey <224885523+maxisbey@users.noreply.github.com> Date: Fri, 2 Oct 2026 21:43:53 +0000 Subject: [PATCH] docs: refresh translations for recent English changes Re-run scripts/docs/translations.py translate for all twelve languages: two new pages (handlers/cancellation.md, advanced/header-parameters.md) and the changed sections of fourteen others. --- i18n/de/pages/advanced/header-parameters.md | 65 +++++++++++++++++++ i18n/de/pages/advanced/index.md | 4 +- i18n/de/pages/advanced/low-level-server.md | 3 +- i18n/de/pages/advanced/middleware.md | 32 +++++++-- i18n/de/pages/client/identity-assertion.md | 5 +- i18n/de/pages/client/transports.md | 25 ++++++- i18n/de/pages/handlers/cancellation.md | 60 +++++++++++++++++ i18n/de/pages/handlers/index.md | 3 +- i18n/de/pages/handlers/lifespan.md | 19 +----- i18n/de/pages/handlers/progress.md | 4 +- i18n/de/pages/handlers/subscriptions.md | 46 ++++++++++++- i18n/de/pages/run/deploy.md | 3 +- i18n/de/pages/servers/structured-output.md | 4 +- i18n/de/pages/servers/tools.md | 4 +- i18n/de/pages/troubleshooting.md | 11 +++- i18n/de/pages/whats-new.md | 4 +- i18n/es/pages/advanced/header-parameters.md | 65 +++++++++++++++++++ i18n/es/pages/advanced/index.md | 4 +- i18n/es/pages/advanced/low-level-server.md | 3 +- i18n/es/pages/advanced/middleware.md | 38 +++++++++-- i18n/es/pages/client/identity-assertion.md | 5 +- i18n/es/pages/client/transports.md | 25 ++++++- i18n/es/pages/handlers/cancellation.md | 60 +++++++++++++++++ i18n/es/pages/handlers/index.md | 7 +- i18n/es/pages/handlers/lifespan.md | 19 +----- i18n/es/pages/handlers/progress.md | 4 +- i18n/es/pages/handlers/subscriptions.md | 46 ++++++++++++- i18n/es/pages/run/deploy.md | 3 +- i18n/es/pages/servers/structured-output.md | 4 +- i18n/es/pages/servers/tools.md | 4 +- i18n/es/pages/troubleshooting.md | 11 +++- i18n/es/pages/whats-new.md | 8 +-- i18n/fr/pages/advanced/header-parameters.md | 65 +++++++++++++++++++ i18n/fr/pages/advanced/index.md | 4 +- i18n/fr/pages/advanced/low-level-server.md | 3 +- i18n/fr/pages/advanced/middleware.md | 35 ++++++++-- i18n/fr/pages/client/identity-assertion.md | 5 +- i18n/fr/pages/client/transports.md | 25 ++++++- i18n/fr/pages/handlers/cancellation.md | 60 +++++++++++++++++ i18n/fr/pages/handlers/index.md | 3 +- i18n/fr/pages/handlers/lifespan.md | 19 +----- i18n/fr/pages/handlers/progress.md | 4 +- i18n/fr/pages/handlers/subscriptions.md | 46 ++++++++++++- i18n/fr/pages/run/deploy.md | 3 +- i18n/fr/pages/servers/structured-output.md | 4 +- i18n/fr/pages/servers/tools.md | 4 +- i18n/fr/pages/troubleshooting.md | 11 +++- i18n/fr/pages/whats-new.md | 4 +- i18n/hi/pages/advanced/header-parameters.md | 65 +++++++++++++++++++ i18n/hi/pages/advanced/index.md | 4 +- i18n/hi/pages/advanced/low-level-server.md | 3 +- i18n/hi/pages/advanced/middleware.md | 39 ++++++++--- i18n/hi/pages/client/identity-assertion.md | 27 ++++---- i18n/hi/pages/client/transports.md | 31 +++++++-- i18n/hi/pages/handlers/cancellation.md | 60 +++++++++++++++++ i18n/hi/pages/handlers/index.md | 3 +- i18n/hi/pages/handlers/lifespan.md | 19 +----- i18n/hi/pages/handlers/progress.md | 10 +-- i18n/hi/pages/handlers/subscriptions.md | 46 ++++++++++++- i18n/hi/pages/run/deploy.md | 11 ++-- i18n/hi/pages/servers/structured-output.md | 4 +- i18n/hi/pages/servers/tools.md | 4 +- i18n/hi/pages/troubleshooting.md | 11 +++- i18n/hi/pages/whats-new.md | 4 +- i18n/ja/pages/advanced/header-parameters.md | 65 +++++++++++++++++++ i18n/ja/pages/advanced/index.md | 3 +- i18n/ja/pages/advanced/low-level-server.md | 3 +- i18n/ja/pages/advanced/middleware.md | 22 ++++++- i18n/ja/pages/client/identity-assertion.md | 5 +- i18n/ja/pages/client/transports.md | 19 +++++- i18n/ja/pages/handlers/cancellation.md | 57 ++++++++++++++++ i18n/ja/pages/handlers/index.md | 3 +- i18n/ja/pages/handlers/lifespan.md | 15 +---- i18n/ja/pages/handlers/progress.md | 4 +- i18n/ja/pages/handlers/subscriptions.md | 46 ++++++++++++- i18n/ja/pages/run/deploy.md | 3 +- i18n/ja/pages/servers/structured-output.md | 4 +- i18n/ja/pages/servers/tools.md | 4 +- i18n/ja/pages/troubleshooting.md | 11 +++- i18n/ja/pages/whats-new.md | 4 +- i18n/ko/pages/advanced/header-parameters.md | 65 +++++++++++++++++++ i18n/ko/pages/advanced/index.md | 3 +- i18n/ko/pages/advanced/low-level-server.md | 3 +- i18n/ko/pages/advanced/middleware.md | 31 +++++++-- i18n/ko/pages/client/identity-assertion.md | 5 +- i18n/ko/pages/client/transports.md | 25 ++++++- i18n/ko/pages/handlers/cancellation.md | 60 +++++++++++++++++ i18n/ko/pages/handlers/index.md | 3 +- i18n/ko/pages/handlers/lifespan.md | 19 +----- i18n/ko/pages/handlers/progress.md | 4 +- i18n/ko/pages/handlers/subscriptions.md | 46 ++++++++++++- i18n/ko/pages/run/deploy.md | 3 +- i18n/ko/pages/servers/structured-output.md | 4 +- i18n/ko/pages/servers/tools.md | 4 +- i18n/ko/pages/troubleshooting.md | 11 +++- i18n/ko/pages/whats-new.md | 4 +- i18n/pt/pages/advanced/header-parameters.md | 65 +++++++++++++++++++ i18n/pt/pages/advanced/index.md | 4 +- i18n/pt/pages/advanced/low-level-server.md | 3 +- i18n/pt/pages/advanced/middleware.md | 35 ++++++++-- i18n/pt/pages/client/identity-assertion.md | 5 +- i18n/pt/pages/client/transports.md | 25 ++++++- i18n/pt/pages/handlers/cancellation.md | 60 +++++++++++++++++ i18n/pt/pages/handlers/index.md | 4 +- i18n/pt/pages/handlers/lifespan.md | 19 +----- i18n/pt/pages/handlers/progress.md | 4 +- i18n/pt/pages/handlers/subscriptions.md | 46 ++++++++++++- i18n/pt/pages/run/deploy.md | 5 +- i18n/pt/pages/servers/structured-output.md | 4 +- i18n/pt/pages/servers/tools.md | 4 +- i18n/pt/pages/troubleshooting.md | 11 +++- i18n/pt/pages/whats-new.md | 4 +- i18n/ru/pages/advanced/header-parameters.md | 65 +++++++++++++++++++ i18n/ru/pages/advanced/index.md | 4 +- i18n/ru/pages/advanced/low-level-server.md | 3 +- i18n/ru/pages/advanced/middleware.md | 24 +++++-- i18n/ru/pages/client/identity-assertion.md | 5 +- i18n/ru/pages/client/transports.md | 25 ++++++- i18n/ru/pages/handlers/cancellation.md | 60 +++++++++++++++++ i18n/ru/pages/handlers/index.md | 4 +- i18n/ru/pages/handlers/lifespan.md | 19 +----- i18n/ru/pages/handlers/progress.md | 4 +- i18n/ru/pages/handlers/subscriptions.md | 46 ++++++++++++- i18n/ru/pages/run/deploy.md | 3 +- i18n/ru/pages/servers/structured-output.md | 4 +- i18n/ru/pages/servers/tools.md | 4 +- i18n/ru/pages/troubleshooting.md | 11 +++- i18n/ru/pages/whats-new.md | 8 +-- i18n/tr/pages/advanced/header-parameters.md | 65 +++++++++++++++++++ i18n/tr/pages/advanced/index.md | 4 +- i18n/tr/pages/advanced/low-level-server.md | 3 +- i18n/tr/pages/advanced/middleware.md | 33 ++++++++-- i18n/tr/pages/client/identity-assertion.md | 5 +- i18n/tr/pages/client/transports.md | 25 ++++++- i18n/tr/pages/handlers/cancellation.md | 60 +++++++++++++++++ i18n/tr/pages/handlers/index.md | 4 +- i18n/tr/pages/handlers/lifespan.md | 19 +----- i18n/tr/pages/handlers/progress.md | 4 +- i18n/tr/pages/handlers/subscriptions.md | 46 ++++++++++++- i18n/tr/pages/run/deploy.md | 3 +- i18n/tr/pages/servers/structured-output.md | 4 +- i18n/tr/pages/servers/tools.md | 4 +- i18n/tr/pages/troubleshooting.md | 11 +++- i18n/tr/pages/whats-new.md | 4 +- i18n/uk/pages/advanced/header-parameters.md | 65 +++++++++++++++++++ i18n/uk/pages/advanced/index.md | 4 +- i18n/uk/pages/advanced/low-level-server.md | 3 +- i18n/uk/pages/advanced/middleware.md | 30 +++++++-- i18n/uk/pages/client/identity-assertion.md | 7 +- i18n/uk/pages/client/transports.md | 25 ++++++- i18n/uk/pages/handlers/cancellation.md | 60 +++++++++++++++++ i18n/uk/pages/handlers/index.md | 4 +- i18n/uk/pages/handlers/lifespan.md | 19 +----- i18n/uk/pages/handlers/progress.md | 4 +- i18n/uk/pages/handlers/subscriptions.md | 46 ++++++++++++- i18n/uk/pages/run/deploy.md | 5 +- i18n/uk/pages/servers/structured-output.md | 4 +- i18n/uk/pages/servers/tools.md | 4 +- i18n/uk/pages/troubleshooting.md | 11 +++- i18n/uk/pages/whats-new.md | 4 +- .../pages/advanced/header-parameters.md | 65 +++++++++++++++++++ i18n/zh-hant/pages/advanced/index.md | 3 +- .../pages/advanced/low-level-server.md | 3 +- i18n/zh-hant/pages/advanced/middleware.md | 22 ++++++- .../pages/client/identity-assertion.md | 5 +- i18n/zh-hant/pages/client/transports.md | 19 +++++- i18n/zh-hant/pages/handlers/cancellation.md | 57 ++++++++++++++++ i18n/zh-hant/pages/handlers/index.md | 3 +- i18n/zh-hant/pages/handlers/lifespan.md | 15 +---- i18n/zh-hant/pages/handlers/progress.md | 4 +- i18n/zh-hant/pages/handlers/subscriptions.md | 46 ++++++++++++- i18n/zh-hant/pages/run/deploy.md | 3 +- .../pages/servers/structured-output.md | 4 +- i18n/zh-hant/pages/servers/tools.md | 4 +- i18n/zh-hant/pages/troubleshooting.md | 11 +++- i18n/zh-hant/pages/whats-new.md | 4 +- i18n/zh/pages/advanced/header-parameters.md | 65 +++++++++++++++++++ i18n/zh/pages/advanced/index.md | 3 +- i18n/zh/pages/advanced/low-level-server.md | 3 +- i18n/zh/pages/advanced/middleware.md | 22 ++++++- i18n/zh/pages/client/identity-assertion.md | 5 +- i18n/zh/pages/client/transports.md | 19 +++++- i18n/zh/pages/handlers/cancellation.md | 57 ++++++++++++++++ i18n/zh/pages/handlers/index.md | 3 +- i18n/zh/pages/handlers/lifespan.md | 15 +---- i18n/zh/pages/handlers/progress.md | 4 +- i18n/zh/pages/handlers/subscriptions.md | 46 ++++++++++++- i18n/zh/pages/run/deploy.md | 5 +- i18n/zh/pages/servers/structured-output.md | 4 +- i18n/zh/pages/servers/tools.md | 4 +- i18n/zh/pages/troubleshooting.md | 11 +++- i18n/zh/pages/whats-new.md | 4 +- 192 files changed, 2996 insertions(+), 510 deletions(-) create mode 100644 i18n/de/pages/advanced/header-parameters.md create mode 100644 i18n/de/pages/handlers/cancellation.md create mode 100644 i18n/es/pages/advanced/header-parameters.md create mode 100644 i18n/es/pages/handlers/cancellation.md create mode 100644 i18n/fr/pages/advanced/header-parameters.md create mode 100644 i18n/fr/pages/handlers/cancellation.md create mode 100644 i18n/hi/pages/advanced/header-parameters.md create mode 100644 i18n/hi/pages/handlers/cancellation.md create mode 100644 i18n/ja/pages/advanced/header-parameters.md create mode 100644 i18n/ja/pages/handlers/cancellation.md create mode 100644 i18n/ko/pages/advanced/header-parameters.md create mode 100644 i18n/ko/pages/handlers/cancellation.md create mode 100644 i18n/pt/pages/advanced/header-parameters.md create mode 100644 i18n/pt/pages/handlers/cancellation.md create mode 100644 i18n/ru/pages/advanced/header-parameters.md create mode 100644 i18n/ru/pages/handlers/cancellation.md create mode 100644 i18n/tr/pages/advanced/header-parameters.md create mode 100644 i18n/tr/pages/handlers/cancellation.md create mode 100644 i18n/uk/pages/advanced/header-parameters.md create mode 100644 i18n/uk/pages/handlers/cancellation.md create mode 100644 i18n/zh-hant/pages/advanced/header-parameters.md create mode 100644 i18n/zh-hant/pages/handlers/cancellation.md create mode 100644 i18n/zh/pages/advanced/header-parameters.md create mode 100644 i18n/zh/pages/handlers/cancellation.md diff --git a/i18n/de/pages/advanced/header-parameters.md b/i18n/de/pages/advanced/header-parameters.md new file mode 100644 index 0000000000..075d0f575b --- /dev/null +++ b/i18n/de/pages/advanced/header-parameters.md @@ -0,0 +1,65 @@ +--- +translation: + sections: [81862a209b483d27, b76e8073487afa03, bc214b2fc2bcdae4, d5835477c0b60163, a5d9786f902ad8e1] + tool: 1 +--- +# Header-Parameter {#header-parameters} + +Die meisten Server brauchen das nie. + +Ein Gateway oder Load Balancer vor deinem Server kann nur anhand dessen routen, was er lesen kann, ohne den Body zu parsen. Markiere ein Tool-Argument mit `x-mcp-header`, und Clients mit der **[Protokollversion](../protocol-versions.md)** `2026-07-28` senden seinen Wert zusätzlich als HTTP-Header. + +## Ein Argument markieren {#mark-an-argument} + +Die Markierung ist ein zusätzlicher Schlüssel im JSON Schema des Arguments. Bei `MCPServer` setzt `Field` ihn dort: + +```python title="server.py" hl_lines="13" +--8<-- "docs_src/header_parameters/tutorial001.py" +``` + +* Über Streamable HTTP mit `2026-07-28` sendet ein Client `Mcp-Param-Region` zusätzlich zum Body, und der Server lehnt einen Aufruf ab, bei dem beide nicht übereinstimmen. +* Ein Client, der das Tool nicht aufgelistet hat, hat die Markierung nie gesehen: Er sendet keinen Header, und der Aufruf wird abgelehnt. Der `Client` dieses SDK listet dann die Tools auf und sendet den Aufruf einmal erneut. Vorher aufzulisten spart also nur einen Roundtrip. +* Jede andere Verbindung ignoriert die Annotation. + +Deine Funktion ändert sich nicht: `region` kommt weiterhin als Argument an. + +## Was sich markieren lässt {#what-can-be-marked} + +Argumente vom Typ `str`, `int` und `bool`. Alles andere wird beim Registrieren des Tools mit `InvalidSignature` abgewiesen. + +Das gilt auch für `str | None`, das keinen einzelnen Typ hat. Ein optionales Argument braucht ein ausgeschriebenes Schema, mit `WithJsonSchema` von Pydantic: + +```python +region: Annotated[str | None, WithJsonSchema({"type": "string", "x-mcp-header": "Region"})] = None +``` + +## Beim Low-Level-`Server` {#on-the-low-level-server} + +Dort schreibst du `input_schema` von Hand, der Schlüssel kommt also direkt hinein: + +```python title="server.py" hl_lines="18" +--8<-- "docs_src/header_parameters/tutorial002.py" +``` + +* Nichts prüft die Annotation für dich: Eine ungültige wird ausgeliefert, und `2026-07-28`-Clients lassen das Tool aus ihrer Auflistung weg. + +### Schemas nach Namen {#schemas-by-name} + +Um den Header zu prüfen, braucht das SDK das Eingabeschema des Tools, bevor es den Aufruf weiterleitet. Ohne `get_tool_input_schema` holt es sich das Schema, indem es bei jedem Aufruf mit Argumenten deinen `on_list_tools`-Handler ausführt – egal, ob überhaupt ein Tool markiert ist. + +```python title="server.py" hl_lines="26 39-41 48" +--8<-- "docs_src/header_parameters/tutorial003.py" +``` + +* Übergib die Funktion, um aus dem zu antworten, was du schon hast. +* Gib `None` für ein Tool zurück, bei dem es nichts zu prüfen gibt. + +## Zusammenfassung {#recap} + +* `x-mcp-header` an einem Tool-Argument sorgt dafür, dass `2026-07-28`-Clients es als HTTP-Header `Mcp-Param-*` wiederholen. +* Der Server lehnt einen Aufruf ab, bei dem Header und Body nicht übereinstimmen. +* Nur Argumente vom Typ `str`, `int` und `bool` lassen sich markieren. Bei allem anderen löst `MCPServer` `InvalidSignature` aus. +* Der Low-Level-`Server` prüft nichts, und Clients verwerfen ein Tool mit ungültiger Annotation. +* `get_tool_input_schema` verhindert, dass der Low-Level-`Server` bei jedem Aufruf `on_list_tools` ausführt. + +Der Rest der handgeschriebenen `Server`-API steht in **[Der Low-Level-Server](low-level-server.md)**. diff --git a/i18n/de/pages/advanced/index.md b/i18n/de/pages/advanced/index.md index ed5e2b1d44..b58c2abf32 100644 --- a/i18n/de/pages/advanced/index.md +++ b/i18n/de/pages/advanced/index.md @@ -1,6 +1,6 @@ --- translation: - sections: [ca6988b7503cd2d3] + sections: [348f8697c6b12cd0] tool: 1 --- # Für Fortgeschrittene {#advanced} @@ -14,6 +14,8 @@ von `MCPServer` im Weg ist: eigene JSON-RPC-Methoden. * **[Paginierung](pagination.md)** und **[Middleware](middleware.md)**: zwei Dinge, die *nur* auf dem Low-Level-`Server` gehen. +* **[Header-Parameter](header-parameters.md)**: lassen ein Gateway einen Tool-Aufruf anhand + eines seiner Argumente routen. * **[Erweiterungen](extensions.md)** und **[MCP Apps](apps.md)**: die Erweiterungsfläche des Protokolls. Kombiniere Erweiterungspakete zu einem Server oder schreibe deine eigenen. diff --git a/i18n/de/pages/advanced/low-level-server.md b/i18n/de/pages/advanced/low-level-server.md index 0158d393fa..a0d8b928da 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, 9d5d10a5f0405d0a, 1086e77ce561cd7f, a3f71823df5efc31, 9fc7109f72201cae, d50fe7faead8cf68, 7bf25983df655b66, 6330e1f4c6029683, 2f1749c8c133fa1c, 8db7116fc8ddd0ee, ebc33704fbd74262, 0fde3bcea081ba3a] + sections: [2c79b6338e09b7ac, 9d5d10a5f0405d0a, 1086e77ce561cd7f, a3f71823df5efc31, 9fc7109f72201cae, d50fe7faead8cf68, 7bf25983df655b66, 6330e1f4c6029683, 2f1749c8c133fa1c, 8db7116fc8ddd0ee, 2090d99b355bc2c7, 0fde3bcea081ba3a] tool: 1 --- # Der Low-Level-Server {#the-low-level-server} @@ -209,6 +209,7 @@ Jeder davon ist eine Idee, für die du jetzt das Vokabular hast; jeder hat seine * `on_call_tool`, `on_get_prompt` und `on_read_resource` dürfen statt ihres normalen Ergebnisses ein `InputRequiredResult` zurückgeben, um den Aufruf anzuhalten und den Client um Eingaben zu bitten; siehe **[Multi-Roundtrip-Requests](../handlers/multi-round-trip.md)** (multi-round-trip requests). Getreu dieser Ebene wird nichts für dich installiert: Wo `MCPServer` `requestState` standardmäßig versiegelt, geht hier der `request_state`, den du setzt, genau so über die Leitung, wie du ihn geschrieben hast, bis du dich mit `server.middleware.append(RequestStateBoundary(RequestStateSecurity(keys=[...]), default_audience=server.name))` dafür entscheidest: eine Zeile (beide Namen lassen sich aus `mcp.server.request_state` importieren) für genau die Versiegelung und Verifizierung, die `MCPServer` vornimmt (**[`requestState` schützen](../handlers/multi-round-trip.md#protecting-requeststate)**). * `on_list_resources`, `on_read_resource`, `on_list_prompts`, `on_get_prompt`, `on_completion` haben dieselbe Form `(ctx, params) -> result` für die anderen Primitive. * `on_subscriptions_listen` bedient den Stream `subscriptions/listen` aus 2026-07-28. Übergib einen `ListenHandler`, der auf einem `SubscriptionBus` aufgebaut ist, und veröffentliche Ereignisse aus deinen anderen Handlern auf dem Bus; die vollständige Zusammensetzung steht in **[Abonnements](../handlers/subscriptions.md)**. +* `get_tool_input_schema` hält `on_list_tools` aus dem Aufrufpfad heraus; siehe **[Header-Parameter](header-parameters.md#schemas-by-name)**. * `server.streamable_http_app()` gibt dieselbe Starlette-App zurück wie die von `MCPServer`; stelle sie bereit, wie **[Den Server betreiben](../run/index.md)** jede andere ASGI-App bereitstellt. Hier unten gibt es kein `server.run(transport=...)`: `server.run(read_stream, write_stream, server.create_initialization_options())` treibt eine Verbindung über ein Paar Streams, und diese eine Zeile ist alles. ## Zusammenfassung {#recap} diff --git a/i18n/de/pages/advanced/middleware.md b/i18n/de/pages/advanced/middleware.md index b378872a29..d908ac8ca7 100644 --- a/i18n/de/pages/advanced/middleware.md +++ b/i18n/de/pages/advanced/middleware.md @@ -1,6 +1,6 @@ --- translation: - sections: [6048b4f308edbb8c, 46056f318ef205e4, c3e565b61acd75c5, c62422b159c6ed09, 420968f514138f43] + sections: [58e1103d9a323ccf, 46056f318ef205e4, 812b414557fb0c35, 4df162eea2518d38, c62422b159c6ed09, 420968f514138f43] tool: 1 --- # Middleware {#middleware} @@ -16,7 +16,7 @@ Du schreibst sie als `async (ctx, call_next)` und hängst sie an `server.middlew `MCPServer` nimmt die Liste bei der Konstruktion entgegen (`MCPServer(name, middleware=[...])`) und stellt sie als `mcp.middleware` bereit; der Low-Level-`Server` stellt dieselbe Liste als `server.middleware` -bereit. Das Beispiel unten verwendet den Low-Level-`Server`; wenn `Server(name, on_call_tool=...)` neu +bereit. Die Beispiele unten verwenden den Low-Level-`Server`; wenn `Server(name, on_call_tool=...)` neu für dich ist, lies zuerst **[Der Low-Level-Server](low-level-server.md)**. ## Eine Timing-Middleware {#a-timing-middleware} @@ -61,14 +61,38 @@ Genau darum geht es. Middleware umschließt **jede** eingehende Nachricht: * Sogar eine Methode, für die der Server keinen Handler hat: `call_next` wirft den `MCPError(-32601, "Method not found")` *durch* deine Middleware hindurch auf dem Weg zum Client. +## Eine Obergrenze für gleichzeitige Aufrufe {#a-concurrency-cap} + +Eine Middleware muss `call_next(ctx)` nicht aufrufen. Wirf stattdessen einen `MCPError`, und diese eine +Nachricht wird **abgelehnt**: Die Verbindung bleibt bestehen, und die nächste Nachricht geht durch. + +Angenommen, jede Suche belegt eine Verbindung aus einem Pool von vier. Diese Middleware lässt vier +Tool-Aufrufe gleichzeitig laufen und lehnt den fünften ab: + +```python title="server.py" hl_lines="15-16 40-55 59" +--8<-- "docs_src/middleware/tutorial002.py" +``` + +* Gezählt wird nur `tools/call`. Der Server beantwortet `server/discover` und `tools/list` also + weiter, während er Tool-Aufrufe ablehnt. +* MCP definiert keinen Fehlercode für „Server ausgelastet“, also ist `SERVER_BUSY` ein eigener Code + dieses Servers. +* Das Ablehnen sagt dem Client sofort, dass der Server überlastet ist. Wenn du Aufrufer lieber warten + lässt, umschließe stattdessen `call_next(ctx)` mit einem `anyio.CapacityLimiter`. + +Ein geworfener `MCPError` geht an die Client-Anwendung, nicht an das Modell. Soll das Modell die +Meldung lesen, gib stattdessen ein Tool-Ergebnis mit `is_error=True` zurück: Das ist **Antworten**, +weiter unten. + ## Was du in einer Middleware tun kannst {#what-you-can-do-inside-one} In aufsteigender Reihenfolge danach, wie sehr du zögern solltest: -* **Beobachten.** Miss es, zähle es, logge es. Das Beispiel oben. +* **Beobachten.** Miss es, zähle es, logge es. Die Timing-Middleware oben. * **Ablehnen.** Wirf einen `MCPError` *statt* `call_next(ctx)` aufzurufen, und diese eine Nachricht wird mit einem JSON-RPC-Fehler beantwortet. Die Verbindung bleibt bestehen; die nächste Nachricht - geht durch. So beschränkt ein Server `subscriptions/listen` pro Aufrufer: + geht durch. Die Obergrenze für gleichzeitige Aufrufe oben. So beschränkt ein Server auch + `subscriptions/listen` pro Aufrufer: **[Entscheiden, wer zusehen darf](../handlers/subscriptions.md#deciding-who-may-watch)** auf der Seite Abonnements führt es Schritt für Schritt vor. * **Umschreiben.** `ctx` ist eine Dataclass: `await call_next(dataclasses.replace(ctx, params=...))` diff --git a/i18n/de/pages/client/identity-assertion.md b/i18n/de/pages/client/identity-assertion.md index bdd38318d8..2a042dbd82 100644 --- a/i18n/de/pages/client/identity-assertion.md +++ b/i18n/de/pages/client/identity-assertion.md @@ -1,6 +1,6 @@ --- translation: - sections: [a91322c46111d16d, 8e6fd6d6f59bb568, e7828fd2729b2c9d, a03ec26bfc678b65, 1034c653c0bcf1b0] + sections: [a91322c46111d16d, 8e6fd6d6f59bb568, 7cf38181f6c99fd5, 37804d4fb36d6302, 1034c653c0bcf1b0] tool: 1 --- # Identity Assertion {#identity-assertion} @@ -66,7 +66,7 @@ Die Erweiterung verlangt das nicht; es ist eine bewusst strengere Entscheidung. ### Ein vertraulicher Client {#a-confidential-client} -`client_secret` ist erforderlich; ohne löst der Konstruktor einen `ValueError` aus. Das IETF-Profil unter [SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990) reserviert diesen Grant für vertrauliche Clients, SEP-990 verlangt, dass sich der Client authentifiziert, und dieses SDK setzt beides durch, indem es auf einem geteilten Secret besteht. `token_endpoint_auth_method` legt fest, wo es mitreist: `client_secret_post` (der Standardwert, im Formular-Body) oder `client_secret_basic` (ein HTTP-Basic-Header). Das Profil erlaubt außerdem `private_key_jwt`; dieser Provider unterstützt es nicht. +`client_secret` ist erforderlich; ohne löst der Konstruktor einen `ValueError` aus. Das IETF-Profil unter [SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990) empfiehlt diesen Grant nur für vertrauliche Clients, und [RFC 7521](https://datatracker.ietf.org/doc/html/rfc7521) überlässt diese Richtlinie dem Autorisierungsserver. Dieses SDK wählt auf beiden Seiten die vorsichtige Lesart: Der eingebaute Autorisierungsserver weist einen Client ab, der kein geteiltes Secret hat, und dieser Provider besteht auf einem. `token_endpoint_auth_method` legt fest, wo es mitreist: `client_secret_post` (der Standardwert, im Formular-Body) oder `client_secret_basic` (ein HTTP-Basic-Header). Das Profil erlaubt außerdem `private_key_jwt`; dieser Provider unterstützt es nicht. !!! tip Lies `client_secret` aus der Umgebung oder einem Secret-Manager, nie aus der Versionsverwaltung. @@ -93,6 +93,7 @@ Das SDK kann aber auch selbst der Autorisierungsserver *sein*: `create_auth_rout * `identity_assertion_enabled=True` schaltet alles frei. Ausgeschaltet – das ist der Standardwert – beantwortet `/token` diesen Grant mit `unsupported_grant_type`, selbst wenn du den Hook implementiert hast, und die Metadaten erwähnen ihn nicht. Eingeschaltet erhalten die Metadaten den Grant-Typ `jwt-bearer` und listen `urn:ietf:params:oauth:grant-profile:id-jag` in `authorization_grant_profiles_supported`, dem Feld, mit dem die Erweiterung Unterstützung bekannt gibt. (Der Client dieses SDK liest es nie: Er ist für genau einen Issuer eingerichtet und fragt einfach.) * **`exchange_identity_assertion`** ist der Hook. Bevor er läuft, hat das SDK den Client authentifiziert, öffentliche Clients abgewiesen und Clients abgewiesen, deren Registrierung den Grant nicht aufführt. Du bekommst ein `IdentityAssertionParams` (die rohe `assertion`, die angeforderten `scopes` und `resource`) und gibst ein schlichtes `OAuthToken` zurück. +* Öffentliche Clients abzuweisen ist eine Richtlinie des SDK, keine Vorgabe der Spezifikation. Der eingebaute Server authentifiziert Clients nur per geteiltem Secret: Er unterstützt kein `private_key_jwt` und löst Client ID Metadata Documents noch nicht auf ([#1801](https://github.com/modelcontextprotocol/python-sdk/issues/1801)), deshalb kann ein Client, der sich über ein solches Dokument ausweist, diesen Grant hier nicht nutzen. Ein Deployment, das eine andere Richtlinie will, kann die `/token`-Route, die `create_auth_routes` zurückgibt, durch eine eigene ersetzen. * Die dynamische Client-Registrierung lehnt diesen Grant ausnahmslos ab, deshalb bedient `get_client` hier einen von Hand eingerichteten Client. Ein ID-JAG-Client kann sich nicht selbst ins Leben registrieren. * Die halbe Klasse besteht aus Ablehnungen. `OAuthAuthorizationServerProvider` ist der *ganze* Autorisierungsserver, also verlangt er auch den Authorization-Code-Flow; ein Server, der Personen zusätzlich anmeldet, implementiert diese Methoden wirklich, und dieser hier hat genau eine Tür. diff --git a/i18n/de/pages/client/transports.md b/i18n/de/pages/client/transports.md index 75de6f810f..8274188bbd 100644 --- a/i18n/de/pages/client/transports.md +++ b/i18n/de/pages/client/transports.md @@ -1,6 +1,6 @@ --- translation: - sections: [9cac816674181eb0, 7c157764133fea1f, 40b4916d82eaf1d4, 10d151f2cc75317f, 3d0832f39b0d7059, 92742ba36533633d, 0aeca6145e7bd302] + sections: [9cac816674181eb0, 5619e950d206e6c8, 40b4916d82eaf1d4, 10d151f2cc75317f, 3d0832f39b0d7059, 92742ba36533633d, 991c10e47fda2636] tool: 1 --- # Client-Transporte {#client-transports} @@ -44,6 +44,8 @@ Zwei Dinge fallen auf: * Der `httpx2.AsyncClient` gehört dir, also betrittst und verlässt **du** ihn. Das SDK schließt nie einen Client, den es nicht selbst erzeugt hat. * `streamable_http_client(url, http_client=...)` gibt einen Transport zurück, und `Client(transport)` nimmt ihn an wie alles andere auch. +Behalte das `timeout=` bei. Es ist dasselbe, das der SDK-eigene Client verwendet (30 Sekunden, 300 für Lesevorgänge); ein `httpx2.AsyncClient`, der ohne gebaut wird, bekommt den 5-Sekunden-Standardwert von `httpx2`, und ein Tool-Aufruf, der länger läuft, schlägt mit einem Read-Timeout fehl. + Eine Anmerkung zu TLS: `httpx2` prüft Zertifikate gegen den Trust Store des Betriebssystems (über [`truststore`](https://pypi.org/project/truststore/)), nicht gegen eine mitgelieferte CA-Liste. In einer Umgebung ohne nutzbaren System-CA-Store (manche minimalen Container) setzt du die Standard-Umgebungsvariablen `SSL_CERT_FILE`/`SSL_CERT_DIR` @@ -51,16 +53,32 @@ oder übergibst deinem `httpx2.AsyncClient` ein explizites `verify=ssl_context` (Hintergrund in [`httpx` und `httpx-sse` durch `httpx2` ersetzt](../migration.md#httpx-and-httpx-sse-replaced-by-httpx2)). +### Größere SSE-Events {#larger-sse-events} + +Übergib `max_sse_event_size`, wenn ein Server ein großes Tool-Ergebnis oder eine große Benachrichtigung in einem einzigen SSE-Event sendet: + +```python title="client.py" hl_lines="6-9" +--8<-- "docs_src/client_transports/tutorial005.py" +``` + +Der Standardwert ist 1 MiB pro Event, gemessen in Bytes, bevor das Event geparst wird. Das Limit gilt für +POST-Responses, den GET-Stream und wiederaufgenommene Streams. Ein zu großes Event in einer POST-Response oder einem +wiederaufgenommenen Stream lässt diesen Request mit einem SSE-Fehler fehlschlagen. Beim GET-Stream im Hintergrund loggt +der Client den Fehler und startet den Stream neu. Setze `max_sse_event_size=None`, um die Obergrenze abzuschalten, wenn du dem +Server vertraust und größere Events brauchst. JSON-Responses sind nicht betroffen. Wenn du `ClientSessionGroup` verwendest, setze +dieselbe Option an `StreamableHttpParameters`. + !!! warning `streamable_http_client` nahm früher `headers=` und `timeout=` direkt entgegen. Das tut er nicht mehr: - seine einzigen Parameter sind `url`, `http_client` und `terminate_on_close`. Greifst du aus + Seine Parameter sind `url`, `http_client`, `terminate_on_close` und `max_sse_event_size`. Greifst du aus Gewohnheit zu `headers=`, bekommst du: ```text TypeError: streamable_http_client() got an unexpected keyword argument 'headers' ``` - Alles, was mit HTTP zu tun hat, lebt jetzt auf dem einen `httpx2.AsyncClient`, den du übergibst. + Header, Authentifizierung, Proxys und Timeouts leben auf dem einen `httpx2.AsyncClient`, den du übergibst. + `max_sse_event_size` gilt dagegen für die SSE-Reader des MCP-Transports. !!! info `httpx2` behält die vertraute `httpx`-API bei. Wenn du `httpx` kennst, weißt du hier also bereits, wie Auth, @@ -137,6 +155,7 @@ Ein **Transport** ist ein beliebiger asynchroner Kontextmanager, der ein `(read, * `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=`. +* Verwende `streamable_http_client(url, max_sse_event_size=...)`, um das Byte-Limit für jedes SSE-Event zu ändern. * 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. diff --git a/i18n/de/pages/handlers/cancellation.md b/i18n/de/pages/handlers/cancellation.md new file mode 100644 index 0000000000..7901637c2f --- /dev/null +++ b/i18n/de/pages/handlers/cancellation.md @@ -0,0 +1,60 @@ +--- +translation: + sections: [07968345fdc0b84e, 4ea8416db9efa0dc, 336a7b4c5d0a4578, 18392e805dde6717, c30d50df43f9b55c] + tool: 1 +--- +# Abbruch {#cancellation} + +Ein Client kann einen Aufruf aufgeben: Die Person am Host hat auf Stopp gedrückt, oder ein Timeout ist abgelaufen. + +In diesem Fall **bricht das SDK deinen Handler ab**. Das `await`, an dem er gerade wartet, löst eine Exception aus, die Funktion wird abgewickelt, und nichts, was sie zurückgibt, wird gesendet. Die meisten Handler müssen dafür nichts tun. + +Für zwei Arten gilt das nicht: einen Handler, der etwas aufzuräumen hat, und einen Handler, der ein einfaches `def` ist. + +## In einem `async def`-Tool aufräumen {#clean-up-in-an-async-def-tool} + +Lege den Aufräumcode in ein `finally`: + +```python title="server.py" hl_lines="23 26-28" +--8<-- "docs_src/cancellation/tutorial001.py" +``` + +* Das `finally` läuft, egal wie das Tool endet: ob es zurückgekehrt ist, eine Exception ausgelöst hat oder abgebrochen wurde. +* Aufräumcode, der `await` verwenden muss, braucht `shield=True`. In einem abgebrochenen Handler löst auch jedes weitere `await` eine Exception aus. Ohne die Abschirmung würde `release_hold` also schon in seiner ersten Zeile stoppen. +* Einen abgeschirmten Block kann nichts abbrechen, gib ihm also ein Zeitlimit. Hier sind das `5` Sekunden. + +!!! tip + Greife zu `finally`, nicht zu `except`. Der Abbruch muss weiter nach oben wandern, sobald du + aufgeräumt hast, und ein `finally` lässt das zu. + +## In einem einfachen `def`-Tool vorzeitig stoppen {#stop-early-in-a-plain-def-tool} + +Ein einfaches `def`-Tool läuft in einem Thread, und einen Thread kann nichts von außen unterbrechen. Das Tool muss selbst nachfragen: + +```python title="server.py" hl_lines="22 25-26" +--8<-- "docs_src/cancellation/tutorial002.py" +``` + +* `anyio.from_thread.check_cancelled()` tut nichts, solange der Aufruf aktiv ist, und löst eine Exception aus, sobald er abgebrochen wurde. Rufe die Funktion zwischen den einzelnen Arbeitsschritten auf. +* Auch hier gehört der Aufräumcode in ein `finally`. In einem Thread gibt es kein await, er braucht also keine Abschirmung. +* Ein `def`-Tool, das nie nachfragt, läuft bis zum Ende, und sein Ergebnis wird verworfen. + +## Geltungsbereich {#where-it-applies} + +Prompt- und Ressourcenfunktionen werden genauso abgebrochen wie Tools. + +Über stdio und Streamable HTTP funktioniert das gleich. Mit dem `Client` dieses SDK heißt aufgeben: den Task abbrechen, der auf `call_tool` wartet, oder dessen `read_timeout_seconds` ablaufen lassen. + +!!! warning + Bei zwei Streamable-HTTP-Optionen erfährt dein Handler nichts davon: `json_response=True` auf einer + `2026-07-28`-Verbindung und `stateless_http=True` auf einer Legacy-Verbindung. Dort läuft der Handler + bis zum Ende, egal was der Client getan hat. + +## Zusammenfassung {#recap} + +* Gibt der Client einen Aufruf auf, bricht das SDK den Handler ab: Tool, Prompt oder Ressource. +* `async def`: Räume in einem `finally` auf und lege Aufräumcode, der await braucht, in `anyio.move_on_after(seconds, shield=True)`. +* Einfaches `def`: Rufe zwischen den Arbeitsschritten `anyio.from_thread.check_cancelled()` auf, sonst läuft das Tool bis zum Ende. Zum Aufräumen genügt ein einfaches `finally`. +* `json_response=True` (moderne Verbindungen) und `stateless_http=True` (Legacy-Verbindungen) schalten den Abbruch ab. + +Fortschritt und Abbruch spielen sich zwischen einem laufenden Tool und seinem *Aufrufer* ab. Die Zeilen, die es für *dich* loggt, also für die Person, die den Server betreibt, sind ein anderer Kanal: **[Logging](logging.md)**. diff --git a/i18n/de/pages/handlers/index.md b/i18n/de/pages/handlers/index.md index 4d0996ecc9..9bf191c81b 100644 --- a/i18n/de/pages/handlers/index.md +++ b/i18n/de/pages/handlers/index.md @@ -1,6 +1,6 @@ --- translation: - sections: [424930166c4bc6f3] + sections: [22ca41e50cc1b536] tool: 1 --- # Im Handler {#inside-your-handler} @@ -18,6 +18,7 @@ Was er tun kann, während er läuft: * Die Person am Host um weitere Eingaben bitten – mit **[Elicitation](elicitation.md)** (Rückfrage bei der Person am Host) und **[Multi-Roundtrip-Requests](multi-round-trip.md)** (multi-round-trip requests), dem Muster aus 2026-07-28, das sie transportiert. * Den Client um die Antwort eines LLM oder um seine Arbeitsverzeichnisse bitten – mit **[Sampling und Roots](sampling-and-roots.md)**, veraltet, aber weiterhin bedient. * **[Fortschritt](progress.md)** bei etwas Langsamem melden. +* Aufräumen oder vorzeitig aufhören, wenn der Client den Aufruf aufgibt – mit **[Abbruch](cancellation.md)**. * Logs schreiben (auf die Standardfehlerausgabe, für alle, die den Server betreiben) – mit **[Logging](logging.md)**. * Abonnierten Clients mitteilen, dass sich etwas geändert hat – mit **[Abonnements](subscriptions.md)**. diff --git a/i18n/de/pages/handlers/lifespan.md b/i18n/de/pages/handlers/lifespan.md index af15b1251e..b615ca849e 100644 --- a/i18n/de/pages/handlers/lifespan.md +++ b/i18n/de/pages/handlers/lifespan.md @@ -1,6 +1,6 @@ --- translation: - sections: [f3ca8ac5f90f2dfa, 85a1ef3588ba0736, 563346d4d5804933, 9e3528340d0bab53] + sections: [f3ca8ac5f90f2dfa, 48e478ef7bd688b1, 563346d4d5804933, 52ac6a7734d6f581] tool: 1 --- # Lifespan {#lifespan} @@ -46,7 +46,7 @@ Nichts Neues. `ctx` ist ein **Context**-Parameter, also injiziert das SDK ihn, u `genre` ist das einzige Argument, das das Modell übergeben kann. Der Lifespan ist Sache deines Servers. -Auch `@mcp.resource()`- und `@mcp.prompt()`-Funktionen können einen `ctx`-Parameter annehmen, geschrieben als bloßer `Context` – aus einem Grund, zu dem der nächste Abschnitt kommt. Alles, was `ctx` mitbringt, steht in **[Der Context](context.md)**. +Auch `@mcp.resource()`- und `@mcp.prompt()`-Funktionen können einen `ctx`-Parameter annehmen. Alles, was `ctx` mitbringt, steht in **[Der Context](context.md)**. ### Es ist wirklich typisiert {#it-really-is-typed} @@ -56,19 +56,6 @@ Dieser eine Typparameter ist der Grund, warum `ctx.request_context.lifespan_cont Schreibst du stattdessen einen bloßen `Context`, ist `lifespan_context` als `dict[str, Any]` typisiert: Der Type Checker kann nicht wissen, was dein Lifespan geliefert hat. Das Objekt ist zur Laufzeit immer noch da; du hast nur die Hilfe verloren. -!!! warning - `Context[AppContext]` ist eine Schreibweise **nur für Tools**. Setzt du sie auf eine `@mcp.resource()`- oder - `@mcp.prompt()`-Funktion, schlägt jeder Aufruf dieses Handlers fehl. Der Client bekommt einen Fehler zurück, - und das Server-Log zeigt, warum: - - ```text - Context is not available outside of a request - ``` - - In Ressourcen und Prompts schreibst du das bloße `ctx: Context`. Das Objekt, das dein Lifespan geliefert hat, ist - zur Laufzeit immer noch `ctx.request_context.lifespan_context`; du gibst den Typparameter auf, nicht - das Objekt. - !!! tip Es gibt immer einen Lifespan. Übergibst du keinen, liefert der Standard des SDK ein leeres `dict`, also ist `ctx.request_context.lifespan_context` `{}`, nie `None`. Dieser Standard ist auch der Grund, warum ein @@ -101,7 +88,7 @@ Reduziere den Server auf den Lebenszyklus: Gib `Database` ein `connected`-Flag, * Code vor dem `yield` ist der Start. Das `finally` danach ist der Stopp. * Er läuft einmal, rund um die gesamte Lebensdauer des Servers, nicht pro Request. * Was immer du per `yield` lieferst, ist `ctx.request_context.lifespan_context` in jedem Tool, jeder Ressource und jedem Prompt. -* `ctx: Context[AppContext]` macht diesen Zugriff in Tools vollständig typisiert. Ressourcen und Prompts nehmen den bloßen `Context`. +* `ctx: Context[AppContext]` macht diesen Zugriff vollständig typisiert. * Kein `lifespan=` bedeutet ein leeres `dict`, nie `None`. Ein Handler, der mitten im Aufruf anhält, um die Person am Host nach etwas zu fragen, das nur sie weiß, ist **[Elicitation](elicitation.md)** (Rückfrage bei der Person am Host). diff --git a/i18n/de/pages/handlers/progress.md b/i18n/de/pages/handlers/progress.md index 77e947a2ac..6024d725fe 100644 --- a/i18n/de/pages/handlers/progress.md +++ b/i18n/de/pages/handlers/progress.md @@ -1,6 +1,6 @@ --- translation: - sections: [5315262fe26b33e1, 9d8e98840f1b78f0, 52d6009a07e770ea, 8534d8dbb4053a70, 2966fac6fe697007] + sections: [5315262fe26b33e1, 9d8e98840f1b78f0, 52d6009a07e770ea, 8534d8dbb4053a70, 2e9aff14d3a882c0] tool: 1 --- # Fortschritt {#progress} @@ -120,4 +120,4 @@ Der Callback erhält `total=None`. Ein Client kann weiterhin *Aktivität* anzeig * Kein Callback am Aufruf heißt: `report_progress` tut nichts. Melde bedingungslos. * Lass `total` weg, wenn du es nicht kennst; der Callback bekommt `None`. -Fortschritt ist das, was ein laufendes Tool der *Person am Host* zeigt. Die Zeilen, die es für *dich* loggt – für dich, weil du den Server betreibst –, sind ein anderer Kanal: **[Logging](logging.md)**. +Fortschritt ist für einen Client gedacht, der noch wartet. Was dein Tool sieht, wenn der Client nicht mehr wartet, ist der **[Abbruch](cancellation.md)**. diff --git a/i18n/de/pages/handlers/subscriptions.md b/i18n/de/pages/handlers/subscriptions.md index 809f3aca69..df6132c121 100644 --- a/i18n/de/pages/handlers/subscriptions.md +++ b/i18n/de/pages/handlers/subscriptions.md @@ -1,6 +1,6 @@ --- translation: - sections: [60a9de8a0bdaa531, 317bbe7e4355cdcc, a61d660c8029e04a, 8f7e82fcb88df8a9, b165db51249ff8ed, 266f56fb798068a4, 7c0e57030b622139, df18d7c2417a9883] + sections: [60a9de8a0bdaa531, 6693607ea56d8bd6, a61d660c8029e04a, 8f7e82fcb88df8a9, b165db51249ff8ed, b8bc624a627ead9b, 2139e68e36d9e621, 7c0e57030b622139, 34ab1af2b9ab5b45] tool: 1 --- # Abonnements {#subscriptions} @@ -22,7 +22,7 @@ Dein Anteil daran ist eine Zeile: Veröffentliche die Änderung. * Die Geschwister heißen `notify_prompts_changed()` und `notify_resources_changed()`. * Keine Abonnenten, keine Arbeit. Auf einem untätigen Server zu veröffentlichen ist ein No-op, deshalb prüfst du nie, ob jemand zuhört. Du gibst an, was sich geändert hat. -`MCPServer` bedient `subscriptions/listen` für dich. Die Pflichten auf der Leitung (die Bestätigung als erster Frame, das Filtern pro Stream, die Abonnement-ID auf jedem Frame) sind Sache des SDK. +`MCPServer` bedient `subscriptions/listen` für dich, es sei denn, du [schaltest es ab](#turning-it-off). Die Pflichten auf der Leitung (die Bestätigung als erster Frame, das Filtern pro Stream, die Abonnement-ID auf jedem Frame) sind Sache des SDK. !!! check Auf der Leitung sieht ein Stream, dessen Filter `board://sprint` nannte, so aus, nachdem `complete_task` gelaufen ist: @@ -79,7 +79,32 @@ Beim Betreten von `client.listen(...)` wird der Request gesendet und auf deine B Veröffentlichungen wandern von deinem Handler über einen `SubscriptionBus` zu den offenen Streams. Der Standard arbeitet im Speicher: ein Prozess, jeder Stream darin. Das ist die richtige Antwort, bis du Replikate hinter einem Load Balancer betreibst, denn dann ist der Stream eines Clients an ein Replikat gebunden, und eine Veröffentlichung auf einem anderen Replikat muss ihn erreichen. -Diese Nahtstelle implementierst du selbst: zwei Methoden über deinem Pub/Sub-Backend. +Mit dem Standard-Bus kann sie das nicht, denn jedes Replikat hat seinen eigenen: + +```mermaid +flowchart LR + client[Client] --> lb[Load balancer] + lb --> stream + lb ~~~~ gap + lb --> tool + subgraph B [Replica B] + tool[tools/call] -- publishes --> busB[(bus B)] + end + gap[(no shared bus)] + subgraph A [Replica A] + stream[listen stream] -- subscribed --> busA[(bus A)] + end + style A fill:none + style B fill:none + style gap fill:none,stroke-dasharray:4 4 +``` + +Nichts schlägt fehl: Der Aufruf gelingt, und der Stream bleibt still. Entscheide dich hinter einem Load Balancer also für eines von beiden: + +* **Du brauchst Änderungsbenachrichtigungen.** Gib jedem Replikat denselben Bus, siehe unten. +* **Du brauchst sie nicht.** [Schalte sie ab](#turning-it-off), damit keinem Client Ereignisse versprochen werden, die er verpassen wird, und keiner dafür einen Stream offen hält. + +Den gemeinsamen Bus implementierst du selbst: zwei Methoden über deinem Pub/Sub-Backend. ```python from collections.abc import Callable @@ -128,6 +153,20 @@ async def tools_reloaded() -> None: await bus.publish(ToolsListChanged()) # from a lifespan task, a webhook, anywhere ``` +## Abschalten {#turning-it-off} + +Ein Server, dessen Katalog sich nie ändert, hat nichts zu veröffentlichen. Sag das gleich, wenn du ihn erzeugst: + +```python title="server.py" hl_lines="3" +--8<-- "docs_src/subscriptions/tutorial007.py" +``` + +* Ein `2026-07-28`-Client sieht keine angekündigten Änderungsbenachrichtigungen, und ein `subscriptions/listen`-Request bekommt *Method not found* statt eines offenen Streams. +* `ctx.notify_*` funktioniert weiterhin und erreicht niemanden, deine Handler ändern sich also nicht. +* Clients mit früheren Protokollversionen bemerken keinen Unterschied. + +Ein offener Stream ist ein Request, der nie endet. Das ist also auch auf einem Host relevant, der nach Request-Dauer abrechnet. + ## Die Low-Level-Komposition {#the-low-level-composition} Unten auf dem Low-Level-`Server` ist nichts vorverdrahtet, und dieselben Teile setzen sich in drei Zeilen zusammen: @@ -148,5 +187,6 @@ Unten auf dem Low-Level-`Server` ist nichts vorverdrahtet, und dieselben Teile s * Die Client-Seite ist `async with client.listen(...)`: Alles Weitere steht in **[Abonnements](../client/subscriptions.md)** unter *Clients*. * Auf dem Low-Level-`Server` setzt du dieselben Teile selbst zusammen: einen Bus, `ListenHandler(bus)`, den Slot `on_subscriptions_listen`. * Horizontal skalieren heißt, `SubscriptionBus` zu implementieren, zwei Methoden, und ihn als `MCPServer(subscriptions=...)` zu übergeben. +* Nichts zu veröffentlichen oder Replikate ohne gemeinsamen Bus: `MCPServer(subscriptions=False)` kündigt keine Änderungsbenachrichtigungen an und hält keinen Stream. Den Server zu betreiben, der all das bedient, hinter einem Replikat oder zwanzig, ist **[Bereitstellen und skalieren](../run/deploy.md)**. diff --git a/i18n/de/pages/run/deploy.md b/i18n/de/pages/run/deploy.md index 66e451d118..9c913ff274 100644 --- a/i18n/de/pages/run/deploy.md +++ b/i18n/de/pages/run/deploy.md @@ -1,6 +1,6 @@ --- translation: - sections: [28221886b198784f, f88ea1f1614f3a1d, 2e76f5cb9df15042, ce926d686730b6d0, 3be24f8ad8bb5ab9, 3fad24032b2224ff, f25a7f860e579ecb, 697b01d95080880d] + sections: [28221886b198784f, f88ea1f1614f3a1d, 2e76f5cb9df15042, ce926d686730b6d0, 3be24f8ad8bb5ab9, aaf489e944ecf5d1, f25a7f860e579ecb, 697b01d95080880d] tool: 1 --- # Bereitstellen und skalieren {#deploy-scale} @@ -173,6 +173,7 @@ Dem Fan-out ist es egal, an welchem Server-Objekt ein Stream hängt. Zwei Server * Über echte Prozesse hinweg **liefert das SDK keinen Bus mit, der dir helfen kann.** `SubscriptionBus` ist ein `Protocol` mit zwei Methoden (`publish` und `subscribe`), das du über deinem eigenen Pub/Sub-Backend implementierst (Redis, NATS, was auch immer du schon betreibst) und als `MCPServer(subscriptions=...)` übergibst. Die Skizze und den Vertrag findest du in **[Abonnements](../handlers/subscriptions.md#scaling-past-one-process)**. * Der Bus transportiert vier kleine typisierte Events, nie JSON-RPC. Bestätigung, Filterung und Stream-Lebenszyklus bleiben im SDK, sodass dein Bus das Protokoll nicht kaputt machen kann; er kann nur Events zwischen Prozessen bewegen. * Streams sind **nicht** wiederaufnehmbar, und Events werden **nicht** erneut abgespielt. Fällt ein Replikat weg, fallen seine Streams weg; die Clients lauschen erneut und holen die Daten erneut ab. Es gibt keinen Event Store zu teilen und sonst nichts zu konfigurieren. Das ist die eine Stelle, an der horizontales Skalieren wirklich nur mehr vom Gleichen ist. +* Ein Server, der keine Änderungsbenachrichtigungen braucht, lässt den Bus weg: **[schalte sie ab](../handlers/subscriptions.md#turning-it-off)**. ## Was das SDK dir nicht gibt {#what-the-sdk-does-not-give-you} diff --git a/i18n/de/pages/servers/structured-output.md b/i18n/de/pages/servers/structured-output.md index b1317cd312..b63e28064d 100644 --- a/i18n/de/pages/servers/structured-output.md +++ b/i18n/de/pages/servers/structured-output.md @@ -1,6 +1,6 @@ --- translation: - sections: [a838d57f003aed44, 857d03886a0137ed, 42d9efcb9f542867, 2290ff08435b5573, 91be9b73602abcf1, 6cdbad079f7b47f0, d4b607372fb28b51, 18dbf726ac45e0b7, c7eff2a5698225fa, c851964bb3301907, 8f296f1f09e4c400, d715db6f8dccc9cc, a0c344a48450dbe4] + sections: [a838d57f003aed44, 857d03886a0137ed, 42d9efcb9f542867, 2290ff08435b5573, 91be9b73602abcf1, 6cdbad079f7b47f0, d4b607372fb28b51, 7608fc5ebc31d6ea, c7eff2a5698225fa, c851964bb3301907, 8f296f1f09e4c400, d715db6f8dccc9cc, a0c344a48450dbe4] tool: 1 --- # Strukturierte Ausgabe {#structured-output} @@ -174,6 +174,8 @@ result.structured_content # {"London": 16.2, "Reykjavik": 4.4} Die Schlüssel müssen `str` sein. Ein `dict[int, float]` kann kein JSON-Objekt sein und fällt deshalb auf die `{"result": ...}`-Hülle zurück. +Dictionary-Ergebnisse nutzen Pydantics `TypeAdapter` für Validierung und Serialisierung. Wenn du dir `FuncMetadata.output_model` eines Tools ansiehst, enthält es die Typannotation des Dictionarys mit ihrem Schematitel. + ## Validierung {#validation} `output_schema` ist keine Dokumentation. Was auch immer deine Funktion zurückgibt, wird **dagegen validiert**, bevor es den Server verlässt. diff --git a/i18n/de/pages/servers/tools.md b/i18n/de/pages/servers/tools.md index a87204ad60..a0ee259aa5 100644 --- a/i18n/de/pages/servers/tools.md +++ b/i18n/de/pages/servers/tools.md @@ -1,6 +1,6 @@ --- translation: - sections: [e4cc390d56573409, f30cf8103a6e918c, 2c97b9f888398951, 048e5471dfa71aea, 3076b1e16ad95950, edbedf2a16e71311, 3d8ef8da89fa87c1, f6c0e02e6ea5a363] + sections: [e4cc390d56573409, f30cf8103a6e918c, 2c97b9f888398951, 048e5471dfa71aea, 3076b1e16ad95950, c1115cd005b81e8f, 3d8ef8da89fa87c1, f6c0e02e6ea5a363] tool: 1 --- # Tools {#tools} @@ -141,7 +141,7 @@ Du kannst frei kombinieren: einfache Parameter neben Modell-Parametern, verschac Macht ein Tool I/O (ruft eine API auf, liest eine Datei, fragt eine Datenbank ab), deklariere es als `async def` und verwende `await` darin. Das SDK wartet darauf. -Ein Tool mit einfachem `def` funktioniert auch: Das SDK führt es in einem Thread aus, damit es den Server nie blockiert. +Ein Tool mit einfachem `def` funktioniert auch: Das SDK führt es in einem Thread aus, damit es den Server nie blockiert. Ein lang laufendes Tool kann prüfen, ob der Client noch wartet; siehe **[Abbruch](../handlers/cancellation.md)**. Mehr gibt es nicht zu konfigurieren. diff --git a/i18n/de/pages/troubleshooting.md b/i18n/de/pages/troubleshooting.md index dc98239cc3..fe5f84dd82 100644 --- a/i18n/de/pages/troubleshooting.md +++ b/i18n/de/pages/troubleshooting.md @@ -1,6 +1,6 @@ --- translation: - sections: [3d58228e81b99543, 170514ce901c4139, 17d61fad0a50d62b, 8a6e351ec756904d, 137454d469c867f5, 6392596bd6df54f0, 41126fa9c4fe432f, 480b6d7897e30ab4, d83bb682e708dde0, ebbed3449c499db4, 525cdf1755e29d4c, 30fd31be74169d9a, d2e88333d4f7841f, c2dc3b1007d2e987, d6eabf60cc366341, f798e815252852c2, 0cba47bae78d04eb, 2c218ba829abf74e] + sections: [3d58228e81b99543, 170514ce901c4139, 17d61fad0a50d62b, 8a6e351ec756904d, 137454d469c867f5, fcf984fa0615ed11, 6392596bd6df54f0, 41126fa9c4fe432f, 480b6d7897e30ab4, d83bb682e708dde0, ebbed3449c499db4, 525cdf1755e29d4c, 30fd31be74169d9a, d2e88333d4f7841f, c2dc3b1007d2e987, d6eabf60cc366341, f798e815252852c2, 0cba47bae78d04eb, cdc6d86a4dae8a34] tool: 1 --- # Fehlerbehebung {#troubleshooting} @@ -128,6 +128,14 @@ Füge die Klammern hinzu. `@mcp.resource(...)` und `@mcp.prompt()` melden dassel Tools, hat also diese Form: Führe `python server.py` selbst aus und lies den Traceback. Ein Type-Checker fängt es ebenfalls ab: Eine Funktion ist kein gültiges `name=`. +## `InvalidSignature: Tool '' has an invalid x-mcp-header annotation: ` {#invalidsignature-tool-name-has-an-invalid-x-mcp-header-annotation-reason} + +Ein Tool-Argument ist auf eine Weise mit `x-mcp-header` markiert, die die Spezifikation nicht erlaubt, und `` sagt, welche Regel es verletzt. Clients auf `2026-07-28` würden ein solches Tool aus ihrer Auflistung weglassen, deshalb weigert sich das SDK, es zu registrieren. + +Nur Argumente vom Typ `str`, `int` und `bool` können markiert werden, und `str | None` ist keiner davon. **[Header-Parameter](advanced/header-parameters.md)** zeigt die Schreibweise für ein optionales Argument. + +Wie beim Eintrag darüber wird das beim **Import** des Moduls ausgelöst, bevor sich ein Client verbindet. + ## `Tool already exists: ` {#tool-already-exists-name} Zwei Registrierungen haben denselben Tool-Namen verwendet. Die **erste** gewinnt, die zweite wird stillschweigend verworfen, und diese Warnung im *Server-Log* ist das einzige Signal: @@ -427,6 +435,7 @@ mcp = MCPServer("Weather", request_state_security=RequestStateSecurity(keys=[key * `ExceptionGroup: unhandled errors in a TaskGroup` ist nie der Fehler. Lies die **letzte Zeile**; fängst du `MCPError` *innerhalb* des `async with Client(...)`-Blocks ab, entfällt die Verpackung komplett. * `call_tool` löst bei einem fehlschlagenden Tool keine Exception aus. `Error executing tool ...` und `Unknown tool: ...` sind Ergebnisse: Prüfe `result.is_error`. Keine Meldung nach dem Tool-Namen heißt, es ist abgestürzt, und der Traceback steht im Server-Log. * `Client must be used within an async context manager` -> verwende `async with`. `Use @tool() instead of @tool` -> füge die Klammern hinzu. +* `has an invalid x-mcp-header annotation` -> nur Argumente vom Typ `str`, `int` und `bool` können markiert werden. * `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. diff --git a/i18n/de/pages/whats-new.md b/i18n/de/pages/whats-new.md index 44b57adfbc..7a089d5e41 100644 --- a/i18n/de/pages/whats-new.md +++ b/i18n/de/pages/whats-new.md @@ -1,6 +1,6 @@ --- translation: - sections: [cfe01c0c5863dfa2, 0dbb68d8b210177b, 80cf193023af3ed4, 875eb2889263424e] + sections: [cfe01c0c5863dfa2, 0dbb68d8b210177b, bd42ee3a268f6ea4, 875eb2889263424e] tool: 1 --- # Was ist neu in v2 {#whats-new-in-v2} @@ -204,7 +204,7 @@ Bei 2026-07-28 werden der eigenständige HTTP-GET-Stream und `resources/subscrib ### Der Rest, in Kürze {#the-rest-quickly} * **Identität ist optionale Metadaten pro Nachricht.** Der `_meta`-Schlüssel `clientInfo` auf der Request-Seite ist optional (das Pflichtpaar ist `protocolVersion` + `clientCapabilities`), und `serverInfo` ist aus dem Result-Body von `server/discover` ausgezogen: Server stempeln es stattdessen in das `_meta` jedes Results der 2026er-Generation ([Spec #3002](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/3002)). Das SDK stempelt immer; `client.server_info` ist `None`, wenn ein Server sich nicht zu erkennen gibt (zum Beispiel, weil eine Middleware den Schlüssel entfernt hat). **[Der Low-Level-Server](advanced/low-level-server.md)** zeigt den Stempel auf der Leitung. -* **Requests lassen sich routen, ohne Bodies zu parsen.** Moderne HTTP-Requests tragen `Mcp-Method` (und für die drei Tool-artigen Aufrufe `Mcp-Name`); eine Eigenschaft im Eingabeschema eines Tools, die mit `x-mcp-header` annotiert ist, wird in einen `Mcp-Param-*`-Header gespiegelt und vom Server gegengeprüft ([SEP-2243](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2243)). Gateways und Rate-Limiter können allein anhand der Header routen; die Regeln stehen im **[Migrationsleitfaden](migration.md#servers-validate-mcp-param-headers-against-the-request-body-sep-2243)**. +* **Requests lassen sich routen, ohne Bodies zu parsen.** Moderne HTTP-Requests tragen `Mcp-Method` (und für die drei Tool-artigen Aufrufe `Mcp-Name`); eine Eigenschaft im Eingabeschema eines Tools, die mit `x-mcp-header` annotiert ist, wird in einen `Mcp-Param-*`-Header gespiegelt und vom Server gegengeprüft ([SEP-2243](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2243)). Gateways und Rate-Limiter können allein anhand der Header routen. **[Header-Parameter](advanced/header-parameters.md)** zeigt, wie du ein Argument markierst. * **Results tragen Cache-Hinweise.** List- und Read-Results deklarieren `ttlMs` und `cacheScope` ([SEP-2549](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2549)); du setzt sie pro Methode mit `cache_hints=`, und `Client` beachtet sie mit einem eingebauten Response-Cache. Ein Server, der keine Hinweise sendet (jeder Server vor 2026), sieht identischen, ungecachten Verkehr. **[Caching-Hinweise](client/caching.md)**. * **Erweiterungen sind vollwertig.** Server und Clients deklarieren optionale Capability-Bündel unter Reverse-DNS-Bezeichnern ([SEP-2133](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2133)); die eingebaute Erweiterung `Apps` (MCP Apps) ist die Referenz. **[Erweiterungen](advanced/extensions.md)** und **[MCP Apps](advanced/apps.md)**. * **Fehlercodes wurden standardisiert.** Eine fehlende Ressource ist `-32602` mit dem URI in `error.data`, und die neuen von der Spezifikation reservierten Codes erscheinen als `-32020` (Header-Abweichung), `-32021` (fehlende erforderliche Capability) und `-32022` (nicht unterstützte Protokollversion). **[Fehlerbehebung](troubleshooting.md)** ist nach den exakten Meldungen geordnet. diff --git a/i18n/es/pages/advanced/header-parameters.md b/i18n/es/pages/advanced/header-parameters.md new file mode 100644 index 0000000000..5ae991cf72 --- /dev/null +++ b/i18n/es/pages/advanced/header-parameters.md @@ -0,0 +1,65 @@ +--- +translation: + sections: [81862a209b483d27, b76e8073487afa03, bc214b2fc2bcdae4, d5835477c0b60163, a5d9786f902ad8e1] + tool: 1 +--- +# Parámetros de encabezado {#header-parameters} + +La mayoría de los servidores nunca necesita esto. + +Un gateway o un balanceador de carga delante del servidor solo puede enrutar según lo que puede leer sin analizar el cuerpo. Marca un argumento de una herramienta con `x-mcp-header` y los clientes de la **[versión del protocolo](../protocol-versions.md)** `2026-07-28` envían su valor también como encabezado HTTP. + +## Marcar un argumento {#mark-an-argument} + +La marca es una clave adicional en el JSON Schema del argumento. En `MCPServer`, `Field` la pone ahí: + +```python title="server.py" hl_lines="13" +--8<-- "docs_src/header_parameters/tutorial001.py" +``` + +* Con Streamable HTTP en `2026-07-28`, el cliente envía `Mcp-Param-Region` junto con el cuerpo, y el servidor rechaza una llamada en la que los dos no coinciden. +* Un cliente que no ha listado la herramienta nunca ha visto la marca: no envía ningún encabezado y la llamada se rechaza. El `Client` de este SDK lista entonces las herramientas y reenvía la llamada una vez, así que listar primero solo ahorra una ida y vuelta. +* Cualquier otra conexión ignora la anotación. + +Tu función no cambia: `region` sigue llegando como argumento. + +## Qué se puede marcar {#what-can-be-marked} + +Los argumentos `str`, `int` y `bool`. Cualquier otra cosa se rechaza al registrar la herramienta, con `InvalidSignature`. + +Eso incluye `str | None`, que no tiene un tipo único. Un argumento opcional necesita su esquema escrito de forma explícita, con `WithJsonSchema` de Pydantic: + +```python +region: Annotated[str | None, WithJsonSchema({"type": "string", "x-mcp-header": "Region"})] = None +``` + +## En el `Server` de bajo nivel {#on-the-low-level-server} + +Ahí escribes `input_schema` a mano, así que la clave va directamente dentro: + +```python title="server.py" hl_lines="18" +--8<-- "docs_src/header_parameters/tutorial002.py" +``` + +* Nada verifica la anotación por ti: una no válida se sirve tal cual, y los clientes `2026-07-28` dejan la herramienta fuera de su listado. + +### Esquemas por nombre {#schemas-by-name} + +Para verificar el encabezado, el SDK necesita el esquema de entrada de la herramienta antes de despachar la llamada. Sin `get_tool_input_schema`, lo obtiene ejecutando tu handler `on_list_tools` en cada llamada que lleva argumentos, haya o no alguna herramienta marcada. + +```python title="server.py" hl_lines="26 39-41 48" +--8<-- "docs_src/header_parameters/tutorial003.py" +``` + +* Pasa la función para responder a partir de lo que ya tienes. +* Devuelve `None` para una herramienta que no tiene nada que verificar. + +## Resumen {#recap} + +* `x-mcp-header` en un argumento de una herramienta hace que los clientes `2026-07-28` lo repitan como encabezado HTTP `Mcp-Param-*`. +* El servidor rechaza una llamada cuyo encabezado y cuerpo no coinciden. +* Solo se pueden marcar argumentos `str`, `int` y `bool`. `MCPServer` lanza `InvalidSignature` para cualquier otra cosa. +* El `Server` de bajo nivel no verifica nada, y los clientes descartan una herramienta cuya anotación no es válida. +* `get_tool_input_schema` evita que el `Server` de bajo nivel ejecute `on_list_tools` en cada llamada. + +El resto de la API escrita a mano de `Server` está en **[El Server de bajo nivel](low-level-server.md)**. diff --git a/i18n/es/pages/advanced/index.md b/i18n/es/pages/advanced/index.md index 3bf3e1a79c..5cf0d9e588 100644 --- a/i18n/es/pages/advanced/index.md +++ b/i18n/es/pages/advanced/index.md @@ -1,6 +1,6 @@ --- translation: - sections: [ca6988b7503cd2d3] + sections: [348f8697c6b12cd0] tool: 1 --- # Avanzado {#advanced} @@ -14,6 +14,8 @@ te estorba: personalizados propios. * **[Paginación](pagination.md)** y **[Middleware](middleware.md)**: dos cosas que *solo* puedes hacer en el `Server` de bajo nivel. +* **[Parámetros de cabecera](header-parameters.md)**: permiten que un gateway enrute una llamada a una + herramienta según uno de sus argumentos. * **[Extensiones](extensions.md)** y **[MCP Apps](apps.md)**: la superficie de extensión del protocolo. Compón paquetes de extensión en un servidor o escribe los tuyos. diff --git a/i18n/es/pages/advanced/low-level-server.md b/i18n/es/pages/advanced/low-level-server.md index 71286b6677..c412af2eba 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, 9d5d10a5f0405d0a, 1086e77ce561cd7f, a3f71823df5efc31, 9fc7109f72201cae, d50fe7faead8cf68, 7bf25983df655b66, 6330e1f4c6029683, 2f1749c8c133fa1c, 8db7116fc8ddd0ee, ebc33704fbd74262, 0fde3bcea081ba3a] + sections: [2c79b6338e09b7ac, 9d5d10a5f0405d0a, 1086e77ce561cd7f, a3f71823df5efc31, 9fc7109f72201cae, d50fe7faead8cf68, 7bf25983df655b66, 6330e1f4c6029683, 2f1749c8c133fa1c, 8db7116fc8ddd0ee, 2090d99b355bc2c7, 0fde3bcea081ba3a] tool: 1 --- # El Server de bajo nivel {#the-low-level-server} @@ -209,6 +209,7 @@ Cada uno de estos es una idea para la que ya tienes el vocabulario; cada uno tie * `on_call_tool`, `on_get_prompt` y `on_read_resource` pueden devolver un `InputRequiredResult` en lugar de su resultado normal para pausar la llamada y pedir datos al cliente; consulta **[Solicitudes de varias idas y vueltas (multi-round-trip)](../handlers/multi-round-trip.md)**. Fiel a este nivel, nada se instala por ti: donde `MCPServer` sella `requestState` por defecto, aquí el `request_state` que estableces se transmite exactamente como lo escribiste hasta que optas por `server.middleware.append(RequestStateBoundary(RequestStateSecurity(keys=[...]), default_audience=server.name))`: una línea (ambos nombres se importan de `mcp.server.request_state`) para el mismo sellado y verificación que realiza `MCPServer` (**[Proteger `requestState`](../handlers/multi-round-trip.md#protecting-requeststate)**). * `on_list_resources`, `on_read_resource`, `on_list_prompts`, `on_get_prompt` y `on_completion` tienen la misma forma `(ctx, params) -> result` para las demás primitivas. * `on_subscriptions_listen` sirve el stream `subscriptions/listen` de 2026-07-28. Pasa un `ListenHandler` construido sobre un `SubscriptionBus` y publica eventos en el bus desde tus otros handlers; consulta **[Suscripciones](../handlers/subscriptions.md)** para la composición completa. +* `get_tool_input_schema` mantiene `on_list_tools` fuera de la ruta de llamada; consulta **[Parámetros de encabezado](header-parameters.md#schemas-by-name)**. * `server.streamable_http_app()` devuelve la misma app de Starlette que la de `MCPServer`; despliégala como **[Ejecutar tu servidor](../run/index.md)** despliega cualquier otra app ASGI. Aquí abajo no hay `server.run(transport=...)`: `server.run(read_stream, write_stream, server.create_initialization_options())` conduce una conexión sobre un par de streams, y esa única línea es todo lo que hay. ## Resumen {#recap} diff --git a/i18n/es/pages/advanced/middleware.md b/i18n/es/pages/advanced/middleware.md index 1739c1f1ab..fb5685dd5c 100644 --- a/i18n/es/pages/advanced/middleware.md +++ b/i18n/es/pages/advanced/middleware.md @@ -1,6 +1,6 @@ --- translation: - sections: [6048b4f308edbb8c, 46056f318ef205e4, c3e565b61acd75c5, c62422b159c6ed09, 420968f514138f43] + sections: [58e1103d9a323ccf, 46056f318ef205e4, 812b414557fb0c35, 4df162eea2518d38, c62422b159c6ed09, 420968f514138f43] tool: 1 --- # Middleware {#middleware} @@ -15,8 +15,8 @@ Lo escribes como `async (ctx, call_next)` y lo añades a `server.middleware`. Es registrar, trazar) y para *rechazar* mensajes; no la conviertas en los cimientos del servidor. `MCPServer` recibe la lista en el constructor (`MCPServer(name, middleware=[...])`) y la expone como -`mcp.middleware`; el `Server` de bajo nivel expone la misma lista como `server.middleware`. El ejemplo -de abajo usa el `Server` de bajo nivel; si `Server(name, on_call_tool=...)` es nuevo para ti, lee +`mcp.middleware`; el `Server` de bajo nivel expone la misma lista como `server.middleware`. Los ejemplos +de abajo usan el `Server` de bajo nivel; si `Server(name, on_call_tool=...)` es nuevo para ti, lee primero **[El Server de bajo nivel](low-level-server.md)**. ## Un middleware que mide tiempos {#a-timing-middleware} @@ -62,14 +62,38 @@ Ese es el punto. El middleware envuelve **cada** mensaje entrante: * Incluso un método para el que el servidor no tiene handler: `call_next` lanza el `MCPError(-32601, "Method not found")` *a través de* tu middleware de camino al cliente. +## Un límite de concurrencia {#a-concurrency-cap} + +Un middleware no tiene por qué llamar a `call_next(ctx)`. Lanza un `MCPError` en su lugar y ese +único mensaje se **rechaza**: la conexión sigue activa y el siguiente mensaje pasa. + +Supón que cada búsqueda ocupa una conexión de un pool de cuatro. Este middleware deja que se +ejecuten cuatro llamadas a herramientas a la vez y rechaza la quinta: + +```python title="server.py" hl_lines="15-16 40-55 59" +--8<-- "docs_src/middleware/tutorial002.py" +``` + +* Solo se cuenta `tools/call`, así que el servidor sigue respondiendo a `server/discover` y + `tools/list` mientras rechaza llamadas a herramientas. +* MCP no define ningún código de error de "servidor ocupado", así que `SERVER_BUSY` es propio de + este servidor. +* Rechazar le indica al cliente de inmediato que el servidor está sobrecargado. Si prefieres hacer + esperar a los llamantes, envuelve `call_next(ctx)` con un `anyio.CapacityLimiter` en su lugar. + +Un `MCPError` lanzado llega a la aplicación cliente, no al modelo. Si el modelo debe leer el +mensaje, devuelve en su lugar un resultado de herramienta con `is_error=True`: eso es +**Responder**, más abajo. + ## Qué puedes hacer dentro de uno {#what-you-can-do-inside-one} En orden creciente de cuánto deberías dudar: -* **Observar.** Cronométralo, cuéntalo, regístralo. El ejemplo de arriba. +* **Observar.** Cronométralo, cuéntalo, regístralo. El middleware de arriba que mide tiempos. * **Rechazar.** Lanza un `MCPError` *en lugar de* llamar a `call_next(ctx)` y ese único mensaje se - responde con un error JSON-RPC. La conexión sigue activa; el siguiente mensaje pasa. Así es - como un servidor restringe `subscriptions/listen` por llamante: + responde con un error JSON-RPC. La conexión sigue activa; el siguiente mensaje pasa. El límite + de concurrencia de arriba. También es así como un servidor restringe `subscriptions/listen` por + llamante: **[Decidir quién puede observar](../handlers/subscriptions.md#deciding-who-may-watch)** en la página de Suscripciones lo recorre paso a paso. * **Reescribir.** `ctx` es una dataclass: `await call_next(dataclasses.replace(ctx, params=...))` @@ -96,7 +120,7 @@ En orden creciente de cuánto deberías dudar: !!! warning `initialize` se maneja en línea: el servidor no lee más mensajes entrantes hasta que tu cadena de middleware devuelve. Esperar con await una solicitud del servidor al cliente - (`ctx.session.send_request(...)`, una elicitación) mientras se maneja `initialize` **bloquea + (`ctx.session.send_request(...)`, una elicitación (elicitation)) mientras se maneja `initialize` **bloquea la conexión por completo**: la respuesta que esperas nunca se podrá leer. Las notificaciones que se envían sin esperar respuesta no dan problemas. diff --git a/i18n/es/pages/client/identity-assertion.md b/i18n/es/pages/client/identity-assertion.md index 9566936cab..71614a6176 100644 --- a/i18n/es/pages/client/identity-assertion.md +++ b/i18n/es/pages/client/identity-assertion.md @@ -1,6 +1,6 @@ --- translation: - sections: [a91322c46111d16d, 8e6fd6d6f59bb568, e7828fd2729b2c9d, a03ec26bfc678b65, 1034c653c0bcf1b0] + sections: [a91322c46111d16d, 8e6fd6d6f59bb568, 7cf38181f6c99fd5, 37804d4fb36d6302, 1034c653c0bcf1b0] tool: 1 --- # Aserción de identidad {#identity-assertion} @@ -65,7 +65,7 @@ La extensión no exige esto; es una elección deliberadamente más estricta. Est ### Un cliente confidencial {#a-confidential-client} -`client_secret` es obligatorio; el constructor lanza `ValueError` si falta. El perfil del IETF que hay debajo de [SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990) reserva esta concesión para clientes confidenciales, SEP-990 exige que el cliente se autentique, y este SDK hace cumplir ambas cosas insistiendo en un secreto compartido. `token_endpoint_auth_method` elige por dónde viaja: `client_secret_post` (el valor por defecto, en el cuerpo del formulario) o `client_secret_basic` (una cabecera HTTP Basic). El perfil también permite `private_key_jwt`; este proveedor no lo admite. +`client_secret` es obligatorio; el constructor lanza `ValueError` si falta. El perfil del IETF que hay debajo de [SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990) recomienda esta concesión solo para clientes confidenciales, y [RFC 7521](https://datatracker.ietf.org/doc/html/rfc7521) deja esa política en manos del servidor de autorización. Este SDK adopta la lectura conservadora en ambos lados: el servidor de autorización integrado rechaza a un cliente que no tiene secreto compartido, y este proveedor insiste en uno. `token_endpoint_auth_method` elige por dónde viaja: `client_secret_post` (el valor por defecto, en el cuerpo del formulario) o `client_secret_basic` (una cabecera HTTP Basic). El perfil también permite `private_key_jwt`; este proveedor no lo admite. !!! tip Lee `client_secret` del entorno o de un gestor de secretos, nunca del control de versiones. @@ -92,6 +92,7 @@ El SDK también puede *ser* el servidor de autorización: `create_auth_routes` d * `identity_assertion_enabled=True` lo controla todo. Desactivado, que es el valor por defecto, `/token` responde a esta concesión con `unsupported_grant_type` aunque hayas implementado el hook, y los metadatos no la mencionan. Activado, los metadatos ganan el tipo de concesión `jwt-bearer` y listan `urn:ietf:params:oauth:grant-profile:id-jag` en `authorization_grant_profiles_supported`, el campo que la extensión usa para anunciar la compatibilidad. (El cliente de este SDK nunca lo lee: está aprovisionado para un solo emisor y simplemente pregunta.) * **`exchange_identity_assertion`** es el hook. Antes de que se ejecute, el SDK ha autenticado al cliente, ha rechazado los clientes públicos y ha rechazado los clientes cuyo registro no lista la concesión. Recibes un `IdentityAssertionParams` (la `assertion` sin procesar, los `scopes` solicitados y el `resource`) y devuelves un `OAuthToken` simple. +* Rechazar los clientes públicos es una política del SDK, no un requisito de la especificación. El servidor integrado autentica a los clientes solo mediante secreto compartido: no admite `private_key_jwt` y todavía no resuelve los Client ID Metadata Documents ([#1801](https://github.com/modelcontextprotocol/python-sdk/issues/1801)), así que un cliente identificado por uno no puede usar esta concesión aquí. Un despliegue que quiera una política distinta puede sustituir la ruta `/token` que devuelve `create_auth_routes` por la suya propia. * El registro dinámico de clientes rechaza esta concesión sin excepciones, así que `get_client` aquí sirve un cliente aprovisionado a mano. Un cliente ID-JAG no puede registrarse a sí mismo para existir. * La mitad de la clase son rechazos. `OAuthAuthorizationServerProvider` es el servidor de autorización *completo*, así que también pide el flujo de código de autorización; un servidor que además inicia la sesión de los usuarios implementa esos de verdad, y este tiene exactamente una puerta. diff --git a/i18n/es/pages/client/transports.md b/i18n/es/pages/client/transports.md index 84f91ff240..6c26ed4324 100644 --- a/i18n/es/pages/client/transports.md +++ b/i18n/es/pages/client/transports.md @@ -1,6 +1,6 @@ --- translation: - sections: [9cac816674181eb0, 7c157764133fea1f, 40b4916d82eaf1d4, 10d151f2cc75317f, 3d0832f39b0d7059, 92742ba36533633d, 0aeca6145e7bd302] + sections: [9cac816674181eb0, 5619e950d206e6c8, 40b4916d82eaf1d4, 10d151f2cc75317f, 3d0832f39b0d7059, 92742ba36533633d, 991c10e47fda2636] tool: 1 --- # Transportes del cliente {#client-transports} @@ -44,6 +44,8 @@ Dos cosas que notar: * El `httpx2.AsyncClient` es tuyo, así que **tú** entras y sales de él. El SDK nunca cierra un cliente que no creó. * `streamable_http_client(url, http_client=...)` devuelve un transporte, y `Client(transport)` lo acepta como cualquier otra cosa. +Conserva el `timeout=`. Es el que usa el propio cliente del SDK (30 segundos, 300 para las lecturas); un `httpx2.AsyncClient` construido sin uno recibe el valor por defecto de `httpx2`, de 5 segundos, y una llamada a una herramienta que dure más que eso falla con un timeout de lectura. + Una nota sobre TLS: `httpx2` verifica los certificados contra el almacén de confianza del sistema operativo (mediante [`truststore`](https://pypi.org/project/truststore/)), no contra una lista de CA incluida. En un entorno sin un almacén de CA del sistema utilizable (algunos contenedores mínimos), configura las variables de entorno @@ -51,16 +53,32 @@ estándar `SSL_CERT_FILE`/`SSL_CERT_DIR` o pasa un `verify=ssl_context` explíci (el contexto está en [`httpx` y `httpx-sse` sustituidos por `httpx2`](../migration.md#httpx-and-httpx-sse-replaced-by-httpx2)). +### Eventos SSE más grandes {#larger-sse-events} + +Pasa `max_sse_event_size` cuando un servidor envíe un resultado de herramienta o una notificación grande en un solo evento SSE: + +```python title="client.py" hl_lines="6-9" +--8<-- "docs_src/client_transports/tutorial005.py" +``` + +El valor por defecto es 1 MiB por evento, medido en bytes antes de analizar el evento. El límite se aplica a +las respuestas POST, al flujo GET y a los flujos reanudados. Un evento demasiado grande en una respuesta POST o +en un flujo reanudado hace fallar esa solicitud con un error de SSE. En el flujo GET en segundo plano, el cliente +registra el error y reintenta el flujo. Configura `max_sse_event_size=None` para desactivar el límite cuando confíes +en el servidor y necesites eventos más grandes. Las respuestas JSON no se ven afectadas. Si usas `ClientSessionGroup`, +configura la misma opción en `StreamableHttpParameters`. + !!! warning `streamable_http_client` antes aceptaba `headers=` y `timeout=` directamente. Ya no: - sus únicos parámetros son `url`, `http_client` y `terminate_on_close`. Usa `headers=` por + sus parámetros son `url`, `http_client`, `terminate_on_close` y `max_sse_event_size`. Usa `headers=` por costumbre y obtienes: ```text TypeError: streamable_http_client() got an unexpected keyword argument 'headers' ``` - Todo lo que tiene forma de HTTP vive ahora en el único `httpx2.AsyncClient` que pasas. + Los encabezados, la autenticación, los proxies y los timeouts viven en el único `httpx2.AsyncClient` que pasas. + `max_sse_event_size`, en cambio, se aplica a los lectores SSE del transporte MCP. !!! info `httpx2` conserva la API conocida de `httpx`, así que si conoces `httpx` ya sabes cómo hacer la autenticación, @@ -137,6 +155,7 @@ Un **transporte** es cualquier gestor de contexto asíncrono que produce un par * `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=`. +* Usa `streamable_http_client(url, max_sse_event_size=...)` para cambiar el límite en bytes de cada evento SSE. * 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. diff --git a/i18n/es/pages/handlers/cancellation.md b/i18n/es/pages/handlers/cancellation.md new file mode 100644 index 0000000000..47dc0a730e --- /dev/null +++ b/i18n/es/pages/handlers/cancellation.md @@ -0,0 +1,60 @@ +--- +translation: + sections: [07968345fdc0b84e, 4ea8416db9efa0dc, 336a7b4c5d0a4578, 18392e805dde6717, c30d50df43f9b55c] + tool: 1 +--- +# Cancelación {#cancellation} + +Un cliente puede abandonar una llamada: el usuario pulsó detener o se agotó un timeout. + +Cuando lo hace, el SDK **cancela tu handler**. El `await` en el que está esperando lanza una excepción, la ejecución sale de la función y no se envía nada de lo que devuelva. La mayoría de los handlers no necesita hacer nada al respecto. + +Dos tipos sí: un handler que tiene algo que limpiar y un handler que es un `def` simple. + +## Limpiar en una herramienta `async def` {#clean-up-in-an-async-def-tool} + +Pon la limpieza en un `finally`: + +```python title="server.py" hl_lines="23 26-28" +--8<-- "docs_src/cancellation/tutorial001.py" +``` + +* El `finally` se ejecuta termine como termine la herramienta: devolvió un valor, lanzó una excepción o se canceló. +* La limpieza que tiene que hacer `await` necesita `shield=True`. En un handler cancelado, cada `await` posterior también lanza una excepción, así que sin el escudo `release_hold` se detendría en su primera línea. +* Nada puede cancelar un bloque protegido con escudo, así que ponle un límite de tiempo. Aquí son `5` segundos. + +!!! tip + Usa `finally`, no `except`. La cancelación tiene que seguir propagándose hacia arriba una vez + terminada la limpieza, y un `finally` se lo permite. + +## Detenerse antes en una herramienta `def` simple {#stop-early-in-a-plain-def-tool} + +Una herramienta `def` simple se ejecuta en un hilo, y nada puede interrumpir un hilo desde fuera. La herramienta tiene que preguntar: + +```python title="server.py" hl_lines="22 25-26" +--8<-- "docs_src/cancellation/tutorial002.py" +``` + +* `anyio.from_thread.check_cancelled()` no hace nada mientras la llamada sigue activa, y lanza una excepción una vez que se ha cancelado. Llámala entre unidades de trabajo. +* Aquí la limpieza también va en un `finally`. En un hilo no hay esperas asíncronas, así que no necesita escudo. +* Una herramienta `def` que nunca pregunta se ejecuta hasta el final, y su resultado se descarta. + +## Dónde se aplica {#where-it-applies} + +Las funciones de prompts y de recursos se cancelan exactamente igual que las herramientas. + +Funciona igual sobre stdio y Streamable HTTP. Con el `Client` de este SDK, abandonar significa cancelar la tarea que espera `call_tool` o dejar que se agote su `read_timeout_seconds`. + +!!! warning + Dos opciones de Streamable HTTP impiden que la noticia llegue a tu handler: `json_response=True` + en una conexión `2026-07-28` y `stateless_http=True` en una heredada. Ahí el handler se ejecuta + hasta el final, haya hecho lo que haya hecho el cliente. + +## Resumen {#recap} + +* Cuando el cliente abandona una llamada, el SDK cancela el handler: herramienta, prompt o recurso. +* `async def`: limpia en un `finally` y pon la limpieza con esperas asíncronas dentro de `anyio.move_on_after(seconds, shield=True)`. +* `def` simple: llama a `anyio.from_thread.check_cancelled()` entre unidades de trabajo, o la herramienta se ejecuta hasta el final. Un `finally` simple hace la limpieza. +* `json_response=True` (conexiones modernas) y `stateless_http=True` (las heredadas) desactivan la cancelación. + +El progreso y la cancelación son cosa de una herramienta en ejecución y de *quien la llama*. Las líneas que registra para *ti*, la persona que opera el servidor, son un canal distinto: **[Registro de logs](logging.md)**. diff --git a/i18n/es/pages/handlers/index.md b/i18n/es/pages/handlers/index.md index 8190892631..5cb1b939ba 100644 --- a/i18n/es/pages/handlers/index.md +++ b/i18n/es/pages/handlers/index.md @@ -1,6 +1,6 @@ --- translation: - sections: [424930166c4bc6f3] + sections: [22ca41e50cc1b536] tool: 1 --- # Dentro de tu handler {#inside-your-handler} @@ -15,9 +15,10 @@ Lo que puede leer: Lo que puede hacer mientras se ejecuta: -* Pedir más datos al usuario con **[Elicitación](elicitation.md)**, y con **[Solicitudes de varias idas y vueltas](multi-round-trip.md)**, el patrón de 2026-07-28 que la transporta. -* Pedir al cliente una respuesta de su LLM o sus carpetas de trabajo con **[Muestreo y roots](sampling-and-roots.md)**, obsoletos pero todavía atendidos. +* Pedir más datos al usuario con **[Elicitación (elicitation)](elicitation.md)**, y con **[Solicitudes de varias idas y vueltas (multi-round-trip)](multi-round-trip.md)**, el patrón de 2026-07-28 que la transporta. +* Pedir al cliente una respuesta de su LLM o sus carpetas de trabajo con **[Muestreo (sampling) y roots](sampling-and-roots.md)**, obsoletos pero todavía atendidos. * Informar del **[Progreso](progress.md)** de algo lento. +* Hacer limpieza, o detenerse antes de tiempo, cuando el cliente abandona la llamada, con **[Cancelación](cancellation.md)**. * Escribir logs (en el error estándar, para quien opere el servidor) con **[Logging](logging.md)**. * Avisar a los clientes suscritos de que algo cambió con **[Suscripciones](subscriptions.md)**. diff --git a/i18n/es/pages/handlers/lifespan.md b/i18n/es/pages/handlers/lifespan.md index 3c4fa61362..6d9d14d910 100644 --- a/i18n/es/pages/handlers/lifespan.md +++ b/i18n/es/pages/handlers/lifespan.md @@ -1,6 +1,6 @@ --- translation: - sections: [f3ca8ac5f90f2dfa, 85a1ef3588ba0736, 563346d4d5804933, 9e3528340d0bab53] + sections: [f3ca8ac5f90f2dfa, 48e478ef7bd688b1, 563346d4d5804933, 52ac6a7734d6f581] tool: 1 --- # Lifespan {#lifespan} @@ -46,7 +46,7 @@ Nada nuevo. `ctx` es un parámetro **Context**, así que el SDK lo inyecta y nun `genre` es el único argumento que el modelo puede pasar. El lifespan es asunto de tu servidor. -Las funciones `@mcp.resource()` y `@mcp.prompt()` también pueden recibir un parámetro `ctx`, escrito como un `Context` a secas por una razón que se explica en la siguiente sección. Todo lo que lleva `ctx` está en **[El Context](context.md)**. +Las funciones `@mcp.resource()` y `@mcp.prompt()` también pueden recibir un parámetro `ctx`. Todo lo que lleva `ctx` está en **[El Context](context.md)**. ### De verdad está tipado {#it-really-is-typed} @@ -56,19 +56,6 @@ Ese único parámetro de tipo es la razón por la que `ctx.request_context.lifes Si escribes un `Context` a secas, `lifespan_context` queda tipado como `dict[str, Any]`: el verificador de tipos no tiene forma de saber qué entregó tu lifespan. El objeto sigue ahí en tiempo de ejecución; lo que pierdes es la ayuda. -!!! warning - `Context[AppContext]` es una forma de escribirlo **exclusiva de las herramientas**. Ponla en una - función `@mcp.resource()` o `@mcp.prompt()` y todas las llamadas a ese handler fallan. El cliente - recibe un error, y el log del servidor muestra por qué: - - ```text - Context is not available outside of a request - ``` - - En recursos y prompts, escribe `ctx: Context` a secas. El objeto que entregó tu lifespan - sigue siendo `ctx.request_context.lifespan_context` en tiempo de ejecución; renuncias al - parámetro de tipo, no al objeto. - !!! tip Siempre hay un lifespan. Si no pasas uno, el lifespan por defecto del SDK entrega un `dict` vacío, así que `ctx.request_context.lifespan_context` es `{}`, nunca `None`. Ese valor por defecto es también @@ -101,7 +88,7 @@ Reduce el servidor al ciclo de vida: dale a `Database` un indicador `connected`, * El código anterior al `yield` es el arranque. El `finally` posterior es el apagado. * Se ejecuta una sola vez, alrededor de toda la vida del servidor, no en cada solicitud. * Lo que sea que entregues con `yield` es `ctx.request_context.lifespan_context` en cada herramienta, recurso y prompt. -* `ctx: Context[AppContext]` hace que ese acceso esté completamente tipado en las herramientas. Los recursos y prompts reciben el `Context` a secas. +* `ctx: Context[AppContext]` hace que ese acceso esté completamente tipado. * Sin `lifespan=`, obtienes un `dict` vacío, nunca `None`. Un handler que se detiene a mitad de una llamada para preguntarle al usuario algo que solo él sabe es **[Elicitación](elicitation.md)**. diff --git a/i18n/es/pages/handlers/progress.md b/i18n/es/pages/handlers/progress.md index 6e84b938e1..bc721c33a5 100644 --- a/i18n/es/pages/handlers/progress.md +++ b/i18n/es/pages/handlers/progress.md @@ -1,6 +1,6 @@ --- translation: - sections: [5315262fe26b33e1, 9d8e98840f1b78f0, 52d6009a07e770ea, 8534d8dbb4053a70, 2966fac6fe697007] + sections: [5315262fe26b33e1, 9d8e98840f1b78f0, 52d6009a07e770ea, 8534d8dbb4053a70, 2e9aff14d3a882c0] tool: 1 --- # Progreso {#progress} @@ -120,4 +120,4 @@ El callback recibe `total=None`. Un cliente todavía puede mostrar *actividad* ( * Si la llamada no lleva callback, `report_progress` no hace nada. Reporta sin condiciones. * Omite `total` cuando no lo conozcas; el callback recibe `None`. -El progreso es lo que una herramienta en ejecución le muestra al *usuario*. Las líneas que registra para *ti*, la persona que opera el servidor, van por otro canal: **[Logging](logging.md)**. +El progreso es para un cliente que sigue esperando. Lo que ve tu herramienta cuando el cliente deja de esperar es **[Cancelación](cancellation.md)**. diff --git a/i18n/es/pages/handlers/subscriptions.md b/i18n/es/pages/handlers/subscriptions.md index 153516bc1b..f90d58d547 100644 --- a/i18n/es/pages/handlers/subscriptions.md +++ b/i18n/es/pages/handlers/subscriptions.md @@ -1,6 +1,6 @@ --- translation: - sections: [60a9de8a0bdaa531, 317bbe7e4355cdcc, a61d660c8029e04a, 8f7e82fcb88df8a9, b165db51249ff8ed, 266f56fb798068a4, 7c0e57030b622139, df18d7c2417a9883] + sections: [60a9de8a0bdaa531, 6693607ea56d8bd6, a61d660c8029e04a, 8f7e82fcb88df8a9, b165db51249ff8ed, b8bc624a627ead9b, 2139e68e36d9e621, 7c0e57030b622139, 34ab1af2b9ab5b45] tool: 1 --- # Suscripciones {#subscriptions} @@ -22,7 +22,7 @@ Tu parte es una sola línea: publicar el cambio. * Los métodos hermanos son `notify_prompts_changed()` y `notify_resources_changed()`. * Sin suscriptores, sin trabajo. Publicar en un servidor inactivo no hace nada, así que nunca compruebas si alguien está escuchando. Declaras qué cambió. -`MCPServer` atiende `subscriptions/listen` por ti. Las obligaciones del canal (el acuse de recibo como primera trama, el filtrado por flujo, el id de suscripción en cada trama) son trabajo del SDK. +`MCPServer` atiende `subscriptions/listen` por ti, a menos que [lo desactives](#turning-it-off). Las obligaciones del canal (el acuse de recibo como primera trama, el filtrado por flujo, el id de suscripción en cada trama) son trabajo del SDK. !!! check En el canal, un flujo cuyo filtro nombró `board://sprint` se ve así después de que se ejecuta `complete_task`: @@ -79,7 +79,32 @@ Entrar en `client.listen(...)` envía la solicitud y espera tu acuse de recibo, Las publicaciones viajan desde tu handler hasta los flujos abiertos a través de un `SubscriptionBus`. El valor por defecto es en memoria: un proceso, con todos los flujos dentro. Esa es la respuesta correcta hasta que ejecutas réplicas detrás de un balanceador de carga, porque entonces el flujo de un cliente queda fijado a una réplica, y una publicación en otra réplica tiene que llegar hasta él. -Esa pieza te toca implementarla a ti: dos métodos sobre tu backend de pub/sub. +Con el bus por defecto no puede, porque cada réplica tiene el suyo: + +```mermaid +flowchart LR + client[Client] --> lb[Load balancer] + lb --> stream + lb ~~~~ gap + lb --> tool + subgraph B [Replica B] + tool[tools/call] -- publishes --> busB[(bus B)] + end + gap[(no shared bus)] + subgraph A [Replica A] + stream[listen stream] -- subscribed --> busA[(bus A)] + end + style A fill:none + style B fill:none + style gap fill:none,stroke-dasharray:4 4 +``` + +Nada falla: la llamada tiene éxito y el flujo se queda en silencio. Así que, detrás de un balanceador de carga, elige una opción: + +* **Necesitas notificaciones de cambio.** Dale a cada réplica el mismo bus, como se muestra abajo. +* **No las necesitas.** [Desactívalas](#turning-it-off), para que a ningún cliente se le prometan eventos que se va a perder ni mantenga un flujo abierto para ellos. + +El bus compartido te toca implementarlo a ti: dos métodos sobre tu backend de pub/sub. ```python from collections.abc import Callable @@ -128,6 +153,20 @@ async def tools_reloaded() -> None: await bus.publish(ToolsListChanged()) # from a lifespan task, a webhook, anywhere ``` +## Desactivarlas {#turning-it-off} + +Un servidor cuyo catálogo nunca cambia no tiene nada que publicar. Dilo al construirlo: + +```python title="server.py" hl_lines="3" +--8<-- "docs_src/subscriptions/tutorial007.py" +``` + +* Un cliente `2026-07-28` no ve anunciada ninguna notificación de cambio, y una solicitud `subscriptions/listen` recibe *Method not found* en lugar de un flujo abierto. +* `ctx.notify_*` sigue funcionando y no llega a nadie, así que tus handlers no cambian. +* Los clientes con versiones anteriores del protocolo no notan ninguna diferencia. + +Un flujo abierto es una solicitud que nunca termina, así que esto también importa en un host que factura por la duración de las solicitudes. + ## La composición de bajo nivel {#the-low-level-composition} Abajo, en el `Server` de bajo nivel, no hay nada preconectado, y las mismas piezas se ensamblan en tres líneas: @@ -148,5 +187,6 @@ Abajo, en el `Server` de bajo nivel, no hay nada preconectado, y las mismas piez * El lado del cliente es `async with client.listen(...)`: **[Suscripciones](../client/subscriptions.md)** en *Clientes* tiene todos los detalles. * En el `Server` de bajo nivel ensamblas tú mismo las mismas piezas: un bus, `ListenHandler(bus)`, la ranura `on_subscriptions_listen`. * Escalar horizontalmente significa implementar `SubscriptionBus`, dos métodos, y pasarlo como `MCPServer(subscriptions=...)`. +* Nada que publicar, o réplicas sin bus compartido: `MCPServer(subscriptions=False)` no anuncia notificaciones de cambio y no mantiene ningún flujo. Ejecutar el servidor que atiende todo esto, detrás de una réplica o de veinte, es **[Desplegar y escalar](../run/deploy.md)**. diff --git a/i18n/es/pages/run/deploy.md b/i18n/es/pages/run/deploy.md index 53cd02cb7b..99ab5f6228 100644 --- a/i18n/es/pages/run/deploy.md +++ b/i18n/es/pages/run/deploy.md @@ -1,6 +1,6 @@ --- translation: - sections: [28221886b198784f, f88ea1f1614f3a1d, 2e76f5cb9df15042, ce926d686730b6d0, 3be24f8ad8bb5ab9, 3fad24032b2224ff, f25a7f860e579ecb, 697b01d95080880d] + sections: [28221886b198784f, f88ea1f1614f3a1d, 2e76f5cb9df15042, ce926d686730b6d0, 3be24f8ad8bb5ab9, aaf489e944ecf5d1, f25a7f860e579ecb, 697b01d95080880d] tool: 1 --- # Desplegar y escalar {#deploy-scale} @@ -173,6 +173,7 @@ A nada del fan-out le importa a qué objeto servidor está conectado un stream. * Entre procesos reales, **el SDK no trae ningún bus que pueda ayudarte.** `SubscriptionBus` es un `Protocol` de dos métodos (`publish` y `subscribe`) que implementas sobre tu propio backend pub/sub (Redis, NATS, lo que ya ejecutes) y pasas como `MCPServer(subscriptions=...)`. **[Suscripciones](../handlers/subscriptions.md#scaling-past-one-process)** tiene el esbozo y el contrato. * El bus transporta cuatro eventos tipados pequeños, nunca JSON-RPC. El acuse de recibo, el filtrado y el ciclo de vida de los streams se quedan en el SDK, así que tu bus no puede romper el protocolo; solo puede mover eventos entre procesos. * Los streams **no** se pueden reanudar y los eventos **no** se vuelven a reproducir. Perder una réplica descarta sus streams; los clientes vuelven a escuchar y vuelven a obtener los datos. No hay almacén de eventos que compartir ni nada más que configurar. Este es el único lugar donde escalar horizontalmente es de verdad solo más de lo mismo. +* Un servidor que no necesita notificaciones de cambio se salta el bus: **[desactívalas](../handlers/subscriptions.md#turning-it-off)**. ## Lo que el SDK no te da {#what-the-sdk-does-not-give-you} diff --git a/i18n/es/pages/servers/structured-output.md b/i18n/es/pages/servers/structured-output.md index ece2c5026e..4cc88e3c96 100644 --- a/i18n/es/pages/servers/structured-output.md +++ b/i18n/es/pages/servers/structured-output.md @@ -1,6 +1,6 @@ --- translation: - sections: [a838d57f003aed44, 857d03886a0137ed, 42d9efcb9f542867, 2290ff08435b5573, 91be9b73602abcf1, 6cdbad079f7b47f0, d4b607372fb28b51, 18dbf726ac45e0b7, c7eff2a5698225fa, c851964bb3301907, 8f296f1f09e4c400, d715db6f8dccc9cc, a0c344a48450dbe4] + sections: [a838d57f003aed44, 857d03886a0137ed, 42d9efcb9f542867, 2290ff08435b5573, 91be9b73602abcf1, 6cdbad079f7b47f0, d4b607372fb28b51, 7608fc5ebc31d6ea, c7eff2a5698225fa, c851964bb3301907, 8f296f1f09e4c400, d715db6f8dccc9cc, a0c344a48450dbe4] tool: 1 --- # Salida estructurada {#structured-output} @@ -174,6 +174,8 @@ result.structured_content # {"London": 16.2, "Reykjavik": 4.4} Las claves deben ser `str`. Un `dict[int, float]` no puede ser un objeto JSON, así que recurre al envoltorio `{"result": ...}`. +Los resultados de tipo diccionario usan el `TypeAdapter` de Pydantic para la validación y la serialización. Si inspeccionas el `FuncMetadata.output_model` de una herramienta, contiene la anotación de tipo del diccionario con el título de su esquema. + ## Validación {#validation} `output_schema` no es documentación. Lo que devuelva tu función **se valida contra él** antes de salir del servidor. diff --git a/i18n/es/pages/servers/tools.md b/i18n/es/pages/servers/tools.md index 0ea76b9d47..95bc35aff9 100644 --- a/i18n/es/pages/servers/tools.md +++ b/i18n/es/pages/servers/tools.md @@ -1,6 +1,6 @@ --- translation: - sections: [e4cc390d56573409, f30cf8103a6e918c, 2c97b9f888398951, 048e5471dfa71aea, 3076b1e16ad95950, edbedf2a16e71311, 3d8ef8da89fa87c1, f6c0e02e6ea5a363] + sections: [e4cc390d56573409, f30cf8103a6e918c, 2c97b9f888398951, 048e5471dfa71aea, 3076b1e16ad95950, c1115cd005b81e8f, 3d8ef8da89fa87c1, f6c0e02e6ea5a363] tool: 1 --- # Herramientas {#tools} @@ -141,7 +141,7 @@ Puedes combinar a tu gusto: parámetros simples junto a parámetros de modelo, m Si una herramienta hace E/S (llama a una API, lee un archivo, consulta una base de datos), declárala como `async def` y usa `await` dentro. El SDK se encarga de esperarla. -Una herramienta con `def` normal también funciona: el SDK la ejecuta en un hilo para que nunca bloquee el servidor. +Una herramienta con `def` normal también funciona: el SDK la ejecuta en un hilo para que nunca bloquee el servidor. Una que tarde mucho puede comprobar si el cliente sigue esperando; consulta **[Cancelación](../handlers/cancellation.md)**. No hay nada más que configurar. diff --git a/i18n/es/pages/troubleshooting.md b/i18n/es/pages/troubleshooting.md index 04bf6378ab..91b05eae7c 100644 --- a/i18n/es/pages/troubleshooting.md +++ b/i18n/es/pages/troubleshooting.md @@ -1,6 +1,6 @@ --- translation: - sections: [3d58228e81b99543, 170514ce901c4139, 17d61fad0a50d62b, 8a6e351ec756904d, 137454d469c867f5, 6392596bd6df54f0, 41126fa9c4fe432f, 480b6d7897e30ab4, d83bb682e708dde0, ebbed3449c499db4, 525cdf1755e29d4c, 30fd31be74169d9a, d2e88333d4f7841f, c2dc3b1007d2e987, d6eabf60cc366341, f798e815252852c2, 0cba47bae78d04eb, 2c218ba829abf74e] + sections: [3d58228e81b99543, 170514ce901c4139, 17d61fad0a50d62b, 8a6e351ec756904d, 137454d469c867f5, fcf984fa0615ed11, 6392596bd6df54f0, 41126fa9c4fe432f, 480b6d7897e30ab4, d83bb682e708dde0, ebbed3449c499db4, 525cdf1755e29d4c, 30fd31be74169d9a, d2e88333d4f7841f, c2dc3b1007d2e987, d6eabf60cc366341, f798e815252852c2, 0cba47bae78d04eb, cdc6d86a4dae8a34] tool: 1 --- # Solución de problemas {#troubleshooting} @@ -128,6 +128,14 @@ Añade los paréntesis. `@mcp.resource(...)` y `@mcp.prompt()` dicen lo mismo an conectado con cero herramientas, tiene esta forma: ejecuta `python server.py` tú mismo y lee el traceback. Un verificador de tipos también lo detecta: una función no es un `name=` válido. +## `InvalidSignature: Tool '' has an invalid x-mcp-header annotation: ` {#invalidsignature-tool-name-has-an-invalid-x-mcp-header-annotation-reason} + +Un argumento de una herramienta está marcado con `x-mcp-header` de una forma que la especificación no permite, y `` dice qué regla incumple. Los clientes en `2026-07-28` dejarían esa herramienta fuera de su listado, así que el SDK se niega a registrarla. + +Solo se pueden marcar los argumentos `str`, `int` y `bool`, y `str | None` no es ninguno de ellos. **[Parámetros de cabecera](advanced/header-parameters.md)** tiene la forma de escribir un argumento opcional. + +Como en la entrada anterior, esto se lanza al **importar** el módulo, antes de que se conecte ningún cliente. + ## `Tool already exists: ` {#tool-already-exists-name} Dos registros usaron el mismo nombre de herramienta. Gana el **primero**, el segundo se descarta en silencio, y este aviso en el *log del servidor* es la única señal: @@ -425,6 +433,7 @@ mcp = MCPServer("Weather", request_state_security=RequestStateSecurity(keys=[key * `ExceptionGroup: unhandled errors in a TaskGroup` nunca es el error. Lee la **última línea**; capturar `MCPError` *dentro* del bloque `async with Client(...)` evita el envoltorio por completo. * `call_tool` no lanza nada para una herramienta que falla. `Error executing tool ...` y `Unknown tool: ...` son resultados: comprueba `result.is_error`. Si no hay mensaje después del nombre de la herramienta, se cayó, y el traceback está en el log del servidor. * `Client must be used within an async context manager` -> usa `async with`. `Use @tool() instead of @tool` -> añade los paréntesis. +* `has an invalid x-mcp-header annotation` -> solo se pueden marcar los argumentos `str`, `int` y `bool`. * `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()`. diff --git a/i18n/es/pages/whats-new.md b/i18n/es/pages/whats-new.md index 01a07527f6..e7490c0771 100644 --- a/i18n/es/pages/whats-new.md +++ b/i18n/es/pages/whats-new.md @@ -1,6 +1,6 @@ --- translation: - sections: [cfe01c0c5863dfa2, 0dbb68d8b210177b, 80cf193023af3ed4, 875eb2889263424e] + sections: [cfe01c0c5863dfa2, 0dbb68d8b210177b, bd42ee3a268f6ea4, 875eb2889263424e] tool: 1 --- # Novedades de la v2 {#whats-new-in-v2} @@ -168,7 +168,7 @@ Sobre Streamable HTTP no hay `Mcp-Session-Id` en el camino 2026, y ese es el tit ### El servidor no puede llamar al cliente: solicitudes de varias idas y vueltas {#the-server-cannot-call-the-client-multi-round-trip-requests} -Toda solicitud iniciada por el servidor desaparece en 2026-07-28: elicitación por push, muestreo, `roots/list`. En una conexión 2026 no hay canal para ellas, así que `ctx.elicit()` y `ctx.session.create_message()` fallan ahí con `NoBackChannelError`, porque no hay canal de retorno (back-channel) (siguen funcionando para clientes heredados). +Toda solicitud iniciada por el servidor desaparece en 2026-07-28: elicitación por push, muestreo, `roots/list`. En una conexión 2026 no hay canal para ellas, así que `ctx.elicit()` y `ctx.session.create_message()` fallan ahí con `NoBackChannelError` (siguen funcionando para clientes heredados). 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: @@ -191,7 +191,7 @@ Esos dos archivos son toda la propuesta: un servidor, una herramienta respaldada ### Roots, muestreo y logging del protocolo quedan obsoletos; `ping` se elimina {#roots-sampling-and-protocol-logging-are-deprecated-ping-is-removed} -[SEP-2577](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2577) declara obsoletas tres *capacidades* enteras, en todas las versiones del protocolo: roots, muestreo y logging a nivel MCP (`ctx.info()` y compañía). Es un eje distinto del canal de retorno ausente de arriba; obsoleto es solo un aviso, todo sigue funcionando contra sesiones de la generación 2025 y nada cambia en lo que se transmite. Lo que notas es `MCPDeprecationWarning`, que es un `UserWarning`, así que se imprime por defecto; cuenta con que tu primer `ctx.info(...)` tras la actualización lo diga. +[SEP-2577](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2577) declara obsoletas tres *capacidades* enteras, en todas las versiones del protocolo: roots, muestreo y logging a nivel MCP (`ctx.info()` y compañía). Es un eje distinto del canal de retorno (back-channel) ausente de arriba; obsoleto es solo un aviso, todo sigue funcionando contra sesiones de la generación 2025 y nada cambia en lo que se transmite. Lo que notas es `MCPDeprecationWarning`, que es un `UserWarning`, así que se imprime por defecto; cuenta con que tu primer `ctx.info(...)` tras la actualización lo diga. `ping` es más estricto: eliminado del protocolo, no obsoleto. Dos de los métodos independientes de las funcionalidades obsoletas se eliminan en 2026-07-28 del mismo modo, `logging/setLevel` y el `notifications/roots/list_changed` del cliente, y las notificaciones de progreso son ahora solo de servidor a cliente. @@ -206,7 +206,7 @@ En 2026-07-28 el flujo HTTP GET independiente y `resources/subscribe` se sustitu ### El resto, rápido {#the-rest-quickly} * **La identidad es opcional, metadatos por mensaje.** La clave `clientInfo` de `_meta` del lado de la solicitud es opcional (el par obligatorio es `protocolVersion` + `clientCapabilities`), y `serverInfo` salió del cuerpo del resultado de `server/discover`: los servidores la estampan en el `_meta` de cada resultado de la generación 2026 en su lugar ([spec #3002](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/3002)). El SDK siempre la estampa; `client.server_info` es `None` cuando un servidor no se identifica (por ejemplo, un middleware quitó la clave). **[El Server de bajo nivel](advanced/low-level-server.md)** muestra la marca en lo que se transmite. -* **Las solicitudes se pueden enrutar sin analizar cuerpos.** Las solicitudes HTTP modernas llevan `Mcp-Method` (y, para las tres llamadas de tipo herramienta, `Mcp-Name`); una propiedad del esquema de entrada de una herramienta anotada con `x-mcp-header` se refleja en una cabecera `Mcp-Param-*` y el servidor la contrasta ([SEP-2243](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2243)). Las pasarelas y los limitadores de tasa pueden enrutar solo con cabeceras; la **[Guía de migración](migration.md#servers-validate-mcp-param-headers-against-the-request-body-sep-2243)** tiene las reglas. +* **Las solicitudes se pueden enrutar sin analizar cuerpos.** Las solicitudes HTTP modernas llevan `Mcp-Method` (y, para las tres llamadas de tipo herramienta, `Mcp-Name`); una propiedad del esquema de entrada de una herramienta anotada con `x-mcp-header` se refleja en una cabecera `Mcp-Param-*` y el servidor la contrasta ([SEP-2243](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2243)). Las pasarelas y los limitadores de tasa pueden enrutar solo con cabeceras. **[Parámetros de cabecera](advanced/header-parameters.md)** muestra cómo marcar un argumento. * **Los resultados llevan indicaciones de caché.** Los resultados de listado y lectura declaran `ttlMs` y `cacheScope` ([SEP-2549](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2549)); los fijas por método con `cache_hints=`, y `Client` los respeta con una caché de respuestas integrada. Un servidor que no envía indicaciones (todo servidor anterior a 2026) ve un tráfico idéntico, sin caché. **[Indicaciones de caché](client/caching.md)**. * **Las extensiones son de primera clase.** Servidores y clientes declaran paquetes de capacidades opcionales bajo identificadores DNS inversos ([SEP-2133](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2133)); la extensión integrada `Apps` (MCP Apps) es la referencia. **[Extensiones](advanced/extensions.md)** y **[MCP Apps](advanced/apps.md)**. * **Los códigos de error se estandarizaron.** Un recurso inexistente es `-32602` con la URI en `error.data`, y los nuevos códigos reservados por la especificación aparecen como `-32020` (cabecera no coincidente), `-32021` (falta una capacidad obligatoria) y `-32022` (versión de protocolo no admitida). **[Solución de problemas](troubleshooting.md)** está indexada por los mensajes exactos. diff --git a/i18n/fr/pages/advanced/header-parameters.md b/i18n/fr/pages/advanced/header-parameters.md new file mode 100644 index 0000000000..969d098a18 --- /dev/null +++ b/i18n/fr/pages/advanced/header-parameters.md @@ -0,0 +1,65 @@ +--- +translation: + sections: [81862a209b483d27, b76e8073487afa03, bc214b2fc2bcdae4, d5835477c0b60163, a5d9786f902ad8e1] + tool: 1 +--- +# Paramètres d’en-tête {#header-parameters} + +La plupart des serveurs n’en ont jamais besoin. + +Une passerelle ou un répartiteur de charge placé devant votre serveur ne peut router que d’après ce qu’il lit sans analyser le corps. Marquez un argument d’outil avec `x-mcp-header`, et les clients en **[version du protocole](../protocol-versions.md)** `2026-07-28` envoient aussi sa valeur sous forme d’en-tête HTTP. + +## Marquer un argument {#mark-an-argument} + +La marque est une clé supplémentaire dans le schéma JSON de l’argument. Avec `MCPServer`, c’est `Field` qui l’y place : + +```python title="server.py" hl_lines="13" +--8<-- "docs_src/header_parameters/tutorial001.py" +``` + +* Sur Streamable HTTP en version `2026-07-28`, un client envoie `Mcp-Param-Region` en plus du corps, et le serveur rejette tout appel où les deux ne concordent pas. +* Un client qui n’a pas listé l’outil n’a jamais vu la marque : il n’envoie aucun en-tête, et l’appel est rejeté. Le `Client` de ce SDK liste alors les outils et renvoie l’appel une seule fois ; lister d’abord ne fait donc qu’économiser un aller-retour. +* Toutes les autres connexions ignorent l’annotation. + +Votre fonction ne change pas : `region` arrive toujours sous forme d’argument. + +## Ce qui peut être marqué {#what-can-be-marked} + +Les arguments `str`, `int` et `bool`. Tout le reste est refusé à l’enregistrement de l’outil, avec `InvalidSignature`. + +Cela vaut aussi pour `str | None`, qui n’a pas de type unique. Pour un argument facultatif, il faut écrire son schéma explicitement, avec `WithJsonSchema` de Pydantic : + +```python +region: Annotated[str | None, WithJsonSchema({"type": "string", "x-mcp-header": "Region"})] = None +``` + +## Avec le `Server` de bas niveau {#on-the-low-level-server} + +Là, vous écrivez `input_schema` à la main ; la clé s’y place donc directement : + +```python title="server.py" hl_lines="18" +--8<-- "docs_src/header_parameters/tutorial002.py" +``` + +* Rien ne vérifie l’annotation à votre place : une annotation invalide est servie telle quelle, et les clients en version `2026-07-28` omettent l’outil de leur liste. + +### Schémas par nom {#schemas-by-name} + +Pour vérifier l’en-tête, le SDK a besoin du schéma d’entrée de l’outil avant d’acheminer l’appel. Sans `get_tool_input_schema`, il l’obtient en exécutant votre gestionnaire `on_list_tools` à chaque appel comportant des arguments, qu’un outil soit marqué ou non. + +```python title="server.py" hl_lines="26 39-41 48" +--8<-- "docs_src/header_parameters/tutorial003.py" +``` + +* Passez cette fonction pour répondre à partir de ce que vous avez déjà. +* Renvoyez `None` pour un outil qui n’a rien à vérifier. + +## Récapitulatif {#recap} + +* `x-mcp-header` sur un argument d’outil amène les clients en version `2026-07-28` à le répéter dans un en-tête HTTP `Mcp-Param-*`. +* Le serveur rejette tout appel dont l’en-tête et le corps ne concordent pas. +* Seuls les arguments `str`, `int` et `bool` peuvent être marqués. `MCPServer` lève `InvalidSignature` pour tout le reste. +* Le `Server` de bas niveau ne vérifie rien, et les clients écartent tout outil dont l’annotation est invalide. +* `get_tool_input_schema` évite au `Server` de bas niveau d’exécuter `on_list_tools` à chaque appel. + +Le reste de l’API du `Server` écrit à la main est décrit dans **[Le Server de bas niveau](low-level-server.md)**. diff --git a/i18n/fr/pages/advanced/index.md b/i18n/fr/pages/advanced/index.md index ddf9629c8c..f6d9e0637b 100644 --- a/i18n/fr/pages/advanced/index.md +++ b/i18n/fr/pages/advanced/index.md @@ -1,6 +1,6 @@ --- translation: - sections: [ca6988b7503cd2d3] + sections: [348f8697c6b12cd0] tool: 1 --- # Avancé {#advanced} @@ -14,6 +14,8 @@ de `MCPServer` vous gêne : méthodes JSON-RPC personnalisées. * **[Pagination](pagination.md)** et **[Middleware](middleware.md)** : deux choses que vous ne pouvez faire *que* sur le `Server` de bas niveau. +* **[Paramètres d’en-tête](header-parameters.md)** : permettent à une passerelle de router un appel d’outil + selon l’un de ses arguments. * **[Extensions](extensions.md)** et **[MCP Apps](apps.md)** : la surface d’extension du protocole. Combinez des paquets d’extension dans un serveur, ou écrivez les vôtres. diff --git a/i18n/fr/pages/advanced/low-level-server.md b/i18n/fr/pages/advanced/low-level-server.md index 3fc1758b65..6460d40941 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, 9d5d10a5f0405d0a, 1086e77ce561cd7f, a3f71823df5efc31, 9fc7109f72201cae, d50fe7faead8cf68, 7bf25983df655b66, 6330e1f4c6029683, 2f1749c8c133fa1c, 8db7116fc8ddd0ee, ebc33704fbd74262, 0fde3bcea081ba3a] + sections: [2c79b6338e09b7ac, 9d5d10a5f0405d0a, 1086e77ce561cd7f, a3f71823df5efc31, 9fc7109f72201cae, d50fe7faead8cf68, 7bf25983df655b66, 6330e1f4c6029683, 2f1749c8c133fa1c, 8db7116fc8ddd0ee, 2090d99b355bc2c7, 0fde3bcea081ba3a] tool: 1 --- # Le Server de bas niveau {#the-low-level-server} @@ -209,6 +209,7 @@ Chacun d’eux correspond à une idée pour laquelle vous avez désormais le voc * `on_call_tool`, `on_get_prompt` et `on_read_resource` peuvent renvoyer un `InputRequiredResult` au lieu de leur résultat normal pour mettre l’appel en pause et demander une saisie au client ; voir **[Requêtes à plusieurs allers-retours (multi-round-trip)](../handlers/multi-round-trip.md)**. Fidèle à ce niveau, rien n’est installé pour vous : là où `MCPServer` scelle `requestState` par défaut, ici le `request_state` que vous définissez traverse la liaison exactement tel qu’écrit, jusqu’à ce que vous optiez pour `server.middleware.append(RequestStateBoundary(RequestStateSecurity(keys=[...]), default_audience=server.name))` : une seule ligne (les deux noms s’importent depuis `mcp.server.request_state`) pour un scellement et une vérification identiques à ceux qu’effectue `MCPServer` (**[Protéger `requestState`](../handlers/multi-round-trip.md#protecting-requeststate)**). * `on_list_resources`, `on_read_resource`, `on_list_prompts`, `on_get_prompt`, `on_completion` ont la même forme `(ctx, params) -> result` pour les autres primitives. * `on_subscriptions_listen` sert le flux `subscriptions/listen` de la version 2026-07-28. Passez un `ListenHandler` construit sur un `SubscriptionBus` et publiez des événements sur le bus depuis vos autres gestionnaires ; voir **[Abonnements](../handlers/subscriptions.md)** pour la composition complète. +* `get_tool_input_schema` maintient `on_list_tools` hors du chemin d’appel ; voir **[Paramètres d’en-tête](header-parameters.md#schemas-by-name)**. * `server.streamable_http_app()` renvoie la même application Starlette que celle de `MCPServer` ; déployez-la comme **[Exécuter votre serveur](../run/index.md)** déploie n’importe quelle autre application ASGI. Il n’y a pas de `server.run(transport=...)` à ce niveau : `server.run(read_stream, write_stream, server.create_initialization_options())` pilote une connexion sur une paire de flux, et cette seule ligne dit tout. ## Récapitulatif {#recap} diff --git a/i18n/fr/pages/advanced/middleware.md b/i18n/fr/pages/advanced/middleware.md index 6bd22a924a..3b83b3fbaa 100644 --- a/i18n/fr/pages/advanced/middleware.md +++ b/i18n/fr/pages/advanced/middleware.md @@ -1,6 +1,6 @@ --- translation: - sections: [6048b4f308edbb8c, 46056f318ef205e4, c3e565b61acd75c5, c62422b159c6ed09, 420968f514138f43] + sections: [58e1103d9a323ccf, 46056f318ef205e4, 812b414557fb0c35, 4df162eea2518d38, c62422b159c6ed09, 420968f514138f43] tool: 1 --- # Middleware {#middleware} @@ -16,8 +16,8 @@ Vous l’écrivez sous la forme `async (ctx, call_next)` et vous l’ajoutez à fondation sur laquelle repose votre serveur. `MCPServer` reçoit la liste à la construction (`MCPServer(name, middleware=[...])`) et l’expose sous -`mcp.middleware` ; le `Server` bas niveau expose la même liste sous `server.middleware`. L’exemple -ci-dessous utilise le `Server` bas niveau ; si `Server(name, on_call_tool=...)` est nouveau pour +`mcp.middleware` ; le `Server` bas niveau expose la même liste sous `server.middleware`. Les exemples +ci-dessous utilisent le `Server` bas niveau ; si `Server(name, on_call_tool=...)` est nouveau pour vous, lisez d’abord **[Le Server bas niveau](low-level-server.md)**. ## Un middleware de chronométrage {#a-timing-middleware} @@ -65,15 +65,38 @@ C’est tout l’intérêt. Le middleware enveloppe **chaque** message entrant : * Même une méthode pour laquelle le serveur n’a pas de gestionnaire : `call_next` lève `MCPError(-32601, "Method not found")` *à travers* votre middleware en route vers le client. +## Un plafond de concurrence {#a-concurrency-cap} + +Un middleware n’est pas obligé d’appeler `call_next(ctx)`. Levez une `MCPError` à la place et ce +message-là est **refusé** : la connexion reste ouverte et le message suivant passe. + +Supposons que chaque recherche occupe une connexion d’un pool de quatre. Ce middleware laisse +quatre appels d’outil s’exécuter en même temps et refuse le cinquième : + +```python title="server.py" hl_lines="15-16 40-55 59" +--8<-- "docs_src/middleware/tutorial002.py" +``` + +* Seul `tools/call` est compté ; le serveur continue donc de répondre à `server/discover` et à + `tools/list` pendant qu’il refuse des appels d’outil. +* MCP ne définit aucun code d’erreur « serveur occupé » ; `SERVER_BUSY` est donc propre à ce + serveur. +* Refuser indique tout de suite au client que le serveur est surchargé. Si vous préférez faire + attendre les appelants, entourez plutôt `call_next(ctx)` d’un `anyio.CapacityLimiter`. + +Une `MCPError` levée parvient à l’application cliente, pas au modèle. Si le modèle doit lire le +message, renvoyez plutôt un résultat d’outil avec `is_error=True` : c’est **Répondre**, plus bas. + ## Ce que vous pouvez y faire {#what-you-can-do-inside-one} Du geste le plus anodin à celui devant lequel vous devriez le plus hésiter : -* **Observer.** Chronométrer, compter, journaliser. C’est l’exemple ci-dessus. +* **Observer.** Chronométrer, compter, journaliser. C’est le middleware de chronométrage ci-dessus. * **Refuser.** Levez une `MCPError` *au lieu* d’appeler `call_next(ctx)` et ce message-là reçoit pour réponse une erreur JSON-RPC. La connexion reste ouverte ; le message suivant passe. - C’est ainsi qu’un serveur contrôle l’accès à `subscriptions/listen` appelant par appelant : la - section **[Décider qui peut observer](../handlers/subscriptions.md#deciding-who-may-watch)** de + C’est le plafond de concurrence ci-dessus. C’est aussi ainsi qu’un serveur contrôle l’accès à + `subscriptions/listen` appelant par appelant : la section + **[Décider qui peut observer](../handlers/subscriptions.md#deciding-who-may-watch)** de la page Abonnements détaille la démarche. * **Réécrire.** `ctx` est une dataclass : `await call_next(dataclasses.replace(ctx, params=...))` transmet au reste de la chaîne d’autres paramètres que ceux envoyés par le client. Ne faites diff --git a/i18n/fr/pages/client/identity-assertion.md b/i18n/fr/pages/client/identity-assertion.md index 805976269e..caaff89342 100644 --- a/i18n/fr/pages/client/identity-assertion.md +++ b/i18n/fr/pages/client/identity-assertion.md @@ -1,6 +1,6 @@ --- translation: - sections: [a91322c46111d16d, 8e6fd6d6f59bb568, e7828fd2729b2c9d, a03ec26bfc678b65, 1034c653c0bcf1b0] + sections: [a91322c46111d16d, 8e6fd6d6f59bb568, 7cf38181f6c99fd5, 37804d4fb36d6302, 1034c653c0bcf1b0] tool: 1 --- # Assertion d’identité {#identity-assertion} @@ -65,7 +65,7 @@ L’extension ne l’exige pas ; c’est un choix délibérément plus strict. C ### Un client confidentiel {#a-confidential-client} -`client_secret` est obligatoire ; sans lui, le constructeur lève `ValueError`. Le profil IETF sous-jacent à la [SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990) réserve ce grant aux clients confidentiels, la SEP-990 exige que le client s’authentifie, et ce SDK fait respecter les deux en imposant un secret partagé. `token_endpoint_auth_method` choisit par où il transite : `client_secret_post` (la valeur par défaut, dans le corps du formulaire) ou `client_secret_basic` (un en-tête HTTP Basic). Le profil autorise aussi `private_key_jwt` ; ce fournisseur ne le prend pas en charge. +`client_secret` est obligatoire ; sans lui, le constructeur lève `ValueError`. Le profil IETF sous-jacent à la [SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990) ne recommande ce grant que pour les clients confidentiels, et la [RFC 7521](https://datatracker.ietf.org/doc/html/rfc7521) laisse cette politique au serveur d’autorisation. Ce SDK retient la lecture prudente des deux côtés : le serveur d’autorisation intégré refuse un client qui n’a pas de secret partagé, et ce fournisseur en exige un. `token_endpoint_auth_method` choisit par où il transite : `client_secret_post` (la valeur par défaut, dans le corps du formulaire) ou `client_secret_basic` (un en-tête HTTP Basic). Le profil autorise aussi `private_key_jwt` ; ce fournisseur ne le prend pas en charge. !!! tip Lisez `client_secret` depuis l’environnement ou un gestionnaire de secrets, jamais depuis le dépôt de code. @@ -92,6 +92,7 @@ Le SDK peut aussi *être* le serveur d’autorisation : `create_auth_routes` ren * `identity_assertion_enabled=True` conditionne tout. Désactivé, ce qui est la valeur par défaut, `/token` répond à ce grant par `unsupported_grant_type` même si vous avez implémenté le hook, et les métadonnées n’en font pas mention. Activé, les métadonnées gagnent le type de grant `jwt-bearer` et listent `urn:ietf:params:oauth:grant-profile:id-jag` dans `authorization_grant_profiles_supported`, le champ par lequel l’extension annonce sa prise en charge. (Le client de ce SDK ne le lit jamais : il est provisionné pour un seul émetteur et demande, tout simplement.) * **`exchange_identity_assertion`** est le hook. Avant qu’il ne s’exécute, le SDK a authentifié le client, refusé les clients publics, et refusé les clients dont l’enregistrement ne liste pas le grant. Vous recevez un `IdentityAssertionParams` (la valeur `assertion` brute, les `scopes` et `resource` demandés) et renvoyez un simple `OAuthToken`. +* Refuser les clients publics est une politique du SDK, pas une exigence de la spécification. Le serveur intégré n’authentifie les clients que par secret partagé : il ne prend pas en charge `private_key_jwt` et ne résout pas encore les Client ID Metadata Documents ([#1801](https://github.com/modelcontextprotocol/python-sdk/issues/1801)), si bien qu’un client identifié par l’un d’eux ne peut pas utiliser ce grant ici. Un déploiement qui veut une autre politique peut remplacer la route `/token` que renvoie `create_auth_routes` par la sienne. * L’enregistrement dynamique des clients refuse ce grant sans condition, si bien que `get_client` sert ici un client provisionné à la main. Un client ID-JAG ne peut pas se faire exister en s’enregistrant lui-même. * La moitié de la classe est faite de refus. `OAuthAuthorizationServerProvider` est le serveur d’autorisation *tout entier*, il réclame donc aussi le flux authorization code ; un serveur qui connecte aussi des utilisateurs implémente ces méthodes pour de bon, et celui-ci n’a qu’une seule porte. diff --git a/i18n/fr/pages/client/transports.md b/i18n/fr/pages/client/transports.md index e7aea55405..0a6b555798 100644 --- a/i18n/fr/pages/client/transports.md +++ b/i18n/fr/pages/client/transports.md @@ -1,6 +1,6 @@ --- translation: - sections: [9cac816674181eb0, 7c157764133fea1f, 40b4916d82eaf1d4, 10d151f2cc75317f, 3d0832f39b0d7059, 92742ba36533633d, 0aeca6145e7bd302] + sections: [9cac816674181eb0, 5619e950d206e6c8, 40b4916d82eaf1d4, 10d151f2cc75317f, 3d0832f39b0d7059, 92742ba36533633d, 991c10e47fda2636] tool: 1 --- # Transports côté client {#client-transports} @@ -44,6 +44,8 @@ Deux points à remarquer : * Le `httpx2.AsyncClient` vous appartient, donc c’est **vous** qui y entrez et en sortez. Le SDK ne ferme jamais un client qu’il n’a pas créé. * `streamable_http_client(url, http_client=...)` renvoie un transport, et `Client(transport)` l’accepte comme n’importe quoi d’autre. +Conservez le `timeout=`. C’est celui qu’utilise le client du SDK lui-même (30 secondes, 300 pour les lectures) ; un `httpx2.AsyncClient` construit sans délai d’expiration reçoit la valeur par défaut de `httpx2`, soit 5 secondes, et un appel d’outil qui dure plus longtemps échoue sur une expiration du délai de lecture. + Une remarque sur TLS : `httpx2` vérifie les certificats par rapport au magasin de confiance du système d’exploitation (via [`truststore`](https://pypi.org/project/truststore/)), et non par rapport à une liste d’autorités de certification embarquée. Dans un environnement sans magasin d’autorités de certification système utilisable (certains conteneurs minimaux), définissez les variables d’environnement standard `SSL_CERT_FILE`/`SSL_CERT_DIR` @@ -51,16 +53,32 @@ ou passez un `verify=ssl_context` explicite à votre `httpx2.AsyncClient` (le contexte se trouve dans [`httpx` et `httpx-sse` remplacés par `httpx2`](../migration.md#httpx-and-httpx-sse-replaced-by-httpx2)). +### Événements SSE plus volumineux {#larger-sse-events} + +Passez `max_sse_event_size` lorsqu’un serveur envoie un résultat d’outil ou une notification de grande taille dans un seul événement SSE : + +```python title="client.py" hl_lines="6-9" +--8<-- "docs_src/client_transports/tutorial005.py" +``` + +La valeur par défaut est de 1 Mio par événement, mesurée en octets avant l’analyse de l’événement. La limite s’applique aux +réponses POST, au flux GET et aux flux repris. Un événement trop volumineux dans une réponse POST ou un flux +repris fait échouer cette requête avec une erreur SSE. Sur le flux GET d’arrière-plan, le client journalise +l’erreur et relance le flux. Définissez `max_sse_event_size=None` pour désactiver le plafond lorsque vous faites confiance au +serveur et avez besoin d’événements plus volumineux. Les réponses JSON ne sont pas concernées. Si vous utilisez `ClientSessionGroup`, définissez la +même option sur `StreamableHttpParameters`. + !!! warning `streamable_http_client` acceptait autrefois `headers=` et `timeout=` directement. Ce n’est plus le cas : - ses seuls paramètres sont `url`, `http_client` et `terminate_on_close`. Utilisez `headers=` par + ses paramètres sont `url`, `http_client`, `terminate_on_close` et `max_sse_event_size`. Utilisez `headers=` par habitude et vous obtenez : ```text TypeError: streamable_http_client() got an unexpected keyword argument 'headers' ``` - Tout ce qui relève de HTTP se trouve désormais sur l’unique `httpx2.AsyncClient` que vous passez. + Les en-têtes, l’authentification, les proxys et les délais d’expiration se trouvent sur l’unique `httpx2.AsyncClient` que vous passez. + `max_sse_event_size`, lui, s’applique aux lecteurs SSE du transport MCP. !!! info `httpx2` conserve l’API familière de `httpx` ; si vous connaissez `httpx`, vous savez déjà comment gérer ici l’authentification, @@ -137,6 +155,7 @@ Un **transport** est n’importe quel gestionnaire de contexte asynchrone qui pr * `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=`. +* Utilisez `streamable_http_client(url, max_sse_event_size=...)` pour modifier la limite en octets de chaque événement SSE. * 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. diff --git a/i18n/fr/pages/handlers/cancellation.md b/i18n/fr/pages/handlers/cancellation.md new file mode 100644 index 0000000000..e609ea0765 --- /dev/null +++ b/i18n/fr/pages/handlers/cancellation.md @@ -0,0 +1,60 @@ +--- +translation: + sections: [07968345fdc0b84e, 4ea8416db9efa0dc, 336a7b4c5d0a4578, 18392e805dde6717, c30d50df43f9b55c] + tool: 1 +--- +# Annulation {#cancellation} + +Un client peut abandonner un appel : l’utilisateur a appuyé sur stop, ou un délai d’attente a expiré. + +Dans ce cas, le SDK **annule votre gestionnaire** (handler). Le `await` sur lequel il est en attente lève une exception, la fonction est dépilée, et rien de ce qu’elle renvoie n’est envoyé. La plupart des gestionnaires n’ont rien à faire de particulier. + +Deux catégories font exception : un gestionnaire qui a quelque chose à nettoyer, et un gestionnaire écrit comme un simple `def`. + +## Nettoyer dans un outil `async def` {#clean-up-in-an-async-def-tool} + +Placez le nettoyage dans un `finally` : + +```python title="server.py" hl_lines="23 26-28" +--8<-- "docs_src/cancellation/tutorial001.py" +``` + +* Le `finally` s’exécute quelle que soit la façon dont l’outil se termine : il a renvoyé une valeur, il a levé une exception ou il a été annulé. +* Un nettoyage qui doit faire un `await` a besoin de `shield=True`. Dans un gestionnaire annulé, chaque `await` suivant lève lui aussi une exception : sans cette protection, `release_hold` s’arrêterait dès sa première ligne. +* Rien ne peut annuler un bloc protégé, alors donnez-lui une limite de temps. Ici, elle est de `5` secondes. + +!!! tip + Utilisez `finally`, pas `except`. L’annulation doit continuer à remonter une fois votre nettoyage + terminé, et un `finally` la laisse passer. + +## S’arrêter tôt dans un outil `def` simple {#stop-early-in-a-plain-def-tool} + +Un outil `def` simple s’exécute dans un thread, et rien ne peut interrompre un thread de l’extérieur. C’est à l’outil de poser la question : + +```python title="server.py" hl_lines="22 25-26" +--8<-- "docs_src/cancellation/tutorial002.py" +``` + +* `anyio.from_thread.check_cancelled()` ne fait rien tant que l’appel est en cours, et lève une exception une fois qu’il a été annulé. Appelez-la entre deux unités de travail. +* Ici aussi, le nettoyage va dans un `finally`. Dans un thread, il n’y a aucune attente asynchrone, donc aucune protection n’est nécessaire. +* Un outil `def` qui ne pose jamais la question s’exécute jusqu’au bout, et son résultat est jeté. + +## Où cela s’applique {#where-it-applies} + +Les fonctions de prompt et de ressource sont annulées exactement comme les outils. + +Cela fonctionne de la même façon avec stdio et Streamable HTTP. Avec la classe `Client` de ce SDK, abandonner revient à annuler la tâche qui attend `call_tool`, ou à laisser son délai `read_timeout_seconds` expirer. + +!!! warning + Deux options de Streamable HTTP empêchent la nouvelle d’atteindre votre gestionnaire : `json_response=True` sur une + connexion `2026-07-28`, et `stateless_http=True` sur une connexion historique. Dans ces cas, le gestionnaire s’exécute + jusqu’au bout, quoi qu’ait fait le client. + +## Récapitulatif {#recap} + +* Quand le client abandonne un appel, le SDK annule le gestionnaire : outil, prompt ou ressource. +* `async def` : nettoyez dans un `finally`, et placez tout nettoyage qui comporte une attente asynchrone dans `anyio.move_on_after(seconds, shield=True)`. +* `def` simple : appelez `anyio.from_thread.check_cancelled()` entre deux unités de travail, sinon l’outil s’exécute jusqu’au bout. Un simple `finally` se charge du nettoyage. +* `json_response=True` (connexions modernes) et `stateless_http=True` (connexions historiques) désactivent l’annulation. + +La progression et l’annulation se jouent entre un outil en cours d’exécution et son *appelant*. Les lignes qu’il journalise pour *vous*, la personne qui exploite le serveur, passent par un autre canal : **[Journalisation](logging.md)**. diff --git a/i18n/fr/pages/handlers/index.md b/i18n/fr/pages/handlers/index.md index 59595ee542..294b26aebd 100644 --- a/i18n/fr/pages/handlers/index.md +++ b/i18n/fr/pages/handlers/index.md @@ -1,6 +1,6 @@ --- translation: - sections: [424930166c4bc6f3] + sections: [22ca41e50cc1b536] tool: 1 --- # Dans votre gestionnaire {#inside-your-handler} @@ -18,6 +18,7 @@ Ce qu’il peut faire pendant son exécution : * Demander davantage d’informations à l’utilisateur avec **[l’élicitation](elicitation.md)** (elicitation), et les **[requêtes à plusieurs allers-retours](multi-round-trip.md)** (multi-round-trip), le mécanisme de la version 2026-07-28 qui la véhicule. * Demander au client une complétion de LLM ou les dossiers de son espace de travail avec **[l’échantillonnage et les racines](sampling-and-roots.md)** (sampling et roots), obsolètes mais toujours pris en charge. * Signaler la **[progression](progress.md)** d’une opération lente. +* Faire le ménage, ou s’arrêter plus tôt, lorsque le client abandonne l’appel, avec **[l’annulation](cancellation.md)**. * Écrire des journaux (sur la sortie d’erreur standard, pour quiconque exploite le serveur) avec la **[journalisation](logging.md)**. * Prévenir les clients abonnés que quelque chose a changé avec les **[abonnements](subscriptions.md)**. diff --git a/i18n/fr/pages/handlers/lifespan.md b/i18n/fr/pages/handlers/lifespan.md index d583cb9eb3..fe067911dc 100644 --- a/i18n/fr/pages/handlers/lifespan.md +++ b/i18n/fr/pages/handlers/lifespan.md @@ -1,6 +1,6 @@ --- translation: - sections: [f3ca8ac5f90f2dfa, 85a1ef3588ba0736, 563346d4d5804933, 9e3528340d0bab53] + sections: [f3ca8ac5f90f2dfa, 48e478ef7bd688b1, 563346d4d5804933, 52ac6a7734d6f581] tool: 1 --- # Cycle de vie {#lifespan} @@ -46,7 +46,7 @@ Rien de nouveau. `ctx` est un paramètre **Context** : le SDK l’injecte et il `genre` est le seul argument que le modèle peut passer. Le cycle de vie, c’est l’affaire de votre serveur. -Les fonctions `@mcp.resource()` et `@mcp.prompt()` peuvent elles aussi prendre un paramètre `ctx`, annoté d’un simple `Context` pour une raison que la section suivante explique. Tout ce que transporte `ctx` est décrit dans **[L’objet Context](context.md)**. +Les fonctions `@mcp.resource()` et `@mcp.prompt()` peuvent elles aussi prendre un paramètre `ctx`. Tout ce que transporte `ctx` est décrit dans **[L’objet Context](context.md)**. ### C’est réellement typé {#it-really-is-typed} @@ -56,19 +56,6 @@ Ce seul paramètre de type est la raison pour laquelle `ctx.request_context.life Écrivez un simple `Context` à la place et `lifespan_context` est typé `dict[str, Any]` : le vérificateur de types n’a aucun moyen de savoir ce que votre cycle de vie a produit. L’objet est toujours là à l’exécution ; vous avez perdu l’assistance. -!!! warning - `Context[AppContext]` est une écriture **réservée aux outils**. Mettez-la sur une fonction - `@mcp.resource()` ou `@mcp.prompt()` et chaque appel à ce gestionnaire échoue. Le client - reçoit une erreur en retour, et le journal du serveur montre pourquoi : - - ```text - Context is not available outside of a request - ``` - - Dans les ressources et les prompts, écrivez simplement `ctx: Context`. L’objet produit par - votre cycle de vie reste `ctx.request_context.lifespan_context` à l’exécution ; vous renoncez - au paramètre de type, pas à l’objet. - !!! tip Il y a toujours un cycle de vie. Si vous n’en passez pas, celui par défaut du SDK produit un `dict` vide, si bien que `ctx.request_context.lifespan_context` vaut `{}`, jamais `None`. @@ -101,7 +88,7 @@ Réduisez le serveur à son cycle de vie : donnez à `Database` un indicateur `c * Le code avant le `yield` est le démarrage. Le `finally` qui suit est l’arrêt. * Il s’exécute une seule fois, autour de toute la vie du serveur, pas à chaque requête. * Ce que vous produisez avec `yield` est `ctx.request_context.lifespan_context` dans chaque outil, ressource et prompt. -* `ctx: Context[AppContext]` rend cet accès entièrement typé dans les outils. Les ressources et les prompts prennent le simple `Context`. +* `ctx: Context[AppContext]` rend cet accès entièrement typé. * Pas de `lifespan=` signifie un `dict` vide, jamais `None`. Un gestionnaire qui s’interrompt en plein appel pour demander à l’utilisateur quelque chose que lui seul connaît, c’est l’**[Élicitation](elicitation.md)**. diff --git a/i18n/fr/pages/handlers/progress.md b/i18n/fr/pages/handlers/progress.md index 64b08fcf7b..051a5e7f49 100644 --- a/i18n/fr/pages/handlers/progress.md +++ b/i18n/fr/pages/handlers/progress.md @@ -1,6 +1,6 @@ --- translation: - sections: [5315262fe26b33e1, 9d8e98840f1b78f0, 52d6009a07e770ea, 8534d8dbb4053a70, 2966fac6fe697007] + sections: [5315262fe26b33e1, 9d8e98840f1b78f0, 52d6009a07e770ea, 8534d8dbb4053a70, 2e9aff14d3a882c0] tool: 1 --- # Progression {#progress} @@ -111,4 +111,4 @@ La fonction de rappel reçoit `total=None`. Un client peut toujours montrer une * Sans fonction de rappel sur l’appel, `report_progress` ne fait rien. Signalez sans condition. * Omettez `total` quand vous ne le connaissez pas ; la fonction de rappel reçoit `None`. -La progression est ce qu’un outil en cours d’exécution montre à l’*utilisateur*. Les lignes qu’il journalise pour *vous*, la personne qui exploite le serveur, passent par un autre canal : la **[journalisation](logging.md)**. +La progression s’adresse à un client qui attend encore. Ce que votre outil voit quand le client cesse d’attendre, c’est l’**[annulation](cancellation.md)**. diff --git a/i18n/fr/pages/handlers/subscriptions.md b/i18n/fr/pages/handlers/subscriptions.md index 17aa6eace4..fe27e7ddf2 100644 --- a/i18n/fr/pages/handlers/subscriptions.md +++ b/i18n/fr/pages/handlers/subscriptions.md @@ -1,6 +1,6 @@ --- translation: - sections: [60a9de8a0bdaa531, 317bbe7e4355cdcc, a61d660c8029e04a, 8f7e82fcb88df8a9, b165db51249ff8ed, 266f56fb798068a4, 7c0e57030b622139, df18d7c2417a9883] + sections: [60a9de8a0bdaa531, 6693607ea56d8bd6, a61d660c8029e04a, 8f7e82fcb88df8a9, b165db51249ff8ed, b8bc624a627ead9b, 2139e68e36d9e621, 7c0e57030b622139, 34ab1af2b9ab5b45] tool: 1 --- # Abonnements {#subscriptions} @@ -22,7 +22,7 @@ Votre part se résume à une ligne : publier le changement. * Les méthodes sœurs sont `notify_prompts_changed()` et `notify_resources_changed()`. * Pas d’abonnés, pas de travail. Publier sur un serveur inactif est sans effet, vous ne vérifiez donc jamais si quelqu’un écoute. Vous indiquez ce qui a changé. -`MCPServer` sert `subscriptions/listen` pour vous. Les obligations sur la liaison (l’accusé de réception comme première trame, le filtrage par flux, l’identifiant d’abonnement sur chaque trame) sont l’affaire du SDK. +`MCPServer` sert `subscriptions/listen` pour vous, sauf si vous [le désactivez](#turning-it-off). Les obligations sur la liaison (l’accusé de réception comme première trame, le filtrage par flux, l’identifiant d’abonnement sur chaque trame) sont l’affaire du SDK. !!! check Sur la liaison, un flux dont le filtre nommait `board://sprint` ressemble à ceci après l’exécution de `complete_task` : @@ -79,7 +79,32 @@ Entrer dans `client.listen(...)` envoie la requête et attend votre accusé de r Les publications voyagent de votre gestionnaire vers les flux ouverts via un `SubscriptionBus`. Le bus par défaut est en mémoire : un processus, et tous les flux qu’il contient. C’est la bonne réponse jusqu’au jour où vous exécutez des réplicas derrière un répartiteur de charge, car le flux d’un client est alors épinglé à un réplica, et une publication sur un autre réplica doit l’atteindre. -Cette jointure est à vous d’implémenter : deux méthodes au-dessus de votre backend pub/sub. +Avec le bus par défaut, elle ne le peut pas, car chaque réplica a le sien : + +```mermaid +flowchart LR + client[Client] --> lb[Load balancer] + lb --> stream + lb ~~~~ gap + lb --> tool + subgraph B [Replica B] + tool[tools/call] -- publishes --> busB[(bus B)] + end + gap[(no shared bus)] + subgraph A [Replica A] + stream[listen stream] -- subscribed --> busA[(bus A)] + end + style A fill:none + style B fill:none + style gap fill:none,stroke-dasharray:4 4 +``` + +Rien n’échoue : l’appel réussit, et le flux reste silencieux. Derrière un répartiteur de charge, choisissez donc l’une des deux options : + +* **Vous avez besoin des notifications de changement.** Donnez le même bus à chaque réplica, comme décrit ci-dessous. +* **Vous n’en avez pas besoin.** [Désactivez-les](#turning-it-off), afin qu’aucun client ne se voie promettre des événements qu’il manquera ni ne garde un flux ouvert pour les attendre. + +Le bus partagé est à vous d’implémenter : deux méthodes au-dessus de votre backend pub/sub. ```python from collections.abc import Callable @@ -128,6 +153,20 @@ async def tools_reloaded() -> None: await bus.publish(ToolsListChanged()) # from a lifespan task, a webhook, anywhere ``` +## Désactiver les abonnements {#turning-it-off} + +Un serveur dont le catalogue ne change jamais n’a rien à publier. Dites-le au moment de le construire : + +```python title="server.py" hl_lines="3" +--8<-- "docs_src/subscriptions/tutorial007.py" +``` + +* Un client `2026-07-28` ne voit aucune notification de changement annoncée, et une requête `subscriptions/listen` reçoit *Method not found* au lieu d’un flux ouvert. +* `ctx.notify_*` fonctionne toujours et n’atteint personne : vos gestionnaires ne changent donc pas. +* Les clients qui utilisent des versions antérieures du protocole ne voient aucune différence. + +Un flux ouvert est une requête qui ne se termine jamais : cela compte donc aussi chez un hébergeur qui facture à la durée des requêtes. + ## La composition bas niveau {#the-low-level-composition} Sur le `Server` bas niveau, rien n’est précâblé, et les mêmes pièces s’assemblent en trois lignes : @@ -148,5 +187,6 @@ Sur le `Server` bas niveau, rien n’est précâblé, et les mêmes pièces s’ * Le côté client, c’est `async with client.listen(...)` : tous les détails sont dans **[Abonnements](../client/subscriptions.md)** sous *Clients*. * Sur le `Server` bas niveau, vous assemblez vous-même les mêmes pièces : un bus, `ListenHandler(bus)`, l’emplacement `on_subscriptions_listen`. * Passer à l’échelle horizontalement signifie implémenter `SubscriptionBus`, deux méthodes, et le passer via `MCPServer(subscriptions=...)`. +* Rien à publier, ou des réplicas sans bus partagé : `MCPServer(subscriptions=False)` n’annonce aucune notification de changement et ne garde aucun flux ouvert. Exécuter le serveur qui sert tout cela, derrière un réplica ou vingt, c’est **[Déployer et passer à l’échelle](../run/deploy.md)**. diff --git a/i18n/fr/pages/run/deploy.md b/i18n/fr/pages/run/deploy.md index 91ba48e4db..ac9d539015 100644 --- a/i18n/fr/pages/run/deploy.md +++ b/i18n/fr/pages/run/deploy.md @@ -1,6 +1,6 @@ --- translation: - sections: [28221886b198784f, f88ea1f1614f3a1d, 2e76f5cb9df15042, ce926d686730b6d0, 3be24f8ad8bb5ab9, 3fad24032b2224ff, f25a7f860e579ecb, 697b01d95080880d] + sections: [28221886b198784f, f88ea1f1614f3a1d, 2e76f5cb9df15042, ce926d686730b6d0, 3be24f8ad8bb5ab9, aaf489e944ecf5d1, f25a7f860e579ecb, 697b01d95080880d] tool: 1 --- # Déployer et passer à l’échelle {#deploy-scale} @@ -176,6 +176,7 @@ Rien dans la diffusion ne se soucie de l’objet serveur auquel un flux est atta * Entre de vrais processus, **le SDK ne fournit aucun bus qui puisse vous aider.** `SubscriptionBus` est un `Protocol` à deux méthodes (`publish` et `subscribe`) que vous implémentez par-dessus votre propre backend pub/sub (Redis, NATS, ce que vous exploitez déjà) et passez sous la forme `MCPServer(subscriptions=...)`. **[Abonnements](../handlers/subscriptions.md#scaling-past-one-process)** contient l’esquisse et le contrat. * Le bus transporte quatre petits événements typés, jamais de JSON-RPC. L’accusé de réception, le filtrage et le cycle de vie des flux restent dans le SDK, si bien que votre bus ne peut pas casser le protocole ; il ne peut que déplacer des événements entre processus. * Les flux ne sont **pas** reprenables et les événements ne sont **pas** rejoués. Perdre une réplique abandonne ses flux ; les clients se remettent à l’écoute et récupèrent de nouveau les données. Il n’y a pas de magasin d’événements à partager et rien d’autre à configurer. C’est le seul endroit où la montée en charge horizontale revient réellement à faire la même chose en plus grand. +* Un serveur qui n’a besoin d’aucune notification de changement se passe du bus : **[désactivez-les](../handlers/subscriptions.md#turning-it-off)**. ## Ce que le SDK ne vous donne pas {#what-the-sdk-does-not-give-you} diff --git a/i18n/fr/pages/servers/structured-output.md b/i18n/fr/pages/servers/structured-output.md index a97262194f..a9fdc317c5 100644 --- a/i18n/fr/pages/servers/structured-output.md +++ b/i18n/fr/pages/servers/structured-output.md @@ -1,6 +1,6 @@ --- translation: - sections: [a838d57f003aed44, 857d03886a0137ed, 42d9efcb9f542867, 2290ff08435b5573, 91be9b73602abcf1, 6cdbad079f7b47f0, d4b607372fb28b51, 18dbf726ac45e0b7, c7eff2a5698225fa, c851964bb3301907, 8f296f1f09e4c400, d715db6f8dccc9cc, a0c344a48450dbe4] + sections: [a838d57f003aed44, 857d03886a0137ed, 42d9efcb9f542867, 2290ff08435b5573, 91be9b73602abcf1, 6cdbad079f7b47f0, d4b607372fb28b51, 7608fc5ebc31d6ea, c7eff2a5698225fa, c851964bb3301907, 8f296f1f09e4c400, d715db6f8dccc9cc, a0c344a48450dbe4] tool: 1 --- # Sortie structurée {#structured-output} @@ -174,6 +174,8 @@ result.structured_content # {"London": 16.2, "Reykjavik": 4.4} Les clés doivent être des `str`. Un `dict[int, float]` ne peut pas être un objet JSON ; il retombe donc sur l’enveloppe `{"result": ...}`. +Les résultats de type dictionnaire utilisent le `TypeAdapter` de Pydantic pour la validation et la sérialisation. Si vous inspectez le `FuncMetadata.output_model` d’un outil, il contient l’annotation de type du dictionnaire avec le titre de son schéma. + ## Validation {#validation} `output_schema` n’est pas de la documentation. Tout ce que renvoie votre fonction est **validé par rapport à lui** avant de quitter le serveur. diff --git a/i18n/fr/pages/servers/tools.md b/i18n/fr/pages/servers/tools.md index 5ad522392d..edb030a1e0 100644 --- a/i18n/fr/pages/servers/tools.md +++ b/i18n/fr/pages/servers/tools.md @@ -1,6 +1,6 @@ --- translation: - sections: [e4cc390d56573409, f30cf8103a6e918c, 2c97b9f888398951, 048e5471dfa71aea, 3076b1e16ad95950, edbedf2a16e71311, 3d8ef8da89fa87c1, f6c0e02e6ea5a363] + sections: [e4cc390d56573409, f30cf8103a6e918c, 2c97b9f888398951, 048e5471dfa71aea, 3076b1e16ad95950, c1115cd005b81e8f, 3d8ef8da89fa87c1, f6c0e02e6ea5a363] tool: 1 --- # Outils {#tools} @@ -141,7 +141,7 @@ Vous pouvez combiner librement : des paramètres simples à côté de paramètre Si un outil fait des E/S (appelle une API, lit un fichier, interroge une base de données), déclarez-le en `async def` et utilisez `await` à l’intérieur. Le SDK se charge de l’attendre. -Un outil en simple `def` fonctionne aussi : le SDK l’exécute dans un thread, si bien qu’il ne bloque jamais le serveur. +Un outil en simple `def` fonctionne aussi : le SDK l’exécute dans un thread, si bien qu’il ne bloque jamais le serveur. Un outil long peut vérifier si le client attend toujours ; consultez **[Annulation](../handlers/cancellation.md)**. Il n’y a rien d’autre à configurer. diff --git a/i18n/fr/pages/troubleshooting.md b/i18n/fr/pages/troubleshooting.md index 1cfa29d8fa..2af9022774 100644 --- a/i18n/fr/pages/troubleshooting.md +++ b/i18n/fr/pages/troubleshooting.md @@ -1,6 +1,6 @@ --- translation: - sections: [3d58228e81b99543, 170514ce901c4139, 17d61fad0a50d62b, 8a6e351ec756904d, 137454d469c867f5, 6392596bd6df54f0, 41126fa9c4fe432f, 480b6d7897e30ab4, d83bb682e708dde0, ebbed3449c499db4, 525cdf1755e29d4c, 30fd31be74169d9a, d2e88333d4f7841f, c2dc3b1007d2e987, d6eabf60cc366341, f798e815252852c2, 0cba47bae78d04eb, 2c218ba829abf74e] + sections: [3d58228e81b99543, 170514ce901c4139, 17d61fad0a50d62b, 8a6e351ec756904d, 137454d469c867f5, fcf984fa0615ed11, 6392596bd6df54f0, 41126fa9c4fe432f, 480b6d7897e30ab4, d83bb682e708dde0, ebbed3449c499db4, 525cdf1755e29d4c, 30fd31be74169d9a, d2e88333d4f7841f, c2dc3b1007d2e987, d6eabf60cc366341, f798e815252852c2, 0cba47bae78d04eb, cdc6d86a4dae8a34] tool: 1 --- # Dépannage {#troubleshooting} @@ -129,6 +129,14 @@ Ajoutez les parenthèses. `@mcp.resource(...)` et `@mcp.prompt()` disent la mêm lisez le traceback. Un vérificateur de types l’attrape aussi : une fonction n’est pas un `name=` valide. +## `InvalidSignature: Tool '' has an invalid x-mcp-header annotation: ` {#invalidsignature-tool-name-has-an-invalid-x-mcp-header-annotation-reason} + +Un argument d’outil est marqué avec `x-mcp-header` d’une manière que la spécification n’autorise pas, et `` indique quelle règle il enfreint. Les clients en version `2026-07-28` écarteraient un tel outil de leur liste, le SDK refuse donc de l’enregistrer. + +Seuls les arguments `str`, `int` et `bool` peuvent être marqués, et `str | None` n’est aucun des trois. **[Paramètres d’en-tête](advanced/header-parameters.md)** montre comment écrire un argument optionnel. + +Comme l’entrée précédente, cette exception est levée à l’**import** du module, avant qu’un client ne se connecte. + ## `Tool already exists: ` {#tool-already-exists-name} Deux enregistrements ont utilisé le même nom d’outil. Le **premier** l’emporte, le second est silencieusement écarté, et cet avertissement dans le *journal du serveur* est le seul signal : @@ -428,6 +436,7 @@ mcp = MCPServer("Weather", request_state_security=RequestStateSecurity(keys=[key * `ExceptionGroup: unhandled errors in a TaskGroup` n’est jamais l’erreur. Lisez la **dernière ligne** ; intercepter `MCPError` *à l’intérieur* du bloc `async with Client(...)` évite entièrement l’enveloppe. * `call_tool` ne lève pas d’exception pour un outil qui échoue. `Error executing tool ...` et `Unknown tool: ...` sont des résultats : vérifiez `result.is_error`. L’absence de message après le nom de l’outil signifie qu’il a planté, et le traceback est dans le journal du serveur. * `Client must be used within an async context manager` -> utilisez `async with`. `Use @tool() instead of @tool` -> ajoutez les parenthèses. +* `has an invalid x-mcp-header annotation` -> seuls les arguments `str`, `int` et `bool` peuvent être marqués. * `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()`. diff --git a/i18n/fr/pages/whats-new.md b/i18n/fr/pages/whats-new.md index 28a15ebbf1..e654172397 100644 --- a/i18n/fr/pages/whats-new.md +++ b/i18n/fr/pages/whats-new.md @@ -1,6 +1,6 @@ --- translation: - sections: [cfe01c0c5863dfa2, 0dbb68d8b210177b, 80cf193023af3ed4, 875eb2889263424e] + sections: [cfe01c0c5863dfa2, 0dbb68d8b210177b, bd42ee3a268f6ea4, 875eb2889263424e] tool: 1 --- # Nouveautés de la v2 {#whats-new-in-v2} @@ -205,7 +205,7 @@ En version 2026-07-28, le flux HTTP GET autonome et `resources/subscribe` sont r ### Le reste, rapidement {#the-rest-quickly} * **L’identité est une métadonnée optionnelle, par message.** La clé `_meta` `clientInfo` côté requête est optionnelle (la paire obligatoire est `protocolVersion` + `clientCapabilities`), et `serverInfo` a quitté le corps du résultat de `server/discover` : les serveurs l’inscrivent à la place dans le `_meta` de chaque résultat de génération 2026 ([spec #3002](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/3002)). Le SDK l’inscrit toujours ; `client.server_info` vaut `None` lorsqu’un serveur ne s’identifie pas (par exemple, un middleware a retiré la clé). **[Le Server bas niveau](advanced/low-level-server.md)** montre cette inscription sur la liaison. -* **Les requêtes sont routables sans analyser les corps.** Les requêtes HTTP modernes portent `Mcp-Method` (et, pour les trois appels de type outil, `Mcp-Name`) ; une propriété de schéma d’entrée d’outil annotée avec `x-mcp-header` est recopiée dans un en-tête `Mcp-Param-*` et recoupée par le serveur ([SEP-2243](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2243)). Les passerelles et limiteurs de débit peuvent router sur les seuls en-têtes ; le **[Guide de migration](migration.md#servers-validate-mcp-param-headers-against-the-request-body-sep-2243)** a les règles. +* **Les requêtes sont routables sans analyser les corps.** Les requêtes HTTP modernes portent `Mcp-Method` (et, pour les trois appels de type outil, `Mcp-Name`) ; une propriété de schéma d’entrée d’outil annotée avec `x-mcp-header` est recopiée dans un en-tête `Mcp-Param-*` et recoupée par le serveur ([SEP-2243](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2243)). Les passerelles et limiteurs de débit peuvent router sur les seuls en-têtes. **[Paramètres d’en-tête](advanced/header-parameters.md)** montre comment marquer un argument. * **Les résultats portent des indications de cache.** Les résultats de liste et de lecture déclarent `ttlMs` et `cacheScope` ([SEP-2549](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2549)) ; vous les définissez par méthode avec `cache_hints=`, et `Client` les honore avec un cache de réponses intégré. Un serveur qui n’envoie aucune indication (tout serveur antérieur à 2026) voit un trafic identique, non mis en cache. **[Indications de mise en cache](client/caching.md)**. * **Les extensions sont de premier plan.** Serveurs et clients déclarent des lots de capacités optionnels sous des identifiants en DNS inversé ([SEP-2133](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2133)) ; l’extension intégrée `Apps` (MCP Apps) sert de référence. **[Extensions](advanced/extensions.md)** et **[MCP Apps](advanced/apps.md)**. * **Les codes d’erreur ont été normalisés.** Une ressource manquante est un `-32602` avec l’URI dans `error.data`, et les nouveaux codes réservés par la spécification apparaissent comme `-32020` (incohérence d’en-tête), `-32021` (capacité obligatoire manquante) et `-32022` (version de protocole non prise en charge). **[Dépannage](troubleshooting.md)** est indexé par les messages exacts. diff --git a/i18n/hi/pages/advanced/header-parameters.md b/i18n/hi/pages/advanced/header-parameters.md new file mode 100644 index 0000000000..01a0aa0626 --- /dev/null +++ b/i18n/hi/pages/advanced/header-parameters.md @@ -0,0 +1,65 @@ +--- +translation: + sections: [81862a209b483d27, b76e8073487afa03, bc214b2fc2bcdae4, d5835477c0b60163, a5d9786f902ad8e1] + tool: 1 +--- +# Header parameters {#header-parameters} + +ज़्यादातर servers को इसकी कभी ज़रूरत नहीं पड़ती। + +server के आगे लगा gateway या load balancer सिर्फ़ उसी के आधार पर route कर सकता है जिसे वह body को parse किए बिना पढ़ सके। tool के किसी argument को `x-mcp-header` से mark करें, और `2026-07-28` **[protocol version](../protocol-versions.md)** वाले clients उसकी value HTTP header के रूप में भी भेजते हैं। + +## argument को mark करना {#mark-an-argument} + +यह mark argument के JSON Schema में बस एक अतिरिक्त key है। `MCPServer` पर `Field` इसे वहाँ रख देता है: + +```python title="server.py" hl_lines="13" +--8<-- "docs_src/header_parameters/tutorial001.py" +``` + +* `2026-07-28` पर Streamable HTTP के ज़रिए client body के साथ `Mcp-Param-Region` भी भेजता है, और जिस call में दोनों मेल नहीं खाते उसे server reject कर देता है। +* जिस client ने tool को list नहीं किया है, उसने mark कभी देखा ही नहीं: वह कोई header नहीं भेजता, और call reject हो जाता है। तब इस SDK का `Client` tools को list करता है और call एक बार दोबारा भेजता है, इसलिए पहले list करने से सिर्फ़ एक round trip बचता है। +* बाकी हर connection इस annotation को अनदेखा करता है। + +function में कोई बदलाव नहीं होता: `region` अब भी argument के रूप में ही आता है। + +## क्या mark किया जा सकता है {#what-can-be-marked} + +`str`, `int` और `bool` arguments। इनके अलावा कुछ भी हो, तो tool register होते समय `InvalidSignature` के साथ मना कर दिया जाता है। + +इसमें `str | None` भी शामिल है, जिसका कोई एक type नहीं होता। optional argument के लिए उसका schema साफ़-साफ़ लिखना पड़ता है, Pydantic के `WithJsonSchema` से: + +```python +region: Annotated[str | None, WithJsonSchema({"type": "string", "x-mcp-header": "Region"})] = None +``` + +## low-level `Server` पर {#on-the-low-level-server} + +वहाँ `input_schema` आप खुद हाथ से लिखते हैं, इसलिए key सीधे उसी में जाती है: + +```python title="server.py" hl_lines="18" +--8<-- "docs_src/header_parameters/tutorial002.py" +``` + +* यहाँ annotation की जाँच आपके लिए कोई नहीं करता: invalid annotation भी serve हो जाता है, और `2026-07-28` clients उस tool को अपनी listing से बाहर रखते हैं। + +### नाम से schemas {#schemas-by-name} + +header की जाँच के लिए SDK को call dispatch करने से पहले tool का input schema चाहिए। `get_tool_input_schema` के बिना SDK यह schema हर उस call पर आपका `on_list_tools` handler चलाकर लेता है जिसमें arguments हों, चाहे कोई tool mark किया गया हो या नहीं। + +```python title="server.py" hl_lines="26 39-41 48" +--8<-- "docs_src/header_parameters/tutorial003.py" +``` + +* यह function pass करें, ताकि जवाब उसी से दिया जा सके जो आपके पास पहले से है। +* जिस tool में जाँचने को कुछ नहीं है, उसके लिए `None` लौटाएँ। + +## सारांश {#recap} + +* tool argument पर `x-mcp-header` होने से `2026-07-28` clients उसे `Mcp-Param-*` HTTP header के रूप में भी दोहराते हैं। +* जिस call के header और body मेल नहीं खाते, उसे server reject कर देता है। +* सिर्फ़ `str`, `int` और `bool` arguments mark किए जा सकते हैं। बाकी किसी भी चीज़ के लिए `MCPServer` `InvalidSignature` raise करता है। +* low-level `Server` कुछ भी नहीं जाँचता, और जिस tool का annotation invalid हो उसे clients छोड़ देते हैं। +* `get_tool_input_schema` की वजह से low-level `Server` को हर call पर `on_list_tools` नहीं चलाना पड़ता। + +हाथ से लिखी जाने वाली `Server` API का बाकी हिस्सा **[low-level Server](low-level-server.md)** में है। diff --git a/i18n/hi/pages/advanced/index.md b/i18n/hi/pages/advanced/index.md index 2261d45087..3ecafc2e38 100644 --- a/i18n/hi/pages/advanced/index.md +++ b/i18n/hi/pages/advanced/index.md @@ -1,6 +1,6 @@ --- translation: - sections: [ca6988b7503cd2d3] + sections: [348f8697c6b12cd0] tool: 1 --- # Advanced {#advanced} @@ -14,6 +14,8 @@ layer आड़े आने लगे: methods। * **[Pagination](pagination.md)** और **[Middleware](middleware.md)**: दो चीज़ें जो आप **सिर्फ़** low-level `Server` पर ही कर सकते हैं। +* **[Header parameters](header-parameters.md)**: gateway को tool call उसके किसी एक + argument के आधार पर route करने दें। * **[Extensions](extensions.md)** और **[MCP Apps](apps.md)**: protocol की extension surface। extension packages को server में जोड़ें, या अपना खुद का लिखें। diff --git a/i18n/hi/pages/advanced/low-level-server.md b/i18n/hi/pages/advanced/low-level-server.md index 90f0b3a105..a5f9aab9bd 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, 9d5d10a5f0405d0a, 1086e77ce561cd7f, a3f71823df5efc31, 9fc7109f72201cae, d50fe7faead8cf68, 7bf25983df655b66, 6330e1f4c6029683, 2f1749c8c133fa1c, 8db7116fc8ddd0ee, ebc33704fbd74262, 0fde3bcea081ba3a] + sections: [2c79b6338e09b7ac, 9d5d10a5f0405d0a, 1086e77ce561cd7f, a3f71823df5efc31, 9fc7109f72201cae, d50fe7faead8cf68, 7bf25983df655b66, 6330e1f4c6029683, 2f1749c8c133fa1c, 8db7116fc8ddd0ee, 2090d99b355bc2c7, 0fde3bcea081ba3a] tool: 1 --- # Low-level Server {#the-low-level-server} @@ -209,6 +209,7 @@ Handshake runner का है। `server/discover`, `ping`, और बाकी * `on_call_tool`, `on_get_prompt`, और `on_read_resource` अपने सामान्य result के बजाय `InputRequiredResult` लौटा सकते हैं, ताकि call रुक जाए और client से input माँगा जाए; देखें **[Multi-round-trip requests](../handlers/multi-round-trip.md)**। इस tier के मुताबिक, आपके लिए कुछ install नहीं होता: जहाँ `MCPServer` default रूप से `requestState` को seal करता है, वहीं यहाँ आपका set किया `request_state` ठीक वैसे ही wire पार करता है जैसा लिखा गया, जब तक आप `server.middleware.append(RequestStateBoundary(RequestStateSecurity(keys=[...]), default_audience=server.name))` से opt in न करें: एक line (दोनों नाम `mcp.server.request_state` से import होते हैं) और ठीक वही sealing और verification मिलती है जो `MCPServer` करता है (**[`requestState` की सुरक्षा](../handlers/multi-round-trip.md#protecting-requeststate)**)। * `on_list_resources`, `on_read_resource`, `on_list_prompts`, `on_get_prompt`, `on_completion` बाकी primitives के लिए वही `(ctx, params) -> result` आकार हैं। * `on_subscriptions_listen` 2026-07-28 की `subscriptions/listen` stream serve करता है। `SubscriptionBus` के ऊपर बना `ListenHandler` pass करें और अपने बाकी handlers से bus पर events publish करें; पूरी रचना के लिए देखें **[Subscriptions](../handlers/subscriptions.md)**। +* `get_tool_input_schema` `on_list_tools` को call path से बाहर रखता है; देखें **[Header parameters](header-parameters.md#schemas-by-name)**। * `server.streamable_http_app()` वही Starlette app लौटाता है जो `MCPServer` का लौटाता है; इसे वैसे ही deploy करें जैसे **[अपना server चलाना](../run/index.md)** किसी भी दूसरे ASGI app को deploy करता है। यहाँ नीचे कोई `server.run(transport=...)` नहीं है: `server.run(read_stream, write_stream, server.create_initialization_options())` streams की एक जोड़ी पर एक connection चलाता है, और पूरी जानकारी बस वही एक line है। ## सारांश {#recap} diff --git a/i18n/hi/pages/advanced/middleware.md b/i18n/hi/pages/advanced/middleware.md index 3bddd001af..c026e9133f 100644 --- a/i18n/hi/pages/advanced/middleware.md +++ b/i18n/hi/pages/advanced/middleware.md @@ -1,6 +1,6 @@ --- translation: - sections: [6048b4f308edbb8c, 46056f318ef205e4, c3e565b61acd75c5, c62422b159c6ed09, 420968f514138f43] + sections: [58e1103d9a323ccf, 46056f318ef205e4, 812b414557fb0c35, 4df162eea2518d38, c62422b159c6ed09, 420968f514138f43] tool: 1 --- # Middleware {#middleware} @@ -15,8 +15,8 @@ translation: **अस्वीकार करने** के लिए करें; इसे वह नींव न बनाएँ जिस पर आपका server खड़ा हो। `MCPServer` यह list construction के समय लेता है (`MCPServer(name, middleware=[...])`) और इसे -`mcp.middleware` के रूप में उपलब्ध कराता है; low-level `Server` वही list `server.middleware` के रूप में देता है। नीचे दिया गया -उदाहरण low-level `Server` इस्तेमाल करता है; अगर `Server(name, on_call_tool=...)` आपके लिए नया है, तो पहले +`mcp.middleware` के रूप में उपलब्ध कराता है; low-level `Server` वही list `server.middleware` के रूप में देता है। नीचे दिए गए +उदाहरण low-level `Server` इस्तेमाल करते हैं; अगर `Server(name, on_call_tool=...)` आपके लिए नया है, तो पहले **[Low-level Server](low-level-server.md)** पढ़ें। ## Timing middleware {#a-timing-middleware} @@ -60,22 +60,43 @@ client ने connection तैयार करने के लिए भेज * वह method भी जिसके लिए server के पास कोई handler नहीं है: `call_next` `MCPError(-32601, "Method not found")` को client की ओर जाते हुए आपके middleware के **बीच से** raise करता है। +## concurrency की सीमा {#a-concurrency-cap} + +middleware के लिए `call_next(ctx)` call करना ज़रूरी नहीं है। इसकी जगह `MCPError` raise करें और वह एक +message **अस्वीकार** कर दिया जाता है: connection बना रहता है और अगला message निकल जाता है। + +मान लें कि हर search चार connections वाले pool का एक connection थामे रखता है। यह middleware चार tool calls को +एक साथ चलने देता है और पाँचवें को अस्वीकार कर देता है: + +```python title="server.py" hl_lines="15-16 40-55 59" +--8<-- "docs_src/middleware/tutorial002.py" +``` + +* सिर्फ़ `tools/call` गिना जाता है, इसलिए tool calls अस्वीकार करते समय भी server `server/discover` और `tools/list` + का जवाब देता रहता है। +* MCP कोई "server busy" error code define नहीं करता, इसलिए `SERVER_BUSY` इस server का अपना है। +* अस्वीकार करने से client को तुरंत पता चल जाता है कि server overloaded है। अगर आप callers को इंतज़ार कराना + बेहतर समझते हैं, तो इसकी जगह `call_next(ctx)` के चारों ओर `anyio.CapacityLimiter` hold करें। + +raise किया गया `MCPError` client application को जाता है, model को नहीं। अगर message model को पढ़ना चाहिए, +तो इसकी जगह `is_error=True` वाला tool result लौटाएँ: यही नीचे वाला **जवाब दें** है। + ## इसके अंदर आप क्या कर सकते हैं {#what-you-can-do-inside-one} इस क्रम में कि आपको कितना हिचकना चाहिए, कम से ज़्यादा की ओर: -* **देखें (Observe)।** समय मापें, गिनें, log करें। ऊपर वाला उदाहरण। -* **अस्वीकार करें (Refuse)।** `call_next(ctx)` call करने के **बजाय** `MCPError` raise करें और उस एक message का - जवाब JSON-RPC error से दिया जाता है। connection बना रहता है; अगला message निकल जाता है। इसी तरह - server हर caller के लिए `subscriptions/listen` को gate करता है: +* **देखें।** समय मापें, गिनें, log करें। ऊपर वाला timing middleware। +* **अस्वीकार करें।** `call_next(ctx)` call करने के **बजाय** `MCPError` raise करें और उस एक message का + जवाब JSON-RPC error से दिया जाता है। connection बना रहता है; अगला message निकल जाता है। ऊपर वाली + concurrency की सीमा। server हर caller के लिए `subscriptions/listen` को भी इसी तरह gate करता है: Subscriptions page पर **[यह तय करना कि कौन देख सकता है](../handlers/subscriptions.md#deciding-who-may-watch)** इसे चरण दर चरण समझाता है। -* **फिर से लिखें (Rewrite)।** `ctx` dataclass है: `await call_next(dataclasses.replace(ctx, params=...))` +* **फिर से लिखें।** `ctx` dataclass है: `await call_next(dataclasses.replace(ctx, params=...))` बाकी chain को client के भेजे params से अलग params देता है। `initialize` के साथ ऐसा कभी न करें: client को जो result वापस मिलता है वह आपके बदले हुए params से बनता है, लेकिन server अपनी connection state मूल wire params से commit करता है। दोनों पक्ष handshake इस असहमति के साथ पूरा कर सकते हैं कि उन्होंने क्या negotiate किया। -* **जवाब दें (Answer)।** `call_next(ctx)` call किए बिना result लौटाएँ और वह आपके response के रूप में client को +* **जवाब दें।** `call_next(ctx)` call किए बिना result लौटाएँ और वह आपके response के रूप में client को जाता है। `call_next` आपको तैयार wire form देता है, और pipeline आप जो लौटाते हैं उसे कभी patch नहीं करता, इसलिए पूरा envelope आपका है: 2026 पीढ़ी के connection पर इसमें `serverInfo` का `_meta` stamp शामिल है, जिसे SDK handler results में जोड़ता है पर आपके results में नहीं। diff --git a/i18n/hi/pages/client/identity-assertion.md b/i18n/hi/pages/client/identity-assertion.md index 899c3d9805..b39145867a 100644 --- a/i18n/hi/pages/client/identity-assertion.md +++ b/i18n/hi/pages/client/identity-assertion.md @@ -1,6 +1,6 @@ --- translation: - sections: [a91322c46111d16d, 8e6fd6d6f59bb568, e7828fd2729b2c9d, a03ec26bfc678b65, 1034c653c0bcf1b0] + sections: [a91322c46111d16d, 8e6fd6d6f59bb568, 7cf38181f6c99fd5, 37804d4fb36d6302, 1034c653c0bcf1b0] tool: 1 --- # Identity assertion {#identity-assertion} @@ -33,43 +33,43 @@ Client इनमें से हर एक को एक token request भे इसे नीचे से पढ़ें। * `main()` वही standard OAuth-client वाला `main()` है (**[OAuth clients](oauth-clients.md)**), पंक्ति-दर-पंक्ति बिना बदलाव। यही बात है: एक बार provider बन जाए तो आगे किसी को पता नहीं चलता कि token किस grant से आया। -* Provider वह लेता है जो बाकी providers discover नहीं कर सकते: `client_id` और `client_secret` जो किसी ने authorization server के साथ **पहले से register** कर रखे हैं, उस authorization server का `issuer`, और `assertion_provider`, एक async callback जो माँगने पर ताज़ा ID-JAG लौटाता है। +* provider वह लेता है जो बाकी providers discover नहीं कर सकते: `client_id` और `client_secret` जो किसी ने authorization server के साथ **पहले से register** कर रखे हैं, उस authorization server का `issuer`, और `assertion_provider`, एक async callback जो माँगने पर ताज़ा ID-JAG लौटाता है। * `storage` वही `TokenStorage` protocol है। सिर्फ़ दो token methods ही कभी call होते हैं; यहाँ dynamic registration नहीं है, इसलिए याद रखने को कोई `client_info` नहीं है। ### Assertion provider {#the-assertion-provider} `fetch_id_jag(audience, resource)` ही वह इकलौता code है जो आप लिखते हैं। यह हर token exchange पर एक बार await होता है, construction के समय कभी नहीं, और सिर्फ़ तब **जब** authorization server का metadata fetch और validate हो चुका हो, इसलिए गलत configure किया गया issuer कभी assertion leak नहीं करवाता। इसके दो arguments उन claims में से दो हैं जिनके साथ ID-JAG बनना ज़रूरी है: `audience` authorization server का issuer है (ID-JAG का `aud`) और `resource` MCP server का canonical identifier है (ID-JAG का `resource`)। तीसरा वह है जो आपके पास पहले से है: ID-JAG के `client_id` claim में वही `client_id` होना चाहिए जो आपने provider को दिया, वरना authorization server exchange से मना कर देता है। -उसके ऊपर वाला `idp_issue_id_jag` **आपका code नहीं है**। वह identity provider की जगह खड़ा है, और assertion को उसी process में sign करता है ताकि file पूरी रहे और आप ID-JAG में जाने वाला हर claim पढ़ सकें। असली `fetch_id_jag` इसकी जगह पिछले section की पहली token request भेजता है: आपके IdP के सामने [RFC 8693](https://datatracker.ietf.org/doc/html/rfc8693) token exchange, जिसे Identity Assertion JWT Authorization Grant draft परिभाषित करता है और जिसे [SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990) profile करता है। Sign in किए हुए user का ID token `subject_token` के रूप में जाता है, `requested_token_type` ID-JAG का अपना URN है (`urn:ietf:params:oauth:token-type:id-jag`), `audience` और `resource` जस के तस आगे जाते हैं, और response में ID-JAG आता है। अपने IdP की documentation में इन्हीं नामों के साथ यही exchange ढूँढें। +उसके ऊपर वाला `idp_issue_id_jag` **आपका code नहीं है**। वह identity provider की जगह खड़ा है, और assertion को उसी process में sign करता है ताकि file पूरी रहे और आप ID-JAG में जाने वाला हर claim पढ़ सकें। असली `fetch_id_jag` इसकी जगह पिछले section की पहली token request भेजता है: आपके IdP के सामने [RFC 8693](https://datatracker.ietf.org/doc/html/rfc8693) token exchange, जिसे Identity Assertion JWT Authorization Grant draft परिभाषित करता है और जिसे [SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990) profile करता है। sign in किए हुए user का ID token `subject_token` के रूप में जाता है, `requested_token_type` ID-JAG का अपना URN है (`urn:ietf:params:oauth:token-type:id-jag`), `audience` और `resource` जस के तस आगे जाते हैं, और response में ID-JAG आता है। अपने IdP की documentation में इन्हीं नामों के साथ यही exchange ढूँढें। !!! tip हर exchange के लिए ताज़ा ID-JAG माँगा जाता है, और यही मक़सद है: यह एक बार इस्तेमाल होने वाला, कुछ मिनट जीने वाला grant है, और इस page का authorization server एक ही ID-JAG को दो बार स्वीकार करने से मना कर देता है। इसे cache न करें। इसके बदले जो access token मिलता है, दोबारा इस्तेमाल वही होता है। -### Issuer configuration है {#the-issuer-is-configuration} +### issuer configuration है {#the-issuer-is-configuration} उलटफेर यहाँ है। `OAuthClientProvider` resource server से पूछता है कि कौन सा authorization server इस्तेमाल करे, और जवाब जिधर इशारा करे उधर चला जाता है। यह provider ऐसा करने से मना करता है: `issuer` ज़रूरी है, [RFC 8414](https://datatracker.ietf.org/doc/html/rfc8414) metadata उसी issuer के अपने well-known path से fetch होता है, token endpoint उसी issuer के origin पर होना चाहिए, और resource server से कभी कुछ नहीं पूछा जाता। -Extension इसकी माँग नहीं करता; यह जान-बूझकर चुना गया ज़्यादा सख़्त रास्ता है। इस client के पास चुराने लायक दो चीज़ें हैं, एक pre-registered secret और एक audience-bound assertion, और जो client किसी compromised MCP server को खुद को हमलावर के authorization server की तरफ़ मोड़ने दे, वह दोनों उसी को post कर देगा। Construction के समय issuer को pin कर देने से वह बातचीत ही ख़त्म हो जाती है। +extension इसकी माँग नहीं करता; यह जान-बूझकर चुना गया ज़्यादा सख़्त रास्ता है। इस client के पास चुराने लायक दो चीज़ें हैं, एक pre-registered secret और एक audience-bound assertion, और जो client किसी compromised MCP server को खुद को हमलावर के authorization server की तरफ़ मोड़ने दे, वह दोनों उसी को post कर देगा। construction के समय issuer को pin कर देने से वह बातचीत ही ख़त्म हो जाती है। !!! warning - Configure किए गए `issuer` की तुलना metadata document के `issuer` field से RFC 8414 §3.3 के + configure किए गए `issuer` की तुलना metadata document के `issuer` field से RFC 8414 §3.3 के simple string comparison से होती है: एक-एक character, आख़िरी slash समेत, बिना किसी normalization के। इसका अंदाज़ा न लगाएँ। अपने authorization server से `/.well-known/oauth-authorization-server` fetch करें और जो `issuer` value वह लौटाए उसे copy करें। इस page के authorization server के लिए वह `https://auth.example.com/` है, slash के साथ, क्योंकि उसका issuer pydantic URL object से बना था। - Mismatch होने पर flow एक भी credential या assertion भेजे जाने से पहले `OAuthFlowError: Authorization server metadata issuer + mismatch होने पर flow एक भी credential या assertion भेजे जाने से पहले `OAuthFlowError: Authorization server metadata issuer mismatch` पर रुक जाता है। ### Confidential client {#a-confidential-client} -`client_secret` ज़रूरी है; इसके बिना constructor `ValueError` raise करता है। [SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990) के नीचे वाला IETF profile इस grant को confidential clients के लिए आरक्षित रखता है, SEP-990 client से authenticate करने की माँग करता है, और यह SDK shared secret पर ज़ोर देकर दोनों लागू करता है। `token_endpoint_auth_method` तय करता है कि यह कहाँ से होकर जाए: `client_secret_post` (default, form body में) या `client_secret_basic` (HTTP Basic header)। Profile `private_key_jwt` की भी इजाज़त देता है; यह provider उसे support नहीं करता। +`client_secret` ज़रूरी है; इसके बिना constructor `ValueError` raise करता है। [SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990) के नीचे वाला IETF profile इस grant की सिफ़ारिश सिर्फ़ confidential clients के लिए करता है, और [RFC 7521](https://datatracker.ietf.org/doc/html/rfc7521) यह policy authorization server पर छोड़ता है। यह SDK दोनों तरफ़ ज़्यादा सतर्क मतलब अपनाता है: built-in authorization server ऐसे client को मना कर देता है जिसके पास shared secret नहीं है, और इस provider को secret चाहिए ही। `token_endpoint_auth_method` तय करता है कि यह कहाँ से होकर जाए: `client_secret_post` (default, form body में) या `client_secret_basic` (HTTP Basic header)। profile `private_key_jwt` की भी इजाज़त देता है; यह provider उसे support नहीं करता। !!! tip `client_secret` को environment या किसी secret manager से पढ़ें, source control से कभी नहीं। -### Provider आपके लिए क्या करता है {#what-the-provider-does-for-you} +### provider आपके लिए क्या करता है {#what-the-provider-does-for-you} पहली request बिना authentication के जाती है, और server का `401` flow शुरू करता है। @@ -91,20 +91,21 @@ SDK खुद authorization server भी **बन** सकता है: `creat * `identity_assertion_enabled=True` सब कुछ gate करता है। बंद होने पर, जो default है, `/token` इस grant का जवाब `unsupported_grant_type` से देता है चाहे आपने hook implement किया हो, और metadata में इसका ज़िक्र नहीं होता। चालू होने पर metadata में `jwt-bearer` grant type जुड़ जाता है और `authorization_grant_profiles_supported` में `urn:ietf:params:oauth:grant-profile:id-jag` सूचीबद्ध हो जाता है; यही वह field है जिससे extension support का ऐलान करता है। (इस SDK का client इसे कभी नहीं पढ़ता: वह एक issuer के लिए provision किया गया है और सीधे माँग लेता है।) * **`exchange_identity_assertion`** ही hook है। इसके चलने से पहले SDK client को authenticate कर चुका होता है, public clients को मना कर चुका होता है, और उन clients को मना कर चुका होता है जिनके registration में यह grant सूचीबद्ध नहीं है। आपको `IdentityAssertionParams` मिलता है (कच्चा `assertion`, माँगे गए `scopes` और `resource`) और आप सादा `OAuthToken` लौटाते हैं। -* Dynamic client registration इस grant को बिना शर्त मना करता है, इसलिए यहाँ `get_client` हाथ से provision किया गया client देता है। ID-JAG client खुद को register करके अस्तित्व में नहीं ला सकता। +* public clients को मना करना SDK की policy है, spec की माँग नहीं। built-in server clients को सिर्फ़ shared secret से authenticate करता है: उसमें `private_key_jwt` support नहीं है और वह अभी Client ID Metadata Documents resolve नहीं करता ([#1801](https://github.com/modelcontextprotocol/python-sdk/issues/1801)), इसलिए जिस client की पहचान ऐसे document से होती है वह यहाँ इस grant का इस्तेमाल नहीं कर सकता। जिस deployment को अलग policy चाहिए वह `create_auth_routes` से लौटे `/token` route की जगह अपना route लगा सकता है। +* dynamic client registration इस grant को बिना शर्त मना करता है, इसलिए यहाँ `get_client` हाथ से provision किया गया client देता है। ID-JAG client खुद को register करके अस्तित्व में नहीं ला सकता। * आधी class इनकारों से भरी है। `OAuthAuthorizationServerProvider` **पूरा** authorization server है, इसलिए वह authorization-code flow भी माँगता है; जो server users को sign in भी कराता है वह उन्हें सच में implement करता है, और इस वाले में ठीक एक ही दरवाज़ा है। !!! warning SDK assertion को कभी decode नहीं करता: सिर्फ़ आपके deployment को पता है कि वह किस IdP पर भरोसा करता है और वह IdP कौन सी keys publish करता है, इसलिए `exchange_identity_assertion` के अंदर की हर चीज़ पर पूरा भार टिका है। - Signature को IdP की published keys (उसकी JWKS; यहाँ वाला shared secret demo का है) से verify करें, + signature को IdP की published keys (उसकी JWKS; यहाँ वाला shared secret demo का है) से verify करें, और [RFC 7523](https://datatracker.ietf.org/doc/html/rfc7523) §3 के मुताबिक `iss` और `exp` भी। JWT header का `typ` `oauth-id-jag+jwt` होना ज़रूरी करें; यह profile का बचाव है ताकि कोई और JWT grant बनाकर replay न किया जा सके। `aud` आपका अपना issuer हो, यह ज़रूरी करें। ID-JAG का `client_id` claim उसी client के बराबर हो जिसे handler ने authenticate किया, और उसका `resource` claim किसी ऐसे resource का नाम ले जिसे आप सच में serve करते हैं, यह ज़रूरी करें। `jti` को assertion के `exp` तक track करें ताकि वह एक ही बार स्वीकार हो। और दिए गए scopes, और सबसे बढ़कर जारी किए गए token का `resource`, validated ID-JAG से लें, request से कभी नहीं: - `params.resource` वही है जो client ने type किया। Processing के पूरे नियम + `params.resource` वही है जो client ने type किया। processing के पूरे नियम [Enterprise-Managed Authorization specification](https://modelcontextprotocol.io/extensions/auth/enterprise-managed-authorization) में हैं। ख़राब assertion को `TokenError("invalid_grant", ...)` से reject करें। इस flow का दूसरा error code `invalid_target` है: जो ID-JAG किसी ऐसे resource का नाम ले जिसे आप serve नहीं करते, उसे इसी से मना किया जाता है, और यही इस server को किसी और के resource के लिए tokens बनाने से रोकता है। और दिए गए scopes ID-JAG के `scope` claim से आते हैं (जिस assertion में यह न हो उसे भी मना किया जाता है); आपका server शायद इसकी जगह user के groups map करे। @@ -131,7 +132,7 @@ SDK खुद authorization server भी **बन** सकता है: `creat {"access_token": "mcp_...", "token_type": "Bearer", "expires_in": 300, "scope": "notes:read"} ``` - न `/authorize`, न `/register`, न protected-resource-metadata fetch। Wire पर सिर्फ़ ये requests हैं: + न `/authorize`, न `/register`, न protected-resource-metadata fetch। wire पर सिर्फ़ ये requests हैं: वह जिस पर `401` आया, well-known fetch, यह exchange, और फिर bearer लगा हुआ साधारण MCP traffic। और जो `sub` आपके validator ने ID-JAG से पढ़ा, tool के अंदर `get_access_token().subject` ठीक वही बताता है। diff --git a/i18n/hi/pages/client/transports.md b/i18n/hi/pages/client/transports.md index d049588dfe..a0e693209a 100644 --- a/i18n/hi/pages/client/transports.md +++ b/i18n/hi/pages/client/transports.md @@ -1,6 +1,6 @@ --- translation: - sections: [9cac816674181eb0, 7c157764133fea1f, 40b4916d82eaf1d4, 10d151f2cc75317f, 3d0832f39b0d7059, 92742ba36533633d, 0aeca6145e7bd302] + sections: [9cac816674181eb0, 5619e950d206e6c8, 40b4916d82eaf1d4, 10d151f2cc75317f, 3d0832f39b0d7059, 92742ba36533633d, 991c10e47fda2636] tool: 1 --- # Client transports {#client-transports} @@ -44,23 +44,41 @@ URL string पास करें और आपको **Streamable HTTP** मि * `httpx2.AsyncClient` आपका है, इसलिए उसे enter और exit भी **आप** ही करते हैं। SDK कभी ऐसे client को बंद नहीं करता जो उसने नहीं बनाया। * `streamable_http_client(url, http_client=...)` एक transport लौटाता है, और `Client(transport)` उसे किसी भी दूसरी चीज़ की तरह स्वीकार करता है। +`timeout=` को बनाए रखें। यह वही timeout है जो SDK का अपना client इस्तेमाल करता है (30 सेकंड, reads के लिए 300); इसके बिना बने `httpx2.AsyncClient` को `httpx2` का 5 सेकंड का default मिलता है, और उससे ज़्यादा देर चलने वाली tool call read timeout के साथ fail हो जाती है। + TLS पर एक बात: `httpx2` certificates को operating system के trust store ( [`truststore`](https://pypi.org/project/truststore/) के ज़रिए) से verify करता है, किसी bundled CA list से नहीं। ऐसे environment में जहाँ काम का system CA store न हो (कुछ minimal containers), standard `SSL_CERT_FILE`/`SSL_CERT_DIR` environment variables set करें या अपने `httpx2.AsyncClient` को explicit `verify=ssl_context` पास करें (पृष्ठभूमि -[`httpx` and `httpx-sse` replaced by `httpx2`](../migration.md#httpx-and-httpx-sse-replaced-by-httpx2) में है)। +[`httpx` और `httpx-sse` की जगह `httpx2`](../migration.md#httpx-and-httpx-sse-replaced-by-httpx2) में है)। + +### बड़े SSE events {#larger-sse-events} + +जब server कोई बड़ा tool result या notification एक ही SSE event में भेजता हो, तब `max_sse_event_size` पास करें: + +```python title="client.py" hl_lines="6-9" +--8<-- "docs_src/client_transports/tutorial005.py" +``` + +default हर event के लिए 1 MiB है, जिसे event के parse होने से पहले bytes में नापा जाता है। यह सीमा +POST responses, GET stream और resume हुए streams पर लागू होती है। POST response या resume हुए +stream में सीमा से बड़ा event उस request को SSE error के साथ fail कर देता है। background GET stream पर client +error को log करता है और stream को retry करता है। जब server पर भरोसा हो और बड़े events चाहिए हों, तो यह सीमा हटाने के लिए +`max_sse_event_size=None` set करें। JSON responses पर इसका असर नहीं पड़ता। अगर आप `ClientSessionGroup` इस्तेमाल करते हैं, तो +यही option `StreamableHttpParameters` पर set करें। !!! warning `streamable_http_client` पहले `headers=` और `timeout=` सीधे लेता था। अब नहीं लेता: - इसके parameters सिर्फ़ `url`, `http_client` और `terminate_on_close` हैं। आदत से `headers=` + इसके parameters `url`, `http_client`, `terminate_on_close` और `max_sse_event_size` हैं। आदत से `headers=` लिख दें तो यह मिलता है: ```text TypeError: streamable_http_client() got an unexpected keyword argument 'headers' ``` - HTTP से जुड़ी हर चीज़ अब उसी एक `httpx2.AsyncClient` पर रहती है जो आप पास करते हैं। + headers, authentication, proxies और timeouts उसी एक `httpx2.AsyncClient` पर रहते हैं जो आप पास करते हैं। + `max_sse_event_size` इसके बजाय MCP transport के SSE readers पर लागू होता है। !!! info `httpx2` जाना-पहचाना `httpx` API ही रखता है, इसलिए अगर आप `httpx` जानते हैं तो यहाँ auth, @@ -136,8 +154,9 @@ test में न कुछ deploy करना है, न कुछ launch ## सारांश {#recap} * `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 करें। +* headers, auth, proxies और timeouts उस `httpx2.AsyncClient` पर होने चाहिए जो आप `streamable_http_client(url, http_client=...)` को पास करते हैं। कोई `headers=` keyword नहीं है। +* हर SSE event की byte सीमा बदलने के लिए `streamable_http_client(url, max_sse_event_size=...)` इस्तेमाल करें। +* 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 करने के लिए जिसने उसे बनाया। diff --git a/i18n/hi/pages/handlers/cancellation.md b/i18n/hi/pages/handlers/cancellation.md new file mode 100644 index 0000000000..cf334b3319 --- /dev/null +++ b/i18n/hi/pages/handlers/cancellation.md @@ -0,0 +1,60 @@ +--- +translation: + sections: [07968345fdc0b84e, 4ea8416db9efa0dc, 336a7b4c5d0a4578, 18392e805dde6717, c30d50df43f9b55c] + tool: 1 +--- +# Cancellation {#cancellation} + +client किसी call को बीच में छोड़ सकता है: user ने stop दबा दिया, या timeout पूरा हो गया। + +ऐसा होने पर SDK **आपके handler को cancel कर देता है**। handler जिस `await` पर रुका है वह raise करता है, function unwind हो जाता है, और वह जो कुछ भी लौटाता है उसमें से कुछ नहीं भेजा जाता। ज़्यादातर handlers को इस बारे में कुछ करने की ज़रूरत नहीं होती। + +दो तरह के handlers को ज़रूरत होती है: वह handler जिसे कुछ cleanup करना हो, और वह handler जो सादा `def` हो। + +## `async def` tool में cleanup करना {#clean-up-in-an-async-def-tool} + +cleanup को `finally` में रखें: + +```python title="server.py" hl_lines="23 26-28" +--8<-- "docs_src/cancellation/tutorial001.py" +``` + +* tool चाहे जैसे खत्म हो, `finally` चलता है: tool ने कुछ लौटाया हो, raise किया हो, या cancel हुआ हो। +* जिस cleanup को `await` करना पड़े, उसे `shield=True` चाहिए। cancel हो चुके handler में आगे का हर `await` भी raise करता है, इसलिए shield के बिना `release_hold` अपनी पहली line पर ही रुक जाता। +* shielded block को कोई cancel नहीं कर सकता, इसलिए उसे समय सीमा दें। यहाँ वह `5` सेकंड है। + +!!! tip + `except` नहीं, `finally` इस्तेमाल करें। cleanup पूरा होने के बाद cancellation को ऊपर की ओर + बढ़ते रहना होता है, और `finally` उसे बढ़ने देता है। + +## सादे `def` tool में जल्दी रुकना {#stop-early-in-a-plain-def-tool} + +सादा `def` tool thread में चलता है, और thread को बाहर से कोई interrupt नहीं कर सकता। tool को खुद पूछना पड़ता है: + +```python title="server.py" hl_lines="22 25-26" +--8<-- "docs_src/cancellation/tutorial002.py" +``` + +* जब तक call चालू है, `anyio.from_thread.check_cancelled()` कुछ नहीं करता, और call cancel हो जाने के बाद raise करता है। इसे काम की इकाइयों के बीच call करें। +* यहाँ भी cleanup `finally` में जाता है। thread में कुछ भी await नहीं करता, इसलिए इसे shield की ज़रूरत नहीं होती। +* जो `def` tool कभी नहीं पूछता, वह अंत तक चलता है, और उसका नतीजा फेंक दिया जाता है। + +## यह कहाँ लागू होता है {#where-it-applies} + +prompt और resource functions ठीक tools की तरह cancel होते हैं। + +stdio और Streamable HTTP दोनों पर यह एक जैसा काम करता है। इस SDK के `Client` में call छोड़ने का मतलब है उस task को cancel करना जो `call_tool` को await कर रहा है, या उसका `read_timeout_seconds` पूरा हो जाने देना। + +!!! warning + Streamable HTTP के दो options यह खबर handler तक नहीं पहुँचने देते: `2026-07-28` connection पर + `json_response=True`, और legacy connection पर `stateless_http=True`। वहाँ client ने चाहे जो + किया हो, handler अंत तक चलता है। + +## सारांश {#recap} + +* जब client किसी call को छोड़ देता है, तो SDK handler को cancel कर देता है: tool, prompt या resource। +* `async def`: cleanup `finally` में करें, और जो cleanup await करता है उसे `anyio.move_on_after(seconds, shield=True)` के अंदर रखें। +* सादा `def`: काम की इकाइयों के बीच `anyio.from_thread.check_cancelled()` call करें, नहीं तो tool अंत तक चलता है। सादा `finally` cleanup कर देता है। +* `json_response=True` (modern connections) और `stateless_http=True` (legacy connections) cancellation को बंद कर देते हैं। + +progress और cancellation चलते हुए tool और उसके *caller* के बीच की बात हैं। tool जो lines **आपके** लिए, यानी server चलाने वाले व्यक्ति के लिए, log करता है, वे एक अलग channel हैं: **[Logging](logging.md)**। diff --git a/i18n/hi/pages/handlers/index.md b/i18n/hi/pages/handlers/index.md index 692d760eae..a142c2d8cf 100644 --- a/i18n/hi/pages/handlers/index.md +++ b/i18n/hi/pages/handlers/index.md @@ -1,6 +1,6 @@ --- translation: - sections: [424930166c4bc6f3] + sections: [22ca41e50cc1b536] tool: 1 --- # आपके handler के अंदर {#inside-your-handler} @@ -18,6 +18,7 @@ handler के arguments client से आते हैं। इसके **अ * **[Elicitation](elicitation.md)** से user से और input माँगना, और **[Multi-round-trip requests](multi-round-trip.md)**, 2026-07-28 का वह pattern जो इसे ले जाता है। * **[Sampling और roots](sampling-and-roots.md)** से client से LLM completion या उसके workspace folders माँगना; ये deprecated हैं पर अब भी serve होते हैं। * किसी धीमे काम पर **[Progress](progress.md)** बताना। +* client के call छोड़ देने पर **[Cancellation](cancellation.md)** से clean up करना, या पहले ही रुक जाना। * **[Logging](logging.md)** से logs लिखना (standard error पर, server चलाने वाले के लिए)। * **[Subscriptions](subscriptions.md)** से subscribe किए हुए clients को बताना कि कुछ बदला है। diff --git a/i18n/hi/pages/handlers/lifespan.md b/i18n/hi/pages/handlers/lifespan.md index 3b66d2b5c7..f6a7f35173 100644 --- a/i18n/hi/pages/handlers/lifespan.md +++ b/i18n/hi/pages/handlers/lifespan.md @@ -1,6 +1,6 @@ --- translation: - sections: [f3ca8ac5f90f2dfa, 85a1ef3588ba0736, 563346d4d5804933, 9e3528340d0bab53] + sections: [f3ca8ac5f90f2dfa, 48e478ef7bd688b1, 563346d4d5804933, 52ac6a7734d6f581] tool: 1 --- # Lifespan {#lifespan} @@ -46,7 +46,7 @@ lifespan **एक बार** चलता है। server शुरू हो `genre` ही एकमात्र argument है जो model दे सकता है। lifespan आपके server का अपना मामला है। -`@mcp.resource()` और `@mcp.prompt()` functions भी `ctx` parameter ले सकते हैं, जिसे सिर्फ़ `Context` लिखा जाता है; इसकी वजह अगला section बताता है। `ctx` में जो कुछ भी है, वह सब **[Context](context.md)** में है। +`@mcp.resource()` और `@mcp.prompt()` functions भी `ctx` parameter ले सकते हैं। `ctx` में जो कुछ भी है, वह सब **[Context](context.md)** में है। ### यह सच में typed है {#it-really-is-typed} @@ -56,19 +56,6 @@ annotation को फिर से देखें: `ctx: Context[AppContext]` इसकी जगह सिर्फ़ `Context` लिखें तो `lifespan_context` का type `dict[str, Any]` हो जाता है: type checker के पास यह जानने का कोई तरीका नहीं कि आपके lifespan ने क्या yield किया। runtime पर object फिर भी मौजूद रहता है; बस मदद चली जाती है। -!!! warning - `Context[AppContext]` **सिर्फ़ tools के लिए** लिखने का तरीका है। इसे किसी `@mcp.resource()` या - `@mcp.prompt()` function पर लगाएँ तो उस handler की हर call विफल हो जाती है। client को error वापस मिलता है, - और server log बताता है क्यों: - - ```text - Context is not available outside of a request - ``` - - resources और prompts में सिर्फ़ `ctx: Context` लिखें। आपके lifespan ने जो object yield किया वह - runtime पर अब भी `ctx.request_context.lifespan_context` ही है; आप type parameter छोड़ते हैं, - object नहीं। - !!! tip lifespan हमेशा होता है। अगर आप कोई pass नहीं करते, तो SDK का default एक खाली `dict` yield करता है, इसलिए `ctx.request_context.lifespan_context` `{}` होता है, कभी `None` नहीं। इसी default की वजह से @@ -101,7 +88,7 @@ server को सिर्फ़ lifecycle तक सीमित कर दे * `yield` से पहले का code startup है। उसके बाद का `finally` shutdown है। * यह एक बार चलता है, server की पूरी ज़िंदगी के इर्द-गिर्द, हर request पर नहीं। * आप जो भी `yield` करते हैं, वह हर tool, resource और prompt में `ctx.request_context.lifespan_context` है। -* `ctx: Context[AppContext]` tools में इस access को पूरी तरह typed बना देता है। resources और prompts सिर्फ़ `Context` लेते हैं। +* `ctx: Context[AppContext]` इस access को पूरी तरह typed बना देता है। * `lifespan=` न हो तो खाली `dict` मिलता है, कभी `None` नहीं। जो handler call के बीच रुककर user से वह पूछता है जो सिर्फ़ user ही जानता है, वह **[Elicitation](elicitation.md)** है। diff --git a/i18n/hi/pages/handlers/progress.md b/i18n/hi/pages/handlers/progress.md index 0ccf140b72..971974fdc2 100644 --- a/i18n/hi/pages/handlers/progress.md +++ b/i18n/hi/pages/handlers/progress.md @@ -1,6 +1,6 @@ --- translation: - sections: [5315262fe26b33e1, 9d8e98840f1b78f0, 52d6009a07e770ea, 8534d8dbb4053a70, 2966fac6fe697007] + sections: [5315262fe26b33e1, 9d8e98840f1b78f0, 52d6009a07e770ea, 8534d8dbb4053a70, 2e9aff14d3a882c0] tool: 1 --- # Progress {#progress} @@ -115,9 +115,9 @@ Callback को `total=None` मिलता है। Client अब भी **ac ## सारांश {#recap} * `Context` लेने वाले किसी भी tool से `await ctx.report_progress(progress, total=None, message=None)`। -* Client `call_tool` को `progress_callback=` देता है: हर call पर, कभी `Client` पर नहीं। -* Callback `async (progress, total, message) -> None` है और tool के चलते रहने के दौरान ही fire होता है। -* Call पर callback न हो तो `report_progress` कुछ नहीं करता। बिना शर्त report करें। +* client `call_tool` को `progress_callback=` देता है: हर call पर, कभी `Client` पर नहीं। +* callback `async (progress, total, message) -> None` है और tool के चलते रहने के दौरान ही fire होता है। +* call पर callback न हो तो `report_progress` कुछ नहीं करता। बिना शर्त report करें। * जब `total` पता न हो तो उसे छोड़ दें; callback को `None` मिलता है। -Progress वह है जो चलता हुआ tool **user** को दिखाता है। जो lines वह **आपके** लिए, यानी server चलाने वाले व्यक्ति के लिए log करता है, वे एक अलग channel हैं: **[Logging](logging.md)**। +progress उस client के लिए है जो अब भी इंतज़ार कर रहा है। client के इंतज़ार करना छोड़ देने पर आपके tool को जो दिखता है, वह **[Cancellation](cancellation.md)** है। diff --git a/i18n/hi/pages/handlers/subscriptions.md b/i18n/hi/pages/handlers/subscriptions.md index b7fc4833e8..c1512b1589 100644 --- a/i18n/hi/pages/handlers/subscriptions.md +++ b/i18n/hi/pages/handlers/subscriptions.md @@ -1,6 +1,6 @@ --- translation: - sections: [60a9de8a0bdaa531, 317bbe7e4355cdcc, a61d660c8029e04a, 8f7e82fcb88df8a9, b165db51249ff8ed, 266f56fb798068a4, 7c0e57030b622139, df18d7c2417a9883] + sections: [60a9de8a0bdaa531, 6693607ea56d8bd6, a61d660c8029e04a, 8f7e82fcb88df8a9, b165db51249ff8ed, b8bc624a627ead9b, 2139e68e36d9e621, 7c0e57030b622139, 34ab1af2b9ab5b45] tool: 1 --- # Subscriptions {#subscriptions} @@ -22,7 +22,7 @@ client को इसकी खबर **subscriptions** से मिलती * इसके साथी `notify_prompts_changed()` और `notify_resources_changed()` हैं। * कोई subscriber नहीं, तो कोई काम नहीं। खाली बैठे server पर publish करना no-op है, इसलिए आपको कभी जाँचना नहीं पड़ता कि कोई सुन रहा है या नहीं। आप बस बताते हैं कि क्या बदला। -`MCPServer` आपके लिए `subscriptions/listen` serve करता है। wire की ज़िम्मेदारियाँ (पहले frame के रूप में acknowledgment, हर stream के हिसाब से filtering, हर frame पर subscription id) SDK संभालता है। +`MCPServer` आपके लिए `subscriptions/listen` serve करता है, जब तक आप [इसे बंद न कर दें](#turning-it-off)। wire की ज़िम्मेदारियाँ (पहले frame के रूप में acknowledgment, हर stream के हिसाब से filtering, हर frame पर subscription id) SDK संभालता है। !!! check wire पर, जिस stream के filter में `board://sprint` का नाम था वह `complete_task` चलने के बाद ऐसा दिखता है: @@ -79,7 +79,32 @@ middleware का पूरा contract, यह और क्या-क्या publishes आपके handler से खुले streams तक `SubscriptionBus` के ज़रिए पहुँचते हैं। default in-memory है: एक process, उसके अंदर का हर stream। यही सही जवाब है जब तक आप load balancer के पीछे replicas नहीं चलाते, क्योंकि तब client का stream एक replica से बँध जाता है, और किसी दूसरे replica पर हुए publish को उस तक पहुँचना होता है। -यह जोड़ आपको implement करना है: आपके pub/sub backend के ऊपर दो methods। +default bus के साथ वह नहीं पहुँच सकता, क्योंकि हर replica का अपना bus होता है: + +```mermaid +flowchart LR + client[Client] --> lb[Load balancer] + lb --> stream + lb ~~~~ gap + lb --> tool + subgraph B [Replica B] + tool[tools/call] -- publishes --> busB[(bus B)] + end + gap[(no shared bus)] + subgraph A [Replica A] + stream[listen stream] -- subscribed --> busA[(bus A)] + end + style A fill:none + style B fill:none + style gap fill:none,stroke-dasharray:4 4 +``` + +कुछ भी fail नहीं होता: call सफल होता है, और stream चुप रहता है। इसलिए load balancer के पीछे, इनमें से एक चुनें: + +* **आपको change notifications चाहिए।** हर replica को एक ही bus दें, जैसा नीचे है। +* **आपको नहीं चाहिए।** [इन्हें बंद कर दें](#turning-it-off), ताकि किसी client से ऐसे events का वादा न हो जो उसे मिलेंगे ही नहीं, और न कोई client उनके लिए stream खुला रखे। + +shared bus आपको implement करना है: आपके pub/sub backend के ऊपर दो methods। ```python from collections.abc import Callable @@ -128,6 +153,20 @@ async def tools_reloaded() -> None: await bus.publish(ToolsListChanged()) # from a lifespan task, a webhook, anywhere ``` +## इसे बंद करना {#turning-it-off} + +जिस server का catalog कभी नहीं बदलता, उसके पास publish करने को कुछ नहीं होता। server बनाते समय ही यह बता दें: + +```python title="server.py" hl_lines="3" +--8<-- "docs_src/subscriptions/tutorial007.py" +``` + +* `2026-07-28` client को कोई change notification advertise होता नहीं दिखता, और `subscriptions/listen` request को खुले stream की जगह *Method not found* मिलता है। +* `ctx.notify_*` अब भी काम करता है और किसी तक नहीं पहुँचता, इसलिए आपके handlers नहीं बदलते। +* पहले के protocol versions वाले clients को कोई फ़र्क नहीं दिखता। + +खुला stream ऐसी request है जो कभी पूरी नहीं होती, इसलिए यह उस host पर भी मायने रखता है जो request की अवधि के हिसाब से bill करता है। + ## Low-level composition {#the-low-level-composition} low-level `Server` पर पहले से कुछ भी जुड़ा हुआ नहीं है, और वही हिस्से तीन lines में जुड़ जाते हैं: @@ -148,5 +187,6 @@ low-level `Server` पर पहले से कुछ भी जुड़ा * client वाला सिरा `async with client.listen(...)` है: उसकी कहानी *Clients* के नीचे **[Subscriptions](../client/subscriptions.md)** में है। * low-level `Server` पर आप वही हिस्से खुद जोड़ते हैं: एक bus, `ListenHandler(bus)`, `on_subscriptions_listen` slot। * scale out करने का मतलब है `SubscriptionBus` implement करना, बस दो methods, और उसे `MCPServer(subscriptions=...)` के रूप में pass करना। +* publish करने को कुछ नहीं, या replicas के बीच कोई shared bus नहीं: `MCPServer(subscriptions=False)` कोई change notification advertise नहीं करता और कोई stream खुला नहीं रखता। यह सब serve करने वाले server को चलाना, एक replica के पीछे हो या बीस के, **[Deploy और scale](../run/deploy.md)** में है। diff --git a/i18n/hi/pages/run/deploy.md b/i18n/hi/pages/run/deploy.md index 9afb39927f..e31343ce54 100644 --- a/i18n/hi/pages/run/deploy.md +++ b/i18n/hi/pages/run/deploy.md @@ -1,6 +1,6 @@ --- translation: - sections: [28221886b198784f, f88ea1f1614f3a1d, 2e76f5cb9df15042, ce926d686730b6d0, 3be24f8ad8bb5ab9, 3fad24032b2224ff, f25a7f860e579ecb, 697b01d95080880d] + sections: [28221886b198784f, f88ea1f1614f3a1d, 2e76f5cb9df15042, ce926d686730b6d0, 3be24f8ad8bb5ab9, aaf489e944ecf5d1, f25a7f860e579ecb, 697b01d95080880d] tool: 1 --- # Deploy और scale करना {#deploy-scale} @@ -160,19 +160,20 @@ seal के बारे में बाकी सब कुछ **[`requestStat ## अलग-अलग replicas के बीच change notifications {#change-notifications-across-replicas} -client की `subscriptions/listen` stream एक लंबे समय तक चलने वाला response है, इसलिए वह अपनी पूरी ज़िंदगी एक replica से बँधी रहती है। किसी **दूसरे** replica पर publish हुआ `ctx.notify_resource_updated(...)` उस तक पहुँचना चाहिए। +client का `subscriptions/listen` stream एक लंबे समय तक चलने वाला response है, इसलिए वह अपनी पूरी ज़िंदगी एक replica से बँधा रहता है। किसी **दूसरे** replica पर publish हुआ `ctx.notify_resource_updated(...)` उस तक पहुँचना चाहिए। -दोनों के बीच का जोड़ `SubscriptionBus` है। आप server को जो भी bus देते हैं, हर publish उसी में जाता है और हर खुली stream उसी को सुनती है, इसलिए हर replica को वही bus दें: +दोनों के बीच का जोड़ `SubscriptionBus` है। आप server को जो भी bus देते हैं, हर publish उसी में जाता है और हर खुला stream उसी को सुनता है, इसलिए हर replica को वही bus दें: ```python title="server.py" hl_lines="2 7 9" --8<-- "docs_src/deploy/tutorial004.py" ``` -fan-out को इससे कोई मतलब नहीं कि stream किस server object से जुड़ी है। एक ही `InMemorySubscriptionBus` रखने वाले दो servers पहले से ऐसे ही बर्ताव करते हैं: एक पर listen stream खोलें, दूसरे पर `edit_note` चलाएँ, और stream को इसकी ख़बर मिल जाती है। वह in-memory bus सिर्फ़ एक process के अंदर के server objects तक फैलता है, इसलिए यह model है, deployment नहीं: +fan-out को इससे कोई मतलब नहीं कि stream किस server object से जुड़ा है। एक ही `InMemorySubscriptionBus` रखने वाले दो servers पहले से ऐसे ही बर्ताव करते हैं: एक पर listen stream खोलें, दूसरे पर `edit_note` चलाएँ, और stream को इसकी ख़बर मिल जाती है। वह in-memory bus सिर्फ़ एक process के अंदर के server objects तक फैलता है, इसलिए यह model है, deployment नहीं: * असली processes के बीच, **SDK में ऐसा कोई bus नहीं आता जो आपकी मदद कर सके।** `SubscriptionBus` दो methods वाला `Protocol` है (`publish` और `subscribe`) जिसे आप अपने pub/sub backend (Redis, NATS, जो भी आप पहले से चलाते हैं) के ऊपर implement करते हैं और `MCPServer(subscriptions=...)` के रूप में देते हैं। sketch और contract **[Subscriptions](../handlers/subscriptions.md#scaling-past-one-process)** में हैं। * bus चार छोटे typed events ढोता है, JSON-RPC कभी नहीं। Acknowledgment, filtering, और stream lifecycle SDK में ही रहते हैं, इसलिए आपका bus protocol तोड़ नहीं सकता; वह सिर्फ़ events को processes के बीच ले जा सकता है। -* Streams resumable **नहीं** हैं और events replay **नहीं** होते। कोई replica खो जाए तो उसकी streams गिर जाती हैं; clients फिर से listen और फिर से fetch करते हैं। साझा करने को कोई event store नहीं और configure करने को और कुछ नहीं। यह वह एक जगह है जहाँ scale out करना सच में बस वही चीज़ और ज़्यादा है। +* Streams resumable **नहीं** हैं और events replay **नहीं** होते। कोई replica खो जाए तो उसके streams गिर जाते हैं; clients फिर से listen और फिर से fetch करते हैं। साझा करने को कोई event store नहीं और configure करने को और कुछ नहीं। यह वह एक जगह है जहाँ scale out करना सच में बस वही चीज़ और ज़्यादा है। +* जिस server को change notifications की ज़रूरत नहीं, वह bus छोड़ देता है: **[इन्हें बंद कर दें](../handlers/subscriptions.md#turning-it-off)**। ## SDK आपको क्या नहीं देता {#what-the-sdk-does-not-give-you} diff --git a/i18n/hi/pages/servers/structured-output.md b/i18n/hi/pages/servers/structured-output.md index ec29b68bc0..ccbb5e4efb 100644 --- a/i18n/hi/pages/servers/structured-output.md +++ b/i18n/hi/pages/servers/structured-output.md @@ -1,6 +1,6 @@ --- translation: - sections: [a838d57f003aed44, 857d03886a0137ed, 42d9efcb9f542867, 2290ff08435b5573, 91be9b73602abcf1, 6cdbad079f7b47f0, d4b607372fb28b51, 18dbf726ac45e0b7, c7eff2a5698225fa, c851964bb3301907, 8f296f1f09e4c400, d715db6f8dccc9cc, a0c344a48450dbe4] + sections: [a838d57f003aed44, 857d03886a0137ed, 42d9efcb9f542867, 2290ff08435b5573, 91be9b73602abcf1, 6cdbad079f7b47f0, d4b607372fb28b51, 7608fc5ebc31d6ea, c7eff2a5698225fa, c851964bb3301907, 8f296f1f09e4c400, d715db6f8dccc9cc, a0c344a48450dbe4] tool: 1 --- # Structured output {#structured-output} @@ -174,6 +174,8 @@ result.structured_content # {"London": 16.2, "Reykjavik": 4.4} keys का `str` होना ज़रूरी है। `dict[int, float]` JSON object नहीं बन सकता, इसलिए यह वापस `{"result": ...}` wrapper पर आ जाता है। +dictionary results के validation और serialization के लिए Pydantic का `TypeAdapter` इस्तेमाल होता है। अगर आप किसी tool का `FuncMetadata.output_model` देखें, तो उसमें dictionary का type annotation अपने schema title के साथ होता है। + ## Validation {#validation} `output_schema` documentation नहीं है। आपका function जो भी लौटाता है, server से बाहर जाने से पहले उसे **इसके मुक़ाबले validate** किया जाता है। diff --git a/i18n/hi/pages/servers/tools.md b/i18n/hi/pages/servers/tools.md index 2b6405e13e..52fe69fa32 100644 --- a/i18n/hi/pages/servers/tools.md +++ b/i18n/hi/pages/servers/tools.md @@ -1,6 +1,6 @@ --- translation: - sections: [e4cc390d56573409, f30cf8103a6e918c, 2c97b9f888398951, 048e5471dfa71aea, 3076b1e16ad95950, edbedf2a16e71311, 3d8ef8da89fa87c1, f6c0e02e6ea5a363] + sections: [e4cc390d56573409, f30cf8103a6e918c, 2c97b9f888398951, 048e5471dfa71aea, 3076b1e16ad95950, c1115cd005b81e8f, 3d8ef8da89fa87c1, f6c0e02e6ea5a363] tool: 1 --- # Tools {#tools} @@ -141,7 +141,7 @@ type को `Annotated` में लपेटें और एक Pydantic `Fie अगर कोई tool I/O करता है (कोई API call करता है, file पढ़ता है, database से query करता है), तो उसे `async def` declare करें और उसके अंदर `await` करें। SDK उसे await करता है। -सादा `def` tool भी चलता है: SDK उसे एक thread में चलाता है ताकि वह server को कभी block न करे। +सादा `def` tool भी चलता है: SDK उसे एक thread में चलाता है ताकि वह server को कभी block न करे। लंबा चलने वाला tool यह जाँच सकता है कि client अभी भी इंतज़ार कर रहा है या नहीं; देखें **[Cancellation](../handlers/cancellation.md)**। और कुछ configure करने को नहीं है। diff --git a/i18n/hi/pages/troubleshooting.md b/i18n/hi/pages/troubleshooting.md index a8088dd31d..3c1967b4dc 100644 --- a/i18n/hi/pages/troubleshooting.md +++ b/i18n/hi/pages/troubleshooting.md @@ -1,6 +1,6 @@ --- translation: - sections: [3d58228e81b99543, 170514ce901c4139, 17d61fad0a50d62b, 8a6e351ec756904d, 137454d469c867f5, 6392596bd6df54f0, 41126fa9c4fe432f, 480b6d7897e30ab4, d83bb682e708dde0, ebbed3449c499db4, 525cdf1755e29d4c, 30fd31be74169d9a, d2e88333d4f7841f, c2dc3b1007d2e987, d6eabf60cc366341, f798e815252852c2, 0cba47bae78d04eb, 2c218ba829abf74e] + sections: [3d58228e81b99543, 170514ce901c4139, 17d61fad0a50d62b, 8a6e351ec756904d, 137454d469c867f5, fcf984fa0615ed11, 6392596bd6df54f0, 41126fa9c4fe432f, 480b6d7897e30ab4, d83bb682e708dde0, ebbed3449c499db4, 525cdf1755e29d4c, 30fd31be74169d9a, d2e88333d4f7841f, c2dc3b1007d2e987, d6eabf60cc366341, f798e815252852c2, 0cba47bae78d04eb, cdc6d86a4dae8a34] tool: 1 --- # समस्याएँ सुलझाना {#troubleshooting} @@ -128,6 +128,14 @@ parentheses जोड़ें। यही चूक होने पर `@mcp. **disconnected**) दिखाता है, वह इसी shape का है: खुद `python server.py` चलाएँ और traceback पढ़ें। type checker भी इसे पकड़ लेता है: function कोई valid `name=` नहीं है। +## `InvalidSignature: Tool '' has an invalid x-mcp-header annotation: ` {#invalidsignature-tool-name-has-an-invalid-x-mcp-header-annotation-reason} + +किसी tool argument को `x-mcp-header` से ऐसे तरीके से mark किया गया है जिसकी spec अनुमति नहीं देता, और `` बताता है कि कौन-सा नियम टूट रहा है। `2026-07-28` वाले clients ऐसे tool को अपनी listing से बाहर छोड़ देते, इसलिए SDK उसे register करने से मना कर देता है। + +सिर्फ़ `str`, `int` और `bool` arguments ही mark किए जा सकते हैं, और `str | None` इनमें से कोई नहीं है। optional argument को कैसे लिखना है, यह **[Header parameters](advanced/header-parameters.md)** में है। + +ऊपर वाली entry की तरह, यह भी module **import** होते ही raise हो जाता है, किसी भी client के जुड़ने से पहले। + ## `Tool already exists: ` {#tool-already-exists-name} दो registrations ने एक ही tool नाम इस्तेमाल किया। **पहला** जीतता है, दूसरा चुपचाप छोड़ दिया जाता है, और **server log** में आने वाली यह warning ही इसका इकलौता संकेत है: @@ -425,6 +433,7 @@ mcp = MCPServer("Weather", request_state_security=RequestStateSecurity(keys=[key * `ExceptionGroup: unhandled errors in a TaskGroup` कभी असली error नहीं है। **आखिरी line** पढ़ें; `async with Client(...)` block के **अंदर** `MCPError` catch करने से wrapping पूरी तरह टल जाती है। * `call_tool` fail होने वाले tool के लिए raise नहीं करता। `Error executing tool ...` और `Unknown tool: ...` results हैं: `result.is_error` जाँचें। tool के नाम के बाद कोई message न हो तो मतलब वह crash हुआ, और traceback server log में है। * `Client must be used within an async context manager` -> `async with` इस्तेमाल करें। `Use @tool() instead of @tool` -> parentheses जोड़ें। +* `has an invalid x-mcp-header annotation` -> सिर्फ़ `str`, `int` और `bool` arguments ही mark किए जा सकते हैं। * 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 नहीं किया। diff --git a/i18n/hi/pages/whats-new.md b/i18n/hi/pages/whats-new.md index 7178b87f28..8bffd6ec1f 100644 --- a/i18n/hi/pages/whats-new.md +++ b/i18n/hi/pages/whats-new.md @@ -1,6 +1,6 @@ --- translation: - sections: [cfe01c0c5863dfa2, 0dbb68d8b210177b, 80cf193023af3ed4, 875eb2889263424e] + sections: [cfe01c0c5863dfa2, 0dbb68d8b210177b, bd42ee3a268f6ea4, 875eb2889263424e] tool: 1 --- # v2 में नया क्या है {#whats-new-in-v2} @@ -204,7 +204,7 @@ publishing और serving **[Subscriptions](handlers/subscriptions.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)** में हैं। +* **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 कर सकते हैं। **[Header parameters](advanced/header-parameters.md)** दिखाता है कि किसी argument को कैसे mark करें। * **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 के हिसाब से व्यवस्थित है। diff --git a/i18n/ja/pages/advanced/header-parameters.md b/i18n/ja/pages/advanced/header-parameters.md new file mode 100644 index 0000000000..22fed434b2 --- /dev/null +++ b/i18n/ja/pages/advanced/header-parameters.md @@ -0,0 +1,65 @@ +--- +translation: + sections: [81862a209b483d27, b76e8073487afa03, bc214b2fc2bcdae4, d5835477c0b60163, a5d9786f902ad8e1] + tool: 1 +--- +# ヘッダーパラメーター {#header-parameters} + +ほとんどのサーバーでは、この機能は必要ありません。 + +サーバーの前段にあるゲートウェイやロードバランサーは、ボディを解析せずに読み取れる情報でしかルーティングできません。ツールの引数を `x-mcp-header` でマークすると、`2026-07-28` の **[プロトコルバージョン](../protocol-versions.md)** を使うクライアントは、その値を HTTP ヘッダーとしても送信します。 + +## 引数をマークする {#mark-an-argument} + +マークは、引数の JSON Schema に追加するキー 1 つです。`MCPServer` では、`Field` がそのキーを追加します。 + +```python title="server.py" hl_lines="13" +--8<-- "docs_src/header_parameters/tutorial001.py" +``` + +* `2026-07-28` の Streamable HTTP では、クライアントはボディに加えて `Mcp-Param-Region` を送信し、サーバーは両者が食い違う呼び出しを拒否します。 +* ツールを一覧取得していないクライアントは、マークを見たことがありません。そのためヘッダーを送信せず、呼び出しは拒否されます。この SDK の `Client` は、その場合にツールを一覧取得して呼び出しを 1 回だけ再送するので、先に一覧取得しておいても節約できるのはラウンドトリップ 1 回分だけです。 +* それ以外の接続では、このアノテーションは無視されます。 + +関数は変わりません。`region` はこれまでどおり引数として渡されます。 + +## マークできるもの {#what-can-be-marked} + +`str`、`int`、`bool` の引数です。それ以外は、ツールの登録時に `InvalidSignature` で拒否されます。 + +単一の型を持たない `str | None` も同様に拒否されます。省略可能な引数では、Pydantic の `WithJsonSchema` を使ってスキーマを明示する必要があります。 + +```python +region: Annotated[str | None, WithJsonSchema({"type": "string", "x-mcp-header": "Region"})] = None +``` + +## 低レベルの `Server` の場合 {#on-the-low-level-server} + +こちらでは `input_schema` を手書きするので、キーをそのまま書き込みます。 + +```python title="server.py" hl_lines="18" +--8<-- "docs_src/header_parameters/tutorial002.py" +``` + +* アノテーションは何もチェックされません。不正なものもそのまま配信され、`2026-07-28` のクライアントはそのツールを一覧から除外します。 + +### 名前からスキーマを取得する {#schemas-by-name} + +ヘッダーをチェックするには、SDK は呼び出しをディスパッチする前にツールの入力スキーマを必要とします。`get_tool_input_schema` がない場合、SDK は引数を伴う呼び出しのたびに `on_list_tools` ハンドラーを実行してスキーマを取得します。マークされたツールがあるかどうかは関係ありません。 + +```python title="server.py" hl_lines="26 39-41 48" +--8<-- "docs_src/header_parameters/tutorial003.py" +``` + +* この関数を渡すと、すでに手元にある情報から応答できます。 +* チェックするものがないツールには `None` を返してください。 + +## まとめ {#recap} + +* ツールの引数に `x-mcp-header` を付けると、`2026-07-28` のクライアントはその値を `Mcp-Param-*` HTTP ヘッダーとしても送信します。 +* サーバーは、ヘッダーとボディが食い違う呼び出しを拒否します。 +* マークできるのは `str`、`int`、`bool` の引数だけです。それ以外の場合、`MCPServer` は `InvalidSignature` を送出します。 +* 低レベルの `Server` は何もチェックせず、クライアントはアノテーションが不正なツールを除外します。 +* `get_tool_input_schema` を使うと、低レベルの `Server` が呼び出しのたびに `on_list_tools` を実行するのを避けられます。 + +手書きで扱う `Server` API の残りの部分は、**[低レベルの Server](low-level-server.md)** で説明しています。 diff --git a/i18n/ja/pages/advanced/index.md b/i18n/ja/pages/advanced/index.md index 191fb34c8d..0b7ae52e72 100644 --- a/i18n/ja/pages/advanced/index.md +++ b/i18n/ja/pages/advanced/index.md @@ -1,6 +1,6 @@ --- translation: - sections: [ca6988b7503cd2d3] + sections: [348f8697c6b12cd0] tool: 1 --- # 高度なトピック {#advanced} @@ -9,6 +9,7 @@ translation: * **[低レベルの Server](low-level-server.md)**:`MCPServer` の土台になっているクラスです。スキーマは手書き、ハンドラーは `on_*`、代わりにチェックしてくれるものは何もなく、独自のカスタム JSON-RPC メソッドも定義できます。 * **[ページネーション](pagination.md)** と **[ミドルウェア](middleware.md)**:どちらも低レベルの `Server` でしかできないことです。 +* **[ヘッダーパラメーター](header-parameters.md)**:ゲートウェイが、ツール呼び出しをその引数の 1 つに基づいてルーティングできるようにします。 * **[拡張機能](extensions.md)** と **[MCP Apps](apps.md)**:プロトコルの拡張のための領域です。拡張パッケージをサーバーに組み込むことも、自分で書くこともできます。 ここにありそうだと思われるもののいくつかは、実際に使う場所のほうに置かれています。 diff --git a/i18n/ja/pages/advanced/low-level-server.md b/i18n/ja/pages/advanced/low-level-server.md index 6681b622e2..ce706333df 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, 9d5d10a5f0405d0a, 1086e77ce561cd7f, a3f71823df5efc31, 9fc7109f72201cae, d50fe7faead8cf68, 7bf25983df655b66, 6330e1f4c6029683, 2f1749c8c133fa1c, 8db7116fc8ddd0ee, ebc33704fbd74262, 0fde3bcea081ba3a] + sections: [2c79b6338e09b7ac, 9d5d10a5f0405d0a, 1086e77ce561cd7f, a3f71823df5efc31, 9fc7109f72201cae, d50fe7faead8cf68, 7bf25983df655b66, 6330e1f4c6029683, 2f1749c8c133fa1c, 8db7116fc8ddd0ee, 2090d99b355bc2c7, 0fde3bcea081ba3a] tool: 1 --- # 低レベルの Server {#the-low-level-server} @@ -208,6 +208,7 @@ use Server.middleware to observe or wrap initialization * `on_call_tool`、`on_get_prompt`、`on_read_resource` は、通常の結果の代わりに `InputRequiredResult` を返して呼び出しを一時停止し、クライアントに入力を求めることができます。**[マルチラウンドトリップ(multi-round-trip)リクエスト](../handlers/multi-round-trip.md)** を参照してください。この層らしく、何も代わりにインストールされません。`MCPServer` はデフォルトで `requestState` を封印しますが、ここでは設定した `request_state` は書いたとおりに通信路を渡ります。`server.middleware.append(RequestStateBoundary(RequestStateSecurity(keys=[...]), default_audience=server.name))` でオプトインするまではそうです。この 1 行(どちらの名前も `mcp.server.request_state` からインポートします)で、`MCPServer` が行うのとまったく同じ封印と検証が得られます(**[`requestState` の保護](../handlers/multi-round-trip.md#protecting-requeststate)**)。 * `on_list_resources`、`on_read_resource`、`on_list_prompts`、`on_get_prompt`、`on_completion` は、ほかのプリミティブ向けの同じ `(ctx, params) -> result` の形です。 * `on_subscriptions_listen` は 2026-07-28 の `subscriptions/listen` ストリームを提供します。`SubscriptionBus` の上に構築した `ListenHandler` を渡し、ほかのハンドラーからバスにイベントを発行してください。全体の組み立て方については **[サブスクリプション](../handlers/subscriptions.md)** を参照してください。 +* `get_tool_input_schema` は `on_list_tools` を呼び出し経路から外します。**[ヘッダーパラメーター](header-parameters.md#schemas-by-name)** を参照してください。 * `server.streamable_http_app()` は `MCPServer` のものと同じ Starlette アプリを返します。**[サーバーの実行](../run/index.md)** がほかの ASGI アプリをデプロイするのと同じ方法でデプロイしてください。この層には `server.run(transport=...)` はありません。`server.run(read_stream, write_stream, server.create_initialization_options())` が 1 組のストリーム上で 1 つの接続を駆動し、その 1 行がすべてです。 ## まとめ {#recap} diff --git a/i18n/ja/pages/advanced/middleware.md b/i18n/ja/pages/advanced/middleware.md index c7a8c6226b..84408e1c66 100644 --- a/i18n/ja/pages/advanced/middleware.md +++ b/i18n/ja/pages/advanced/middleware.md @@ -1,6 +1,6 @@ --- translation: - sections: [6048b4f308edbb8c, 46056f318ef205e4, c3e565b61acd75c5, c62422b159c6ed09, 420968f514138f43] + sections: [58e1103d9a323ccf, 46056f318ef205e4, 812b414557fb0c35, 4df162eea2518d38, c62422b159c6ed09, 420968f514138f43] tool: 1 --- # ミドルウェア {#middleware} @@ -45,12 +45,28 @@ tools/call took 0.1 ms * サーバーに届くすべてのリクエストとすべての通知。通知の場合は `ctx.request_id is None` であり、`call_next(ctx)` は `None` を返し、何を返しても破棄されます。(`2026-07-28` の Streamable HTTP 経路では、クライアントの通知 POST はトランスポートで `202` として受領されるだけでディスパッチされないため、ミドルウェアにも届きません。このリビジョンは HTTP 上でのクライアントからサーバーへの通知を定義していません。) * サーバーにハンドラーがないメソッドでさえ対象です。`call_next` は `MCPError(-32601, "Method not found")` を送出し、それがミドルウェアを「通り抜けて」クライアントへ向かいます。 +## 同時実行数の上限 {#a-concurrency-cap} + +ミドルウェアは必ずしも `call_next(ctx)` を呼ぶ必要はありません。代わりに `MCPError` を送出すると、そのメッセージ 1 つが**拒否**されます。接続は維持され、次のメッセージは通ります。 + +たとえば、検索のたびに 4 本の接続プールから接続を 1 本占有するとします。このミドルウェアは、ツール呼び出しを同時に 4 つまで実行させ、5 つ目は拒否します。 + +```python title="server.py" hl_lines="15-16 40-55 59" +--8<-- "docs_src/middleware/tutorial002.py" +``` + +* 数えるのは `tools/call` だけなので、ツール呼び出しを拒否している間も、サーバーは `server/discover` と `tools/list` に応答し続けます。 +* MCP は「サーバーがビジー」を表すエラーコードを定義していないので、`SERVER_BUSY` はこのサーバー独自のものです。 +* 拒否すると、サーバーが過負荷であることがクライアントにすぐ伝わります。呼び出し側を待たせたい場合は、代わりに `call_next(ctx)` を `anyio.CapacityLimiter` で囲んでください。 + +送出した `MCPError` は、モデルではなくクライアントアプリケーションに届きます。モデルにメッセージを読ませたい場合は、代わりに `is_error=True` のツール結果を返してください。これは下で説明する**応答する**にあたります。 + ## ミドルウェアの中でできること {#what-you-can-do-inside-one} ためらうべき度合いが小さいものから順に並べます。 -* **観察する。** 時間を計る、数える、ログに出す。上の例がこれです。 -* **拒否する。** `call_next(ctx)` を呼ぶ「代わりに」`MCPError` を送出すると、そのメッセージ 1 つに JSON-RPC エラーで応答します。接続は維持され、次のメッセージは通ります。サーバーが呼び出し側ごとに `subscriptions/listen` を制御するのはこの方法です。サブスクリプションのページの**[誰が監視できるかを決める](../handlers/subscriptions.md#deciding-who-may-watch)**で順を追って説明しています。 +* **観察する。** 時間を計る、数える、ログに出す。上の計時ミドルウェアがこれです。 +* **拒否する。** `call_next(ctx)` を呼ぶ「代わりに」`MCPError` を送出すると、そのメッセージ 1 つに JSON-RPC エラーで応答します。接続は維持され、次のメッセージは通ります。上の同時実行数の上限がこれです。サーバーが呼び出し側ごとに `subscriptions/listen` を制御するのもこの方法です。サブスクリプションのページの**[誰が監視できるかを決める](../handlers/subscriptions.md#deciding-who-may-watch)**で順を追って説明しています。 * **書き換える。** `ctx` はデータクラスです。`await call_next(dataclasses.replace(ctx, params=...))` とすると、クライアントが送ったものとは異なるパラメーターをチェーンの残りに渡せます。`initialize` に対しては決して行わないでください。クライアントが受け取る結果は書き換えたパラメーターから組み立てられますが、サーバーは元の通信路上のパラメーターから接続状態を確定します。両者が、ネゴシエートした内容について食い違ったままハンドシェイクを終える可能性があります。 * **応答する。** `call_next(ctx)` を呼ばずに結果を返すと、それがレスポンスとしてクライアントへ送られます。`call_next` が渡してくるのは完成した送信形式であり、パイプラインは返したものに一切手を加えないので、エンベロープ全体が自分の責任になります。2026 年世代の接続ではこれに `serverInfo` の `_meta` スタンプが含まれます。SDK はハンドラーの結果にはこれを付けますが、ミドルウェアが返すものには付けません。 diff --git a/i18n/ja/pages/client/identity-assertion.md b/i18n/ja/pages/client/identity-assertion.md index 5f10acf273..3852d30c2b 100644 --- a/i18n/ja/pages/client/identity-assertion.md +++ b/i18n/ja/pages/client/identity-assertion.md @@ -1,6 +1,6 @@ --- translation: - sections: [a91322c46111d16d, 8e6fd6d6f59bb568, e7828fd2729b2c9d, a03ec26bfc678b65, 1034c653c0bcf1b0] + sections: [a91322c46111d16d, 8e6fd6d6f59bb568, 7cf38181f6c99fd5, 37804d4fb36d6302, 1034c653c0bcf1b0] tool: 1 --- # アイデンティティアサーション {#identity-assertion} @@ -57,7 +57,7 @@ translation: ### コンフィデンシャルクライアント {#a-confidential-client} -`client_secret` は必須で、ないとコンストラクターが `ValueError` を送出します。[SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990) の下敷きになっている IETF プロファイルはこのグラントをコンフィデンシャルクライアント専用としており、SEP-990 はクライアントの認証を要求しています。この SDK は、共有シークレットを必須とすることでその両方を強制しています。`token_endpoint_auth_method` で、シークレットをどこに載せて送るかを選びます。`client_secret_post`(デフォルト、フォーム本体の中)か `client_secret_basic`(HTTP Basic ヘッダー)です。プロファイルは `private_key_jwt` も許可していますが、このプロバイダーはサポートしていません。 +`client_secret` は必須で、ないとコンストラクターが `ValueError` を送出します。[SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990) の下敷きになっている IETF プロファイルは、このグラントをコンフィデンシャルクライアントに限って使うよう推奨しており、[RFC 7521](https://datatracker.ietf.org/doc/html/rfc7521) はそのポリシーを認可サーバーに委ねています。この SDK は、どちら側でも保守的な解釈を採っています。組み込みの認可サーバーは共有シークレットを持たないクライアントを拒否し、このプロバイダーは共有シークレットを必須とします。`token_endpoint_auth_method` で、シークレットをどこに載せて送るかを選びます。`client_secret_post`(デフォルト、フォーム本体の中)か `client_secret_basic`(HTTP Basic ヘッダー)です。プロファイルは `private_key_jwt` も許可していますが、このプロバイダーはサポートしていません。 !!! tip `client_secret` は環境変数かシークレットマネージャーから読み込んでください。ソース管理には決して入れないでください。 @@ -84,6 +84,7 @@ SDK が認可サーバー「そのもの」になることもできます。`cre * `identity_assertion_enabled=True` がすべての門番です。オフ(これがデフォルト)のときは、フックを実装していても `/token` はこのグラントに `unsupported_grant_type` で応答し、メタデータにも載りません。オンにすると、メタデータに `jwt-bearer` グラントタイプが加わり、`authorization_grant_profiles_supported` に `urn:ietf:params:oauth:grant-profile:id-jag` が列挙されます。これは拡張仕様がサポートを告知するために使うフィールドです。(この SDK のクライアントはそれを読みません。1 つの issuer 向けにプロビジョニングされており、単に要求するだけです。) * **`exchange_identity_assertion`** がフックです。これが実行される前に、SDK はクライアントを認証し、パブリッククライアントを拒否し、登録内容にこのグラントが含まれていないクライアントを拒否しています。受け取るのは `IdentityAssertionParams`(生の `assertion`、要求された `scopes` と `resource`)で、返すのは素の `OAuthToken` です。 +* パブリッククライアントの拒否は SDK のポリシーであり、仕様の要件ではありません。組み込みのサーバーは共有シークレットでしかクライアントを認証しません。`private_key_jwt` をサポートしておらず、Client ID Metadata Document の解決にもまだ対応していない([#1801](https://github.com/modelcontextprotocol/python-sdk/issues/1801))ため、それで識別されるクライアントはここではこのグラントを使えません。別のポリシーにしたいデプロイメントは、`create_auth_routes` が返す `/token` ルートを独自のものに差し替えられます。 * 動的クライアント登録はこのグラントを無条件に拒否するので、ここでの `get_client` は手作業でプロビジョニングしたクライアントを返します。ID-JAG クライアントが自分で自分を登録して存在するようになることはできません。 * クラスの半分は拒否です。`OAuthAuthorizationServerProvider` は認可サーバー「全体」なので、認可コードフローも求められます。ユーザーのサインインも行うサーバーならそれらを本当に実装しますが、このサーバーには入口がちょうど 1 つしかありません。 diff --git a/i18n/ja/pages/client/transports.md b/i18n/ja/pages/client/transports.md index 409d491633..69dd0bde8d 100644 --- a/i18n/ja/pages/client/transports.md +++ b/i18n/ja/pages/client/transports.md @@ -1,6 +1,6 @@ --- translation: - sections: [9cac816674181eb0, 7c157764133fea1f, 40b4916d82eaf1d4, 10d151f2cc75317f, 3d0832f39b0d7059, 92742ba36533633d, 0aeca6145e7bd302] + sections: [9cac816674181eb0, 5619e950d206e6c8, 40b4916d82eaf1d4, 10d151f2cc75317f, 3d0832f39b0d7059, 92742ba36533633d, 991c10e47fda2636] tool: 1 --- # クライアントのトランスポート {#client-transports} @@ -43,16 +43,28 @@ URL の文字列を渡すと **Streamable HTTP** になります。デプロイ * `httpx2.AsyncClient` の所有者は**自分**なので、入るのも出るのも自分で行います。SDK は自身が作成していないクライアントを決して閉じません。 * `streamable_http_client(url, http_client=...)` はトランスポートを返し、`Client(transport)` はそれを他のものと同じように受け取ります。 +`timeout=` はそのまま残してください。これは SDK 自身のクライアントが使うのと同じ値です(30 秒、read は 300 秒)。タイムアウトを指定せずに組み立てた `httpx2.AsyncClient` には `httpx2` のデフォルトである 5 秒が適用され、それより長くかかるツール呼び出しは read タイムアウトで失敗します。 + TLS について 1 点。`httpx2` は、同梱の CA リストではなく、オペレーティングシステムのトラストストアに対して証明書を検証します([`truststore`](https://pypi.org/project/truststore/) を使用)。利用できるシステム 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)を参照)。 +### より大きな SSE イベント {#larger-sse-events} + +サーバーが大きなツール結果や通知を 1 つの SSE イベントで送る場合は、`max_sse_event_size` を渡します。 + +```python title="client.py" hl_lines="6-9" +--8<-- "docs_src/client_transports/tutorial005.py" +``` + +デフォルトは 1 イベントあたり 1 MiB で、イベントがパースされる前のバイト数で測ります。この上限は、POST レスポンス、GET ストリーム、再開されたストリームに適用されます。POST レスポンスや再開されたストリームで上限を超えるイベントがあると、そのリクエストは SSE エラーで失敗します。バックグラウンドの GET ストリームでは、クライアントはエラーをログに記録し、ストリームをリトライします。サーバーを信頼していて、より大きなイベントが必要な場合は、`max_sse_event_size=None` を設定して上限を無効にしてください。JSON レスポンスは影響を受けません。`ClientSessionGroup` を使う場合は、同じオプションを `StreamableHttpParameters` に設定してください。 + !!! warning - `streamable_http_client` は以前、`headers=` と `timeout=` を直接受け取っていました。今はもう受け取りません。パラメーターは `url`、`http_client`、`terminate_on_close` だけです。習慣で `headers=` を渡すと、次のようになります。 + `streamable_http_client` は以前、`headers=` と `timeout=` を直接受け取っていました。今はもう受け取りません。パラメーターは `url`、`http_client`、`terminate_on_close`、`max_sse_event_size` です。習慣で `headers=` を渡すと、次のようになります。 ```text TypeError: streamable_http_client() got an unexpected keyword argument 'headers' ``` - HTTP に関わるものはすべて、渡す 1 つの `httpx2.AsyncClient` に集約されています。 + ヘッダー、認証、プロキシ、タイムアウトは、渡す 1 つの `httpx2.AsyncClient` に設定します。一方、`max_sse_event_size` は MCP トランスポートの SSE リーダーに適用されます。 !!! info `httpx2` はおなじみの `httpx` の API をそのまま保っているので、`httpx` を知っていれば、認証、プロキシ、イベントフック、リトライ、接続数の制限のやり方はすでに知っていることになります。SDK はその上に何も足さず、何も引きません。唯一の例外が[リダイレクトの扱い](#redirects)です。OAuth が差し込まれるのもここです。`httpx2.AsyncClient(auth=OAuthClientProvider(...))` のように書きます。そのフロー全体については **[OAuth クライアント](oauth-clients.md)** を参照してください。 @@ -120,6 +132,7 @@ TLS について 1 点。`httpx2` は、同梱の CA リストではなく、オ * `Client("http://.../mcp")`(URL)は、本番用のトランスポートである Streamable HTTP で接続します。 * ヘッダー、認証、プロキシ、タイムアウトは、`streamable_http_client(url, http_client=...)` に渡す `httpx2.AsyncClient` に設定します。`headers=` キーワードはありません。 +* SSE イベントごとのバイト数の上限を変更するには、`streamable_http_client(url, max_sse_event_size=...)` を使います。 * リダイレクトに従うのは、URL 自身のオリジン内(末尾スラッシュの `307`/`308`)と、同じホスト上の `http`→`https` だけです。それ以外は `Redirect to … not followed` で失敗します。最終的な URL を設定してください。 * stdio は `Client(StdioServerParameters(...))` です。自分で `stdio_client(...)` に包むのは、子プロセスの stderr をリダイレクトしたいときだけです。 * サブプロセスが受け取るのは自分の環境ではなく、許可リストに基づく環境です。`env=` でそこに追加します。 diff --git a/i18n/ja/pages/handlers/cancellation.md b/i18n/ja/pages/handlers/cancellation.md new file mode 100644 index 0000000000..72e6c8457f --- /dev/null +++ b/i18n/ja/pages/handlers/cancellation.md @@ -0,0 +1,57 @@ +--- +translation: + sections: [07968345fdc0b84e, 4ea8416db9efa0dc, 336a7b4c5d0a4578, 18392e805dde6717, c30d50df43f9b55c] + tool: 1 +--- +# キャンセル {#cancellation} + +クライアントは呼び出しを途中で諦めることがあります。ユーザーが停止ボタンを押した場合や、タイムアウトの時間が切れた場合です。 + +そのとき、SDK は**ハンドラーをキャンセルします**。ハンドラーが待機している `await` が例外を送出し、関数は巻き戻され、戻り値は何も送信されません。ほとんどのハンドラーでは、これについて何もする必要はありません。 + +対応が必要なのは 2 種類です。クリーンアップするものがあるハンドラーと、通常の `def` で書かれたハンドラーです。 + +## `async def` ツールでクリーンアップする {#clean-up-in-an-async-def-tool} + +クリーンアップは `finally` に書いてください。 + +```python title="server.py" hl_lines="23 26-28" +--8<-- "docs_src/cancellation/tutorial001.py" +``` + +* `finally` は、ツールがどのように終了しても実行されます。値を返した場合も、例外を送出した場合も、キャンセルされた場合もです。 +* `await` が必要なクリーンアップには `shield=True` が必要です。キャンセルされたハンドラーでは、以降の `await` もすべて例外を送出するため、シールドがなければ `release_hold` は最初の行で止まってしまいます。 +* シールドされたブロックは何からもキャンセルできないので、制限時間を設定してください。ここでは `5` 秒です。 + +!!! tip + `except` ではなく `finally` を使ってください。クリーンアップが終わったあとも、キャンセルは上位へ伝わり続ける必要があります。`finally` ならそれができます。 + +## 通常の `def` ツールで早めに停止する {#stop-early-in-a-plain-def-tool} + +通常の `def` ツールはスレッドで実行され、スレッドを外部から中断する手段はありません。ツールの側から確認する必要があります。 + +```python title="server.py" hl_lines="22 25-26" +--8<-- "docs_src/cancellation/tutorial002.py" +``` + +* `anyio.from_thread.check_cancelled()` は、呼び出しが有効な間は何もせず、キャンセルされたあとは例外を送出します。作業の区切りごとに呼び出してください。 +* ここでもクリーンアップは `finally` に書きます。スレッド内では何も await しないので、シールドは不要です。 +* 一度も確認しない `def` ツールは最後まで実行され、その結果は破棄されます。 + +## 適用される範囲 {#where-it-applies} + +プロンプトとリソースの関数も、ツールとまったく同じようにキャンセルされます。 + +stdio でも Streamable HTTP でも同じように動作します。この SDK の `Client` で呼び出しを諦めるとは、`call_tool` を await しているタスクをキャンセルするか、`read_timeout_seconds` が切れるのに任せることです。 + +!!! warning + Streamable HTTP の 2 つのオプションでは、キャンセルがハンドラーに伝わりません。`2026-07-28` の接続での `json_response=True` と、レガシー接続での `stateless_http=True` です。この場合、クライアントが何をしても、ハンドラーは最後まで実行されます。 + +## まとめ {#recap} + +* クライアントが呼び出しを諦めると、SDK はハンドラーをキャンセルします。ツール、プロンプト、リソースのいずれも対象です。 +* `async def`:`finally` でクリーンアップし、await を伴うクリーンアップは `anyio.move_on_after(seconds, shield=True)` の中に置いてください。 +* 通常の `def`:作業の区切りごとに `anyio.from_thread.check_cancelled()` を呼び出してください。そうしないと、ツールは最後まで実行されます。クリーンアップは通常の `finally` でできます。 +* `json_response=True`(モダンな接続)と `stateless_http=True`(レガシー接続)は、キャンセルを無効にします。 + +進捗とキャンセルは、実行中のツールとその「呼び出し側」との間のやり取りです。サーバーを運用する「自分」に向けてツールが記録するログは別のチャネルで、それが **[ロギング](logging.md)** です。 diff --git a/i18n/ja/pages/handlers/index.md b/i18n/ja/pages/handlers/index.md index 8971a5ccad..0c2b19c814 100644 --- a/i18n/ja/pages/handlers/index.md +++ b/i18n/ja/pages/handlers/index.md @@ -1,6 +1,6 @@ --- translation: - sections: [424930166c4bc6f3] + sections: [22ca41e50cc1b536] tool: 1 --- # ハンドラーの中で {#inside-your-handler} @@ -18,6 +18,7 @@ translation: * **[エリシテーション(elicitation)](elicitation.md)** と、それを運ぶ 2026-07-28 のパターンである **[マルチラウンドトリップ(multi-round-trip)リクエスト](multi-round-trip.md)** を使って、ユーザーに追加の入力を求めます。 * **[サンプリングとルート(roots)](sampling-and-roots.md)** を使って、クライアントに LLM の補完やワークスペースのフォルダーを要求します。非推奨ですが、引き続き提供されています。 * 時間のかかる処理について **[進捗](progress.md)** を報告します。 +* **[キャンセル](cancellation.md)** で、クライアントが呼び出しを諦めたときに後片付けをしたり、処理を早めに打ち切ったりします。 * **[ロギング](logging.md)** でログを書き出します(サーバーを運用する人に向けて、標準エラーに出力します)。 * **[サブスクリプション](subscriptions.md)** で、購読中のクライアントに変更があったことを伝えます。 diff --git a/i18n/ja/pages/handlers/lifespan.md b/i18n/ja/pages/handlers/lifespan.md index 53e8670d85..ad6d3ee174 100644 --- a/i18n/ja/pages/handlers/lifespan.md +++ b/i18n/ja/pages/handlers/lifespan.md @@ -1,6 +1,6 @@ --- translation: - sections: [f3ca8ac5f90f2dfa, 85a1ef3588ba0736, 563346d4d5804933, 9e3528340d0bab53] + sections: [f3ca8ac5f90f2dfa, 48e478ef7bd688b1, 563346d4d5804933, 52ac6a7734d6f581] tool: 1 --- # ライフスパン {#lifespan} @@ -46,7 +46,7 @@ translation: モデルが渡せる引数は `genre` だけです。ライフスパンはサーバー側の内部事情です。 -`@mcp.resource()` と `@mcp.prompt()` の関数も `ctx` パラメーターを受け取れます。ただし型は裸の `Context` と書きます。理由は次の節で説明します。`ctx` が持っているものはすべて **[Context](context.md)** にまとめてあります。 +`@mcp.resource()` と `@mcp.prompt()` の関数も `ctx` パラメーターを受け取れます。`ctx` が持っているものはすべて **[Context](context.md)** にまとめてあります。 ### 本当に型が付いている {#it-really-is-typed} @@ -56,15 +56,6 @@ translation: 代わりに裸の `Context` と書くと、`lifespan_context` の型は `dict[str, Any]` になります。ライフスパンが何を yield したのか、型チェッカーには知りようがないからです。実行時にはオブジェクトはそこにありますが、型による補助は失われます。 -!!! warning - `Context[AppContext]` は**ツール専用**の書き方です。`@mcp.resource()` や `@mcp.prompt()` の関数に付けると、そのハンドラーの呼び出しはすべて失敗します。クライアントにはエラーが返り、サーバーのログには理由が記録されます。 - - ```text - Context is not available outside of a request - ``` - - リソースとプロンプトでは、裸の `ctx: Context` と書いてください。ライフスパンが yield したオブジェクトは、実行時には引き続き `ctx.request_context.lifespan_context` にあります。手放すのは型パラメーターであって、オブジェクトではありません。 - !!! tip ライフスパンは必ず存在します。渡さなければ SDK のデフォルトが空の `dict` を yield するので、`ctx.request_context.lifespan_context` は `{}` であり、`None` になることはありません。裸の `Context` で型が `dict[str, Any]` になるのも、このデフォルトがあるためです。 @@ -95,7 +86,7 @@ translation: * `yield` の前のコードが起動処理です。その後の `finally` が終了処理です。 * 実行は 1 回だけで、サーバーの一生全体を囲みます。リクエストごとではありません。 * `yield` したものは、すべてのツール、リソース、プロンプトで `ctx.request_context.lifespan_context` として使えます。 -* `ctx: Context[AppContext]` と書けば、ツールではそのアクセスに完全に型が付きます。リソースとプロンプトでは裸の `Context` を使います。 +* `ctx: Context[AppContext]` と書けば、そのアクセスに完全に型が付きます。 * `lifespan=` を渡さなければ空の `dict` です。`None` になることはありません。 呼び出しの途中で止まり、本人にしかわからないことをユーザーに尋ねるハンドラーについては、**[エリシテーション(elicitation)](elicitation.md)** を参照してください。 diff --git a/i18n/ja/pages/handlers/progress.md b/i18n/ja/pages/handlers/progress.md index 9729ced62c..36a062ad42 100644 --- a/i18n/ja/pages/handlers/progress.md +++ b/i18n/ja/pages/handlers/progress.md @@ -1,6 +1,6 @@ --- translation: - sections: [5315262fe26b33e1, 9d8e98840f1b78f0, 52d6009a07e770ea, 8534d8dbb4053a70, 2966fac6fe697007] + sections: [5315262fe26b33e1, 9d8e98840f1b78f0, 52d6009a07e770ea, 8534d8dbb4053a70, 2e9aff14d3a882c0] tool: 1 --- # 進捗 {#progress} @@ -111,4 +111,4 @@ Imported https://example.com/b.json (2.0/2.0) * 呼び出しにコールバックがなければ、`report_progress` は何もしません。無条件に報告してください。 * わからないときは `total` を省略します。コールバックは `None` を受け取ります。 -進捗は、実行中のツールが「ユーザー」に見せるものです。サーバーを運用する「自分」のために記録する行は、別のチャネルです。**[ロギング](logging.md)** を参照してください。 +進捗は、まだ待っているクライアントのためのものです。クライアントが待つのをやめたときにツールから何が見えるかは、**[キャンセル](cancellation.md)** で説明します。 diff --git a/i18n/ja/pages/handlers/subscriptions.md b/i18n/ja/pages/handlers/subscriptions.md index 9c567837bc..9cf280ce99 100644 --- a/i18n/ja/pages/handlers/subscriptions.md +++ b/i18n/ja/pages/handlers/subscriptions.md @@ -1,6 +1,6 @@ --- translation: - sections: [60a9de8a0bdaa531, 317bbe7e4355cdcc, a61d660c8029e04a, 8f7e82fcb88df8a9, b165db51249ff8ed, 266f56fb798068a4, 7c0e57030b622139, df18d7c2417a9883] + sections: [60a9de8a0bdaa531, 6693607ea56d8bd6, a61d660c8029e04a, 8f7e82fcb88df8a9, b165db51249ff8ed, b8bc624a627ead9b, 2139e68e36d9e621, 7c0e57030b622139, 34ab1af2b9ab5b45] tool: 1 --- # サブスクリプション {#subscriptions} @@ -22,7 +22,7 @@ translation: * 兄弟にあたるのが `notify_prompts_changed()` と `notify_resources_changed()` です。 * サブスクライバーがいなければ、何も起こりません。アイドル状態のサーバーへの発行は no-op なので、誰かが聞いているかどうかを確認することはありません。何が変わったかを述べるだけです。 -`MCPServer` は `subscriptions/listen` を代わりに処理します。通信上の義務(最初のフレームとしての確認応答、ストリームごとのフィルタリング、全フレームへのサブスクリプション ID の付与)は SDK の仕事です。 +`MCPServer` は、[オフにしない](#turning-it-off)かぎり `subscriptions/listen` を代わりに処理します。通信上の義務(最初のフレームとしての確認応答、ストリームごとのフィルタリング、全フレームへのサブスクリプション ID の付与)は SDK の仕事です。 !!! check 実際の通信では、フィルターに `board://sprint` を指定したストリームは、`complete_task` の実行後に次のようになります。 @@ -79,7 +79,32 @@ translation: 発行はハンドラーから開いているストリームへ、`SubscriptionBus` を経由して伝わります。デフォルトはインメモリで、1 つのプロセスとその中のすべてのストリームです。ロードバランサーの背後でレプリカを動かすまでは、これが正解です。レプリカを動かすと、クライアントのストリームは 1 つのレプリカに固定され、別のレプリカでの発行がそこに届かなければならないからです。 -その継ぎ目は自分で実装します。pub/sub バックエンドの上に 2 つのメソッドを載せるだけです。 +デフォルトのバスでは届きません。レプリカごとに別々のバスを持つからです。 + +```mermaid +flowchart LR + client[Client] --> lb[Load balancer] + lb --> stream + lb ~~~~ gap + lb --> tool + subgraph B [Replica B] + tool[tools/call] -- publishes --> busB[(bus B)] + end + gap[(no shared bus)] + subgraph A [Replica A] + stream[listen stream] -- subscribed --> busA[(bus A)] + end + style A fill:none + style B fill:none + style gap fill:none,stroke-dasharray:4 4 +``` + +何も失敗しません。呼び出しは成功し、ストリームは沈黙したままです。そのため、ロードバランサーの背後では次のどちらかを選んでください。 + +* **変更通知が必要な場合。** 下記のとおり、すべてのレプリカに同じバスを渡してください。 +* **必要ない場合。** 変更通知を[オフにして](#turning-it-off)ください。そうすれば、届かないイベントをクライアントに約束することも、クライアントがそのためにストリームを開いたままにすることもありません。 + +共有バスは自分で実装します。pub/sub バックエンドの上に 2 つのメソッドを載せるだけです。 ```python from collections.abc import Callable @@ -128,6 +153,20 @@ async def tools_reloaded() -> None: await bus.publish(ToolsListChanged()) # from a lifespan task, a webhook, anywhere ``` +## オフにする {#turning-it-off} + +カタログがまったく変わらないサーバーには、発行するものがありません。サーバーを組み立てるときに、そう指定します。 + +```python title="server.py" hl_lines="3" +--8<-- "docs_src/subscriptions/tutorial007.py" +``` + +* `2026-07-28` のクライアントには変更通知がアドバタイズされず、`subscriptions/listen` リクエストには開いたストリームの代わりに *Method not found* が返ります。 +* `ctx.notify_*` は引き続き動作しますが、誰にも届きません。そのため、ハンドラーを変更する必要はありません。 +* それより前のプロトコルバージョンのクライアントには、違いはありません。 + +開いているストリームは、終わることのないリクエストです。そのため、リクエストの継続時間で課金されるホストでもこの設定は重要です。 + ## 低レベルでの組み立て {#the-low-level-composition} 低レベルの `Server` には、あらかじめ配線されたものは何もありません。同じ部品を 3 行で組み立てます。 @@ -148,5 +187,6 @@ async def tools_reloaded() -> None: * クライアント側は `async with client.listen(...)` です。詳しくは「クライアント」の下の **[サブスクリプション](../client/subscriptions.md)** を参照してください。 * 低レベルの `Server` では同じ部品を自分で組み立てます。バス、`ListenHandler(bus)`、`on_subscriptions_listen` スロットです。 * スケールアウトとは、`SubscriptionBus`(メソッド 2 つ)を実装し、`MCPServer(subscriptions=...)` として渡すことです。 +* 発行するものがない場合や、共有バスのないレプリカ構成の場合は、`MCPServer(subscriptions=False)` を使います。変更通知はアドバタイズされず、ストリームも保持されません。 これらすべてを処理するサーバーを、レプリカ 1 つでも 20 でも動かす方法は、**[デプロイとスケール](../run/deploy.md)** にあります。 diff --git a/i18n/ja/pages/run/deploy.md b/i18n/ja/pages/run/deploy.md index 6a1529a2c7..f05df41d49 100644 --- a/i18n/ja/pages/run/deploy.md +++ b/i18n/ja/pages/run/deploy.md @@ -1,6 +1,6 @@ --- translation: - sections: [28221886b198784f, f88ea1f1614f3a1d, 2e76f5cb9df15042, ce926d686730b6d0, 3be24f8ad8bb5ab9, 3fad24032b2224ff, f25a7f860e579ecb, 697b01d95080880d] + sections: [28221886b198784f, f88ea1f1614f3a1d, 2e76f5cb9df15042, ce926d686730b6d0, 3be24f8ad8bb5ab9, aaf489e944ecf5d1, f25a7f860e579ecb, 697b01d95080880d] tool: 1 --- # デプロイとスケール {#deploy-scale} @@ -157,6 +157,7 @@ python -c "import secrets; print(secrets.token_hex(32))" * 本物のプロセスをまたぐ場合、**SDK には役に立つバスが同梱されていません。** `SubscriptionBus` は 2 つのメソッド(`publish` と `subscribe`)からなる `Protocol` であり、自前の pub/sub バックエンド(Redis、NATS、そのほかすでに運用しているもの)の上に実装して、`MCPServer(subscriptions=...)` として渡します。スケッチと契約は **[サブスクリプション](../handlers/subscriptions.md#scaling-past-one-process)** にあります。 * バスが運ぶのは 4 種類の小さな型付きイベントであり、JSON-RPC ではありません。確認応答、フィルタリング、ストリームのライフサイクルは SDK に残るので、バスがプロトコルを壊すことはできません。できるのはプロセス間でイベントを運ぶことだけです。 * ストリームは再開可能**ではなく**、イベントはリプレイ**されません**。レプリカを失えばそのストリームは切れ、クライアントは listen し直し、取得し直します。共有すべきイベントストアはなく、ほかに設定するものもありません。スケールアウトが本当に「同じことを増やすだけ」で済むのは、ここだけです。 +* 変更通知が不要なサーバーにバスは必要ありません。**[変更通知を無効にしてください](../handlers/subscriptions.md#turning-it-off)**。 ## SDK が提供しないもの {#what-the-sdk-does-not-give-you} diff --git a/i18n/ja/pages/servers/structured-output.md b/i18n/ja/pages/servers/structured-output.md index 73fa0bb949..1533f857ed 100644 --- a/i18n/ja/pages/servers/structured-output.md +++ b/i18n/ja/pages/servers/structured-output.md @@ -1,6 +1,6 @@ --- translation: - sections: [a838d57f003aed44, 857d03886a0137ed, 42d9efcb9f542867, 2290ff08435b5573, 91be9b73602abcf1, 6cdbad079f7b47f0, d4b607372fb28b51, 18dbf726ac45e0b7, c7eff2a5698225fa, c851964bb3301907, 8f296f1f09e4c400, d715db6f8dccc9cc, a0c344a48450dbe4] + sections: [a838d57f003aed44, 857d03886a0137ed, 42d9efcb9f542867, 2290ff08435b5573, 91be9b73602abcf1, 6cdbad079f7b47f0, d4b607372fb28b51, 7608fc5ebc31d6ea, c7eff2a5698225fa, c851964bb3301907, 8f296f1f09e4c400, d715db6f8dccc9cc, a0c344a48450dbe4] tool: 1 --- # 構造化出力 {#structured-output} @@ -172,6 +172,8 @@ result.structured_content # {"London": 16.2, "Reykjavik": 4.4} キーは `str` でなければなりません。`dict[int, float]` は JSON オブジェクトになれないため、`{"result": ...}` ラッパーにフォールバックします。 +辞書の結果は、バリデーションとシリアライズに Pydantic の `TypeAdapter` を使います。ツールの `FuncMetadata.output_model` を調べると、そこにはスキーマのタイトルが付いた辞書の型アノテーションが入っています。 + ## バリデーション {#validation} `output_schema` は単なるドキュメントではありません。関数が返すものは何であれ、サーバーを出る前に**このスキーマに照らして検証されます**。 diff --git a/i18n/ja/pages/servers/tools.md b/i18n/ja/pages/servers/tools.md index 74c1a58c6d..b7c252941d 100644 --- a/i18n/ja/pages/servers/tools.md +++ b/i18n/ja/pages/servers/tools.md @@ -1,6 +1,6 @@ --- translation: - sections: [e4cc390d56573409, f30cf8103a6e918c, 2c97b9f888398951, 048e5471dfa71aea, 3076b1e16ad95950, edbedf2a16e71311, 3d8ef8da89fa87c1, f6c0e02e6ea5a363] + sections: [e4cc390d56573409, f30cf8103a6e918c, 2c97b9f888398951, 048e5471dfa71aea, 3076b1e16ad95950, c1115cd005b81e8f, 3d8ef8da89fa87c1, f6c0e02e6ea5a363] tool: 1 --- # ツール {#tools} @@ -137,7 +137,7 @@ Inspector は、必須の `query` テキストフィールドと必須の `limit ツールが I/O を行う場合(API を呼ぶ、ファイルを読む、データベースに問い合わせるなど)は、`async def` で宣言し、その中で `await` してください。SDK 側がそれを await します。 -普通の `def` のツールも使えます。SDK がスレッド内で実行するので、サーバーをブロックすることはありません。 +普通の `def` のツールも使えます。SDK がスレッド内で実行するので、サーバーをブロックすることはありません。実行時間の長いツールでは、クライアントがまだ待っているかどうかを確認できます。詳しくは **[キャンセル](../handlers/cancellation.md)** を参照してください。 ほかに設定することはありません。 diff --git a/i18n/ja/pages/troubleshooting.md b/i18n/ja/pages/troubleshooting.md index d351427d0a..86d808f864 100644 --- a/i18n/ja/pages/troubleshooting.md +++ b/i18n/ja/pages/troubleshooting.md @@ -1,6 +1,6 @@ --- translation: - sections: [3d58228e81b99543, 170514ce901c4139, 17d61fad0a50d62b, 8a6e351ec756904d, 137454d469c867f5, 6392596bd6df54f0, 41126fa9c4fe432f, 480b6d7897e30ab4, d83bb682e708dde0, ebbed3449c499db4, 525cdf1755e29d4c, 30fd31be74169d9a, d2e88333d4f7841f, c2dc3b1007d2e987, d6eabf60cc366341, f798e815252852c2, 0cba47bae78d04eb, 2c218ba829abf74e] + sections: [3d58228e81b99543, 170514ce901c4139, 17d61fad0a50d62b, 8a6e351ec756904d, 137454d469c867f5, fcf984fa0615ed11, 6392596bd6df54f0, 41126fa9c4fe432f, 480b6d7897e30ab4, d83bb682e708dde0, ebbed3449c499db4, 525cdf1755e29d4c, 30fd31be74169d9a, d2e88333d4f7841f, c2dc3b1007d2e987, d6eabf60cc366341, f798e815252852c2, 0cba47bae78d04eb, cdc6d86a4dae8a34] tool: 1 --- # トラブルシューティング {#troubleshooting} @@ -123,6 +123,14 @@ TypeError: The @tool decorator was used incorrectly. Did you forget to call it? !!! note これはモジュールが**インポート**された時点で送出されます。どのクライアントが接続するよりも前です。そのため、ホストがサーバーを「接続済みでツール 0 個」ではなく「起動失敗」(または「切断」)と表示している場合は、この形を疑ってください。自分で `python server.py` を実行し、トレースバックを読んでください。型チェッカーでも検出できます。関数は有効な `name=` ではないからです。 +## `InvalidSignature: Tool '' has an invalid x-mcp-header annotation: ` {#invalidsignature-tool-name-has-an-invalid-x-mcp-header-annotation-reason} + +ツールの引数が、仕様の認めていない形で `x-mcp-header` によってマークされています。どの規則に違反しているかは `` に書かれています。`2026-07-28` のクライアントはそのようなツールを一覧から外してしまうので、SDK は登録を拒否します。 + +マークできるのは `str`、`int`、`bool` の引数だけで、`str | None` はそのどれでもありません。オプションの引数の書き方は **[ヘッダーパラメーター](advanced/header-parameters.md)** にあります。 + +上の項目と同じく、これはモジュールが**インポート**された時点で送出されます。どのクライアントが接続するよりも前です。 + ## `Tool already exists: ` {#tool-already-exists-name} 2 つの登録が同じツール名を使いました。勝つのは**最初の**登録で、2 つ目は黙って捨てられます。「サーバーログ」に出るこの警告が唯一の合図です。 @@ -409,6 +417,7 @@ mcp = MCPServer("Weather", request_state_security=RequestStateSecurity(keys=[key * `ExceptionGroup: unhandled errors in a TaskGroup` がエラーであることは決してありません。**最後の行**を読んでください。`async with Client(...)` ブロックの「内側」で `MCPError` を捕まえれば、包まれること自体を完全に避けられます。 * `call_tool` は、ツールが失敗しても例外を送出しません。`Error executing tool ...` と `Unknown tool: ...` は結果です。`result.is_error` を確認してください。ツール名の後にメッセージがなければクラッシュしたという意味で、トレースバックはサーバーログにあります。 * `Client must be used within an async context manager` -> `async with` を使ってください。`Use @tool() instead of @tool` -> 括弧を付けてください。 +* `has an invalid x-mcp-header annotation` -> マークできるのは `str`、`int`、`bool` の引数だけです。 * サーバーログの `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()` に入っていません。 diff --git a/i18n/ja/pages/whats-new.md b/i18n/ja/pages/whats-new.md index 71aacc66ba..8405542c76 100644 --- a/i18n/ja/pages/whats-new.md +++ b/i18n/ja/pages/whats-new.md @@ -1,6 +1,6 @@ --- translation: - sections: [cfe01c0c5863dfa2, 0dbb68d8b210177b, 80cf193023af3ed4, 875eb2889263424e] + sections: [cfe01c0c5863dfa2, 0dbb68d8b210177b, bd42ee3a268f6ea4, 875eb2889263424e] tool: 1 --- # v2 の新機能 {#whats-new-in-v2} @@ -196,7 +196,7 @@ Streamable HTTP では、2026 の経路に `Mcp-Session-Id` がありません ### そのほかを手短に {#the-rest-quickly} * **識別情報は省略可能な、メッセージごとのメタデータです。** リクエスト側の `clientInfo` `_meta` キーは省略可能で(必須の組は `protocolVersion` と `clientCapabilities` です)、`serverInfo` は `server/discover` の結果本体の外に出ました。サーバーは代わりに、2026 年世代のすべての結果の `_meta` にそれを書き込みます([spec #3002](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/3002))。SDK は常に書き込みます。サーバーが自身を名乗らない場合(たとえばミドルウェアがキーを取り除いた場合)、`client.server_info` は `None` です。通信路上での書き込みの様子は**[低レベルの Server](advanced/low-level-server.md)** が示します。 -* **リクエストは本体をパースしなくてもルーティングできます。** 新世代の HTTP リクエストは `Mcp-Method`(と、ツール系の 3 つの呼び出しでは `Mcp-Name`)を運びます。`x-mcp-header` で注釈したツールの入力スキーマのプロパティは `Mcp-Param-*` ヘッダーに写され、サーバーが本体と突き合わせて検査します([SEP-2243](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2243))。ゲートウェイやレートリミッターはヘッダーだけでルーティングできます。ルールは**[移行ガイド](migration.md#servers-validate-mcp-param-headers-against-the-request-body-sep-2243)**にあります。 +* **リクエストは本体をパースしなくてもルーティングできます。** 新世代の HTTP リクエストは `Mcp-Method`(と、ツール系の 3 つの呼び出しでは `Mcp-Name`)を運びます。`x-mcp-header` で注釈したツールの入力スキーマのプロパティは `Mcp-Param-*` ヘッダーに写され、サーバーが本体と突き合わせて検査します([SEP-2243](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2243))。ゲートウェイやレートリミッターはヘッダーだけでルーティングできます。引数に印を付ける方法は**[ヘッダーパラメーター](advanced/header-parameters.md)**が示します。 * **結果はキャッシュのヒントを運びます。** 一覧と読み取りの結果は `ttlMs` と `cacheScope` を宣言します([SEP-2549](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2549))。メソッドごとに `cache_hints=` で設定し、`Client` は組み込みのレスポンスキャッシュでそれに従います。ヒントを送らないサーバー(2026 より前のサーバーはすべてそうです)には、これまでと同じキャッシュされない通信が届きます。詳しくは**[キャッシュのヒント](client/caching.md)**を参照してください。 * **拡張は第一級です。** サーバーとクライアントは、逆引き 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)**は正確なメッセージで引けるようになっています。 diff --git a/i18n/ko/pages/advanced/header-parameters.md b/i18n/ko/pages/advanced/header-parameters.md new file mode 100644 index 0000000000..0a8aca9f1c --- /dev/null +++ b/i18n/ko/pages/advanced/header-parameters.md @@ -0,0 +1,65 @@ +--- +translation: + sections: [81862a209b483d27, b76e8073487afa03, bc214b2fc2bcdae4, d5835477c0b60163, a5d9786f902ad8e1] + tool: 1 +--- +# 헤더 매개변수 {#header-parameters} + +대부분의 서버에는 필요 없는 내용입니다. + +서버 앞에 놓인 게이트웨이나 로드 밸런서는 본문을 파싱하지 않고 읽을 수 있는 정보만으로 라우팅할 수 있습니다. 도구 인수에 `x-mcp-header`를 표시하면 `2026-07-28` **[프로토콜 버전](../protocol-versions.md)**을 사용하는 클라이언트가 그 값을 HTTP 헤더로도 보냅니다. + +## 인수 표시하기 {#mark-an-argument} + +표시는 인수의 JSON Schema에 추가하는 키 하나입니다. `MCPServer`에서는 `Field`가 이 키를 넣어 줍니다. + +```python title="server.py" hl_lines="13" +--8<-- "docs_src/header_parameters/tutorial001.py" +``` + +* `2026-07-28` 버전의 Streamable HTTP에서는 클라이언트가 본문과 함께 `Mcp-Param-Region` 헤더를 보내며, 서버는 둘이 일치하지 않는 호출을 거부합니다. +* 도구 목록을 조회하지 않은 클라이언트는 표시를 본 적이 없으므로 헤더를 보내지 않고, 호출은 거부됩니다. 이 SDK의 `Client`는 이때 도구 목록을 조회한 뒤 호출을 한 번 다시 보내므로, 목록을 먼저 조회해도 왕복 한 번을 아낄 뿐입니다. +* 그 밖의 모든 연결은 이 애너테이션을 무시합니다. + +함수는 바뀌지 않습니다. `region`은 여전히 인수로 전달됩니다. + +## 표시할 수 있는 대상 {#what-can-be-marked} + +`str`, `int`, `bool` 인수입니다. 그 밖의 타입은 도구를 등록할 때 `InvalidSignature`로 거부됩니다. + +단일 타입이 없는 `str | None`도 여기에 포함됩니다. 선택적 인수는 Pydantic의 `WithJsonSchema`로 스키마를 직접 명시해야 합니다. + +```python +region: Annotated[str | None, WithJsonSchema({"type": "string", "x-mcp-header": "Region"})] = None +``` + +## 저수준 `Server`에서 {#on-the-low-level-server} + +여기서는 `input_schema`를 직접 작성하므로 키를 그대로 넣으면 됩니다. + +```python title="server.py" hl_lines="18" +--8<-- "docs_src/header_parameters/tutorial002.py" +``` + +* 애너테이션을 대신 검사해 주는 것은 없습니다. 잘못된 애너테이션도 그대로 제공되며, `2026-07-28` 클라이언트는 해당 도구를 목록에서 제외합니다. + +### 이름으로 찾는 스키마 {#schemas-by-name} + +헤더를 검사하려면 SDK는 호출을 디스패치하기 전에 도구의 입력 스키마가 필요합니다. `get_tool_input_schema`가 없으면 SDK는 표시된 도구가 있든 없든 인수가 있는 모든 호출마다 `on_list_tools` 핸들러를 실행해 스키마를 얻습니다. + +```python title="server.py" hl_lines="26 39-41 48" +--8<-- "docs_src/header_parameters/tutorial003.py" +``` + +* 이미 가지고 있는 정보로 응답하도록 이 함수를 전달하세요. +* 검사할 것이 없는 도구에는 `None`을 반환하세요. + +## 요약 {#recap} + +* 도구 인수에 `x-mcp-header`를 표시하면 `2026-07-28` 클라이언트가 그 인수를 `Mcp-Param-*` HTTP 헤더로도 한 번 더 보냅니다. +* 서버는 헤더와 본문이 일치하지 않는 호출을 거부합니다. +* `str`, `int`, `bool` 인수만 표시할 수 있습니다. 그 밖의 경우 `MCPServer`는 `InvalidSignature`를 발생시킵니다. +* 저수준 `Server`는 아무것도 검사하지 않으며, 클라이언트는 애너테이션이 잘못된 도구를 목록에서 제외합니다. +* `get_tool_input_schema`가 있으면 저수준 `Server`가 호출마다 `on_list_tools`를 실행하지 않습니다. + +직접 작성하는 `Server` API의 나머지 내용은 **[저수준 Server](low-level-server.md)**에서 다룹니다. diff --git a/i18n/ko/pages/advanced/index.md b/i18n/ko/pages/advanced/index.md index 7f88264fd1..56678e34e6 100644 --- a/i18n/ko/pages/advanced/index.md +++ b/i18n/ko/pages/advanced/index.md @@ -1,6 +1,6 @@ --- translation: - sections: [ca6988b7503cd2d3] + sections: [348f8697c6b12cd0] tool: 1 --- # 고급 {#advanced} @@ -9,6 +9,7 @@ translation: * **[저수준 Server](low-level-server.md)**: `MCPServer`가 기반으로 삼는 클래스입니다. 손으로 작성하는 스키마, `on_*` 핸들러, 아무것도 대신 검사해 주지 않는 구조, 그리고 직접 정의하는 커스텀 JSON-RPC 메서드를 다룹니다. * **[페이지네이션](pagination.md)**과 **[미들웨어](middleware.md)**: 저수준 `Server`에서**만** 할 수 있는 두 가지입니다. +* **[헤더 매개변수](header-parameters.md)**: 게이트웨이가 도구 호출을 인수 중 하나를 기준으로 라우팅할 수 있게 합니다. * **[확장](extensions.md)**과 **[MCP Apps](apps.md)**: 프로토콜의 확장 지점입니다. 확장 패키지를 서버에 조합해 넣거나 직접 작성할 수 있습니다. 여기서 찾을 법한 몇 가지 항목은 실제로 사용하는 곳에 배치되어 있습니다. diff --git a/i18n/ko/pages/advanced/low-level-server.md b/i18n/ko/pages/advanced/low-level-server.md index 218397f0c6..ac55995839 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, 9d5d10a5f0405d0a, 1086e77ce561cd7f, a3f71823df5efc31, 9fc7109f72201cae, d50fe7faead8cf68, 7bf25983df655b66, 6330e1f4c6029683, 2f1749c8c133fa1c, 8db7116fc8ddd0ee, ebc33704fbd74262, 0fde3bcea081ba3a] + sections: [2c79b6338e09b7ac, 9d5d10a5f0405d0a, 1086e77ce561cd7f, a3f71823df5efc31, 9fc7109f72201cae, d50fe7faead8cf68, 7bf25983df655b66, 6330e1f4c6029683, 2f1749c8c133fa1c, 8db7116fc8ddd0ee, 2090d99b355bc2c7, 0fde3bcea081ba3a] tool: 1 --- # 저수준 Server {#the-low-level-server} @@ -209,6 +209,7 @@ use Server.middleware to observe or wrap initialization * `on_call_tool`, `on_get_prompt`, `on_read_resource`는 호출을 일시 중지하고 클라이언트에 입력을 요청하기 위해 평소의 결과 대신 `InputRequiredResult`를 반환할 수 있습니다. **[다중 왕복 요청](../handlers/multi-round-trip.md)**을 참고하세요. 이 계층답게 대신 설치해 주는 것은 없습니다. `MCPServer`가 기본적으로 `requestState`를 봉인하는 반면, 여기서는 설정한 `request_state`가 `server.middleware.append(RequestStateBoundary(RequestStateSecurity(keys=[...]), default_audience=server.name))`로 명시적으로 켜기 전까지 쓴 그대로 와이어를 건너갑니다. 이 한 줄(두 이름 모두 `mcp.server.request_state`에서 임포트합니다)이면 `MCPServer`가 수행하는 것과 동일한 봉인과 검증이 이루어집니다(**[`requestState` 보호하기](../handlers/multi-round-trip.md#protecting-requeststate)**). * `on_list_resources`, `on_read_resource`, `on_list_prompts`, `on_get_prompt`, `on_completion`은 나머지 프리미티브용으로 같은 `(ctx, params) -> result` 형태입니다. * `on_subscriptions_listen`은 2026-07-28의 `subscriptions/listen` 스트림을 제공합니다. `SubscriptionBus` 위에 만든 `ListenHandler`를 전달하고 다른 핸들러에서 버스로 이벤트를 발행하세요. 전체 구성은 **[구독](../handlers/subscriptions.md)**에서 확인하세요. +* `get_tool_input_schema`는 `on_list_tools`가 호출 경로에 끼어들지 않게 해 줍니다. **[헤더 매개변수](header-parameters.md#schemas-by-name)**를 참고하세요. * `server.streamable_http_app()`은 `MCPServer`의 것과 같은 Starlette 앱을 반환합니다. **[서버 실행하기](../run/index.md)**에서 다른 ASGI 앱을 배포하는 방식 그대로 배포하세요. 여기에는 `server.run(transport=...)` 같은 것이 없습니다. `server.run(read_stream, write_stream, server.create_initialization_options())` 호출이 스트림 한 쌍 위에서 연결 하나를 구동하며, 이 한 줄이 전부입니다. ## 요약 {#recap} diff --git a/i18n/ko/pages/advanced/middleware.md b/i18n/ko/pages/advanced/middleware.md index 3045521526..e166808978 100644 --- a/i18n/ko/pages/advanced/middleware.md +++ b/i18n/ko/pages/advanced/middleware.md @@ -1,6 +1,6 @@ --- translation: - sections: [6048b4f308edbb8c, 46056f318ef205e4, c3e565b61acd75c5, c62422b159c6ed09, 420968f514138f43] + sections: [58e1103d9a323ccf, 46056f318ef205e4, 812b414557fb0c35, 4df162eea2518d38, c62422b159c6ed09, 420968f514138f43] tool: 1 --- # 미들웨어 {#middleware} @@ -62,14 +62,37 @@ tools/call took 0.1 ms `MCPError(-32601, "Method not found")`를 일으키고, 이 예외는 클라이언트로 가는 길에 미들웨어를 **통과합니다**. +## 동시 실행 상한 {#a-concurrency-cap} + +미들웨어가 반드시 `call_next(ctx)`를 호출해야 하는 것은 아닙니다. 대신 `MCPError`를 일으키면 +그 메시지 하나가 **거부**됩니다. 연결은 유지되고 다음 메시지는 그대로 통과합니다. + +검색 한 번마다 4개짜리 풀에서 연결 하나를 점유한다고 가정해 보겠습니다. 이 미들웨어는 도구 호출 +4개까지 동시에 실행하게 두고 다섯 번째는 거부합니다. + +```python title="server.py" hl_lines="15-16 40-55 59" +--8<-- "docs_src/middleware/tutorial002.py" +``` + +* `tools/call`만 세므로, 서버는 도구 호출을 거부하는 동안에도 `server/discover`와 `tools/list`에는 + 계속 응답합니다. +* MCP는 "서버가 바쁨"을 뜻하는 오류 코드를 정의하지 않으므로 `SERVER_BUSY`는 이 서버가 자체적으로 + 정한 코드입니다. +* 거부하면 서버가 과부하 상태라는 사실을 클라이언트에 곧바로 알릴 수 있습니다. 호출자를 기다리게 + 하고 싶다면 대신 `call_next(ctx)`를 호출하는 동안 `anyio.CapacityLimiter`를 잡아 두세요. + +일으킨 `MCPError`는 모델이 아니라 클라이언트 애플리케이션으로 전달됩니다. 모델이 메시지를 읽어야 +한다면 대신 `is_error=True`인 도구 결과를 반환하세요. 이것이 아래의 **응답**에 해당합니다. + ## 미들웨어 안에서 할 수 있는 일 {#what-you-can-do-inside-one} 망설임이 적게 필요한 것부터 순서대로 나열합니다. -* **관찰.** 시간을 재고, 횟수를 세고, 로그를 남기세요. 위의 예제가 이에 해당합니다. +* **관찰.** 시간을 재고, 횟수를 세고, 로그를 남기세요. 위의 시간을 재는 미들웨어가 이에 해당합니다. * **거부.** `call_next(ctx)`를 호출하는 **대신** `MCPError`를 일으키면 그 메시지 하나에 - JSON-RPC 오류로 응답합니다. 연결은 유지되고 다음 메시지는 그대로 통과합니다. 서버가 - 호출자별로 `subscriptions/listen`을 제한하는 방법이 바로 이것입니다. 구독 페이지의 + JSON-RPC 오류로 응답합니다. 연결은 유지되고 다음 메시지는 그대로 통과합니다. 위의 동시 실행 + 상한이 이에 해당합니다. 서버가 + 호출자별로 `subscriptions/listen`을 제한하는 방법도 바로 이것입니다. 구독 페이지의 **[누가 지켜볼 수 있는지 정하기](../handlers/subscriptions.md#deciding-who-may-watch)**에서 단계별로 설명합니다. * **재작성.** `ctx`는 데이터클래스입니다. `await call_next(dataclasses.replace(ctx, params=...))`는 diff --git a/i18n/ko/pages/client/identity-assertion.md b/i18n/ko/pages/client/identity-assertion.md index cce752fde6..a64611aced 100644 --- a/i18n/ko/pages/client/identity-assertion.md +++ b/i18n/ko/pages/client/identity-assertion.md @@ -1,6 +1,6 @@ --- translation: - sections: [a91322c46111d16d, 8e6fd6d6f59bb568, e7828fd2729b2c9d, a03ec26bfc678b65, 1034c653c0bcf1b0] + sections: [a91322c46111d16d, 8e6fd6d6f59bb568, 7cf38181f6c99fd5, 37804d4fb36d6302, 1034c653c0bcf1b0] tool: 1 --- # ID 어설션 {#identity-assertion} @@ -64,7 +64,7 @@ translation: ### 기밀 클라이언트 {#a-confidential-client} -`client_secret`은 필수이며, 없으면 생성자가 `ValueError`를 발생시킵니다. [SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990)의 기반이 되는 IETF 프로파일은 이 그랜트를 기밀 클라이언트 전용으로 두고, SEP-990은 클라이언트가 인증할 것을 요구하며, 이 SDK는 공유 시크릿을 반드시 요구함으로써 둘 다 강제합니다. `token_endpoint_auth_method`는 시크릿이 어디에 실려 가는지를 고릅니다. `client_secret_post`(기본값, 폼 본문에)나 `client_secret_basic`(HTTP Basic 헤더) 중 하나입니다. 프로파일은 `private_key_jwt`도 허용하지만, 이 공급자는 지원하지 않습니다. +`client_secret`은 필수이며, 없으면 생성자가 `ValueError`를 발생시킵니다. [SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990)의 기반이 되는 IETF 프로파일은 이 그랜트를 기밀 클라이언트에만 권장하고, [RFC 7521](https://datatracker.ietf.org/doc/html/rfc7521)은 그 정책을 인가 서버에 맡깁니다. 이 SDK는 양쪽 모두에서 보수적인 해석을 택합니다. 내장 인가 서버는 공유 시크릿이 없는 클라이언트를 거부하고, 이 공급자는 공유 시크릿을 반드시 요구합니다. `token_endpoint_auth_method`는 시크릿이 어디에 실려 가는지를 고릅니다. `client_secret_post`(기본값, 폼 본문에)나 `client_secret_basic`(HTTP Basic 헤더) 중 하나입니다. 프로파일은 `private_key_jwt`도 허용하지만, 이 공급자는 지원하지 않습니다. !!! tip `client_secret`은 환경 변수나 시크릿 관리자에서 읽어 오세요. 소스 관리에서 읽어서는 절대 안 됩니다. @@ -91,6 +91,7 @@ SDK가 직접 인가 서버가 **될** 수도 있습니다. `create_auth_routes` * `identity_assertion_enabled=True`가 모든 것의 관문입니다. 꺼져 있으면(기본값), 훅을 구현했더라도 `/token`은 이 그랜트에 `unsupported_grant_type`으로 응답하고 메타데이터에도 언급되지 않습니다. 켜면 메타데이터에 `jwt-bearer` 그랜트 유형이 추가되고, 확장이 지원을 알리는 데 쓰는 필드인 `authorization_grant_profiles_supported`에 `urn:ietf:params:oauth:grant-profile:id-jag`가 나열됩니다. (이 SDK의 클라이언트는 이 필드를 읽지 않습니다. 발급자 하나에 맞춰 프로비저닝되어 있으므로 그냥 요청할 뿐입니다.) * **`exchange_identity_assertion`**이 훅입니다. 이 훅이 실행되기 전에 SDK는 이미 클라이언트를 인증하고, 공개 클라이언트를 거부하고, 등록 정보에 이 그랜트가 나열되지 않은 클라이언트를 거부한 상태입니다. `IdentityAssertionParams`(원시 `assertion`, 요청된 `scopes`와 `resource`)를 받아 평범한 `OAuthToken`을 반환합니다. +* 공개 클라이언트를 거부하는 것은 사양의 요구 사항이 아니라 SDK의 정책입니다. 내장 서버는 공유 시크릿으로만 클라이언트를 인증합니다. `private_key_jwt`를 지원하지 않고 Client ID Metadata Document도 아직 해석하지 않으므로([#1801](https://github.com/modelcontextprotocol/python-sdk/issues/1801)), 그런 문서로 식별되는 클라이언트는 여기서 이 그랜트를 사용할 수 없습니다. 다른 정책이 필요한 배포 환경이라면 `create_auth_routes`가 반환하는 `/token` 라우트를 자체 라우트로 교체할 수 있습니다. * 동적 클라이언트 등록은 이 그랜트를 무조건 거부하므로, 여기서 `get_client`는 수동으로 프로비저닝한 클라이언트를 내줍니다. ID-JAG 클라이언트는 스스로 등록해서 생겨날 수 없습니다. * 클래스의 절반은 거부 코드입니다. `OAuthAuthorizationServerProvider`는 인가 서버 **전체**이므로 인가 코드 흐름도 요구합니다. 사용자 로그인까지 처리하는 서버라면 그 부분을 실제로 구현하지만, 이 서버에는 문이 정확히 하나뿐입니다. diff --git a/i18n/ko/pages/client/transports.md b/i18n/ko/pages/client/transports.md index 19b50e2ed5..414b58022c 100644 --- a/i18n/ko/pages/client/transports.md +++ b/i18n/ko/pages/client/transports.md @@ -1,6 +1,6 @@ --- translation: - sections: [9cac816674181eb0, 7c157764133fea1f, 40b4916d82eaf1d4, 10d151f2cc75317f, 3d0832f39b0d7059, 92742ba36533633d, 0aeca6145e7bd302] + sections: [9cac816674181eb0, 5619e950d206e6c8, 40b4916d82eaf1d4, 10d151f2cc75317f, 3d0832f39b0d7059, 92742ba36533633d, 991c10e47fda2636] tool: 1 --- # 클라이언트 트랜스포트 {#client-transports} @@ -44,6 +44,8 @@ URL 문자열을 전달하면 **Streamable HTTP**를 사용합니다. 배포할 * `httpx2.AsyncClient`의 소유자는 작성한 코드이므로, 진입과 종료도 **직접** 해야 합니다. SDK는 자신이 만들지 않은 클라이언트를 절대 닫지 않습니다. * `streamable_http_client(url, http_client=...)`는 트랜스포트를 반환하고, `Client(transport)`는 이를 다른 것과 마찬가지로 받아들입니다. +`timeout=`은 그대로 두세요. SDK 자체 클라이언트가 쓰는 값(30초, read에는 300초)입니다. 타임아웃 없이 만든 `httpx2.AsyncClient`에는 `httpx2`의 기본값인 5초가 적용되며, 그보다 오래 걸리는 도구 호출은 read 타임아웃으로 실패합니다. + TLS 관련 참고 사항이 하나 있습니다. `httpx2`는 번들된 CA 목록이 아니라 ([`truststore`](https://pypi.org/project/truststore/)를 통해) 운영체제의 신뢰 저장소를 기준으로 인증서를 검증합니다. 사용 가능한 시스템 CA 저장소가 없는 환경(일부 최소 컨테이너)에서는 표준 @@ -51,16 +53,32 @@ TLS 관련 참고 사항이 하나 있습니다. `httpx2`는 번들된 CA 목록 `verify=ssl_context`를 전달하세요(배경 설명은 [`httpx2`로 대체된 `httpx`와 `httpx-sse`](../migration.md#httpx-and-httpx-sse-replaced-by-httpx2)에 있습니다). +### 더 큰 SSE 이벤트 {#larger-sse-events} + +서버가 큰 도구 결과나 알림을 SSE 이벤트 하나로 보낼 때는 `max_sse_event_size`를 전달하세요. + +```python title="client.py" hl_lines="6-9" +--8<-- "docs_src/client_transports/tutorial005.py" +``` + +기본값은 이벤트당 1MiB이며, 이벤트를 파싱하기 전의 바이트 수로 측정합니다. 이 제한은 +POST 응답, GET 스트림, 재개된 스트림에 적용됩니다. POST 응답이나 재개된 스트림에서 크기를 초과한 +이벤트가 오면 해당 요청은 SSE 오류로 실패합니다. 백그라운드 GET 스트림에서는 클라이언트가 +오류를 로그에 남기고 스트림을 다시 시도합니다. 서버를 신뢰하고 더 큰 이벤트가 필요하다면 +`max_sse_event_size=None`으로 설정해 제한을 끄세요. JSON 응답은 영향을 받지 않습니다. `ClientSessionGroup`을 사용한다면 +`StreamableHttpParameters`에 같은 옵션을 설정하세요. + !!! warning `streamable_http_client`는 예전에 `headers=`와 `timeout=`을 직접 받았습니다. 이제는 받지 않습니다. - 매개변수는 `url`, `http_client`, `terminate_on_close`뿐입니다. 습관적으로 `headers=`를 쓰면 + 매개변수는 `url`, `http_client`, `terminate_on_close`, `max_sse_event_size`뿐입니다. 습관적으로 `headers=`를 쓰면 다음 오류가 납니다. ```text TypeError: streamable_http_client() got an unexpected keyword argument 'headers' ``` - HTTP와 관련된 모든 것은 이제 전달하는 `httpx2.AsyncClient` 하나에 담깁니다. + 헤더, 인증, 프록시, 타임아웃은 전달하는 `httpx2.AsyncClient` 하나에 담깁니다. + 반면 `max_sse_event_size`는 MCP 트랜스포트의 SSE 리더에 적용됩니다. !!! info `httpx2`는 익숙한 `httpx` API를 그대로 유지하므로, `httpx`를 안다면 여기서 인증, 프록시, @@ -137,6 +155,7 @@ TLS 관련 참고 사항이 하나 있습니다. `httpx2`는 번들된 CA 목록 * `Client("http://.../mcp")`(URL)는 프로덕션 트랜스포트인 Streamable HTTP로 연결합니다. * 헤더, 인증, 프록시, 타임아웃은 `streamable_http_client(url, http_client=...)`에 전달하는 `httpx2.AsyncClient`에 설정합니다. `headers=` 키워드는 없습니다. +* SSE 이벤트당 바이트 제한을 바꾸려면 `streamable_http_client(url, max_sse_event_size=...)`를 사용하세요. * 리디렉션은 URL 자체의 오리진 안에서만(후행 슬래시 `307`/`308`) 따라가며, 같은 호스트의 `http`→`https`도 따라갑니다. 그 밖의 경우는 `Redirect to … not followed`로 실패하므로 최종 URL을 설정하세요. * stdio는 `Client(StdioServerParameters(...))`입니다. 자식 프로세스의 stderr를 다른 곳으로 돌릴 때만 직접 `stdio_client(...)`로 감싸세요. * 서브프로세스는 현재 환경이 아니라 허용 목록에 있는 환경 변수만 받습니다. `env=`로 여기에 추가합니다. diff --git a/i18n/ko/pages/handlers/cancellation.md b/i18n/ko/pages/handlers/cancellation.md new file mode 100644 index 0000000000..e5e18846dc --- /dev/null +++ b/i18n/ko/pages/handlers/cancellation.md @@ -0,0 +1,60 @@ +--- +translation: + sections: [07968345fdc0b84e, 4ea8416db9efa0dc, 336a7b4c5d0a4578, 18392e805dde6717, c30d50df43f9b55c] + tool: 1 +--- +# 취소 {#cancellation} + +클라이언트는 호출을 포기할 수 있습니다. 사용자가 중지 버튼을 눌렀거나 타임아웃이 만료된 경우입니다. + +그러면 SDK가 **핸들러를 취소**합니다. 핸들러가 기다리던 `await`에서 예외가 발생하고, 함수가 빠져나오며, 함수가 반환하는 값은 전송되지 않습니다. 대부분의 핸들러는 이에 대해 아무것도 할 필요가 없습니다. + +두 가지 경우는 예외입니다. 정리할 것이 있는 핸들러와 일반 `def`로 작성한 핸들러입니다. + +## `async def` 도구에서 정리하기 {#clean-up-in-an-async-def-tool} + +정리 코드를 `finally`에 넣으세요. + +```python title="server.py" hl_lines="23 26-28" +--8<-- "docs_src/cancellation/tutorial001.py" +``` + +* `finally`는 도구가 어떻게 끝나든 실행됩니다. 값을 반환했든, 예외를 발생시켰든, 취소되었든 마찬가지입니다. +* `await`가 필요한 정리 작업에는 `shield=True`가 필요합니다. 취소된 핸들러에서는 이후의 모든 `await`에서도 예외가 발생하므로, shield가 없으면 `release_hold`는 첫 줄에서 멈춥니다. +* shield로 보호된 블록은 무엇으로도 취소할 수 없으므로 시간 제한을 두세요. 여기서는 `5`초입니다. + +!!! tip + `except`가 아니라 `finally`를 사용하세요. 정리가 끝난 뒤에도 취소는 계속 위로 전파되어야 하는데, + `finally`를 쓰면 그렇게 됩니다. + +## 일반 `def` 도구에서 일찍 멈추기 {#stop-early-in-a-plain-def-tool} + +일반 `def` 도구는 스레드에서 실행되며, 스레드는 외부에서 중단시킬 수 없습니다. 도구가 직접 확인해야 합니다. + +```python title="server.py" hl_lines="22 25-26" +--8<-- "docs_src/cancellation/tutorial002.py" +``` + +* `anyio.from_thread.check_cancelled()`는 호출이 살아 있는 동안에는 아무 일도 하지 않고, 호출이 취소된 뒤에는 예외를 발생시킵니다. 작업 단위 사이마다 호출하세요. +* 여기서도 정리 코드는 `finally`에 넣습니다. 스레드에서는 아무것도 await하지 않으므로 shield가 필요 없습니다. +* 한 번도 확인하지 않는 `def` 도구는 끝까지 실행되고, 그 결과는 버려집니다. + +## 적용 범위 {#where-it-applies} + +프롬프트 함수와 리소스 함수도 도구와 똑같이 취소됩니다. + +stdio와 Streamable HTTP에서 동일하게 동작합니다. 이 SDK의 `Client`에서 호출을 포기한다는 것은 `call_tool`을 await하는 태스크를 취소하거나, `read_timeout_seconds`가 만료되도록 두는 것을 뜻합니다. + +!!! warning + Streamable HTTP 옵션 두 가지는 취소 소식이 핸들러에 전달되지 않게 합니다. `2026-07-28` 연결에서의 + `json_response=True`와 레거시 연결에서의 `stateless_http=True`입니다. 이 경우 클라이언트가 무엇을 했든 + 핸들러는 끝까지 실행됩니다. + +## 요약 {#recap} + +* 클라이언트가 호출을 포기하면 SDK가 핸들러를 취소합니다. 도구, 프롬프트, 리소스 모두 해당합니다. +* `async def`: `finally`에서 정리하고, await가 필요한 정리 작업은 `anyio.move_on_after(seconds, shield=True)` 안에 넣으세요. +* 일반 `def`: 작업 단위 사이마다 `anyio.from_thread.check_cancelled()`를 호출하세요. 그렇지 않으면 도구가 끝까지 실행됩니다. 정리는 일반 `finally`로 충분합니다. +* `json_response=True`(최신 연결)와 `stateless_http=True`(레거시 연결)는 취소를 비활성화합니다. + +진행 상황과 취소는 실행 중인 도구와 그 **호출자** 사이의 일입니다. 서버 **운영자**를 위해 도구가 남기는 로그는 별도의 채널이며, **[로깅](logging.md)**에서 다룹니다. diff --git a/i18n/ko/pages/handlers/index.md b/i18n/ko/pages/handlers/index.md index bee33f0381..1fcad31ebf 100644 --- a/i18n/ko/pages/handlers/index.md +++ b/i18n/ko/pages/handlers/index.md @@ -1,6 +1,6 @@ --- translation: - sections: [424930166c4bc6f3] + sections: [22ca41e50cc1b536] tool: 1 --- # 핸들러 내부 {#inside-your-handler} @@ -18,6 +18,7 @@ translation: * **[엘리시테이션(elicitation)](elicitation.md)**으로 사용자에게 추가 입력을 요청합니다. 이를 실어 나르는 2026-07-28 패턴은 **[다중 왕복 요청](multi-round-trip.md)**에서 다룹니다. * **[샘플링과 루트](sampling-and-roots.md)**로 클라이언트에 LLM 완성이나 작업 공간 폴더를 요청합니다. 지원 중단 예정(deprecated)이지만 여전히 제공됩니다. * 오래 걸리는 작업의 **[진행률](progress.md)**을 보고합니다. +* 클라이언트가 호출을 포기하면 **[취소](cancellation.md)**로 뒷정리를 하거나 일찍 중단합니다. * **[로깅](logging.md)**으로 로그를 남깁니다(서버를 운영하는 사람을 위해 표준 오류로 출력됩니다). * **[구독](subscriptions.md)**으로 구독 중인 클라이언트에게 변경 사항을 알립니다. diff --git a/i18n/ko/pages/handlers/lifespan.md b/i18n/ko/pages/handlers/lifespan.md index 3736b8c8cc..8adc8f2c6a 100644 --- a/i18n/ko/pages/handlers/lifespan.md +++ b/i18n/ko/pages/handlers/lifespan.md @@ -1,6 +1,6 @@ --- translation: - sections: [f3ca8ac5f90f2dfa, 85a1ef3588ba0736, 563346d4d5804933, 9e3528340d0bab53] + sections: [f3ca8ac5f90f2dfa, 48e478ef7bd688b1, 563346d4d5804933, 52ac6a7734d6f581] tool: 1 --- # Lifespan {#lifespan} @@ -46,7 +46,7 @@ lifespan은 **한 번** 실행됩니다. 서버가 시작될 때(첫 요청 전) 모델이 전달할 수 있는 인자는 `genre`뿐입니다. lifespan은 서버 내부의 일입니다. -`@mcp.resource()`와 `@mcp.prompt()` 함수도 `ctx` 매개변수를 받을 수 있는데, 다음 절에서 설명할 이유로 타입 매개변수 없는 `Context`로 씁니다. `ctx`가 담고 있는 모든 것은 **[Context](context.md)**에서 확인하세요. +`@mcp.resource()`와 `@mcp.prompt()` 함수도 `ctx` 매개변수를 받을 수 있습니다. `ctx`가 담고 있는 모든 것은 **[Context](context.md)**에서 확인하세요. ### 제대로 된 타입 지정 {#it-really-is-typed} @@ -56,19 +56,6 @@ lifespan은 **한 번** 실행됩니다. 서버가 시작될 때(첫 요청 전) 대신 타입 매개변수 없는 `Context`를 쓰면 `lifespan_context`의 타입은 `dict[str, Any]`가 됩니다. 타입 검사기로서는 lifespan이 무엇을 yield했는지 알 방법이 없기 때문입니다. 객체는 런타임에 여전히 존재하지만, 도움은 잃게 됩니다. -!!! warning - `Context[AppContext]`는 **도구 전용** 표기입니다. `@mcp.resource()`나 - `@mcp.prompt()` 함수에 붙이면 해당 핸들러 호출은 모두 실패합니다. 클라이언트는 오류를 돌려받고, - 서버 로그에 그 이유가 나타납니다. - - ```text - Context is not available outside of a request - ``` - - 리소스와 프롬프트에서는 타입 매개변수 없는 `ctx: Context`를 쓰세요. lifespan이 yield한 객체는 - 런타임에 여전히 `ctx.request_context.lifespan_context`에 있습니다. 포기하는 것은 타입 매개변수이지 - 객체가 아닙니다. - !!! tip lifespan은 항상 있습니다. 전달하지 않으면 SDK의 기본 lifespan이 빈 `dict`를 yield하므로 `ctx.request_context.lifespan_context`는 `{}`이며, 절대 `None`이 아닙니다. 타입 매개변수 없는 @@ -101,7 +88,7 @@ lifespan은 **한 번** 실행됩니다. 서버가 시작될 때(첫 요청 전) * `yield` 앞의 코드는 시작입니다. 뒤의 `finally`는 종료입니다. * 요청마다 실행되는 것이 아니라 서버의 전체 수명을 감싸며 한 번 실행됩니다. * `yield`한 것은 모든 도구, 리소스, 프롬프트에서 `ctx.request_context.lifespan_context`입니다. -* `ctx: Context[AppContext]`는 도구에서 이 접근에 완전한 타입을 부여합니다. 리소스와 프롬프트는 타입 매개변수 없는 `Context`를 받습니다. +* `ctx: Context[AppContext]`는 이 접근에 완전한 타입을 부여합니다. * `lifespan=` 매개변수가 없으면 빈 `dict`이며, 절대 `None`이 아닙니다. 호출 도중 멈추고 사용자만 아는 것을 사용자에게 묻는 핸들러는 **[엘리시테이션(elicitation)](elicitation.md)**에서 다룹니다. diff --git a/i18n/ko/pages/handlers/progress.md b/i18n/ko/pages/handlers/progress.md index d576a9171c..7d6221d46c 100644 --- a/i18n/ko/pages/handlers/progress.md +++ b/i18n/ko/pages/handlers/progress.md @@ -1,6 +1,6 @@ --- translation: - sections: [5315262fe26b33e1, 9d8e98840f1b78f0, 52d6009a07e770ea, 8534d8dbb4053a70, 2966fac6fe697007] + sections: [5315262fe26b33e1, 9d8e98840f1b78f0, 52d6009a07e770ea, 8534d8dbb4053a70, 2e9aff14d3a882c0] tool: 1 --- # 진행 상황 {#progress} @@ -119,4 +119,4 @@ Imported https://example.com/b.json (2.0/2.0) * 호출에 콜백이 없으면 `report_progress`는 아무 일도 하지 않습니다. 조건 없이 보고하세요. * `total`을 모르면 생략하세요. 콜백은 `None`을 받습니다. -진행 상황은 실행 중인 도구가 **사용자**에게 보여 주는 것입니다. 서버를 운영하는 **운영자**를 위해 도구가 남기는 로그 줄은 별개의 채널이며, **[로깅](logging.md)**에서 다룹니다. +진행 상황은 아직 기다리고 있는 클라이언트를 위한 것입니다. 클라이언트가 기다리기를 멈췄을 때 도구가 보게 되는 것이 **[취소](cancellation.md)**입니다. diff --git a/i18n/ko/pages/handlers/subscriptions.md b/i18n/ko/pages/handlers/subscriptions.md index 6592a740e5..35b3528c23 100644 --- a/i18n/ko/pages/handlers/subscriptions.md +++ b/i18n/ko/pages/handlers/subscriptions.md @@ -1,6 +1,6 @@ --- translation: - sections: [60a9de8a0bdaa531, 317bbe7e4355cdcc, a61d660c8029e04a, 8f7e82fcb88df8a9, b165db51249ff8ed, 266f56fb798068a4, 7c0e57030b622139, df18d7c2417a9883] + sections: [60a9de8a0bdaa531, 6693607ea56d8bd6, a61d660c8029e04a, 8f7e82fcb88df8a9, b165db51249ff8ed, b8bc624a627ead9b, 2139e68e36d9e621, 7c0e57030b622139, 34ab1af2b9ab5b45] tool: 1 --- # 구독 {#subscriptions} @@ -22,7 +22,7 @@ translation: * 형제 메서드로 `notify_prompts_changed()`와 `notify_resources_changed()`가 있습니다. * 구독자가 없으면 할 일도 없습니다. 유휴 상태의 서버에 게시하는 것은 아무 동작도 하지 않으므로, 누가 듣고 있는지 확인할 필요가 전혀 없습니다. 무엇이 바뀌었는지만 알리면 됩니다. -`MCPServer`가 `subscriptions/listen`을 대신 처리합니다. 와이어 수준의 의무(첫 프레임으로 보내는 확인 응답, 스트림별 필터링, 모든 프레임에 붙는 구독 id)는 SDK의 몫입니다. +[꺼 두지](#turning-it-off) 않는 한 `MCPServer`가 `subscriptions/listen`을 대신 처리합니다. 와이어 수준의 의무(첫 프레임으로 보내는 확인 응답, 스트림별 필터링, 모든 프레임에 붙는 구독 id)는 SDK의 몫입니다. !!! check 와이어 위에서, 필터에 `board://sprint`를 지정한 스트림은 `complete_task`가 실행된 뒤 다음과 같이 보입니다. @@ -79,7 +79,32 @@ translation: 게시된 이벤트는 핸들러에서 열린 스트림까지 `SubscriptionBus`를 거쳐 이동합니다. 기본은 인메모리입니다. 프로세스 하나, 그 안의 모든 스트림입니다. 로드 밸런서 뒤에서 레플리카를 실행하기 전까지는 이것이 정답입니다. 그 이후에는 클라이언트의 스트림이 한 레플리카에 고정되고, 다른 레플리카에서 게시한 이벤트가 그 스트림에 도달해야 하기 때문입니다. -그 이음매는 직접 구현할 부분입니다. 사용하는 pub/sub 백엔드 위에 메서드 두 개를 만들면 됩니다. +기본 버스로는 그럴 수 없습니다. 레플리카마다 버스가 따로 있기 때문입니다. + +```mermaid +flowchart LR + client[Client] --> lb[Load balancer] + lb --> stream + lb ~~~~ gap + lb --> tool + subgraph B [Replica B] + tool[tools/call] -- publishes --> busB[(bus B)] + end + gap[(no shared bus)] + subgraph A [Replica A] + stream[listen stream] -- subscribed --> busA[(bus A)] + end + style A fill:none + style B fill:none + style gap fill:none,stroke-dasharray:4 4 +``` + +실패하는 것은 없습니다. 호출은 성공하고, 스트림은 조용합니다. 따라서 로드 밸런서 뒤에서는 둘 중 하나를 선택하세요. + +* **변경 알림이 필요한 경우.** 아래와 같이 모든 레플리카에 같은 버스를 제공하세요. +* **필요하지 않은 경우.** [변경 알림을 끄세요](#turning-it-off). 그러면 어떤 클라이언트도 놓치게 될 이벤트를 약속받거나 그 이벤트를 기다리며 스트림을 열어 두지 않습니다. + +공유 버스는 직접 구현할 부분입니다. 사용하는 pub/sub 백엔드 위에 메서드 두 개를 만들면 됩니다. ```python from collections.abc import Callable @@ -128,6 +153,20 @@ async def tools_reloaded() -> None: await bus.publish(ToolsListChanged()) # from a lifespan task, a webhook, anywhere ``` +## 끄기 {#turning-it-off} + +카탈로그가 전혀 바뀌지 않는 서버는 게시할 것이 없습니다. 서버를 만들 때 그렇게 알려 주세요. + +```python title="server.py" hl_lines="3" +--8<-- "docs_src/subscriptions/tutorial007.py" +``` + +* `2026-07-28` 클라이언트에는 변경 알림이 광고되지 않으며, `subscriptions/listen` 요청은 열린 스트림 대신 *Method not found*를 받습니다. +* `ctx.notify_*` 메서드는 여전히 동작하지만 아무에게도 도달하지 않으므로, 핸들러는 그대로 두면 됩니다. +* 이전 프로토콜 버전의 클라이언트에는 아무 차이가 없습니다. + +열린 스트림은 끝나지 않는 요청이므로, 요청 지속 시간에 따라 요금을 부과하는 호스트에서도 이 설정이 중요합니다. + ## 저수준 구성 {#the-low-level-composition} 저수준 `Server`에는 미리 연결된 것이 아무것도 없으며, 같은 부품이 세 줄로 조립됩니다. @@ -148,5 +187,6 @@ async def tools_reloaded() -> None: * 클라이언트 쪽은 `async with client.listen(...)`입니다. 자세한 내용은 **클라이언트** 아래의 **[구독](../client/subscriptions.md)**에서 확인하세요. * 저수준 `Server`에서는 같은 부품을 직접 조립합니다. 버스, `ListenHandler(bus)`, `on_subscriptions_listen` 슬롯입니다. * 스케일 아웃은 메서드 두 개짜리 `SubscriptionBus`를 구현하고 `MCPServer(subscriptions=...)`로 전달하는 것을 뜻합니다. +* 게시할 것이 없거나 레플리카 사이에 공유 버스가 없는 경우, `MCPServer(subscriptions=False)`는 변경 알림을 광고하지 않고 스트림도 열어 두지 않습니다. 레플리카 하나 뒤에서든 스무 개 뒤에서든, 이 모든 것을 처리하는 서버를 실행하는 방법은 **[배포와 확장](../run/deploy.md)**에서 확인하세요. diff --git a/i18n/ko/pages/run/deploy.md b/i18n/ko/pages/run/deploy.md index c994ca1f04..8e770ddb1d 100644 --- a/i18n/ko/pages/run/deploy.md +++ b/i18n/ko/pages/run/deploy.md @@ -1,6 +1,6 @@ --- translation: - sections: [28221886b198784f, f88ea1f1614f3a1d, 2e76f5cb9df15042, ce926d686730b6d0, 3be24f8ad8bb5ab9, 3fad24032b2224ff, f25a7f860e579ecb, 697b01d95080880d] + sections: [28221886b198784f, f88ea1f1614f3a1d, 2e76f5cb9df15042, ce926d686730b6d0, 3be24f8ad8bb5ab9, aaf489e944ecf5d1, f25a7f860e579ecb, 697b01d95080880d] tool: 1 --- # 배포와 확장 {#deploy-scale} @@ -173,6 +173,7 @@ python -c "import secrets; print(secrets.token_hex(32))" * 실제 프로세스 간에는 **SDK가 제공하는 버스 중 도움이 되는 것이 없습니다.** `SubscriptionBus`는 메서드 두 개(`publish`와 `subscribe`)짜리 `Protocol`이며, 자체 pub/sub 백엔드(Redis, NATS, 이미 운영 중인 무엇이든) 위에 구현해서 `MCPServer(subscriptions=...)`로 전달합니다. 스케치와 계약은 **[구독](../handlers/subscriptions.md#scaling-past-one-process)**에서 확인하세요. * 버스는 작은 타입 이벤트 네 가지만 나르며, JSON-RPC는 절대 나르지 않습니다. 확인 응답, 필터링, 스트림 생명 주기는 SDK에 남아 있으므로, 버스가 프로토콜을 깨뜨릴 수는 없고 프로세스 간에 이벤트를 옮길 수만 있습니다. * 스트림은 재개할 수 **없고** 이벤트는 재생되지 **않습니다**. 레플리카를 잃으면 그 스트림도 끊기고, 클라이언트는 다시 listen하고 다시 가져옵니다. 공유할 이벤트 저장소도, 따로 설정할 것도 없습니다. 확장이 정말로 같은 것을 더 늘리는 일에 불과한 곳은 여기 하나뿐입니다. +* 변경 알림이 필요 없는 서버라면 버스는 건너뛰고 **[변경 알림을 끄세요](../handlers/subscriptions.md#turning-it-off)**. ## SDK가 제공하지 않는 것 {#what-the-sdk-does-not-give-you} diff --git a/i18n/ko/pages/servers/structured-output.md b/i18n/ko/pages/servers/structured-output.md index 6884e2e585..d9687f4ba7 100644 --- a/i18n/ko/pages/servers/structured-output.md +++ b/i18n/ko/pages/servers/structured-output.md @@ -1,6 +1,6 @@ --- translation: - sections: [a838d57f003aed44, 857d03886a0137ed, 42d9efcb9f542867, 2290ff08435b5573, 91be9b73602abcf1, 6cdbad079f7b47f0, d4b607372fb28b51, 18dbf726ac45e0b7, c7eff2a5698225fa, c851964bb3301907, 8f296f1f09e4c400, d715db6f8dccc9cc, a0c344a48450dbe4] + sections: [a838d57f003aed44, 857d03886a0137ed, 42d9efcb9f542867, 2290ff08435b5573, 91be9b73602abcf1, 6cdbad079f7b47f0, d4b607372fb28b51, 7608fc5ebc31d6ea, c7eff2a5698225fa, c851964bb3301907, 8f296f1f09e4c400, d715db6f8dccc9cc, a0c344a48450dbe4] tool: 1 --- # 구조화된 출력 {#structured-output} @@ -172,6 +172,8 @@ result.structured_content # {"London": 16.2, "Reykjavik": 4.4} 키는 반드시 `str`이어야 합니다. `dict[int, float]` 타입은 JSON 객체가 될 수 없으므로 `{"result": ...}` 형태로 감싸는 방식으로 되돌아갑니다. +딕셔너리 결과는 검증과 직렬화에 Pydantic의 `TypeAdapter`를 사용합니다. 도구의 `FuncMetadata.output_model`을 살펴보면 스키마 제목이 붙은 딕셔너리 타입 어노테이션이 들어 있습니다. + ## 검증 {#validation} `output_schema`는 문서가 아닙니다. 함수가 무엇을 반환하든 서버를 떠나기 전에 **이 스키마에 맞춰 검증됩니다**. diff --git a/i18n/ko/pages/servers/tools.md b/i18n/ko/pages/servers/tools.md index a9bbe8de0e..e967494b9d 100644 --- a/i18n/ko/pages/servers/tools.md +++ b/i18n/ko/pages/servers/tools.md @@ -1,6 +1,6 @@ --- translation: - sections: [e4cc390d56573409, f30cf8103a6e918c, 2c97b9f888398951, 048e5471dfa71aea, 3076b1e16ad95950, edbedf2a16e71311, 3d8ef8da89fa87c1, f6c0e02e6ea5a363] + sections: [e4cc390d56573409, f30cf8103a6e918c, 2c97b9f888398951, 048e5471dfa71aea, 3076b1e16ad95950, c1115cd005b81e8f, 3d8ef8da89fa87c1, f6c0e02e6ea5a363] tool: 1 --- # 도구 {#tools} @@ -141,7 +141,7 @@ Inspector는 필수 항목인 `query` 텍스트 필드와 필수 항목인 `limi 도구가 I/O를 한다면(API를 호출하거나, 파일을 읽거나, 데이터베이스를 조회한다면) `async def`로 선언하고 그 안에서 `await`를 쓰세요. SDK가 알아서 await합니다. -일반 `def` 도구도 잘 동작합니다. SDK가 스레드에서 실행하므로 서버를 막는 일이 없습니다. +일반 `def` 도구도 잘 동작합니다. SDK가 스레드에서 실행하므로 서버를 막는 일이 없습니다. 오래 걸리는 도구는 클라이언트가 아직 기다리고 있는지 확인할 수 있습니다. 자세한 내용은 **[취소](../handlers/cancellation.md)**를 참고하세요. 따로 설정할 것은 아무것도 없습니다. diff --git a/i18n/ko/pages/troubleshooting.md b/i18n/ko/pages/troubleshooting.md index 7a7bed2ca8..11c9e9c445 100644 --- a/i18n/ko/pages/troubleshooting.md +++ b/i18n/ko/pages/troubleshooting.md @@ -1,6 +1,6 @@ --- translation: - sections: [3d58228e81b99543, 170514ce901c4139, 17d61fad0a50d62b, 8a6e351ec756904d, 137454d469c867f5, 6392596bd6df54f0, 41126fa9c4fe432f, 480b6d7897e30ab4, d83bb682e708dde0, ebbed3449c499db4, 525cdf1755e29d4c, 30fd31be74169d9a, d2e88333d4f7841f, c2dc3b1007d2e987, d6eabf60cc366341, f798e815252852c2, 0cba47bae78d04eb, 2c218ba829abf74e] + sections: [3d58228e81b99543, 170514ce901c4139, 17d61fad0a50d62b, 8a6e351ec756904d, 137454d469c867f5, fcf984fa0615ed11, 6392596bd6df54f0, 41126fa9c4fe432f, 480b6d7897e30ab4, d83bb682e708dde0, ebbed3449c499db4, 525cdf1755e29d4c, 30fd31be74169d9a, d2e88333d4f7841f, c2dc3b1007d2e987, d6eabf60cc366341, f798e815252852c2, 0cba47bae78d04eb, cdc6d86a4dae8a34] tool: 1 --- # 문제 해결 {#troubleshooting} @@ -128,6 +128,14 @@ TypeError: The @tool decorator was used incorrectly. Did you forget to call it? 이 경우에 해당합니다. 직접 `python server.py`를 실행해 트레이스백을 읽으세요. 타입 검사기도 이를 잡아냅니다. 함수는 유효한 `name=` 값이 아니기 때문입니다. +## `InvalidSignature: Tool '' has an invalid x-mcp-header annotation: ` {#invalidsignature-tool-name-has-an-invalid-x-mcp-header-annotation-reason} + +도구 인자에 사양이 허용하지 않는 방식으로 `x-mcp-header`가 표시된 경우이며, 어느 규칙을 어겼는지는 `` 부분에 나옵니다. `2026-07-28` 클라이언트는 그런 도구를 목록에서 빼 버리므로, SDK는 등록 자체를 거부합니다. + +표시할 수 있는 것은 `str`, `int`, `bool` 인자뿐이며, `str | None`은 그 어느 것에도 해당하지 않습니다. 선택적 인자를 쓰는 방법은 **[헤더 매개변수](advanced/header-parameters.md)**에 있습니다. + +위 항목과 마찬가지로, 이 예외는 클라이언트가 연결되기 전, 모듈을 **임포트**하는 시점에 발생합니다. + ## `Tool already exists: ` {#tool-already-exists-name} 두 등록이 같은 도구 이름을 사용한 경우입니다. **먼저** 등록된 쪽이 이기고 두 번째는 조용히 버려지며, **서버 로그**에 남는 이 경고가 유일한 신호입니다. @@ -425,6 +433,7 @@ mcp = MCPServer("Weather", request_state_security=RequestStateSecurity(keys=[key * `ExceptionGroup: unhandled errors in a TaskGroup`은 절대 진짜 오류가 아닙니다. **마지막 줄**을 읽으세요. `async with Client(...)` 블록 **안에서** `MCPError`를 잡으면 감싸기를 완전히 건너뜁니다. * `call_tool`은 실패한 도구에 대해 예외를 일으키지 않습니다. `Error executing tool ...` 및 `Unknown tool: ...` 메시지는 결과이므로 `result.is_error`를 확인하세요. 도구 이름 뒤에 메시지가 없으면 도구가 죽은 것이며, 트레이스백은 서버 로그에 있습니다. * `Client must be used within an async context manager` -> `async with`를 사용하세요. `Use @tool() instead of @tool` -> 괄호를 추가하세요. +* `has an invalid x-mcp-header annotation` -> `str`, `int`, `bool` 인자에만 표시할 수 있습니다. * 서버 로그의 `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()`에 진입하지 않은 경우입니다. diff --git a/i18n/ko/pages/whats-new.md b/i18n/ko/pages/whats-new.md index 6c50e98d21..45ced66ce4 100644 --- a/i18n/ko/pages/whats-new.md +++ b/i18n/ko/pages/whats-new.md @@ -1,6 +1,6 @@ --- translation: - sections: [cfe01c0c5863dfa2, 0dbb68d8b210177b, 80cf193023af3ed4, 875eb2889263424e] + sections: [cfe01c0c5863dfa2, 0dbb68d8b210177b, bd42ee3a268f6ea4, 875eb2889263424e] tool: 1 --- # v2에서 달라진 점 {#whats-new-in-v2} @@ -204,7 +204,7 @@ Streamable HTTP에서는 2026 경로에 `Mcp-Session-Id`가 없으며, 운영 ### 나머지 변경 사항 한눈에 보기 {#the-rest-quickly} * **식별 정보는 선택적인 메시지별 메타데이터입니다.** 요청 쪽의 `clientInfo` `_meta` 키는 선택 사항이고(필수 쌍은 `protocolVersion` + `clientCapabilities`입니다), `serverInfo`는 `server/discover` 결과 본문에서 빠졌습니다. 대신 서버가 2026 시대의 모든 결과의 `_meta`에 찍어 넣습니다([사양 #3002](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/3002)). SDK는 항상 찍어 넣으며, 서버가 자신을 식별하지 않는 경우(예를 들어 미들웨어가 키를 제거한 경우) `client.server_info`는 `None`입니다. 와이어에 찍힌 모습은 **[저수준 Server](advanced/low-level-server.md)**에서 볼 수 있습니다. -* **본문을 파싱하지 않고도 요청을 라우팅할 수 있습니다.** 최신 방식의 HTTP 요청에는 `Mcp-Method`가 실리고(도구 성격의 호출 세 가지에는 `Mcp-Name`도 실립니다), `x-mcp-header`로 어노테이션한 도구 입력 스키마 속성은 `Mcp-Param-*` 헤더로 복제되어 서버가 교차 검증합니다([SEP-2243](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2243)). 게이트웨이와 속도 제한기는 헤더만으로 라우팅할 수 있습니다. 규칙은 **[마이그레이션 가이드](migration.md#servers-validate-mcp-param-headers-against-the-request-body-sep-2243)**에 있습니다. +* **본문을 파싱하지 않고도 요청을 라우팅할 수 있습니다.** 최신 방식의 HTTP 요청에는 `Mcp-Method`가 실리고(도구 성격의 호출 세 가지에는 `Mcp-Name`도 실립니다), `x-mcp-header`로 어노테이션한 도구 입력 스키마 속성은 `Mcp-Param-*` 헤더로 복제되어 서버가 교차 검증합니다([SEP-2243](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2243)). 게이트웨이와 속도 제한기는 헤더만으로 라우팅할 수 있습니다. 인자에 표시를 다는 방법은 **[헤더 매개변수](advanced/header-parameters.md)**에서 볼 수 있습니다. * **결과에 캐시 힌트가 실립니다.** 목록 및 읽기 결과는 `ttlMs`, `cacheScope` 두 필드를 선언합니다([SEP-2549](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2549)). 메서드별로 `cache_hints=` 인자로 설정하며, `Client`는 내장 응답 캐시로 이를 따릅니다. 힌트를 보내지 않는 서버(2026 이전의 모든 서버)는 이전과 동일한, 캐시되지 않은 트래픽을 받습니다. **[캐시 힌트](client/caching.md)**를 참고하세요. * **확장은 일급입니다.** 서버와 클라이언트는 역방향 DNS 식별자 아래에 선택적 기능 묶음을 선언합니다([SEP-2133](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2133)). 내장 `Apps` 확장(MCP Apps)이 참조 구현입니다. **[확장](advanced/extensions.md)**과 **[MCP Apps](advanced/apps.md)**를 참고하세요. * **오류 코드가 표준화되었습니다.** 없는 리소스는 `error.data`에 URI를 담은 `-32602` 오류이고, 사양이 새로 예약한 코드는 `-32020`(헤더 불일치), `-32021`(필수 기능 누락), `-32022`(지원하지 않는 프로토콜 버전)입니다. **[문제 해결](troubleshooting.md)**은 정확한 메시지를 기준으로 정리되어 있습니다. diff --git a/i18n/pt/pages/advanced/header-parameters.md b/i18n/pt/pages/advanced/header-parameters.md new file mode 100644 index 0000000000..39431707c1 --- /dev/null +++ b/i18n/pt/pages/advanced/header-parameters.md @@ -0,0 +1,65 @@ +--- +translation: + sections: [81862a209b483d27, b76e8073487afa03, bc214b2fc2bcdae4, d5835477c0b60163, a5d9786f902ad8e1] + tool: 1 +--- +# Parâmetros de cabeçalho {#header-parameters} + +A maioria dos servidores nunca precisa disso. + +Um gateway ou balanceador de carga na frente do seu servidor só consegue rotear com base no que ele lê sem analisar o corpo. Marque um argumento de uma ferramenta (tool) com `x-mcp-header`, e os clientes na **[versão do protocolo](../protocol-versions.md)** `2026-07-28` também enviam o valor dele como um cabeçalho HTTP. + +## Marque um argumento {#mark-an-argument} + +A marca é uma chave a mais no JSON Schema do argumento. No `MCPServer`, o `Field` a coloca lá: + +```python title="server.py" hl_lines="13" +--8<-- "docs_src/header_parameters/tutorial001.py" +``` + +* Por Streamable HTTP na `2026-07-28`, o cliente envia `Mcp-Param-Region` junto com o corpo, e o servidor rejeita uma chamada em que os dois divergem. +* Um cliente que não listou a ferramenta nunca viu a marca: ele não envia cabeçalho nenhum, e a chamada é rejeitada. Nesse caso, o `Client` deste SDK lista as ferramentas e reenvia a chamada uma vez, então listar antes só economiza uma ida e volta. +* Todas as outras conexões ignoram a anotação. + +Sua função não muda: `region` continua chegando como argumento. + +## O que pode ser marcado {#what-can-be-marked} + +Argumentos `str`, `int` e `bool`. Qualquer outra coisa é recusada no registro da ferramenta, com `InvalidSignature`. + +Isso inclui `str | None`, que não tem um tipo único. Um argumento opcional precisa ter o schema escrito por extenso, com o `WithJsonSchema` do pydantic: + +```python +region: Annotated[str | None, WithJsonSchema({"type": "string", "x-mcp-header": "Region"})] = None +``` + +## No `Server` de baixo nível {#on-the-low-level-server} + +Lá você escreve o `input_schema` à mão, então a chave entra direto: + +```python title="server.py" hl_lines="18" +--8<-- "docs_src/header_parameters/tutorial002.py" +``` + +* Nada verifica a anotação para você: uma anotação inválida é servida, e os clientes `2026-07-28` deixam a ferramenta fora da listagem deles. + +### Schemas por nome {#schemas-by-name} + +Para verificar o cabeçalho, o SDK precisa do schema de entrada da ferramenta antes de despachar a chamada. Sem `get_tool_input_schema`, ele obtém esse schema executando o seu handler `on_list_tools` em toda chamada que carrega argumentos, haja ou não alguma ferramenta marcada. + +```python title="server.py" hl_lines="26 39-41 48" +--8<-- "docs_src/header_parameters/tutorial003.py" +``` + +* Passe a função para responder a partir do que você já tem. +* Retorne `None` para uma ferramenta sem nada a verificar. + +## Resumo {#recap} + +* `x-mcp-header` em um argumento de ferramenta faz os clientes `2026-07-28` repetirem esse argumento como um cabeçalho HTTP `Mcp-Param-*`. +* O servidor rejeita uma chamada cujo cabeçalho e corpo divergem. +* Só argumentos `str`, `int` e `bool` podem ser marcados. O `MCPServer` lança `InvalidSignature` para qualquer outra coisa. +* O `Server` de baixo nível não verifica nada, e os clientes descartam uma ferramenta cuja anotação é inválida. +* `get_tool_input_schema` evita que o `Server` de baixo nível execute `on_list_tools` em toda chamada. + +O restante da API do `Server` escrita à mão está em **[O Server de baixo nível](low-level-server.md)**. diff --git a/i18n/pt/pages/advanced/index.md b/i18n/pt/pages/advanced/index.md index 2a8e333931..8e480f1016 100644 --- a/i18n/pt/pages/advanced/index.md +++ b/i18n/pt/pages/advanced/index.md @@ -1,6 +1,6 @@ --- translation: - sections: [ca6988b7503cd2d3] + sections: [348f8697c6b12cd0] tool: 1 --- # Avançado {#advanced} @@ -14,6 +14,8 @@ do `MCPServer` atrapalha: personalizados criados por você. * **[Paginação](pagination.md)** e **[Middleware](middleware.md)**: duas coisas que você *só* consegue fazer no `Server` de baixo nível. +* **[Parâmetros de cabeçalho](header-parameters.md)**: permitem que um gateway roteie uma chamada de ferramenta com base em um + dos argumentos dela. * **[Extensões](extensions.md)** e **[MCP Apps](apps.md)**: a superfície de extensão do protocolo. Componha pacotes de extensão em um servidor ou escreva os seus. diff --git a/i18n/pt/pages/advanced/low-level-server.md b/i18n/pt/pages/advanced/low-level-server.md index a75014bb2d..7df81f9bf8 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, 9d5d10a5f0405d0a, 1086e77ce561cd7f, a3f71823df5efc31, 9fc7109f72201cae, d50fe7faead8cf68, 7bf25983df655b66, 6330e1f4c6029683, 2f1749c8c133fa1c, 8db7116fc8ddd0ee, ebc33704fbd74262, 0fde3bcea081ba3a] + sections: [2c79b6338e09b7ac, 9d5d10a5f0405d0a, 1086e77ce561cd7f, a3f71823df5efc31, 9fc7109f72201cae, d50fe7faead8cf68, 7bf25983df655b66, 6330e1f4c6029683, 2f1749c8c133fa1c, 8db7116fc8ddd0ee, 2090d99b355bc2c7, 0fde3bcea081ba3a] tool: 1 --- # O Server de baixo nível {#the-low-level-server} @@ -209,6 +209,7 @@ Cada um destes é uma ideia para a qual você já tem o vocabulário; cada um te * `on_call_tool`, `on_get_prompt` e `on_read_resource` podem retornar um `InputRequiredResult` em vez do resultado normal para pausar a chamada e pedir entrada ao cliente; veja **[Requisições de múltiplas idas e voltas](../handlers/multi-round-trip.md)**. Fiel a este nível, nada é instalado para você: enquanto o `MCPServer` sela o `requestState` por padrão, aqui o `request_state` que você define atravessa o fio exatamente como foi escrito até você optar com `server.middleware.append(RequestStateBoundary(RequestStateSecurity(keys=[...]), default_audience=server.name))`: uma linha (os dois nomes são importados de `mcp.server.request_state`) para a mesma selagem e verificação que o `MCPServer` faz (**[Protegendo o `requestState`](../handlers/multi-round-trip.md#protecting-requeststate)**). * `on_list_resources`, `on_read_resource`, `on_list_prompts`, `on_get_prompt`, `on_completion` têm o mesmo formato `(ctx, params) -> result` para as outras primitivas. * `on_subscriptions_listen` serve o stream `subscriptions/listen` de 2026-07-28. Passe um `ListenHandler` construído sobre um `SubscriptionBus` e publique eventos no bus a partir dos seus outros handlers; veja **[Assinaturas](../handlers/subscriptions.md)** para a composição completa. +* `get_tool_input_schema` mantém `on_list_tools` fora do caminho de chamada; veja **[Parâmetros de cabeçalho](header-parameters.md#schemas-by-name)**. * `server.streamable_http_app()` retorna o mesmo app Starlette que o do `MCPServer`; faça o deploy dele do jeito que **[Executando o seu servidor](../run/index.md)** faz o deploy de qualquer outro app ASGI. Não existe `server.run(transport=...)` aqui embaixo: `server.run(read_stream, write_stream, server.create_initialization_options())` conduz uma conexão sobre um par de streams, e essa única linha é a história completa. ## Recapitulando {#recap} diff --git a/i18n/pt/pages/advanced/middleware.md b/i18n/pt/pages/advanced/middleware.md index e7917869d7..d7a5fbb742 100644 --- a/i18n/pt/pages/advanced/middleware.md +++ b/i18n/pt/pages/advanced/middleware.md @@ -1,6 +1,6 @@ --- translation: - sections: [6048b4f308edbb8c, 46056f318ef205e4, c3e565b61acd75c5, c62422b159c6ed09, 420968f514138f43] + sections: [58e1103d9a323ccf, 46056f318ef205e4, 812b414557fb0c35, 4df162eea2518d38, c62422b159c6ed09, 420968f514138f43] tool: 1 --- # Middleware {#middleware} @@ -15,8 +15,8 @@ Você o escreve como `async (ctx, call_next)` e o adiciona ao fim de `server.mid para *recusar* mensagens; não faça dela o alicerce sobre o qual o seu servidor se apoia. `MCPServer` recebe a lista na construção (`MCPServer(name, middleware=[...])`) e a expõe como -`mcp.middleware`; o `Server` de baixo nível expõe a mesma lista como `server.middleware`. O exemplo -abaixo usa o `Server` de baixo nível; se `Server(name, on_call_tool=...)` é novidade para você, leia +`mcp.middleware`; o `Server` de baixo nível expõe a mesma lista como `server.middleware`. Os exemplos +abaixo usam o `Server` de baixo nível; se `Server(name, on_call_tool=...)` é novidade para você, leia **[O Server de baixo nível](low-level-server.md)** primeiro. ## Um middleware de medição de tempo {#a-timing-middleware} @@ -61,14 +61,37 @@ cliente enviou para estabelecer a conexão, antes de você pedir qualquer coisa. * Até um método para o qual o servidor não tem handler: `call_next` lança o `MCPError(-32601, "Method not found")` *através* do seu middleware a caminho do cliente. +## Um limite de concorrência {#a-concurrency-cap} + +Um middleware não precisa chamar `call_next(ctx)`. Em vez disso, lance um `MCPError` e essa única +mensagem é **recusada**: a conexão continua de pé e a próxima mensagem passa. + +Digamos que cada busca ocupe uma conexão de um pool de quatro. Este middleware deixa quatro +chamadas de ferramenta rodarem ao mesmo tempo e recusa a quinta: + +```python title="server.py" hl_lines="15-16 40-55 59" +--8<-- "docs_src/middleware/tutorial002.py" +``` + +* Só `tools/call` é contado, então o servidor continua respondendo a `server/discover` e + `tools/list` enquanto recusa chamadas de ferramenta. +* O MCP não define nenhum código de erro de "servidor ocupado", então `SERVER_BUSY` é um código + próprio deste servidor. +* Recusar avisa o cliente na hora de que o servidor está sobrecarregado. Se você prefere fazer os + chamadores esperarem, segure um `anyio.CapacityLimiter` em volta de `call_next(ctx)`. + +Um `MCPError` lançado vai para a aplicação cliente, não para o modelo. Se o modelo deve ler a +mensagem, retorne um resultado de ferramenta com `is_error=True`: esse é o **Responder**, mais abaixo. + ## O que você pode fazer dentro de um {#what-you-can-do-inside-one} Em ordem crescente do quanto você deveria hesitar: -* **Observar.** Cronometre, conte, registre no log. O exemplo acima. +* **Observar.** Cronometre, conte, registre no log. O middleware de medição de tempo acima. * **Recusar.** Lance um `MCPError` *em vez de* chamar `call_next(ctx)` e essa única mensagem é - respondida com um erro JSON-RPC. A conexão continua de pé; a próxima mensagem passa. É assim - que um servidor controla o acesso a `subscriptions/listen` por chamador: + respondida com um erro JSON-RPC. A conexão continua de pé; a próxima mensagem passa. O limite + de concorrência acima. É também assim que um servidor controla o acesso a + `subscriptions/listen` por chamador: **[Decidindo quem pode observar](../handlers/subscriptions.md#deciding-who-may-watch)**, na página de Assinaturas, percorre o passo a passo. * **Reescrever.** `ctx` é uma dataclass: `await call_next(dataclasses.replace(ctx, params=...))` diff --git a/i18n/pt/pages/client/identity-assertion.md b/i18n/pt/pages/client/identity-assertion.md index 646ee921fd..4a1b0e0da9 100644 --- a/i18n/pt/pages/client/identity-assertion.md +++ b/i18n/pt/pages/client/identity-assertion.md @@ -1,6 +1,6 @@ --- translation: - sections: [a91322c46111d16d, 8e6fd6d6f59bb568, e7828fd2729b2c9d, a03ec26bfc678b65, 1034c653c0bcf1b0] + sections: [a91322c46111d16d, 8e6fd6d6f59bb568, 7cf38181f6c99fd5, 37804d4fb36d6302, 1034c653c0bcf1b0] tool: 1 --- # Asserção de identidade {#identity-assertion} @@ -64,7 +64,7 @@ A extensão não exige isso; é uma escolha deliberadamente mais rígida. Este c ### Um cliente confidencial {#a-confidential-client} -`client_secret` é obrigatório; o construtor levanta `ValueError` sem ele. O perfil do IETF por baixo da [SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990) reserva este grant para clientes confidenciais, a SEP-990 exige que o cliente se autentique, e este SDK impõe as duas coisas insistindo em um segredo compartilhado. `token_endpoint_auth_method` escolhe por onde ele viaja: `client_secret_post` (o padrão, no corpo do formulário) ou `client_secret_basic` (um cabeçalho HTTP Basic). O perfil também permite `private_key_jwt`; este provider não oferece suporte a ele. +`client_secret` é obrigatório; o construtor levanta `ValueError` sem ele. O perfil do IETF por baixo da [SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990) recomenda este grant apenas para clientes confidenciais, e a [RFC 7521](https://datatracker.ietf.org/doc/html/rfc7521) deixa essa política a cargo do servidor de autorização. Este SDK adota a leitura conservadora dos dois lados: o servidor de autorização integrado recusa um cliente que não tem segredo compartilhado, e este provider insiste em um. `token_endpoint_auth_method` escolhe por onde ele viaja: `client_secret_post` (o padrão, no corpo do formulário) ou `client_secret_basic` (um cabeçalho HTTP Basic). O perfil também permite `private_key_jwt`; este provider não oferece suporte a ele. !!! tip Leia `client_secret` do ambiente ou de um gerenciador de segredos, nunca do controle de versão. @@ -91,6 +91,7 @@ O SDK também pode *ser* o servidor de autorização: `create_auth_routes` retor * `identity_assertion_enabled=True` controla tudo. Desligada, que é o padrão, `/token` responde a este grant com `unsupported_grant_type` mesmo que você tenha implementado o hook, e os metadados não o mencionam. Ligada, os metadados ganham o grant type `jwt-bearer` e listam `urn:ietf:params:oauth:grant-profile:id-jag` em `authorization_grant_profiles_supported`, o campo que a extensão usa para anunciar suporte. (O cliente deste SDK nunca o lê: ele é provisionado para um único issuer e simplesmente pede.) * **`exchange_identity_assertion`** é o hook. Antes de ele rodar, o SDK já autenticou o cliente, recusou clientes públicos e recusou clientes cujo registro não lista o grant. Você recebe um `IdentityAssertionParams` (a `assertion` crua, os `scopes` e o `resource` solicitados) e retorna um `OAuthToken` simples. +* Recusar clientes públicos é política do SDK, não uma exigência da especificação. O servidor integrado autentica clientes apenas por segredo compartilhado: ele não tem suporte a `private_key_jwt` e ainda não resolve Client ID Metadata Documents ([#1801](https://github.com/modelcontextprotocol/python-sdk/issues/1801)), então um cliente identificado por um desses documentos não pode usar este grant aqui. Um deploy que queira uma política diferente pode trocar a rota `/token` que `create_auth_routes` retorna pela sua própria. * O registro dinâmico de clientes recusa este grant incondicionalmente, então `get_client` aqui serve um cliente provisionado à mão. Um cliente ID-JAG não consegue passar a existir registrando a si mesmo. * Metade da classe são recusas. `OAuthAuthorizationServerProvider` é o servidor de autorização *inteiro*, então também pede o fluxo authorization code; um servidor que também faz login de usuários implementa esses métodos de verdade, e este aqui tem exatamente uma porta. diff --git a/i18n/pt/pages/client/transports.md b/i18n/pt/pages/client/transports.md index 7d8c48018e..d5903da261 100644 --- a/i18n/pt/pages/client/transports.md +++ b/i18n/pt/pages/client/transports.md @@ -1,6 +1,6 @@ --- translation: - sections: [9cac816674181eb0, 7c157764133fea1f, 40b4916d82eaf1d4, 10d151f2cc75317f, 3d0832f39b0d7059, 92742ba36533633d, 0aeca6145e7bd302] + sections: [9cac816674181eb0, 5619e950d206e6c8, 40b4916d82eaf1d4, 10d151f2cc75317f, 3d0832f39b0d7059, 92742ba36533633d, 991c10e47fda2636] tool: 1 --- # Transportes do cliente {#client-transports} @@ -44,6 +44,8 @@ Duas coisas para notar: * Você é o dono do `httpx2.AsyncClient`, então é **você** quem entra e sai dele. O SDK nunca fecha um cliente que não criou. * `streamable_http_client(url, http_client=...)` retorna um transporte, e `Client(transport)` o aceita como qualquer outra coisa. +Mantenha o `timeout=`. É o que o próprio cliente do SDK usa (30 segundos, 300 para leituras); um `httpx2.AsyncClient` construído sem ele fica com o padrão de 5 segundos do `httpx2`, e uma chamada de ferramenta que demore mais do que isso falha com um timeout de leitura. + Uma observação sobre TLS: `httpx2` verifica certificados contra o repositório de confiança do sistema operacional (via [`truststore`](https://pypi.org/project/truststore/)), não contra uma lista de CAs embutida. Em um ambiente sem um repositório de CAs do sistema utilizável (alguns contêineres mínimos), defina as variáveis de ambiente padrão @@ -51,16 +53,32 @@ sem um repositório de CAs do sistema utilizável (alguns contêineres mínimos) (contexto em [`httpx` e `httpx-sse` substituídos por `httpx2`](../migration.md#httpx-and-httpx-sse-replaced-by-httpx2)). +### Eventos SSE maiores {#larger-sse-events} + +Passe `max_sse_event_size` quando um servidor envia um resultado de ferramenta ou uma notificação grandes em um único evento SSE: + +```python title="client.py" hl_lines="6-9" +--8<-- "docs_src/client_transports/tutorial005.py" +``` + +O padrão é 1 MiB por evento, medido em bytes antes de o evento ser analisado. O limite vale para +respostas de POST, para o stream GET e para streams retomados. Um evento grande demais em uma resposta de POST ou em um +stream retomado faz essa requisição falhar com um erro de SSE. No stream GET em segundo plano, o cliente registra +o erro no log e tenta o stream de novo. Defina `max_sse_event_size=None` para desativar o limite quando você confia no +servidor e precisa de eventos maiores. As respostas JSON não são afetadas. Se você usa `ClientSessionGroup`, defina a +mesma opção em `StreamableHttpParameters`. + !!! warning `streamable_http_client` costumava aceitar `headers=` e `timeout=` diretamente. Não aceita mais: - seus únicos parâmetros são `url`, `http_client` e `terminate_on_close`. Use `headers=` por + seus parâmetros são `url`, `http_client`, `terminate_on_close` e `max_sse_event_size`. Use `headers=` por hábito e você recebe: ```text TypeError: streamable_http_client() got an unexpected keyword argument 'headers' ``` - Tudo que tem cara de HTTP agora vive no único `httpx2.AsyncClient` que você passa. + Headers, autenticação, proxies e timeouts vivem no único `httpx2.AsyncClient` que você passa. + Já `max_sse_event_size` se aplica aos leitores SSE do transporte MCP. !!! info `httpx2` mantém a API conhecida do `httpx`, então se você conhece `httpx` já sabe como fazer auth, @@ -137,6 +155,7 @@ Um **transporte** é qualquer gerenciador de contexto assíncrono que produz um * `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=`. +* Use `streamable_http_client(url, max_sse_event_size=...)` para alterar o limite de bytes de cada evento SSE. * 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. diff --git a/i18n/pt/pages/handlers/cancellation.md b/i18n/pt/pages/handlers/cancellation.md new file mode 100644 index 0000000000..a4828bed24 --- /dev/null +++ b/i18n/pt/pages/handlers/cancellation.md @@ -0,0 +1,60 @@ +--- +translation: + sections: [07968345fdc0b84e, 4ea8416db9efa0dc, 336a7b4c5d0a4578, 18392e805dde6717, c30d50df43f9b55c] + tool: 1 +--- +# Cancelamento {#cancellation} + +Um cliente pode desistir de uma chamada: o usuário apertou o botão de parar, ou um timeout se esgotou. + +Quando isso acontece, o SDK **cancela o seu handler**. O `await` em que ele está esperando lança uma exceção, a função é desempilhada, e nada do que ela retornar é enviado. A maioria dos handlers não precisa fazer nada a respeito. + +Dois tipos precisam: um handler com algo para limpar, e um handler que é um `def` comum. + +## Faça a limpeza em uma ferramenta `async def` {#clean-up-in-an-async-def-tool} + +Coloque a limpeza em um `finally`: + +```python title="server.py" hl_lines="23 26-28" +--8<-- "docs_src/cancellation/tutorial001.py" +``` + +* O `finally` executa não importa como a ferramenta termine: ela retornou, lançou uma exceção ou foi cancelada. +* Uma limpeza que precisa fazer `await` exige `shield=True`. Em um handler cancelado, todo `await` seguinte também lança uma exceção, então sem a proteção `release_hold` pararia na primeira linha. +* Nada consegue cancelar um bloco protegido, então dê a ele um limite de tempo. Aqui são `5` segundos. + +!!! tip + Use `finally`, não `except`. O cancelamento precisa continuar subindo depois que a sua limpeza + termina, e um `finally` permite isso. + +## Pare antes do fim em uma ferramenta `def` comum {#stop-early-in-a-plain-def-tool} + +Uma ferramenta `def` comum roda em uma thread, e nada consegue interromper uma thread de fora. A ferramenta precisa perguntar: + +```python title="server.py" hl_lines="22 25-26" +--8<-- "docs_src/cancellation/tutorial002.py" +``` + +* `anyio.from_thread.check_cancelled()` não faz nada enquanto a chamada está ativa, e lança uma exceção assim que ela é cancelada. Chame essa função entre unidades de trabalho. +* Aqui a limpeza também vai em um `finally`. Nada em uma thread faz await, então a limpeza não precisa de proteção. +* Uma ferramenta `def` que nunca pergunta roda até o fim, e o resultado dela é descartado. + +## Onde se aplica {#where-it-applies} + +As funções de prompt e de recurso são canceladas exatamente como as ferramentas. + +Funciona do mesmo jeito sobre stdio e Streamable HTTP. Com o `Client` deste SDK, desistir significa cancelar a tarefa que aguarda `call_tool`, ou deixar o `read_timeout_seconds` da chamada se esgotar. + +!!! warning + Duas opções do Streamable HTTP impedem que a notícia chegue ao seu handler: `json_response=True` em uma + conexão `2026-07-28`, e `stateless_http=True` em uma conexão legada. Nesses casos, o handler roda até + o fim, não importa o que o cliente tenha feito. + +## Resumo {#recap} + +* Quando o cliente desiste de uma chamada, o SDK cancela o handler: ferramenta, prompt ou recurso. +* `async def`: faça a limpeza em um `finally`, e coloque a limpeza que faz await dentro de `anyio.move_on_after(seconds, shield=True)`. +* `def` comum: chame `anyio.from_thread.check_cancelled()` entre unidades de trabalho, ou a ferramenta roda até o fim. Um `finally` simples faz a limpeza. +* `json_response=True` (conexões modernas) e `stateless_http=True` (conexões legadas) desligam o cancelamento. + +Progresso e cancelamento ficam entre uma ferramenta em execução e quem a *chamou*. As linhas que ela registra em log para *você*, a pessoa que opera o servidor, são um canal diferente: **[Logging](logging.md)**. diff --git a/i18n/pt/pages/handlers/index.md b/i18n/pt/pages/handlers/index.md index cfef268d4c..173853a8f9 100644 --- a/i18n/pt/pages/handlers/index.md +++ b/i18n/pt/pages/handlers/index.md @@ -1,6 +1,6 @@ --- translation: - sections: [424930166c4bc6f3] + sections: [22ca41e50cc1b536] tool: 1 --- # Dentro do seu handler {#inside-your-handler} @@ -28,6 +28,8 @@ O que ele pode fazer enquanto executa: **[Amostragem (sampling) e roots](sampling-and-roots.md)**, obsoletos, mas ainda atendidos. * Informar o **[Progresso](progress.md)** de algo demorado. +* Fazer a limpeza, ou parar mais cedo, quando o cliente desiste da chamada, + com **[Cancelamento](cancellation.md)**. * Escrever logs (na saída de erro padrão, para quem opera o servidor) com **[Logging](logging.md)**. * Avisar os clientes assinantes de que algo mudou com diff --git a/i18n/pt/pages/handlers/lifespan.md b/i18n/pt/pages/handlers/lifespan.md index e710a3b8cb..45786431a4 100644 --- a/i18n/pt/pages/handlers/lifespan.md +++ b/i18n/pt/pages/handlers/lifespan.md @@ -1,6 +1,6 @@ --- translation: - sections: [f3ca8ac5f90f2dfa, 85a1ef3588ba0736, 563346d4d5804933, 9e3528340d0bab53] + sections: [f3ca8ac5f90f2dfa, 48e478ef7bd688b1, 563346d4d5804933, 52ac6a7734d6f581] tool: 1 --- # Lifespan {#lifespan} @@ -46,7 +46,7 @@ Nada de novo. `ctx` é um parâmetro **Context**, então o SDK o injeta e ele nu `genre` é o único argumento que o modelo pode passar. O lifespan é assunto do seu servidor. -Funções `@mcp.resource()` e `@mcp.prompt()` também podem receber um parâmetro `ctx`, escrito como um `Context` puro por um motivo que a próxima seção explica. Tudo o que `ctx` carrega está em **[O Context](context.md)**. +Funções `@mcp.resource()` e `@mcp.prompt()` também podem receber um parâmetro `ctx`. Tudo o que `ctx` carrega está em **[O Context](context.md)**. ### É tipado de verdade {#it-really-is-typed} @@ -56,19 +56,6 @@ Esse único parâmetro de tipo é o motivo pelo qual `ctx.request_context.lifesp Escreva um `Context` puro no lugar e `lifespan_context` passa a ser tipado como `dict[str, Any]`: o verificador de tipos não tem como saber o que o seu lifespan entregou. O objeto continua lá em tempo de execução; o que você perdeu foi a ajuda. -!!! warning - `Context[AppContext]` é uma grafia **só para ferramentas**. Coloque-a em uma função - `@mcp.resource()` ou `@mcp.prompt()` e toda chamada a esse handler falha. O cliente recebe um - erro de volta, e o log do servidor mostra o porquê: - - ```text - Context is not available outside of a request - ``` - - Em recursos e prompts, escreva o `ctx: Context` puro. O objeto que o seu lifespan entregou - continua sendo `ctx.request_context.lifespan_context` em tempo de execução; você abre mão do - parâmetro de tipo, não do objeto. - !!! tip Sempre existe um lifespan. Se você não passar um, o padrão do SDK entrega um `dict` vazio, então `ctx.request_context.lifespan_context` é `{}`, nunca `None`. Esse padrão também é o @@ -101,7 +88,7 @@ Reduza o servidor só ao ciclo de vida: dê ao `Database` uma flag `connected`, * O código antes do `yield` é a inicialização. O `finally` depois dele é o encerramento. * Ele executa uma vez, em torno da vida inteira do servidor, não a cada requisição. * O que você entregar no `yield` é `ctx.request_context.lifespan_context` em toda ferramenta, recurso e prompt. -* `ctx: Context[AppContext]` deixa esse acesso totalmente tipado em ferramentas. Recursos e prompts recebem o `Context` puro. +* `ctx: Context[AppContext]` deixa esse acesso totalmente tipado. * Não passar `lifespan=` significa um `dict` vazio, nunca `None`. Um handler que para no meio da chamada para perguntar ao usuário algo que só ele sabe é **[Elicitação (elicitation)](elicitation.md)**. diff --git a/i18n/pt/pages/handlers/progress.md b/i18n/pt/pages/handlers/progress.md index f6b78f8ccd..39905cc63e 100644 --- a/i18n/pt/pages/handlers/progress.md +++ b/i18n/pt/pages/handlers/progress.md @@ -1,6 +1,6 @@ --- translation: - sections: [5315262fe26b33e1, 9d8e98840f1b78f0, 52d6009a07e770ea, 8534d8dbb4053a70, 2966fac6fe697007] + sections: [5315262fe26b33e1, 9d8e98840f1b78f0, 52d6009a07e770ea, 8534d8dbb4053a70, 2e9aff14d3a882c0] tool: 1 --- # Progresso {#progress} @@ -120,4 +120,4 @@ O callback recebe `total=None`. Um cliente ainda consegue mostrar *atividade* (" * Sem callback na chamada, `report_progress` não faz nada. Informe incondicionalmente. * Omita `total` quando não o souber; o callback recebe `None`. -Progresso é o que uma ferramenta em execução mostra ao *usuário*. As linhas que ela registra em log para *você*, a pessoa que opera o servidor, são um canal diferente: **[Logging](logging.md)**. +Progresso é para um cliente que ainda está esperando. O que a sua ferramenta vê quando o cliente para de esperar é o **[Cancelamento](cancellation.md)**. diff --git a/i18n/pt/pages/handlers/subscriptions.md b/i18n/pt/pages/handlers/subscriptions.md index 1480258d42..eb7f1902bc 100644 --- a/i18n/pt/pages/handlers/subscriptions.md +++ b/i18n/pt/pages/handlers/subscriptions.md @@ -1,6 +1,6 @@ --- translation: - sections: [60a9de8a0bdaa531, 317bbe7e4355cdcc, a61d660c8029e04a, 8f7e82fcb88df8a9, b165db51249ff8ed, 266f56fb798068a4, 7c0e57030b622139, df18d7c2417a9883] + sections: [60a9de8a0bdaa531, 6693607ea56d8bd6, a61d660c8029e04a, 8f7e82fcb88df8a9, b165db51249ff8ed, b8bc624a627ead9b, 2139e68e36d9e621, 7c0e57030b622139, 34ab1af2b9ab5b45] tool: 1 --- # Assinaturas {#subscriptions} @@ -22,7 +22,7 @@ A sua parte é uma linha: publicar a mudança. * Os irmãos são `notify_prompts_changed()` e `notify_resources_changed()`. * Sem assinantes, sem trabalho. Publicar em um servidor ocioso é um no-op, então você nunca verifica se há alguém ouvindo. Você declara o que mudou. -O `MCPServer` serve `subscriptions/listen` para você. As obrigações do protocolo na conexão (o acknowledgment como primeiro frame, a filtragem por stream, o id da assinatura em cada frame) são trabalho do SDK. +O `MCPServer` serve `subscriptions/listen` para você, a menos que você [desative isso](#turning-it-off). As obrigações do protocolo na conexão (o acknowledgment como primeiro frame, a filtragem por stream, o id da assinatura em cada frame) são trabalho do SDK. !!! check Na conexão, um stream cujo filtro nomeou `board://sprint` fica assim depois que `complete_task` executa: @@ -79,7 +79,32 @@ Entrar em `client.listen(...)` envia a requisição e espera pelo seu acknowledg As publicações viajam do seu handler até os streams abertos por um `SubscriptionBus`. O padrão é em memória: um processo, todos os streams dentro dele. Essa é a resposta certa até você rodar réplicas atrás de um balanceador de carga, porque aí o stream de um cliente fica preso a uma réplica, e uma publicação em outra réplica precisa chegar até ele. -Essa costura é sua para implementar: dois métodos sobre o seu backend de pub/sub. +Com o bus padrão ela não consegue, porque cada réplica tem o seu: + +```mermaid +flowchart LR + client[Client] --> lb[Load balancer] + lb --> stream + lb ~~~~ gap + lb --> tool + subgraph B [Replica B] + tool[tools/call] -- publishes --> busB[(bus B)] + end + gap[(no shared bus)] + subgraph A [Replica A] + stream[listen stream] -- subscribed --> busA[(bus A)] + end + style A fill:none + style B fill:none + style gap fill:none,stroke-dasharray:4 4 +``` + +Nada falha: a chamada dá certo, e o stream fica em silêncio. Então, atrás de um balanceador de carga, escolha uma opção: + +* **Você precisa de notificações de mudança.** Dê a todas as réplicas o mesmo bus, como mostrado abaixo. +* **Você não precisa.** [Desative as notificações](#turning-it-off), para que nenhum cliente receba a promessa de eventos que vai perder nem mantenha um stream aberto à espera deles. + +O bus compartilhado é seu para implementar: dois métodos sobre o seu backend de pub/sub. ```python from collections.abc import Callable @@ -128,6 +153,20 @@ async def tools_reloaded() -> None: await bus.publish(ToolsListChanged()) # from a lifespan task, a webhook, anywhere ``` +## Desativando {#turning-it-off} + +Um servidor cujo catálogo nunca muda não tem nada para publicar. Diga isso ao construí-lo: + +```python title="server.py" hl_lines="3" +--8<-- "docs_src/subscriptions/tutorial007.py" +``` + +* Um cliente `2026-07-28` não vê nenhuma notificação de mudança anunciada, e uma requisição `subscriptions/listen` recebe *Method not found* em vez de um stream aberto. +* `ctx.notify_*` continua funcionando e não chega a ninguém, então os seus handlers não mudam. +* Clientes em versões anteriores do protocolo não veem diferença nenhuma. + +Um stream aberto é uma requisição que nunca termina, então isso também importa em um host que cobra pela duração da requisição. + ## A composição de baixo nível {#the-low-level-composition} Lá embaixo, no `Server` de baixo nível, nada vem pré-conectado, e as mesmas peças se montam em três linhas: @@ -148,5 +187,6 @@ Lá embaixo, no `Server` de baixo nível, nada vem pré-conectado, e as mesmas p * A ponta do cliente é `async with client.listen(...)`: **[Assinaturas](../client/subscriptions.md)** em *Clientes* conta essa história. * No `Server` de baixo nível você monta as mesmas peças por conta própria: um bus, `ListenHandler(bus)`, o slot `on_subscriptions_listen`. * Escalar horizontalmente significa implementar `SubscriptionBus`, dois métodos, e passá-lo como `MCPServer(subscriptions=...)`. +* Nada para publicar, ou réplicas sem bus compartilhado: `MCPServer(subscriptions=False)` não anuncia nenhuma notificação de mudança e não mantém nenhum stream. Rodar o servidor que serve tudo isso, atrás de uma réplica ou de vinte, é **[Deploy e escala](../run/deploy.md)**. diff --git a/i18n/pt/pages/run/deploy.md b/i18n/pt/pages/run/deploy.md index c4d0544871..427cec8ccc 100644 --- a/i18n/pt/pages/run/deploy.md +++ b/i18n/pt/pages/run/deploy.md @@ -1,6 +1,6 @@ --- translation: - sections: [28221886b198784f, f88ea1f1614f3a1d, 2e76f5cb9df15042, ce926d686730b6d0, 3be24f8ad8bb5ab9, 3fad24032b2224ff, f25a7f860e579ecb, 697b01d95080880d] + sections: [28221886b198784f, f88ea1f1614f3a1d, 2e76f5cb9df15042, ce926d686730b6d0, 3be24f8ad8bb5ab9, aaf489e944ecf5d1, f25a7f860e579ecb, 697b01d95080880d] tool: 1 --- # Deploy e escala {#deploy-scale} @@ -170,9 +170,10 @@ A costura entre os dois é o `SubscriptionBus`. Qualquer bus que você dê a um Nada no fan-out se importa com qual objeto de servidor um stream está ligado. Dois servidores segurando um único `InMemorySubscriptionBus` já se comportam assim: abra um stream de listen em um, `edit_note` no outro, e o stream fica sabendo. Esse bus em memória só abrange objetos de servidor dentro de um processo, o que faz dele o modelo, não o deploy: -* Entre processos de verdade, **o SDK não traz nenhum bus que possa te ajudar.** `SubscriptionBus` é um `Protocol` de dois métodos (`publish` e `subscribe`) que você implementa sobre seu próprio backend de pub/sub (Redis, NATS, o que você já roda) e passa como `MCPServer(subscriptions=...)`. **[Assinaturas](../handlers/subscriptions.md#scaling-past-one-process)** tem o esboço e o contrato. +* Entre processos de verdade, **o SDK não traz nenhum bus que possa ajudar você.** `SubscriptionBus` é um `Protocol` de dois métodos (`publish` e `subscribe`) que você implementa sobre seu próprio backend de pub/sub (Redis, NATS, o que você já roda) e passa como `MCPServer(subscriptions=...)`. **[Assinaturas](../handlers/subscriptions.md#scaling-past-one-process)** tem o esboço e o contrato. * O bus carrega quatro pequenos eventos tipados, nunca JSON-RPC. Confirmação, filtragem e ciclo de vida do stream ficam no SDK, então seu bus não consegue quebrar o protocolo; ele só consegue mover eventos entre processos. * Streams **não** são retomáveis e eventos **não** são reenviados. Perder uma réplica derruba os streams dela; os clientes escutam de novo e buscam de novo. Não há event store para compartilhar e nada mais para configurar. Este é o único lugar em que escalar horizontalmente é de fato só mais do mesmo. +* Um servidor que não precisa de notificações de mudança dispensa o bus: **[desligue-as](../handlers/subscriptions.md#turning-it-off)**. ## O que o SDK não te dá {#what-the-sdk-does-not-give-you} diff --git a/i18n/pt/pages/servers/structured-output.md b/i18n/pt/pages/servers/structured-output.md index d2a98855ae..b7c2b5622c 100644 --- a/i18n/pt/pages/servers/structured-output.md +++ b/i18n/pt/pages/servers/structured-output.md @@ -1,6 +1,6 @@ --- translation: - sections: [a838d57f003aed44, 857d03886a0137ed, 42d9efcb9f542867, 2290ff08435b5573, 91be9b73602abcf1, 6cdbad079f7b47f0, d4b607372fb28b51, 18dbf726ac45e0b7, c7eff2a5698225fa, c851964bb3301907, 8f296f1f09e4c400, d715db6f8dccc9cc, a0c344a48450dbe4] + sections: [a838d57f003aed44, 857d03886a0137ed, 42d9efcb9f542867, 2290ff08435b5573, 91be9b73602abcf1, 6cdbad079f7b47f0, d4b607372fb28b51, 7608fc5ebc31d6ea, c7eff2a5698225fa, c851964bb3301907, 8f296f1f09e4c400, d715db6f8dccc9cc, a0c344a48450dbe4] tool: 1 --- # Saída estruturada {#structured-output} @@ -174,6 +174,8 @@ result.structured_content # {"London": 16.2, "Reykjavik": 4.4} As chaves precisam ser `str`. Um `dict[int, float]` não pode ser um objeto JSON, então ele recai no wrapper `{"result": ...}`. +Resultados do tipo dicionário usam o `TypeAdapter` do Pydantic para validação e serialização. Se você inspecionar o `FuncMetadata.output_model` de uma ferramenta, ele contém a anotação de tipo do dicionário com o título do schema. + ## Validação {#validation} `output_schema` não é documentação. O que quer que a sua função retorne é **validado contra ele** antes de sair do servidor. diff --git a/i18n/pt/pages/servers/tools.md b/i18n/pt/pages/servers/tools.md index eb08f5ff44..8bbd22ac15 100644 --- a/i18n/pt/pages/servers/tools.md +++ b/i18n/pt/pages/servers/tools.md @@ -1,6 +1,6 @@ --- translation: - sections: [e4cc390d56573409, f30cf8103a6e918c, 2c97b9f888398951, 048e5471dfa71aea, 3076b1e16ad95950, edbedf2a16e71311, 3d8ef8da89fa87c1, f6c0e02e6ea5a363] + sections: [e4cc390d56573409, f30cf8103a6e918c, 2c97b9f888398951, 048e5471dfa71aea, 3076b1e16ad95950, c1115cd005b81e8f, 3d8ef8da89fa87c1, f6c0e02e6ea5a363] tool: 1 --- # Ferramentas {#tools} @@ -141,7 +141,7 @@ Você pode misturar à vontade: parâmetros simples ao lado de parâmetros de mo Se uma ferramenta faz I/O (chama uma API, lê um arquivo, consulta um banco de dados), declare-a como `async def` e use `await` dentro dela. O SDK se encarrega de aguardá-la. -Uma ferramenta com `def` comum também funciona: o SDK a executa em uma thread, então ela nunca bloqueia o servidor. +Uma ferramenta com `def` comum também funciona: o SDK a executa em uma thread, então ela nunca bloqueia o servidor. Uma ferramenta demorada pode verificar se o cliente ainda está esperando; veja **[Cancelamento](../handlers/cancellation.md)**. Não há mais nada para configurar. diff --git a/i18n/pt/pages/troubleshooting.md b/i18n/pt/pages/troubleshooting.md index 29e6e3c1a7..871f0f7984 100644 --- a/i18n/pt/pages/troubleshooting.md +++ b/i18n/pt/pages/troubleshooting.md @@ -1,6 +1,6 @@ --- translation: - sections: [3d58228e81b99543, 170514ce901c4139, 17d61fad0a50d62b, 8a6e351ec756904d, 137454d469c867f5, 6392596bd6df54f0, 41126fa9c4fe432f, 480b6d7897e30ab4, d83bb682e708dde0, ebbed3449c499db4, 525cdf1755e29d4c, 30fd31be74169d9a, d2e88333d4f7841f, c2dc3b1007d2e987, d6eabf60cc366341, f798e815252852c2, 0cba47bae78d04eb, 2c218ba829abf74e] + sections: [3d58228e81b99543, 170514ce901c4139, 17d61fad0a50d62b, 8a6e351ec756904d, 137454d469c867f5, fcf984fa0615ed11, 6392596bd6df54f0, 41126fa9c4fe432f, 480b6d7897e30ab4, d83bb682e708dde0, ebbed3449c499db4, 525cdf1755e29d4c, 30fd31be74169d9a, d2e88333d4f7841f, c2dc3b1007d2e987, d6eabf60cc366341, f798e815252852c2, 0cba47bae78d04eb, cdc6d86a4dae8a34] tool: 1 --- # Solução de problemas {#troubleshooting} @@ -128,6 +128,14 @@ Adicione os parênteses. `@mcp.resource(...)` e `@mcp.prompt()` dizem a mesma co zero ferramentas, tem esse formato: execute `python server.py` você mesmo e leia o traceback. Um verificador de tipos também pega isso: uma função não é um `name=` válido. +## `InvalidSignature: Tool '' has an invalid x-mcp-header annotation: ` {#invalidsignature-tool-name-has-an-invalid-x-mcp-header-annotation-reason} + +Um argumento de ferramenta está marcado com `x-mcp-header` de um jeito que a especificação não permite, e `` diz qual regra ele quebra. Clientes em `2026-07-28` deixariam uma ferramenta assim fora da listagem deles, então o SDK se recusa a registrá-la. + +Só argumentos `str`, `int` e `bool` podem ser marcados, e `str | None` não é nenhum deles. **[Parâmetros de header](advanced/header-parameters.md)** tem a forma de escrever um argumento opcional. + +Como a entrada acima, isso lança quando o módulo é **importado**, antes de qualquer cliente se conectar. + ## `Tool already exists: ` {#tool-already-exists-name} Dois registros usaram o mesmo nome de ferramenta. O **primeiro** vence, o segundo é descartado em silêncio, e este aviso no *log do servidor* é o único sinal: @@ -425,6 +433,7 @@ mcp = MCPServer("Weather", request_state_security=RequestStateSecurity(keys=[key * `ExceptionGroup: unhandled errors in a TaskGroup` nunca é o erro. Leia a **última linha**; capturar `MCPError` *dentro* do bloco `async with Client(...)` pula o embrulho por completo. * `call_tool` não lança exceção para uma ferramenta que falha. `Error executing tool ...` e `Unknown tool: ...` são resultados: verifique `result.is_error`. Nenhuma mensagem depois do nome da ferramenta significa que ela quebrou, e o traceback está no log do servidor. * `Client must be used within an async context manager` -> use `async with`. `Use @tool() instead of @tool` -> adicione os parênteses. +* `has an invalid x-mcp-header annotation` -> só argumentos `str`, `int` e `bool` podem ser marcados. * `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()`. diff --git a/i18n/pt/pages/whats-new.md b/i18n/pt/pages/whats-new.md index 8c68702e82..94de174b5a 100644 --- a/i18n/pt/pages/whats-new.md +++ b/i18n/pt/pages/whats-new.md @@ -1,6 +1,6 @@ --- translation: - sections: [cfe01c0c5863dfa2, 0dbb68d8b210177b, 80cf193023af3ed4, 875eb2889263424e] + sections: [cfe01c0c5863dfa2, 0dbb68d8b210177b, bd42ee3a268f6ea4, 875eb2889263424e] tool: 1 --- # O que há de novo na v2 {#whats-new-in-v2} @@ -205,7 +205,7 @@ Na 2026-07-28, o stream HTTP GET avulso e `resources/subscribe` são substituíd ### O resto, rapidamente {#the-rest-quickly} * **A identidade é um metadado opcional, por mensagem.** A chave `clientInfo` de `_meta` no lado da requisição é opcional (o par obrigatório é `protocolVersion` + `clientCapabilities`), e `serverInfo` saiu do corpo do resultado de `server/discover`: em vez disso, os servidores o carimbam no `_meta` de todo resultado da era 2026 ([especificação #3002](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/3002)). O SDK sempre carimba; `client.server_info` é `None` quando um servidor não se identifica (por exemplo, um middleware removeu a chave). **[O Server de baixo nível](advanced/low-level-server.md)** mostra o carimbo no tráfego real. -* **As requisições são roteáveis sem fazer parse do corpo.** Requisições HTTP modernas carregam `Mcp-Method` (e, para as três chamadas no estilo de ferramenta, `Mcp-Name`); uma propriedade do schema de entrada de uma ferramenta anotada com `x-mcp-header` é espelhada em um cabeçalho `Mcp-Param-*` e conferida pelo servidor ([SEP-2243](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2243)). Gateways e rate limiters podem rotear só pelos cabeçalhos; o **[Guia de migração](migration.md#servers-validate-mcp-param-headers-against-the-request-body-sep-2243)** tem as regras. +* **As requisições são roteáveis sem fazer parse do corpo.** Requisições HTTP modernas carregam `Mcp-Method` (e, para as três chamadas no estilo de ferramenta, `Mcp-Name`); uma propriedade do schema de entrada de uma ferramenta anotada com `x-mcp-header` é espelhada em um cabeçalho `Mcp-Param-*` e conferida pelo servidor ([SEP-2243](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2243)). Gateways e rate limiters podem rotear só pelos cabeçalhos. **[Parâmetros de cabeçalho](advanced/header-parameters.md)** mostra como marcar um argumento. * **Os resultados carregam dicas de cache.** Resultados de listagem e de leitura declaram `ttlMs` e `cacheScope` ([SEP-2549](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2549)); você os define por método com `cache_hints=`, e `Client` os respeita com um cache de respostas embutido. Um servidor que não envia dicas (todo servidor pré-2026) vê tráfego idêntico, sem cache. **[Dicas de cache](client/caching.md)**. * **Extensões são de primeira classe.** Servidores e clientes declaram conjuntos opcionais de capacidades sob identificadores em DNS reverso ([SEP-2133](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2133)); a extensão embutida `Apps` (MCP Apps) é a referência. **[Extensões](advanced/extensions.md)** e **[MCP Apps](advanced/apps.md)**. * **Os códigos de erro foram padronizados.** Um recurso inexistente é `-32602` com a URI em `error.data`, e os novos códigos reservados pela especificação aparecem como `-32020` (cabeçalho divergente), `-32021` (capacidade obrigatória ausente) e `-32022` (versão de protocolo não suportada). **[Solução de problemas](troubleshooting.md)** é organizada pelas mensagens exatas. diff --git a/i18n/ru/pages/advanced/header-parameters.md b/i18n/ru/pages/advanced/header-parameters.md new file mode 100644 index 0000000000..32eba9274f --- /dev/null +++ b/i18n/ru/pages/advanced/header-parameters.md @@ -0,0 +1,65 @@ +--- +translation: + sections: [81862a209b483d27, b76e8073487afa03, bc214b2fc2bcdae4, d5835477c0b60163, a5d9786f902ad8e1] + tool: 1 +--- +# Параметры в заголовках {#header-parameters} + +Большинству серверов это не понадобится. + +Шлюз или балансировщик нагрузки перед сервером может маршрутизировать запросы только по тем данным, которые читаются без разбора тела. Пометьте аргумент инструмента ключом `x-mcp-header`, и клиенты, работающие с **[версией протокола](../protocol-versions.md)** `2026-07-28`, будут передавать его значение ещё и в HTTP-заголовке. + +## Пометка аргумента {#mark-an-argument} + +Пометка — это один дополнительный ключ в JSON-схеме аргумента. В `MCPServer` его туда добавляет `Field`: + +```python title="server.py" hl_lines="13" +--8<-- "docs_src/header_parameters/tutorial001.py" +``` + +* При работе через Streamable HTTP с версией `2026-07-28` клиент отправляет заголовок `Mcp-Param-Region` вместе с телом запроса, а сервер отклоняет вызов, в котором они расходятся. +* Клиент, который не запрашивал список инструментов, пометки не видел: заголовок он не отправляет, и вызов отклоняется. Класс `Client` из этого SDK в таком случае запрашивает список инструментов и один раз повторяет вызов, так что предварительный запрос списка лишь экономит один цикл «запрос — ответ». +* Все остальные подключения эту аннотацию игнорируют. + +Сама функция не меняется: `region` по-прежнему приходит как аргумент. + +## Что можно пометить {#what-can-be-marked} + +Аргументы типов `str`, `int` и `bool`. Всё остальное отклоняется при регистрации инструмента с исключением `InvalidSignature`. + +Это касается и `str | None`, у которого нет единственного типа. Для необязательного аргумента схему нужно задать явно, с помощью `WithJsonSchema` из Pydantic: + +```python +region: Annotated[str | None, WithJsonSchema({"type": "string", "x-mcp-header": "Region"})] = None +``` + +## В низкоуровневом классе `Server` {#on-the-low-level-server} + +Там `input_schema` пишется вручную, поэтому ключ добавляется прямо в схему: + +```python title="server.py" hl_lines="18" +--8<-- "docs_src/header_parameters/tutorial002.py" +``` + +* Аннотацию здесь никто не проверяет: некорректная будет отдана как есть, а клиенты версии `2026-07-28` исключат такой инструмент из своего списка. + +### Схемы по имени {#schemas-by-name} + +Чтобы проверить заголовок, SDK нужна входная схема инструмента ещё до того, как вызов будет передан обработчику. Без `get_tool_input_schema` SDK получает её, запуская обработчик `on_list_tools` при каждом вызове с аргументами — независимо от того, помечен ли хоть один инструмент. + +```python title="server.py" hl_lines="26 39-41 48" +--8<-- "docs_src/header_parameters/tutorial003.py" +``` + +* Передайте функцию, чтобы отвечать на основе уже имеющихся данных. +* Для инструмента, в котором нечего проверять, верните `None`. + +## Итоги {#recap} + +* Ключ `x-mcp-header` у аргумента инструмента заставляет клиенты версии `2026-07-28` дублировать этот аргумент в HTTP-заголовке `Mcp-Param-*`. +* Сервер отклоняет вызов, в котором заголовок и тело расходятся. +* Пометить можно только аргументы типов `str`, `int` и `bool`. Для всего остального `MCPServer` выбрасывает исключение `InvalidSignature`. +* Низкоуровневый класс `Server` ничего не проверяет, а клиенты отбрасывают инструмент с некорректной аннотацией. +* С `get_tool_input_schema` низкоуровневому классу `Server` не приходится запускать `on_list_tools` при каждом вызове. + +Остальная часть API класса `Server`, в котором всё пишется вручную, описана на странице **[Низкоуровневый Server](low-level-server.md)**. diff --git a/i18n/ru/pages/advanced/index.md b/i18n/ru/pages/advanced/index.md index 3af0ac2de2..b67d5060a1 100644 --- a/i18n/ru/pages/advanced/index.md +++ b/i18n/ru/pages/advanced/index.md @@ -1,6 +1,6 @@ --- translation: - sections: [ca6988b7503cd2d3] + sections: [348f8697c6b12cd0] tool: 1 --- # Продвинутые темы {#advanced} @@ -14,6 +14,8 @@ translation: JSON-RPC-методы. * **[Пагинация](pagination.md)** и **[Middleware](middleware.md)**: две вещи, которые можно сделать *только* на низкоуровневом `Server`. +* **[Параметры в заголовках](header-parameters.md)**: позволяют шлюзу маршрутизировать + вызов инструмента по одному из его аргументов. * **[Расширения](extensions.md)** и **[MCP Apps](apps.md)**: точки расширения протокола. Подключайте пакеты расширений к серверу или пишите свои. diff --git a/i18n/ru/pages/advanced/low-level-server.md b/i18n/ru/pages/advanced/low-level-server.md index 2bd34e6074..c73212d5ad 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, 9d5d10a5f0405d0a, 1086e77ce561cd7f, a3f71823df5efc31, 9fc7109f72201cae, d50fe7faead8cf68, 7bf25983df655b66, 6330e1f4c6029683, 2f1749c8c133fa1c, 8db7116fc8ddd0ee, ebc33704fbd74262, 0fde3bcea081ba3a] + sections: [2c79b6338e09b7ac, 9d5d10a5f0405d0a, 1086e77ce561cd7f, a3f71823df5efc31, 9fc7109f72201cae, d50fe7faead8cf68, 7bf25983df655b66, 6330e1f4c6029683, 2f1749c8c133fa1c, 8db7116fc8ddd0ee, 2090d99b355bc2c7, 0fde3bcea081ba3a] tool: 1 --- # Низкоуровневый Server {#the-low-level-server} @@ -209,6 +209,7 @@ use Server.middleware to observe or wrap initialization * `on_call_tool`, `on_get_prompt` и `on_read_resource` могут вернуть `InputRequiredResult` вместо обычного результата, чтобы приостановить вызов и запросить ввод у клиента; см. **[Многораундовые запросы](../handlers/multi-round-trip.md)**. Верные духу этого уровня, они ничего не устанавливают за вас: там, где `MCPServer` по умолчанию запечатывает `requestState`, здесь заданный вами `request_state` идёт по сети ровно в том виде, в каком написан, пока вы не включите защиту явно: `server.middleware.append(RequestStateBoundary(RequestStateSecurity(keys=[...]), default_audience=server.name))` — одна строка (оба имени импортируются из `mcp.server.request_state`) для точно такого же запечатывания и проверки, какие выполняет `MCPServer` (**[Защита `requestState`](../handlers/multi-round-trip.md#protecting-requeststate)**). * `on_list_resources`, `on_read_resource`, `on_list_prompts`, `on_get_prompt`, `on_completion` — та же форма `(ctx, params) -> result` для остальных примитивов. * `on_subscriptions_listen` обслуживает поток `subscriptions/listen` версии 2026-07-28. Передайте `ListenHandler`, построенный поверх `SubscriptionBus`, и публикуйте события в шину из остальных обработчиков; полная схема компоновки — на странице **[Подписки](../handlers/subscriptions.md)**. +* `get_tool_input_schema` убирает `on_list_tools` с пути вызова; см. **[Параметры в заголовках](header-parameters.md#schemas-by-name)**. * `server.streamable_http_app()` возвращает то же Starlette-приложение, что и у `MCPServer`; разворачивайте его так же, как страница **[Запуск сервера](../run/index.md)** разворачивает любое другое ASGI-приложение. `server.run(transport=...)` здесь нет: `server.run(read_stream, write_stream, server.create_initialization_options())` ведёт одно подключение по паре потоков, и этой одной строкой всё исчерпывается. ## Итоги {#recap} diff --git a/i18n/ru/pages/advanced/middleware.md b/i18n/ru/pages/advanced/middleware.md index 9db08e7a97..79d8ccb21e 100644 --- a/i18n/ru/pages/advanced/middleware.md +++ b/i18n/ru/pages/advanced/middleware.md @@ -1,6 +1,6 @@ --- translation: - sections: [6048b4f308edbb8c, 46056f318ef205e4, c3e565b61acd75c5, c62422b159c6ed09, 420968f514138f43] + sections: [58e1103d9a323ccf, 46056f318ef205e4, 812b414557fb0c35, 4df162eea2518d38, c62422b159c6ed09, 420968f514138f43] tool: 1 --- # Middleware {#middleware} @@ -12,7 +12,7 @@ translation: !!! warning Список middleware в исходном коде помечен как **provisional** (предварительный): его сигнатура и семантика могут измениться в минорном выпуске 2.x. Используйте его, чтобы *наблюдать* (замер времени, логирование, трассировка) и *отклонять* сообщения; не делайте его фундаментом, на котором держится сервер. -`MCPServer` принимает список при создании (`MCPServer(name, middleware=[...])`) и предоставляет его как `mcp.middleware`; низкоуровневый `Server` предоставляет тот же список как `server.middleware`. В примере ниже используется низкоуровневый `Server`; если конструкция `Server(name, on_call_tool=...)` вам незнакома, сначала прочитайте **[Низкоуровневый Server](low-level-server.md)**. +`MCPServer` принимает список при создании (`MCPServer(name, middleware=[...])`) и предоставляет его как `mcp.middleware`; низкоуровневый `Server` предоставляет тот же список как `server.middleware`. В примерах ниже используется низкоуровневый `Server`; если конструкция `Server(name, on_call_tool=...)` вам незнакома, сначала прочитайте **[Низкоуровневый Server](low-level-server.md)**. ## Middleware для замера времени {#a-timing-middleware} @@ -45,12 +45,28 @@ tools/call took 0.1 ms * Каждый запрос и каждое уведомление, доходящие до сервера. Для уведомления `ctx.request_id is None`, `call_next(ctx)` возвращает `None`, а всё, что вернёте вы, отбрасывается. (В транспорте Streamable HTTP редакции `2026-07-28` POST-запрос клиента с уведомлением подтверждается кодом `202` на транспортном уровне и никогда не передаётся на обработку, так что до middleware он тоже не доходит; эта редакция не определяет уведомлений от клиента к серверу по HTTP.) * Даже метод, для которого у сервера нет обработчика: `call_next` выбрасывает `MCPError(-32601, "Method not found")` *сквозь* middleware по пути к клиенту. +## Ограничение числа одновременных вызовов {#a-concurrency-cap} + +Слой middleware не обязан вызывать `call_next(ctx)`. Выбросьте вместо этого `MCPError` — и это одно сообщение будет **отклонено**: подключение не рвётся, а следующее сообщение проходит. + +Допустим, каждый поиск занимает одно соединение из пула в четыре соединения. Этот слой middleware позволяет четырём вызовам инструментов выполняться одновременно, а пятый отклоняет: + +```python title="server.py" hl_lines="15-16 40-55 59" +--8<-- "docs_src/middleware/tutorial002.py" +``` + +* Учитывается только `tools/call`, поэтому сервер продолжает отвечать на `server/discover` и `tools/list`, пока отклоняет вызовы инструментов. +* В MCP нет кода ошибки «сервер занят», поэтому `SERVER_BUSY` — собственный код этого сервера. +* Отказ сразу сообщает клиенту, что сервер перегружен. Если лучше заставить вызывающих ждать, вместо этого оберните `call_next(ctx)` в `anyio.CapacityLimiter`. + +Выброшенное исключение `MCPError` попадает в клиентское приложение, а не к модели. Если сообщение должна прочитать модель, верните вместо этого результат инструмента с `is_error=True`: это пункт **Отвечать** ниже. + ## Что можно делать внутри {#what-you-can-do-inside-one} В порядке возрастания того, насколько стоит задуматься, прежде чем это делать: -* **Наблюдать.** Замерять, считать, логировать. Пример выше. -* **Отклонять.** Выбросьте `MCPError` *вместо* вызова `call_next(ctx)` — и на это одно сообщение придёт ответ с ошибкой JSON-RPC. Подключение не рвётся; следующее сообщение проходит. Именно так сервер ограничивает `subscriptions/listen` для каждого вызывающего: раздел **[Кому разрешено наблюдать](../handlers/subscriptions.md#deciding-who-may-watch)** на странице о подписках разбирает это пошагово. +* **Наблюдать.** Замерять, считать, логировать. Middleware для замера времени из раздела выше. +* **Отклонять.** Выбросьте `MCPError` *вместо* вызова `call_next(ctx)` — и на это одно сообщение придёт ответ с ошибкой JSON-RPC. Подключение не рвётся; следующее сообщение проходит. Ограничение числа одновременных вызовов из раздела выше. Таким же образом сервер ограничивает `subscriptions/listen` для каждого вызывающего: раздел **[Кому разрешено наблюдать](../handlers/subscriptions.md#deciding-who-may-watch)** на странице о подписках разбирает это пошагово. * **Переписывать.** `ctx` — это dataclass: `await call_next(dataclasses.replace(ctx, params=...))` передаёт остальной цепочке не те параметры, что прислал клиент. Никогда не делайте этого с `initialize`: результат, который получает клиент, строится из переписанных параметров, но состояние подключения сервер фиксирует по исходным параметрам из сети. Стороны могут завершить рукопожатие, расходясь в том, о чём они договорились. * **Отвечать.** Верните результат, не вызывая `call_next(ctx)`, — и он уйдёт клиенту как ваш ответ. `call_next` отдаёт готовую сетевую форму, а конвейер никогда не правит то, что вы возвращаете, так что вся обёртка целиком на вас: на подключении поколения 2026 сюда входит отметка `serverInfo` в `_meta`, которую SDK добавляет к результатам обработчиков, но не к вашим. diff --git a/i18n/ru/pages/client/identity-assertion.md b/i18n/ru/pages/client/identity-assertion.md index 65d82a9089..46ace2bd6f 100644 --- a/i18n/ru/pages/client/identity-assertion.md +++ b/i18n/ru/pages/client/identity-assertion.md @@ -1,6 +1,6 @@ --- translation: - sections: [a91322c46111d16d, 8e6fd6d6f59bb568, e7828fd2729b2c9d, a03ec26bfc678b65, 1034c653c0bcf1b0] + sections: [a91322c46111d16d, 8e6fd6d6f59bb568, 7cf38181f6c99fd5, 37804d4fb36d6302, 1034c653c0bcf1b0] tool: 1 --- # Утверждение идентичности {#identity-assertion} @@ -65,7 +65,7 @@ translation: ### Конфиденциальный клиент {#a-confidential-client} -`client_secret` обязателен; без него конструктор выбрасывает `ValueError`. Профиль IETF, лежащий в основе [SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990), оставляет этот грант только конфиденциальным клиентам, SEP-990 требует, чтобы клиент аутентифицировался, а этот SDK обеспечивает и то и другое, настаивая на общем секрете. `token_endpoint_auth_method` выбирает, где он передаётся: `client_secret_post` (по умолчанию, в теле формы) или `client_secret_basic` (заголовок HTTP Basic). Профиль допускает ещё `private_key_jwt`; этот провайдер его не поддерживает. +`client_secret` обязателен; без него конструктор выбрасывает `ValueError`. Профиль IETF, лежащий в основе [SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990), рекомендует этот грант только для конфиденциальных клиентов, а [RFC 7521](https://datatracker.ietf.org/doc/html/rfc7521) оставляет эту политику на усмотрение сервера авторизации. Этот SDK выбирает осторожное прочтение с обеих сторон: встроенный сервер авторизации отклоняет клиент, у которого нет общего секрета, а этот провайдер настаивает на секрете. `token_endpoint_auth_method` выбирает, где он передаётся: `client_secret_post` (по умолчанию, в теле формы) или `client_secret_basic` (заголовок HTTP Basic). Профиль допускает ещё `private_key_jwt`; этот провайдер его не поддерживает. !!! tip Читайте `client_secret` из переменных окружения или менеджера секретов и никогда — из @@ -93,6 +93,7 @@ SDK может и сам *быть* сервером авторизации: `cr * `identity_assertion_enabled=True` открывает всё остальное. Когда флаг выключен (а по умолчанию это так), `/token` отвечает на этот грант `unsupported_grant_type`, даже если вы реализовали хук, и метаданные о нём не упоминают. Когда включён, в метаданных появляется тип гранта `jwt-bearer`, а в `authorization_grant_profiles_supported` — поле, через которое расширение объявляет о поддержке, — указывается `urn:ietf:params:oauth:grant-profile:id-jag`. (Клиент этого SDK его никогда не читает: он настроен на одного издателя и просто делает запрос.) * **`exchange_identity_assertion`** — это и есть хук. К моменту его запуска SDK уже аутентифицировал клиент, отклонил публичные клиенты и отклонил клиенты, в регистрации которых этот грант не указан. Вы получаете `IdentityAssertionParams` (сырое `assertion`, запрошенные `scopes` и `resource`) и возвращаете обычный `OAuthToken`. +* Отклонять публичные клиенты — политика SDK, а не требование спецификации. Встроенный сервер аутентифицирует клиенты только по общему секрету: он не поддерживает `private_key_jwt` и пока не обрабатывает документы Client ID Metadata Document ([#1801](https://github.com/modelcontextprotocol/python-sdk/issues/1801)), поэтому клиент, идентифицируемый таким документом, не может использовать этот грант здесь. Развёртывание, которому нужна другая политика, может заменить маршрут `/token`, который возвращает `create_auth_routes`, собственным. * Динамическая регистрация клиентов отклоняет этот грант безусловно, поэтому `get_client` здесь отдаёт клиент, заведённый вручную. Клиент ID-JAG не может появиться, зарегистрировав сам себя. * Половина класса — отказы. `OAuthAuthorizationServerProvider` — это *весь* сервер авторизации, поэтому он требует и сценарий с кодом авторизации; сервер, который ещё и выполняет вход пользователей, реализует эти методы по-настоящему, а у этого ровно одна дверь. diff --git a/i18n/ru/pages/client/transports.md b/i18n/ru/pages/client/transports.md index 28a44383c7..4c7e03ffe0 100644 --- a/i18n/ru/pages/client/transports.md +++ b/i18n/ru/pages/client/transports.md @@ -1,6 +1,6 @@ --- translation: - sections: [9cac816674181eb0, 7c157764133fea1f, 40b4916d82eaf1d4, 10d151f2cc75317f, 3d0832f39b0d7059, 92742ba36533633d, 0aeca6145e7bd302] + sections: [9cac816674181eb0, 5619e950d206e6c8, 40b4916d82eaf1d4, 10d151f2cc75317f, 3d0832f39b0d7059, 92742ba36533633d, 991c10e47fda2636] tool: 1 --- # Клиентские транспорты {#client-transports} @@ -44,6 +44,8 @@ translation: * `httpx2.AsyncClient` принадлежит вам, поэтому входите в него и выходите из него **вы**. SDK никогда не закрывает клиент, который он не создавал. * `streamable_http_client(url, http_client=...)` возвращает транспорт, а `Client(transport)` принимает его, как и всё остальное. +Оставьте параметр `timeout=`. Это тот же таймаут, что использует собственный клиент SDK (30 секунд, 300 на чтение); `httpx2.AsyncClient`, созданный без него, получает принятые в `httpx2` по умолчанию 5 секунд, и вызов инструмента, который длится дольше, завершается ошибкой таймаута чтения. + Одно замечание о TLS: `httpx2` проверяет сертификаты по хранилищу доверия операционной системы (через [`truststore`](https://pypi.org/project/truststore/)), а не по встроенному списку CA. В среде без пригодного системного хранилища CA (некоторые минимальные контейнеры) задайте стандартные @@ -51,16 +53,32 @@ translation: (подробности в разделе [`httpx` и `httpx-sse` заменены на `httpx2`](../migration.md#httpx-and-httpx-sse-replaced-by-httpx2)). +### SSE-события большего размера {#larger-sse-events} + +Передайте `max_sse_event_size`, если сервер отправляет крупный результат инструмента или уведомление в одном SSE-событии: + +```python title="client.py" hl_lines="6-9" +--8<-- "docs_src/client_transports/tutorial005.py" +``` + +По умолчанию — 1 МиБ на событие; размер считается в байтах до разбора события. Ограничение действует +для ответов на POST, GET-потока и возобновлённых потоков. Слишком большое событие в ответе на POST или +в возобновлённом потоке приводит к сбою этого запроса с ошибкой SSE. В фоновом GET-потоке клиент +записывает ошибку в лог и пробует открыть поток заново. Чтобы снять ограничение, когда вы доверяете +серверу и нужны события крупнее, задайте `max_sse_event_size=None`. JSON-ответов это не касается. Если +используется `ClientSessionGroup`, задайте тот же параметр в `StreamableHttpParameters`. + !!! warning Раньше `streamable_http_client` принимал `headers=` и `timeout=` напрямую. Больше не принимает: - его единственные параметры — `url`, `http_client` и `terminate_on_close`. Напишите по привычке `headers=` — + его параметры — `url`, `http_client`, `terminate_on_close` и `max_sse_event_size`. Напишите по привычке `headers=` — и получите: ```text TypeError: streamable_http_client() got an unexpected keyword argument 'headers' ``` - Всё, что относится к HTTP, теперь живёт в одном `httpx2.AsyncClient`, который вы передаёте. + Заголовки, аутентификация, прокси и таймауты живут в одном `httpx2.AsyncClient`, который вы передаёте. + А параметр `max_sse_event_size` применяется к компонентам MCP-транспорта, которые читают SSE. !!! info `httpx2` сохраняет привычный API `httpx`, так что, если вы знаете `httpx`, вы уже умеете делать здесь @@ -137,6 +155,7 @@ stderr дочернего процесса идёт в ваш. Чтобы нап * `Client("http://.../mcp")` (URL) подключается по Streamable HTTP, транспорту для продакшена. * Заголовки, аутентификация, прокси и таймауты задаются на `httpx2.AsyncClient`, который передаётся в `streamable_http_client(url, http_client=...)`. Именованного аргумента `headers=` нет. +* Чтобы изменить ограничение в байтах на каждое SSE-событие, используйте `streamable_http_client(url, max_sse_event_size=...)`. * Редиректы выполняются только в пределах источника самого URL (`307`/`308` с добавлением косой черты) плюс `http`→`https` на том же хосте. Всё остальное завершается ошибкой `Redirect to … not followed`; укажите в конфигурации конечный URL. * stdio — это `Client(StdioServerParameters(...))`. Оборачивайте его в `stdio_client(...)` сами, только чтобы перенаправить stderr дочернего процесса. * Подпроцесс получает окружение из разрешённого списка, а не ваше; `env=` добавляет к нему. diff --git a/i18n/ru/pages/handlers/cancellation.md b/i18n/ru/pages/handlers/cancellation.md new file mode 100644 index 0000000000..867b5ddad2 --- /dev/null +++ b/i18n/ru/pages/handlers/cancellation.md @@ -0,0 +1,60 @@ +--- +translation: + sections: [07968345fdc0b84e, 4ea8416db9efa0dc, 336a7b4c5d0a4578, 18392e805dde6717, c30d50df43f9b55c] + tool: 1 +--- +# Отмена {#cancellation} + +Клиент может отказаться от вызова: пользователь нажал «стоп» или истёк тайм-аут. + +В этом случае SDK **отменяет обработчик**. Выражение `await`, на котором он ждёт, выбрасывает исключение, стек функции раскручивается, и то, что она возвращает, никуда не отправляется. Большинству обработчиков ничего с этим делать не нужно. + +Исключений два: обработчик, которому есть что за собой убрать, и обработчик, объявленный через обычный `def`. + +## Очистка в инструменте `async def` {#clean-up-in-an-async-def-tool} + +Поместите очистку в блок `finally`: + +```python title="server.py" hl_lines="23 26-28" +--8<-- "docs_src/cancellation/tutorial001.py" +``` + +* Блок `finally` выполняется, как бы ни завершился инструмент: вернул результат, выбросил исключение или был отменён. +* Очистке, которой нужен `await`, требуется `shield=True`. В отменённом обработчике каждый следующий `await` тоже выбрасывает исключение, поэтому без защиты функция `release_hold` остановилась бы на первой же строке. +* Защищённый блок ничем нельзя отменить, поэтому ограничьте его по времени. Здесь это `5` секунд. + +!!! tip + Используйте `finally`, а не `except`. После очистки отмена должна распространяться дальше + вверх, и `finally` ей это позволяет. + +## Досрочная остановка в обычном инструменте `def` {#stop-early-in-a-plain-def-tool} + +Обычный инструмент `def` выполняется в потоке, а поток нельзя прервать извне. Инструмент должен спросить сам: + +```python title="server.py" hl_lines="22 25-26" +--8<-- "docs_src/cancellation/tutorial002.py" +``` + +* Функция `anyio.from_thread.check_cancelled()` ничего не делает, пока вызов активен, и выбрасывает исключение, как только он отменён. Вызывайте её между порциями работы. +* Очистка и здесь помещается в `finally`. В потоке нет асинхронных ожиданий, поэтому защита не нужна. +* Инструмент `def`, который ни разу не спрашивает, выполняется до конца, а его результат отбрасывается. + +## Где это работает {#where-it-applies} + +Функции промптов и ресурсов отменяются точно так же, как инструменты. + +Через stdio и Streamable HTTP всё работает одинаково. Для класса `Client` из этого SDK отказаться от вызова — значит отменить задачу, которая ожидает `call_tool`, или дождаться, пока истечёт его тайм-аут `read_timeout_seconds`. + +!!! warning + При двух параметрах Streamable HTTP обработчик об отмене не узнаёт: `json_response=True` на + подключении `2026-07-28` и `stateless_http=True` на подключении старого поколения. В этих + случаях обработчик выполняется до конца, что бы ни сделал клиент. + +## Итоги {#recap} + +* Когда клиент отказывается от вызова, SDK отменяет обработчик: инструмент, промпт или ресурс. +* `async def`: выполняйте очистку в `finally`, а очистку с асинхронным ожиданием помещайте в `anyio.move_on_after(seconds, shield=True)`. +* Обычный `def`: вызывайте `anyio.from_thread.check_cancelled()` между порциями работы, иначе инструмент выполнится до конца. Для очистки достаточно обычного `finally`. +* `json_response=True` (современные подключения) и `stateless_http=True` (подключения старого поколения) отключают отмену. + +Ход выполнения и отмена — это дело работающего инструмента и *вызывающей стороны*. Строки, которые он пишет в лог для *вас*, того, кто обслуживает сервер, идут по другому каналу: **[Логирование](logging.md)**. diff --git a/i18n/ru/pages/handlers/index.md b/i18n/ru/pages/handlers/index.md index c3cf7497b8..8bf898a337 100644 --- a/i18n/ru/pages/handlers/index.md +++ b/i18n/ru/pages/handlers/index.md @@ -1,6 +1,6 @@ --- translation: - sections: [424930166c4bc6f3] + sections: [22ca41e50cc1b536] tool: 1 --- # Внутри обработчика {#inside-your-handler} @@ -28,6 +28,8 @@ translation: **[Сэмплирование и корневые каталоги](sampling-and-roots.md)** (sampling и roots), возможности устаревшие, но всё ещё обслуживаемые. * Сообщать о **[ходе выполнения](progress.md)** долгой операции. +* Прибирать за собой или останавливаться досрочно, когда клиент + отказывается от вызова, — **[Отмена](cancellation.md)**. * Писать логи (в стандартный поток ошибок, для тех, кто эксплуатирует сервер) — **[Логирование](logging.md)**. * Сообщать подписанным клиентам, что что-то изменилось, — diff --git a/i18n/ru/pages/handlers/lifespan.md b/i18n/ru/pages/handlers/lifespan.md index 0205bb049d..73b9bf3df6 100644 --- a/i18n/ru/pages/handlers/lifespan.md +++ b/i18n/ru/pages/handlers/lifespan.md @@ -1,6 +1,6 @@ --- translation: - sections: [f3ca8ac5f90f2dfa, 85a1ef3588ba0736, 563346d4d5804933, 9e3528340d0bab53] + sections: [f3ca8ac5f90f2dfa, 48e478ef7bd688b1, 563346d4d5804933, 52ac6a7734d6f581] tool: 1 --- # Жизненный цикл {#lifespan} @@ -46,7 +46,7 @@ translation: `genre` — единственный аргумент, который может передать модель. Жизненный цикл — внутреннее дело вашего сервера. -Функции `@mcp.resource()` и `@mcp.prompt()` тоже могут принимать параметр `ctx`, записанный как просто `Context` — почему, объясняется в следующем разделе. Всё, что несёт в себе `ctx`, описано на странице **[Объект Context](context.md)**. +Функции `@mcp.resource()` и `@mcp.prompt()` тоже могут принимать параметр `ctx`. Всё, что несёт в себе `ctx`, описано на странице **[Объект Context](context.md)**. ### Он действительно типизирован {#it-really-is-typed} @@ -56,19 +56,6 @@ translation: Напишите вместо этого просто `Context` — и `lifespan_context` получит тип `dict[str, Any]`: анализатору типов неоткуда узнать, что отдал ваш жизненный цикл. Во время выполнения объект по-прежнему на месте; вы лишь теряете подсказки. -!!! warning - `Context[AppContext]` — запись **только для инструментов**. Поставьте её на функцию - `@mcp.resource()` или `@mcp.prompt()` — и каждый вызов этого обработчика завершится ошибкой. - Клиент получит ошибку в ответ, а в логе сервера будет видна причина: - - ```text - Context is not available outside of a request - ``` - - В ресурсах и промптах пишите просто `ctx: Context`. Объект, который отдал ваш жизненный - цикл, во время выполнения по-прежнему лежит в `ctx.request_context.lifespan_context`; вы - отказываетесь от параметра типа, а не от объекта. - !!! tip Жизненный цикл есть всегда. Если не передать свой, вариант SDK по умолчанию отдаёт пустой `dict`, так что `ctx.request_context.lifespan_context` равен `{}` и никогда не `None`. Из-за @@ -101,7 +88,7 @@ translation: * Код до `yield` — это запуск. `finally` после него — остановка. * Он выполняется один раз, вокруг всей жизни сервера, а не на каждый запрос. * Всё, что вы отдаёте через `yield`, — это `ctx.request_context.lifespan_context` в каждом инструменте, ресурсе и промпте. -* `ctx: Context[AppContext]` делает этот доступ полностью типизированным в инструментах. Ресурсы и промпты принимают просто `Context`. +* `ctx: Context[AppContext]` делает этот доступ полностью типизированным. * Нет `lifespan=` — значит, пустой `dict`, и никогда не `None`. Обработчик, который останавливается посреди вызова, чтобы спросить пользователя о том, что знает только он, — это **[элицитация (elicitation)](elicitation.md)**. diff --git a/i18n/ru/pages/handlers/progress.md b/i18n/ru/pages/handlers/progress.md index 4bea42be1a..04321e04dc 100644 --- a/i18n/ru/pages/handlers/progress.md +++ b/i18n/ru/pages/handlers/progress.md @@ -1,6 +1,6 @@ --- translation: - sections: [5315262fe26b33e1, 9d8e98840f1b78f0, 52d6009a07e770ea, 8534d8dbb4053a70, 2966fac6fe697007] + sections: [5315262fe26b33e1, 9d8e98840f1b78f0, 52d6009a07e770ea, 8534d8dbb4053a70, 2e9aff14d3a882c0] tool: 1 --- # Ход выполнения {#progress} @@ -121,4 +121,4 @@ Imported https://example.com/b.json (2.0/2.0) * Нет колбэка у вызова — `report_progress` ничего не делает. Сообщайте безусловно. * Опускайте `total`, когда он неизвестен; колбэк получит `None`. -Ход выполнения — это то, что работающий инструмент показывает *пользователю*. Строки, которые он пишет в лог для *вас*, человека, обслуживающего сервер, — это другой канал: **[Логирование](logging.md)**. +Ход выполнения нужен клиенту, который всё ещё ждёт. А то, что видит инструмент, когда клиент перестаёт ждать, — это **[Отмена](cancellation.md)**. diff --git a/i18n/ru/pages/handlers/subscriptions.md b/i18n/ru/pages/handlers/subscriptions.md index f48b7be97c..be9222e518 100644 --- a/i18n/ru/pages/handlers/subscriptions.md +++ b/i18n/ru/pages/handlers/subscriptions.md @@ -1,6 +1,6 @@ --- translation: - sections: [60a9de8a0bdaa531, 317bbe7e4355cdcc, a61d660c8029e04a, 8f7e82fcb88df8a9, b165db51249ff8ed, 266f56fb798068a4, 7c0e57030b622139, df18d7c2417a9883] + sections: [60a9de8a0bdaa531, 6693607ea56d8bd6, a61d660c8029e04a, 8f7e82fcb88df8a9, b165db51249ff8ed, b8bc624a627ead9b, 2139e68e36d9e621, 7c0e57030b622139, 34ab1af2b9ab5b45] tool: 1 --- # Подписки {#subscriptions} @@ -22,7 +22,7 @@ translation: * Родственные методы — `notify_prompts_changed()` и `notify_resources_changed()`. * Нет подписчиков — нет работы. Публикация на сервере, который никто не слушает, ничего не делает, поэтому проверять, слушает ли кто-нибудь, не нужно. Вы просто сообщаете, что изменилось. -`MCPServer` обслуживает `subscriptions/listen` за вас. Протокольные обязательства (подтверждение первым кадром, фильтрация для каждого потока, идентификатор подписки в каждом кадре) — забота SDK. +`MCPServer` обслуживает `subscriptions/listen` за вас, если вы этого не [отключили](#turning-it-off). Протокольные обязательства (подтверждение первым кадром, фильтрация для каждого потока, идентификатор подписки в каждом кадре) — забота SDK. !!! check В передаваемых данных поток, в фильтре которого указан `board://sprint`, после выполнения `complete_task` выглядит так: @@ -79,7 +79,32 @@ translation: Публикации идут от обработчика к открытым потокам через `SubscriptionBus`. По умолчанию шина в памяти: один процесс и все потоки в нём. Это правильный выбор, пока вы не запускаете реплики за балансировщиком нагрузки: тогда поток клиента привязан к одной реплике, а публикация на другой реплике должна до него дойти. -Этот стык реализуете вы: два метода поверх вашего pub/sub-бэкенда. +С шиной по умолчанию это невозможно, потому что у каждой реплики она своя: + +```mermaid +flowchart LR + client[Client] --> lb[Load balancer] + lb --> stream + lb ~~~~ gap + lb --> tool + subgraph B [Replica B] + tool[tools/call] -- publishes --> busB[(bus B)] + end + gap[(no shared bus)] + subgraph A [Replica A] + stream[listen stream] -- subscribed --> busA[(bus A)] + end + style A fill:none + style B fill:none + style gap fill:none,stroke-dasharray:4 4 +``` + +Ничего не ломается: вызов завершается успешно, а поток молчит. Поэтому за балансировщиком нагрузки выберите одно из двух: + +* **Уведомления об изменениях нужны.** Дайте всем репликам одну и ту же шину, как показано ниже. +* **Не нужны.** [Отключите их](#turning-it-off), чтобы ни одному клиенту не были обещаны события, которые он пропустит, и ни один не держал ради них открытый поток. + +Общую шину реализуете вы: два метода поверх вашего pub/sub-бэкенда. ```python from collections.abc import Callable @@ -128,6 +153,20 @@ async def tools_reloaded() -> None: await bus.publish(ToolsListChanged()) # from a lifespan task, a webhook, anywhere ``` +## Отключение {#turning-it-off} + +Серверу, каталог которого никогда не меняется, публиковать нечего. Сообщите об этом при его создании: + +```python title="server.py" hl_lines="3" +--8<-- "docs_src/subscriptions/tutorial007.py" +``` + +* Клиент `2026-07-28` не видит объявленных уведомлений об изменениях, а запрос `subscriptions/listen` получает *Method not found* вместо открытого потока. +* `ctx.notify_*` по-прежнему работает и ни до кого не доходит, поэтому обработчики менять не нужно. +* Клиенты на более ранних версиях протокола разницы не заметят. + +Открытый поток — это запрос, который никогда не завершается, поэтому отключение важно и на хосте, где плата берётся за длительность запроса. + ## Низкоуровневая сборка {#the-low-level-composition} На низкоуровневом `Server` ничего заранее не подключено, и те же детали собираются в три строки: @@ -148,5 +187,6 @@ async def tools_reloaded() -> None: * Клиентская сторона — это `async with client.listen(...)`: подробнее — на странице **[Подписки](../client/subscriptions.md)** в разделе *Клиенты*. * На низкоуровневом `Server` те же детали вы собираете сами: шина, `ListenHandler(bus)`, слот `on_subscriptions_listen`. * Горизонтальное масштабирование — это реализовать `SubscriptionBus` (два метода) и передать его как `MCPServer(subscriptions=...)`. +* Публиковать нечего или у реплик нет общей шины: `MCPServer(subscriptions=False)` не объявляет уведомлений об изменениях и не держит ни одного потока. О запуске сервера, который всё это обслуживает, с одной репликой или с двадцатью, — на странице **[Развёртывание и масштабирование](../run/deploy.md)**. diff --git a/i18n/ru/pages/run/deploy.md b/i18n/ru/pages/run/deploy.md index 1ddeb70d37..62d7f10cec 100644 --- a/i18n/ru/pages/run/deploy.md +++ b/i18n/ru/pages/run/deploy.md @@ -1,6 +1,6 @@ --- translation: - sections: [28221886b198784f, f88ea1f1614f3a1d, 2e76f5cb9df15042, ce926d686730b6d0, 3be24f8ad8bb5ab9, 3fad24032b2224ff, f25a7f860e579ecb, 697b01d95080880d] + sections: [28221886b198784f, f88ea1f1614f3a1d, 2e76f5cb9df15042, ce926d686730b6d0, 3be24f8ad8bb5ab9, aaf489e944ecf5d1, f25a7f860e579ecb, 697b01d95080880d] tool: 1 --- # Развёртывание и масштабирование {#deploy-scale} @@ -174,6 +174,7 @@ python -c "import secrets; print(secrets.token_hex(32))" * Между настоящими процессами **в SDK нет шины, которая могла бы помочь.** `SubscriptionBus` — это `Protocol` из двух методов (`publish` и `subscribe`), который вы реализуете поверх собственного pub/sub-бэкенда (Redis, NATS, что угодно, что у вас уже работает) и передаёте как `MCPServer(subscriptions=...)`. Набросок и контракт — на странице **[Подписки](../handlers/subscriptions.md#scaling-past-one-process)**. * Шина переносит четыре небольших типизированных события и никогда — JSON-RPC. Подтверждение, фильтрация и жизненный цикл потоков остаются в SDK, поэтому ваша шина не может сломать протокол; она может только перемещать события между процессами. * Потоки **не** возобновляемы, и события **не** воспроизводятся повторно. Потеря реплики обрывает её потоки; клиенты заново подписываются и заново запрашивают данные. Нет хранилища событий, которое нужно разделять, и больше нечего настраивать. Это единственное место, где горизонтальное масштабирование — действительно просто больше того же самого. +* Сервер, которому уведомления об изменениях не нужны, обходится без шины: **[отключите их](../handlers/subscriptions.md#turning-it-off)**. ## Чего SDK не даёт {#what-the-sdk-does-not-give-you} diff --git a/i18n/ru/pages/servers/structured-output.md b/i18n/ru/pages/servers/structured-output.md index b335db6a2f..b7d4921923 100644 --- a/i18n/ru/pages/servers/structured-output.md +++ b/i18n/ru/pages/servers/structured-output.md @@ -1,6 +1,6 @@ --- translation: - sections: [a838d57f003aed44, 857d03886a0137ed, 42d9efcb9f542867, 2290ff08435b5573, 91be9b73602abcf1, 6cdbad079f7b47f0, d4b607372fb28b51, 18dbf726ac45e0b7, c7eff2a5698225fa, c851964bb3301907, 8f296f1f09e4c400, d715db6f8dccc9cc, a0c344a48450dbe4] + sections: [a838d57f003aed44, 857d03886a0137ed, 42d9efcb9f542867, 2290ff08435b5573, 91be9b73602abcf1, 6cdbad079f7b47f0, d4b607372fb28b51, 7608fc5ebc31d6ea, c7eff2a5698225fa, c851964bb3301907, 8f296f1f09e4c400, d715db6f8dccc9cc, a0c344a48450dbe4] tool: 1 --- # Структурированный вывод {#structured-output} @@ -174,6 +174,8 @@ result.structured_content # {"London": 16.2, "Reykjavik": 4.4} Ключи должны быть `str`. `dict[int, float]` не может быть JSON-объектом, поэтому для него применяется запасной вариант — обёртка `{"result": ...}`. +Результаты-словари валидируются и сериализуются с помощью `TypeAdapter` из Pydantic. Если заглянуть в `FuncMetadata.output_model` инструмента, там хранится аннотация типа словаря вместе с заголовком схемы. + ## Валидация {#validation} `output_schema` — не документация. Всё, что возвращает функция, **проверяется на соответствие схеме** до того, как покинет сервер. diff --git a/i18n/ru/pages/servers/tools.md b/i18n/ru/pages/servers/tools.md index a020dc5e4c..f5fae369c3 100644 --- a/i18n/ru/pages/servers/tools.md +++ b/i18n/ru/pages/servers/tools.md @@ -1,6 +1,6 @@ --- translation: - sections: [e4cc390d56573409, f30cf8103a6e918c, 2c97b9f888398951, 048e5471dfa71aea, 3076b1e16ad95950, edbedf2a16e71311, 3d8ef8da89fa87c1, f6c0e02e6ea5a363] + sections: [e4cc390d56573409, f30cf8103a6e918c, 2c97b9f888398951, 048e5471dfa71aea, 3076b1e16ad95950, c1115cd005b81e8f, 3d8ef8da89fa87c1, f6c0e02e6ea5a363] tool: 1 --- # Инструменты {#tools} @@ -141,7 +141,7 @@ Inspector отрисует форму с обязательным текстов Если инструмент занимается вводом-выводом (вызывает API, читает файл, обращается к базе данных), объявите его через `async def` и используйте `await` внутри. SDK дождётся его выполнения. -Инструмент с обычным `def` тоже работает: SDK запускает его в отдельном потоке, так что сервер он не блокирует. +Инструмент с обычным `def` тоже работает: SDK запускает его в отдельном потоке, так что сервер он не блокирует. Долго работающий инструмент может проверить, ждёт ли ещё клиент; см. страницу **[Отмена](../handlers/cancellation.md)**. Больше ничего настраивать не нужно. diff --git a/i18n/ru/pages/troubleshooting.md b/i18n/ru/pages/troubleshooting.md index b364dc4a8c..e17e4b24e2 100644 --- a/i18n/ru/pages/troubleshooting.md +++ b/i18n/ru/pages/troubleshooting.md @@ -1,6 +1,6 @@ --- translation: - sections: [3d58228e81b99543, 170514ce901c4139, 17d61fad0a50d62b, 8a6e351ec756904d, 137454d469c867f5, 6392596bd6df54f0, 41126fa9c4fe432f, 480b6d7897e30ab4, d83bb682e708dde0, ebbed3449c499db4, 525cdf1755e29d4c, 30fd31be74169d9a, d2e88333d4f7841f, c2dc3b1007d2e987, d6eabf60cc366341, f798e815252852c2, 0cba47bae78d04eb, 2c218ba829abf74e] + sections: [3d58228e81b99543, 170514ce901c4139, 17d61fad0a50d62b, 8a6e351ec756904d, 137454d469c867f5, fcf984fa0615ed11, 6392596bd6df54f0, 41126fa9c4fe432f, 480b6d7897e30ab4, d83bb682e708dde0, ebbed3449c499db4, 525cdf1755e29d4c, 30fd31be74169d9a, d2e88333d4f7841f, c2dc3b1007d2e987, d6eabf60cc366341, f798e815252852c2, 0cba47bae78d04eb, cdc6d86a4dae8a34] tool: 1 --- # Устранение неполадок {#troubleshooting} @@ -128,6 +128,14 @@ TypeError: The @tool decorator was used incorrectly. Did you forget to call it? подключённый с нулём инструментов, именно эта картина: запустите `python server.py` сами и прочитайте трассировку. Проверка типов тоже это ловит: функция — недопустимое значение для `name=`. +## `InvalidSignature: Tool '' has an invalid x-mcp-header annotation: ` {#invalidsignature-tool-name-has-an-invalid-x-mcp-header-annotation-reason} + +Аргумент инструмента помечен `x-mcp-header` так, как спецификация не разрешает, а `` говорит, какое правило нарушено. Клиенты на `2026-07-28` не включили бы такой инструмент в свой список, поэтому SDK отказывается его регистрировать. + +Помечать можно только аргументы типов `str`, `int` и `bool`, а `str | None` не относится ни к одному из них. Как записать необязательный аргумент — на странице **[Параметры в заголовках](advanced/header-parameters.md)**. + +Как и в записи выше, исключение выбрасывается при **импорте** модуля, ещё до подключения любого клиента. + ## `Tool already exists: ` {#tool-already-exists-name} Две регистрации использовали одно и то же имя инструмента. Побеждает **первая**, вторая молча отбрасывается, и единственный сигнал — это предупреждение в *логе сервера*: @@ -425,6 +433,7 @@ mcp = MCPServer("Weather", request_state_security=RequestStateSecurity(keys=[key * `ExceptionGroup: unhandled errors in a TaskGroup` — никогда не сама ошибка. Читайте **последнюю строку**; перехват `MCPError` *внутри* блока `async with Client(...)` полностью избавляет от обёртки. * `call_tool` не выбрасывает исключение для инструмента, завершившегося с ошибкой. `Error executing tool ...` и `Unknown tool: ...` — это результаты: проверяйте `result.is_error`. Нет сообщения после имени инструмента — значит, он упал, а трассировка в логе сервера. * `Client must be used within an async context manager` -> используйте `async with`. `Use @tool() instead of @tool` -> добавьте скобки. +* `has an invalid x-mcp-header annotation` -> помечать можно только аргументы типов `str`, `int` и `bool`. * `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()`. diff --git a/i18n/ru/pages/whats-new.md b/i18n/ru/pages/whats-new.md index 3382e2d0ae..211b539b73 100644 --- a/i18n/ru/pages/whats-new.md +++ b/i18n/ru/pages/whats-new.md @@ -1,6 +1,6 @@ --- translation: - sections: [cfe01c0c5863dfa2, 0dbb68d8b210177b, 80cf193023af3ed4, 875eb2889263424e] + sections: [cfe01c0c5863dfa2, 0dbb68d8b210177b, bd42ee3a268f6ea4, 875eb2889263424e] tool: 1 --- # Что нового в v2 {#whats-new-in-v2} @@ -167,7 +167,7 @@ v2 реализует ревизию 2026-07-28 и обслуживает **об ### Сервер не может вызывать клиент: многораундовые запросы {#the-server-cannot-call-the-client-multi-round-trip-requests} -На 2026-07-28 исчезли все запросы, инициируемые сервером: push-элицитация, сэмплирование, `roots/list`. На подключении 2026 для них нет обратного канала (back-channel), поэтому `ctx.elicit()` и `ctx.session.create_message()` там падают с `NoBackChannelError` (для клиентов старого поколения они по-прежнему работают). +На 2026-07-28 исчезли все запросы, инициируемые сервером: push-элицитация, сэмплирование, `roots/list`. На подключении 2026 для них нет канала, поэтому `ctx.elicit()` и `ctx.session.create_message()` там падают с `NoBackChannelError` (для клиентов старого поколения они по-прежнему работают). Замена разворачивает вызов. Инструмент, которому что-то нужно от пользователя, *возвращает* вопрос (`InputRequiredResult`), клиент отвечает на него теми же колбэками, что были всегда, и вызов повторяется с приложенными ответами. `Client` ведёт этот цикл за вас. На сервере вы редко собираете результат сами, потому что это делает **[зависимость](handlers/dependencies.md)**: аннотируйте параметр `Resolve(ask_quantity)`, где `ask_quantity` — обычная функция, которую вы пишете, и SDK спросит тем механизмом, который поддерживает подключение: живым запросом элицитации на сессии старого поколения или многораундовым запросом на 2026. Одно тело инструмента, оба поколения: @@ -189,7 +189,7 @@ v2 реализует ревизию 2026-07-28 и обслуживает **об ### Корневые каталоги, сэмплирование и протокольное логирование объявлены устаревшими; `ping` удалён {#roots-sampling-and-protocol-logging-are-deprecated-ping-is-removed} -[SEP-2577](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2577) объявляет устаревшими три *возможности* целиком, на всех версиях протокола: корневые каталоги, сэмплирование и логирование на уровне MCP (`ctx.info()` и ему подобные). Это отдельная ось, не связанная с отсутствующим обратным каналом выше; статус устаревшего — рекомендательный, всё продолжает работать с сессиями поколения 2025, и в передаваемых данных ничего не меняется. Заметите вы `MCPDeprecationWarning` — это `UserWarning`, поэтому он выводится по умолчанию; ожидайте, что первый же `ctx.info(...)` после обновления об этом сообщит. +[SEP-2577](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2577) объявляет устаревшими три *возможности* целиком, на всех версиях протокола: корневые каталоги, сэмплирование и логирование на уровне MCP (`ctx.info()` и ему подобные). Это отдельная ось, не связанная с отсутствующим обратным каналом (back-channel) выше; статус устаревшего — рекомендательный, всё продолжает работать с сессиями поколения 2025, и в передаваемых данных ничего не меняется. Заметите вы `MCPDeprecationWarning` — это `UserWarning`, поэтому он выводится по умолчанию; ожидайте, что первый же `ctx.info(...)` после обновления об этом сообщит. С `ping` строже: он удалён из протокола, а не объявлен устаревшим. Так же на 2026-07-28 удалены два отдельных метода устаревших возможностей — `logging/setLevel` и клиентское `notifications/roots/list_changed`, — а уведомления о ходе выполнения теперь идут только от сервера к клиенту. @@ -204,7 +204,7 @@ v2 реализует ревизию 2026-07-28 и обслуживает **об ### Остальное, вкратце {#the-rest-quickly} * **Идентификация — необязательные метаданные каждого сообщения.** Ключ `clientInfo` в `_meta` на стороне запроса необязателен (обязательная пара — `protocolVersion` + `clientCapabilities`), а `serverInfo` ушёл из тела результата `server/discover`: вместо этого серверы проставляют его в `_meta` каждого результата поколения 2026 ([spec #3002](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/3002)). SDK проставляет всегда; `client.server_info` равен `None`, когда сервер себя не идентифицирует (например, middleware убрал ключ). **[Низкоуровневый Server](advanced/low-level-server.md)** показывает эту отметку в передаваемых данных. -* **Запросы маршрутизируются без разбора тел.** Современные HTTP-запросы несут `Mcp-Method` (а для трёх инструментоподобных вызовов — ещё и `Mcp-Name`); свойство входной схемы инструмента с аннотацией `x-mcp-header` дублируется в заголовок `Mcp-Param-*` и перекрёстно проверяется сервером ([SEP-2243](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2243)). Шлюзы и ограничители частоты могут маршрутизировать по одним заголовкам; правила — в **[Руководстве по миграции](migration.md#servers-validate-mcp-param-headers-against-the-request-body-sep-2243)**. +* **Запросы маршрутизируются без разбора тел.** Современные HTTP-запросы несут `Mcp-Method` (а для трёх инструментоподобных вызовов — ещё и `Mcp-Name`); свойство входной схемы инструмента с аннотацией `x-mcp-header` дублируется в заголовок `Mcp-Param-*` и перекрёстно проверяется сервером ([SEP-2243](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2243)). Шлюзы и ограничители частоты могут маршрутизировать по одним заголовкам. Страница **[Параметры в заголовках](advanced/header-parameters.md)** показывает, как пометить аргумент. * **Результаты несут подсказки кэширования.** Результаты списков и чтения объявляют `ttlMs` и `cacheScope` ([SEP-2549](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2549)); вы задаёте их по методам через `cache_hints=`, а `Client` учитывает их встроенным кэшем ответов. Сервер, не отправляющий подсказок (любой сервер до 2026), видит идентичный, некэшированный трафик. **[Подсказки кэширования](client/caching.md)**. * **Расширения поддерживаются полноценно.** Серверы и клиенты объявляют необязательные наборы возможностей под идентификаторами в обратной 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)** построено по точным сообщениям. diff --git a/i18n/tr/pages/advanced/header-parameters.md b/i18n/tr/pages/advanced/header-parameters.md new file mode 100644 index 0000000000..fd3883d96f --- /dev/null +++ b/i18n/tr/pages/advanced/header-parameters.md @@ -0,0 +1,65 @@ +--- +translation: + sections: [81862a209b483d27, b76e8073487afa03, bc214b2fc2bcdae4, d5835477c0b60163, a5d9786f902ad8e1] + tool: 1 +--- +# Başlık parametreleri {#header-parameters} + +Çoğu sunucunun buna hiç ihtiyacı olmaz. + +Sunucunun önündeki bir ağ geçidi veya yük dengeleyici, yalnızca gövdeyi ayrıştırmadan okuyabildiği bilgilere göre yönlendirme yapabilir. Bir araç argümanını `x-mcp-header` ile işaretleyin; `2026-07-28` **[protokol sürümünü](../protocol-versions.md)** kullanan istemciler bu argümanın değerini HTTP başlığı olarak da gönderir. + +## Bir argümanı işaretleme {#mark-an-argument} + +İşaret, argümanın JSON Schema'sındaki fazladan tek bir anahtardır. `MCPServer`'da bunu oraya `Field` koyar: + +```python title="server.py" hl_lines="13" +--8<-- "docs_src/header_parameters/tutorial001.py" +``` + +* `2026-07-28` sürümünde Streamable HTTP üzerinden istemci, gövdeyle birlikte `Mcp-Param-Region` başlığını da gönderir; ikisi uyuşmazsa sunucu çağrıyı reddeder. +* Aracı listelememiş bir istemci işareti hiç görmemiştir: başlık göndermez ve çağrı reddedilir. Bu SDK'nın `Client`'ı bunun üzerine araçları listeler ve çağrıyı bir kez yeniden gönderir; yani önce listelemek yalnızca bir gidiş-dönüş kazandırır. +* Diğer tüm bağlantılar bu ek açıklamayı yok sayar. + +Fonksiyonunuz değişmez: `region` yine argüman olarak gelir. + +## İşaretlenebilecek argümanlar {#what-can-be-marked} + +`str`, `int` ve `bool` argümanlar. Bunların dışındaki her şey, araç kaydedilirken `InvalidSignature` ile reddedilir. + +Tek bir türü olmayan `str | None` da buna dahildir. İsteğe bağlı bir argümanın şeması, pydantic'in `WithJsonSchema`'sıyla açıkça yazılmalıdır: + +```python +region: Annotated[str | None, WithJsonSchema({"type": "string", "x-mcp-header": "Region"})] = None +``` + +## Düşük seviyeli `Server`'da {#on-the-low-level-server} + +Orada `input_schema`'yı elle yazarsınız, bu yüzden anahtar doğrudan içine yazılır: + +```python title="server.py" hl_lines="18" +--8<-- "docs_src/header_parameters/tutorial002.py" +``` + +* Ek açıklamayı sizin yerinize hiçbir şey denetlemez: geçersiz olanı da sunulur ve `2026-07-28` istemcileri aracı listelerine almaz. + +### Ada göre şemalar {#schemas-by-name} + +Başlığı denetlemek için SDK'nın, çağrıyı iletmeden önce aracın girdi şemasına ihtiyacı vardır. `get_tool_input_schema` yoksa SDK bu şemayı, herhangi bir araç işaretli olsun olmasın, argüman taşıyan her çağrıda `on_list_tools` işleyicinizi çalıştırarak alır. + +```python title="server.py" hl_lines="26 39-41 48" +--8<-- "docs_src/header_parameters/tutorial003.py" +``` + +* Elinizde zaten olan bilgiden yanıt vermek için fonksiyonu geçirin. +* Denetlenecek bir şeyi olmayan araç için `None` döndürün. + +## Özet {#recap} + +* Bir araç argümanındaki `x-mcp-header`, `2026-07-28` istemcilerinin bu argümanı `Mcp-Param-*` HTTP başlığı olarak yinelemesini sağlar. +* Sunucu, başlığı ile gövdesi uyuşmayan çağrıyı reddeder. +* Yalnızca `str`, `int` ve `bool` argümanlar işaretlenebilir. `MCPServer`, bunların dışındaki her şey için `InvalidSignature` fırlatır. +* Düşük seviyeli `Server` hiçbir şeyi denetlemez; istemciler de ek açıklaması geçersiz olan aracı eler. +* `get_tool_input_schema`, düşük seviyeli `Server`'ın her çağrıda `on_list_tools`'u çalıştırmasını önler. + +Elle yazılan `Server` API'sinin geri kalanı **[Düşük seviyeli Server](low-level-server.md)** sayfasında. diff --git a/i18n/tr/pages/advanced/index.md b/i18n/tr/pages/advanced/index.md index fd8a58424c..0c525d53f3 100644 --- a/i18n/tr/pages/advanced/index.md +++ b/i18n/tr/pages/advanced/index.md @@ -1,6 +1,6 @@ --- translation: - sections: [ca6988b7503cd2d3] + sections: [348f8697c6b12cd0] tool: 1 --- # İleri düzey {#advanced} @@ -14,6 +14,8 @@ kaçış yollarını içerir: ve kendinize ait özel JSON-RPC metotları. * **[Sayfalama](pagination.md)** ve **[Middleware](middleware.md)**: *yalnızca* alt düzey `Server` üzerinde yapabileceğiniz iki şey. +* **[Başlık parametreleri](header-parameters.md)**: bir ağ geçidinin, araç çağrısını + argümanlarından birine göre yönlendirmesini sağlayın. * **[Uzantılar](extensions.md)** ve **[MCP Apps](apps.md)**: protokolün uzantı yüzeyi. Uzantı paketlerini bir sunucuda bir araya getirin ya da kendinizinkini yazın. diff --git a/i18n/tr/pages/advanced/low-level-server.md b/i18n/tr/pages/advanced/low-level-server.md index ba9cd121a4..7ac4b7953b 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, 9d5d10a5f0405d0a, 1086e77ce561cd7f, a3f71823df5efc31, 9fc7109f72201cae, d50fe7faead8cf68, 7bf25983df655b66, 6330e1f4c6029683, 2f1749c8c133fa1c, 8db7116fc8ddd0ee, ebc33704fbd74262, 0fde3bcea081ba3a] + sections: [2c79b6338e09b7ac, 9d5d10a5f0405d0a, 1086e77ce561cd7f, a3f71823df5efc31, 9fc7109f72201cae, d50fe7faead8cf68, 7bf25983df655b66, 6330e1f4c6029683, 2f1749c8c133fa1c, 8db7116fc8ddd0ee, 2090d99b355bc2c7, 0fde3bcea081ba3a] tool: 1 --- # Düşük seviyeli Server {#the-low-level-server} @@ -209,6 +209,7 @@ Bunların her biri, artık kavramlarını bildiğiniz birer fikir; her birinin k * `on_call_tool`, `on_get_prompt` ve `on_read_resource`, çağrıyı duraklatıp istemciden girdi istemek için normal sonuçları yerine bir `InputRequiredResult` döndürebilir; bkz. **[Çok turlu istekler](../handlers/multi-round-trip.md)** (multi-round-trip). Bu katmanın ruhuna uygun olarak sizin için hiçbir şey kurulmaz: `MCPServer` varsayılan olarak `requestState`'i mühürlerken burada ayarladığınız `request_state`, siz `server.middleware.append(RequestStateBoundary(RequestStateSecurity(keys=[...]), default_audience=server.name))` ile katılana kadar ağı tam yazıldığı gibi geçer: `MCPServer`'ın yaptığı mühürleme ve doğrulamanın aynısı için tek satır (iki ad da `mcp.server.request_state`'ten içe aktarılır) (**[`requestState`'i koruma](../handlers/multi-round-trip.md#protecting-requeststate)**). * `on_list_resources`, `on_read_resource`, `on_list_prompts`, `on_get_prompt`, `on_completion`, diğer ilkel öğeler için aynı `(ctx, params) -> result` biçimidir. * `on_subscriptions_listen`, 2026-07-28 `subscriptions/listen` akışını sunar. Bir `SubscriptionBus` üzerine kurulu bir `ListenHandler` geçirin ve olayları diğer işleyicilerinizden veri yoluna yayımlayın; bileşimin tamamı için bkz. **[Abonelikler](../handlers/subscriptions.md)**. +* `get_tool_input_schema`, `on_list_tools`'u çağrı yolunun dışında tutar; bkz. **[Başlık parametreleri](header-parameters.md#schemas-by-name)**. * `server.streamable_http_app()`, `MCPServer`'ınkiyle aynı Starlette uygulamasını döndürür; onu **[Sunucunuzu çalıştırma](../run/index.md)** sayfasının herhangi bir ASGI uygulamasını dağıttığı gibi dağıtın. Burada `server.run(transport=...)` yoktur: `server.run(read_stream, write_stream, server.create_initialization_options())` bir akış çifti üzerinden tek bir bağlantıyı yürütür ve bu tek satır işin tamamıdır. ## Özet {#recap} diff --git a/i18n/tr/pages/advanced/middleware.md b/i18n/tr/pages/advanced/middleware.md index 05fa7b6642..f7a16fa47a 100644 --- a/i18n/tr/pages/advanced/middleware.md +++ b/i18n/tr/pages/advanced/middleware.md @@ -1,6 +1,6 @@ --- translation: - sections: [6048b4f308edbb8c, 46056f318ef205e4, c3e565b61acd75c5, c62422b159c6ed09, 420968f514138f43] + sections: [58e1103d9a323ccf, 46056f318ef205e4, 812b414557fb0c35, 4df162eea2518d38, c62422b159c6ed09, 420968f514138f43] tool: 1 --- # Middleware {#middleware} @@ -16,7 +16,7 @@ Onu `async (ctx, call_next)` biçiminde yazar ve `server.middleware` listesine e `MCPServer` listeyi oluşturulurken alır (`MCPServer(name, middleware=[...])`) ve onu `mcp.middleware` olarak sunar; alt düzey `Server` aynı listeyi `server.middleware` olarak sunar. Aşağıdaki -örnek alt düzey `Server`'ı kullanır; `Server(name, on_call_tool=...)` size yeniyse önce +örnekler alt düzey `Server`'ı kullanır; `Server(name, on_call_tool=...)` size yeniyse önce **[Alt düzey Server](low-level-server.md)** sayfasını okuyun. ## Bir zamanlama middleware'i {#a-timing-middleware} @@ -61,14 +61,35 @@ istemeden önce, istemcinin bağlantıyı kurmak için gönderdiği istek. * Sunucunun işleyicisi olmayan bir metot bile: `call_next`, `MCPError(-32601, "Method not found")` istisnasını istemciye giderken middleware'inizin *içinden* fırlatır. -## İçinde neler yapabilirsiniz {#what-you-can-do-inside-one} +## Bir eşzamanlılık sınırı {#a-concurrency-cap} + +Bir middleware `call_next(ctx)`'i çağırmak zorunda değildir. Onun yerine bir `MCPError` fırlatırsanız o tek +mesaj **reddedilir**: bağlantı ayakta kalır ve sonraki mesaj geçer. + +Diyelim ki her arama, dört bağlantılık bir havuzdan bir bağlantı tutuyor. Bu middleware dört araç çağrısının +aynı anda çalışmasına izin verir, beşincisini reddeder: + +```python title="server.py" hl_lines="15-16 40-55 59" +--8<-- "docs_src/middleware/tutorial002.py" +``` + +* Yalnızca `tools/call` sayılır; bu yüzden sunucu araç çağrılarını reddederken `server/discover` ve + `tools/list` isteklerini yanıtlamayı sürdürür. +* MCP bir "sunucu meşgul" hata kodu tanımlamaz; bu yüzden `SERVER_BUSY` bu sunucunun kendi kodudur. +* Reddetmek, sunucunun aşırı yüklü olduğunu istemciye hemen bildirir. Çağıranları bekletmeyi + tercih ederseniz bunun yerine `call_next(ctx)` çağrısı boyunca bir `anyio.CapacityLimiter` tutun. + +Fırlatılan bir `MCPError` modele değil, istemci uygulamasına gider. Mesajı modelin okuması gerekiyorsa +bunun yerine `is_error=True` olan bir araç sonucu döndürün: bu, aşağıdaki **Yanıtlayın** maddesidir. + +## İçinde yapabilecekleriniz {#what-you-can-do-inside-one} Ne kadar tereddüt etmeniz gerektiğine göre artan sırayla: -* **Gözlemleyin.** Süresini ölçün, sayın, loglayın. Yukarıdaki örnek. +* **Gözlemleyin.** Süresini ölçün, sayın, log'a yazın. Yukarıdaki zamanlama middleware'i. * **Reddedin.** `call_next(ctx)`'i çağırmak *yerine* bir `MCPError` fırlatın; o tek mesaj - bir JSON-RPC hatasıyla yanıtlanır. Bağlantı ayakta kalır; sonraki mesaj geçer. Bir sunucu - `subscriptions/listen`'ı çağıran başına böyle denetler: + bir JSON-RPC hatasıyla yanıtlanır. Bağlantı ayakta kalır; sonraki mesaj geçer. Yukarıdaki eşzamanlılık sınırı. Bir sunucu + `subscriptions/listen`'ı çağıran başına da böyle denetler: Abonelikler sayfasındaki **[Kimin izleyebileceğine karar verme](../handlers/subscriptions.md#deciding-who-may-watch)** bölümü bunu adım adım anlatır. * **Yeniden yazın.** `ctx` bir dataclass'tır: `await call_next(dataclasses.replace(ctx, params=...))` diff --git a/i18n/tr/pages/client/identity-assertion.md b/i18n/tr/pages/client/identity-assertion.md index 1ab72e8610..03feba52c3 100644 --- a/i18n/tr/pages/client/identity-assertion.md +++ b/i18n/tr/pages/client/identity-assertion.md @@ -1,6 +1,6 @@ --- translation: - sections: [a91322c46111d16d, 8e6fd6d6f59bb568, e7828fd2729b2c9d, a03ec26bfc678b65, 1034c653c0bcf1b0] + sections: [a91322c46111d16d, 8e6fd6d6f59bb568, 7cf38181f6c99fd5, 37804d4fb36d6302, 1034c653c0bcf1b0] tool: 1 --- # Kimlik beyanı {#identity-assertion} @@ -65,7 +65,7 @@ Uzantı bunu şart koşmaz; bu, bilerek yapılmış daha katı bir tercihtir. Bu ### Gizli istemci {#a-confidential-client} -`client_secret` zorunludur; yapıcı onsuz `ValueError` fırlatır. [SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990) belgesinin dayandığı IETF profili bu grant'i gizli istemcilere ayırır, SEP-990 istemcinin kimliğini doğrulamasını şart koşar ve bu SDK, paylaşılan bir gizli anahtarda ısrar ederek ikisini de uygular. `token_endpoint_auth_method`, anahtarın nereden gideceğini seçer: `client_secret_post` (varsayılan, form gövdesinde) veya `client_secret_basic` (bir HTTP Basic başlığı). Profil `private_key_jwt` yöntemine de izin verir; bu sağlayıcı onu desteklemez. +`client_secret` zorunludur; yapıcı onsuz `ValueError` fırlatır. [SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990) belgesinin dayandığı IETF profili bu grant'i yalnızca gizli istemciler için önerir, [RFC 7521](https://datatracker.ietf.org/doc/html/rfc7521) ise bu politikayı yetkilendirme sunucusuna bırakır. Bu SDK iki tarafta da temkinli yorumu benimser: yerleşik yetkilendirme sunucusu paylaşılan gizli anahtarı olmayan bir istemciyi reddeder, bu sağlayıcı da bir gizli anahtarda ısrar eder. `token_endpoint_auth_method`, anahtarın nereden gideceğini seçer: `client_secret_post` (varsayılan, form gövdesinde) veya `client_secret_basic` (bir HTTP Basic başlığı). Profil `private_key_jwt` yöntemine de izin verir; bu sağlayıcı onu desteklemez. !!! tip `client_secret`'ı ortam değişkenlerinden veya bir gizli anahtar yöneticisinden okuyun, asla @@ -93,6 +93,7 @@ SDK yetkilendirme sunucusunun kendisi de *olabilir*: `create_auth_routes`, yetki * `identity_assertion_enabled=True` her şeyin kapısıdır. Kapalıyken (varsayılan budur) `/token`, hook'u uygulamış olsanız bile bu grant'e `unsupported_grant_type` ile yanıt verir ve meta veri ondan söz etmez. Açıkken meta veri `jwt-bearer` grant türünü kazanır ve uzantının desteği duyurmak için kullandığı alan olan `authorization_grant_profiles_supported` içinde `urn:ietf:params:oauth:grant-profile:id-jag` değerini listeler. (Bu SDK'nın istemcisi onu hiç okumaz: tek bir issuer için hazırlanmıştır ve doğrudan sorar.) * **`exchange_identity_assertion`** hook'un kendisidir. O çalışmadan önce SDK istemcinin kimliğini doğrulamış, açık (public) istemcileri reddetmiş ve kaydında bu grant'in listelenmediği istemcileri reddetmiştir. Size bir `IdentityAssertionParams` gelir (ham `assertion`, istenen `scopes` ve `resource`) ve düz bir `OAuthToken` döndürürsünüz. +* Açık istemcileri reddetmek bir spesifikasyon gereği değil, SDK politikasıdır. Yerleşik sunucu istemcilerin kimliğini yalnızca paylaşılan gizli anahtarla doğrular: `private_key_jwt` desteği yoktur ve Client ID Metadata Document'leri henüz çözümlemez ([#1801](https://github.com/modelcontextprotocol/python-sdk/issues/1801)); bu yüzden kimliği bunlardan biriyle belirlenen bir istemci bu grant'i burada kullanamaz. Farklı bir politika isteyen bir dağıtım, `create_auth_routes` fonksiyonunun döndürdüğü `/token` route'unu kendisininkiyle değiştirebilir. * Dinamik istemci kaydı bu grant'i koşulsuz reddeder; bu yüzden buradaki `get_client` elle hazırlanmış bir istemci sunar. Bir ID-JAG istemcisi kendi kendini kaydederek var olamaz. * Sınıfın yarısı retlerden oluşur. `OAuthAuthorizationServerProvider` yetkilendirme sunucusunun *tamamıdır*, bu yüzden yetkilendirme kodu akışını da ister; kullanıcılara oturum da açtıran bir sunucu onları gerçekten uygular, bunun ise tam olarak tek bir kapısı var. diff --git a/i18n/tr/pages/client/transports.md b/i18n/tr/pages/client/transports.md index bbf104ccf3..7e4c5ed4c8 100644 --- a/i18n/tr/pages/client/transports.md +++ b/i18n/tr/pages/client/transports.md @@ -1,6 +1,6 @@ --- translation: - sections: [9cac816674181eb0, 7c157764133fea1f, 40b4916d82eaf1d4, 10d151f2cc75317f, 3d0832f39b0d7059, 92742ba36533633d, 0aeca6145e7bd302] + sections: [9cac816674181eb0, 5619e950d206e6c8, 40b4916d82eaf1d4, 10d151f2cc75317f, 3d0832f39b0d7059, 92742ba36533633d, 991c10e47fda2636] tool: 1 --- # İstemci aktarımları {#client-transports} @@ -44,22 +44,40 @@ Dikkat edilecek iki şey: * `httpx2.AsyncClient`'ın sahibi sizsiniz, bu yüzden içine **siz** girer ve **siz** çıkarsınız. SDK, kendi oluşturmadığı bir istemciyi asla kapatmaz. * `streamable_http_client(url, http_client=...)` bir aktarım döndürür ve `Client(transport)` onu diğer her şey gibi kabul eder. +`timeout=` değerini koruyun. Bu, SDK'nın kendi istemcisinin kullandığı değerdir (30 saniye, okumalar için 300); zaman aşımı verilmeden oluşturulan bir `httpx2.AsyncClient`, `httpx2`'nin 5 saniyelik varsayılan değerini alır ve bundan uzun süren bir araç çağrısı okuma zaman aşımıyla başarısız olur. + TLS ile ilgili bir not: `httpx2`, sertifikaları paketle gelen bir CA listesine göre değil, işletim sisteminin güven deposuna göre doğrular ( [`truststore`](https://pypi.org/project/truststore/) aracılığıyla). Kullanılabilir bir sistem CA deposu olmayan bir ortamda (bazı minimal kapsayıcılar) standart `SSL_CERT_FILE`/`SSL_CERT_DIR` ortam değişkenlerini ayarlayın ya da `httpx2.AsyncClient`'ınıza açıkça bir `verify=ssl_context` geçirin (arka plan bilgisi için [`httpx` ve `httpx-sse`'nin yerini `httpx2` aldı](../migration.md#httpx-and-httpx-sse-replaced-by-httpx2)). +### Daha büyük SSE olayları {#larger-sse-events} + +Sunucu büyük bir araç sonucunu ya da bildirimi tek bir SSE olayında gönderiyorsa `max_sse_event_size` geçirin: + +```python title="client.py" hl_lines="6-9" +--8<-- "docs_src/client_transports/tutorial005.py" +``` + +Varsayılan değer olay başına 1 MiB; bu, olay ayrıştırılmadan önce bayt cinsinden ölçülür. Sınır +POST yanıtlarına, GET akışına ve sürdürülen akışlara uygulanır. Bir POST yanıtındaki ya da sürdürülen +bir akıştaki aşırı büyük bir olay, o isteği bir SSE hatasıyla başarısız kılar. Arka plandaki GET akışında ise istemci +hatayı log'a yazar ve akışı yeniden dener. Sunucuya güveniyorsanız ve daha büyük olaylara ihtiyacınız varsa +sınırı devre dışı bırakmak için `max_sse_event_size=None` ayarlayın. JSON yanıtları bundan etkilenmez. `ClientSessionGroup` kullanıyorsanız +aynı seçeneği `StreamableHttpParameters` üzerinde ayarlayın. + !!! warning `streamable_http_client` eskiden `headers=` ve `timeout=` parametrelerini doğrudan alırdı. Artık almıyor: - tek parametreleri `url`, `http_client` ve `terminate_on_close`. Alışkanlıkla `headers=`'a + parametreleri `url`, `http_client`, `terminate_on_close` ve `max_sse_event_size`. Alışkanlıkla `headers=`'a uzanırsanız şunu alırsınız: ```text TypeError: streamable_http_client() got an unexpected keyword argument 'headers' ``` - HTTP'yle ilgili her şey artık geçirdiğiniz o tek `httpx2.AsyncClient` üzerinde bulunur. + Başlıklar, kimlik doğrulama, vekil sunucular ve zaman aşımları, geçirdiğiniz o tek `httpx2.AsyncClient` üzerinde yer alır. + `max_sse_event_size` ise MCP aktarımının SSE okuyucularına uygulanır. !!! info `httpx2`, tanıdık `httpx` API'sini korur; yani `httpx`'i biliyorsanız kimlik doğrulama, @@ -136,6 +154,7 @@ Bir **aktarım**, `(read, write)` mesaj akışı çifti veren herhangi bir asenk * `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. +* Her SSE olayının bayt sınırını değiştirmek için `streamable_http_client(url, max_sse_event_size=...)` kullanın. * 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. diff --git a/i18n/tr/pages/handlers/cancellation.md b/i18n/tr/pages/handlers/cancellation.md new file mode 100644 index 0000000000..4e00f38364 --- /dev/null +++ b/i18n/tr/pages/handlers/cancellation.md @@ -0,0 +1,60 @@ +--- +translation: + sections: [07968345fdc0b84e, 4ea8416db9efa0dc, 336a7b4c5d0a4578, 18392e805dde6717, c30d50df43f9b55c] + tool: 1 +--- +# İptal {#cancellation} + +İstemci bir çağrıdan vazgeçebilir: kullanıcı durdur düğmesine basmıştır ya da zaman aşımı süresi dolmuştur. + +Bu olduğunda SDK **işleyicinizi iptal eder**. İşleyicinin beklediği `await` istisna fırlatır, fonksiyon geri sarılır ve döndürdüğü hiçbir şey gönderilmez. Çoğu işleyicinin bu konuda bir şey yapması gerekmez. + +İki tür işleyicinin ise gerekir: temizlemesi gereken bir şey olan işleyici ve düz bir `def` olan işleyici. + +## `async def` araçta temizlik yapma {#clean-up-in-an-async-def-tool} + +Temizliği bir `finally` bloğuna koyun: + +```python title="server.py" hl_lines="23 26-28" +--8<-- "docs_src/cancellation/tutorial001.py" +``` + +* `finally`, araç nasıl biterse bitsin çalışır: değer döndürmüş, istisna fırlatmış ya da iptal edilmiş olabilir. +* `await` kullanmak zorunda olan temizlik için `shield=True` gerekir. İptal edilmiş bir işleyicide sonraki her `await` de istisna fırlatır; bu yüzden koruma olmadan `release_hold` ilk satırında dururdu. +* Korumalı bir bloğu hiçbir şey iptal edemez, bu yüzden ona bir süre sınırı verin. Burada bu sınır `5` saniye. + +!!! tip + `except` değil, `finally` kullanın. Temizliğiniz bittikten sonra iptalin yukarı doğru ilerlemeye + devam etmesi gerekir; `finally` buna izin verir. + +## Düz `def` araçta erken durma {#stop-early-in-a-plain-def-tool} + +Düz bir `def` araç bir iş parçacığında çalışır ve bir iş parçacığını dışarıdan hiçbir şey kesemez. Aracın kendisinin sorması gerekir: + +```python title="server.py" hl_lines="22 25-26" +--8<-- "docs_src/cancellation/tutorial002.py" +``` + +* `anyio.from_thread.check_cancelled()`, çağrı sürerken hiçbir şey yapmaz; çağrı iptal edildikten sonra ise istisna fırlatır. Onu iş birimleri arasında çağırın. +* Temizlik burada da bir `finally` bloğuna girer. İş parçacığında hiçbir şey await kullanmaz, bu yüzden korumaya gerek yok. +* Hiç sormayan bir `def` araç sonuna kadar çalışır ve sonucu atılır. + +## Geçerli olduğu yerler {#where-it-applies} + +Prompt ve kaynak fonksiyonları tam olarak araçlar gibi iptal edilir. + +İptal, stdio ve Streamable HTTP üzerinde aynı şekilde çalışır. Bu SDK'nın `Client`'ında vazgeçmek, `call_tool`'u bekleyen görevi iptal etmek ya da `read_timeout_seconds` süresinin dolmasına izin vermek demektir. + +!!! warning + İki Streamable HTTP seçeneği bu haberi işleyicinize ulaştırmaz: `2026-07-28` bağlantısında + `json_response=True` ve eski nesil bağlantıda `stateless_http=True`. Bu durumlarda işleyici, + istemci ne yapmış olursa olsun sonuna kadar çalışır. + +## Özet {#recap} + +* İstemci bir çağrıdan vazgeçtiğinde SDK işleyiciyi iptal eder: araç, prompt ya da kaynak. +* `async def`: temizliği bir `finally` bloğunda yapın, await kullanan temizliği de `anyio.move_on_after(seconds, shield=True)` içine koyun. +* Düz `def`: iş birimleri arasında `anyio.from_thread.check_cancelled()` fonksiyonunu çağırın, yoksa araç sonuna kadar çalışır. Temizlik için düz bir `finally` yeter. +* `json_response=True` (modern bağlantılar) ve `stateless_http=True` (eski nesil bağlantılar) iptali devre dışı bırakır. + +İlerleme ve iptal, çalışan bir araç ile onu *çağıran* arasındadır. Aracın *sizin* için, yani sunucuyu işleten kişi için log'a yazdığı satırlar ise ayrı bir kanaldır: **[Log kaydı](logging.md)**. diff --git a/i18n/tr/pages/handlers/index.md b/i18n/tr/pages/handlers/index.md index 5922e7b7d4..0eb8f38e23 100644 --- a/i18n/tr/pages/handlers/index.md +++ b/i18n/tr/pages/handlers/index.md @@ -1,6 +1,6 @@ --- translation: - sections: [424930166c4bc6f3] + sections: [22ca41e50cc1b536] tool: 1 --- # İşleyicinin içinde {#inside-your-handler} @@ -28,6 +28,8 @@ Okuyabildikleri: **[Örnekleme (sampling) ve kök dizinler (roots)](sampling-and-roots.md)** ile istemciden bir LLM tamamlaması ya da çalışma alanı klasörlerini istemek. * Yavaş bir işte **[İlerleme](progress.md)** bildirmek. +* İstemci çağrıdan vazgeçtiğinde **[İptal](cancellation.md)** ile temizlik + yapmak ya da erken durmak. * **[Log tutma](logging.md)** ile log yazmak (sunucuyu kim işletiyorsa onun için, standart hataya). * **[Abonelikler](subscriptions.md)** ile abone olmuş istemcilere bir şeyin diff --git a/i18n/tr/pages/handlers/lifespan.md b/i18n/tr/pages/handlers/lifespan.md index 4287eaa04d..746d743d69 100644 --- a/i18n/tr/pages/handlers/lifespan.md +++ b/i18n/tr/pages/handlers/lifespan.md @@ -1,6 +1,6 @@ --- translation: - sections: [f3ca8ac5f90f2dfa, 85a1ef3588ba0736, 563346d4d5804933, 9e3528340d0bab53] + sections: [f3ca8ac5f90f2dfa, 48e478ef7bd688b1, 563346d4d5804933, 52ac6a7734d6f581] tool: 1 --- # Lifespan {#lifespan} @@ -46,7 +46,7 @@ Yeni bir şey yok. `ctx` bir **Context** parametresidir; bu yüzden SDK onu enje Modelin geçirebileceği tek argüman `genre`. Lifespan sunucunuzun kendi işidir. -`@mcp.resource()` ve `@mcp.prompt()` fonksiyonları da `ctx` parametresi alabilir; bir sonraki bölümün açıklayacağı bir nedenle bu parametre yalın `Context` olarak yazılır. `ctx`'in taşıdığı her şey **[Context nesnesi](context.md)** sayfasında. +`@mcp.resource()` ve `@mcp.prompt()` fonksiyonları da `ctx` parametresi alabilir. `ctx`'in taşıdığı her şey **[Context nesnesi](context.md)** sayfasında. ### Gerçekten türü belirli {#it-really-is-typed} @@ -56,19 +56,6 @@ Tür denetleyiciniz için `ctx.request_context.lifespan_context`'in bir `AppCont Bunun yerine yalın `Context` yazarsanız `lifespan_context`'in türü `dict[str, Any]` olur: tür denetleyicisinin, lifespan'inizin ne yield ettiğini bilmesinin yolu yoktur. Nesne çalışma zamanında yine oradadır; yalnızca yardımı kaybedersiniz. -!!! warning - `Context[AppContext]` **yalnızca araçlara özgü** bir yazımdır. Bunu bir `@mcp.resource()` ya da - `@mcp.prompt()` fonksiyonuna koyarsanız o işleyiciye yapılan her çağrı başarısız olur. İstemciye bir hata döner, - sunucu log'u da nedenini gösterir: - - ```text - Context is not available outside of a request - ``` - - Kaynaklarda ve prompt'larda yalın `ctx: Context` yazın. Lifespan'inizin yield ettiği nesne - çalışma zamanında yine `ctx.request_context.lifespan_context`'tir; vazgeçtiğiniz şey nesne değil, - tür parametresidir. - !!! tip Her zaman bir lifespan vardır. Siz bir tane geçirmezseniz SDK'nın varsayılanı boş bir `dict` yield eder; dolayısıyla `ctx.request_context.lifespan_context` `{}` olur, asla `None` değil. Yalın `Context`'in @@ -101,7 +88,7 @@ Sunucuyu yaşam döngüsüne kadar sadeleştirin: `Database`'e bir `connected` b * `yield`'den önceki kod başlatmadır. Sonrasındaki `finally` kapatmadır. * İstek başına değil, sunucunun tüm ömrü boyunca bir kez çalışır. * `yield` ettiğiniz şey her araçta, kaynakta ve prompt'ta `ctx.request_context.lifespan_context` olur. -* `ctx: Context[AppContext]` bu erişimi araçlarda tam tür bilgisiyle donatır. Kaynaklar ve prompt'lar yalın `Context` alır. +* `ctx: Context[AppContext]` bu erişimi tam tür bilgisiyle donatır. * `lifespan=` yoksa boş bir `dict` gelir, asla `None` değil. Çağrının ortasında durup kullanıcıya yalnızca onun bildiği bir şeyi soran işleyici, **[Elicitation](elicitation.md)** (kullanıcıdan bilgi isteme) sayfasının konusu. diff --git a/i18n/tr/pages/handlers/progress.md b/i18n/tr/pages/handlers/progress.md index 98d86bb943..435a4a81cb 100644 --- a/i18n/tr/pages/handlers/progress.md +++ b/i18n/tr/pages/handlers/progress.md @@ -1,6 +1,6 @@ --- translation: - sections: [5315262fe26b33e1, 9d8e98840f1b78f0, 52d6009a07e770ea, 8534d8dbb4053a70, 2966fac6fe697007] + sections: [5315262fe26b33e1, 9d8e98840f1b78f0, 52d6009a07e770ea, 8534d8dbb4053a70, 2e9aff14d3a882c0] tool: 1 --- # İlerleme {#progress} @@ -120,4 +120,4 @@ Callback `total=None` alır. İstemci yine de *etkinlik* gösterebilir ("şimdiy * Çağrıda callback yoksa `report_progress` hiçbir şey yapmaz. Koşulsuz bildirin. * Bilmediğinizde `total`'ı vermeyin; callback `None` alır. -İlerleme, çalışan bir aracın *kullanıcıya* gösterdiği şeydir. *Sizin* için, yani sunucuyu işleten kişi için yazdığı log satırları ise ayrı bir kanaldır: **[Log tutma](logging.md)**. +İlerleme, hâlâ bekleyen bir istemci içindir. İstemci beklemeyi bıraktığında aracınızın gördüğü şey ise **[İptal](cancellation.md)**. diff --git a/i18n/tr/pages/handlers/subscriptions.md b/i18n/tr/pages/handlers/subscriptions.md index 2336b92d8e..02a39e445c 100644 --- a/i18n/tr/pages/handlers/subscriptions.md +++ b/i18n/tr/pages/handlers/subscriptions.md @@ -1,6 +1,6 @@ --- translation: - sections: [60a9de8a0bdaa531, 317bbe7e4355cdcc, a61d660c8029e04a, 8f7e82fcb88df8a9, b165db51249ff8ed, 266f56fb798068a4, 7c0e57030b622139, df18d7c2417a9883] + sections: [60a9de8a0bdaa531, 6693607ea56d8bd6, a61d660c8029e04a, 8f7e82fcb88df8a9, b165db51249ff8ed, b8bc624a627ead9b, 2139e68e36d9e621, 7c0e57030b622139, 34ab1af2b9ab5b45] tool: 1 --- # Abonelikler {#subscriptions} @@ -22,7 +22,7 @@ Size düşen tek satır: değişikliği yayımlayın. * Kardeş metotlar `notify_prompts_changed()` ve `notify_resources_changed()`. * Abone yoksa iş de yok. Boştaki bir sunucuda yayımlamak hiçbir şey yapmaz; bu yüzden kimsenin dinleyip dinlemediğini asla kontrol etmezsiniz. Neyin değiştiğini bildirirsiniz, o kadar. -`MCPServer`, `subscriptions/listen`'ı sizin yerinize sunar. Protokol düzeyindeki yükümlülükler (ilk çerçeve olarak onay, akış başına filtreleme, her çerçevede abonelik kimliği) SDK'nın işidir. +`MCPServer`, [kapatmadığınız](#turning-it-off) sürece `subscriptions/listen`'ı sizin yerinize sunar. Protokol düzeyindeki yükümlülükler (ilk çerçeve olarak onay, akış başına filtreleme, her çerçevede abonelik kimliği) SDK'nın işidir. !!! check Ağ üzerinde, filtresinde `board://sprint` geçen bir akış `complete_task` çalıştıktan sonra şöyle görünür: @@ -79,7 +79,32 @@ Middleware sözleşmesinin tamamı, başka neleri sardığı ve neden geçici (p Yayımlar, işleyicinizden açık akışlara bir `SubscriptionBus` üzerinden gider. Varsayılanı bellek içidir: tek süreç, içindeki tüm akışlar. Bir yük dengeleyicinin arkasında replikalar çalıştırana kadar doğru yanıt budur; çünkü o noktada bir istemcinin akışı tek bir replikaya bağlı kalır ve başka bir replikadaki yayımın ona ulaşması gerekir. -Bu birleşim noktasını siz uygularsınız: pub/sub arka ucunuzun üzerinde iki metot. +Varsayılan veri yoluyla ulaşamaz, çünkü her replikanın kendi veri yolu vardır: + +```mermaid +flowchart LR + client[Client] --> lb[Load balancer] + lb --> stream + lb ~~~~ gap + lb --> tool + subgraph B [Replica B] + tool[tools/call] -- publishes --> busB[(bus B)] + end + gap[(no shared bus)] + subgraph A [Replica A] + stream[listen stream] -- subscribed --> busA[(bus A)] + end + style A fill:none + style B fill:none + style gap fill:none,stroke-dasharray:4 4 +``` + +Hiçbir şey hata vermez: çağrı başarılı olur, akış ise sessiz kalır. Bu yüzden bir yük dengeleyicinin arkasında ikisinden birini seçin: + +* **Değişiklik bildirimlerine ihtiyacınız var.** Aşağıdaki gibi her replikaya aynı veri yolunu verin. +* **İhtiyacınız yok.** [Bildirimleri kapatın](#turning-it-off); böylece hiçbir istemciye kaçıracağı olaylar vaat edilmez ve hiçbir istemci bunlar için bir akışı açık tutmaz. + +Paylaşılan veri yolunu siz uygularsınız: pub/sub arka ucunuzun üzerinde iki metot. ```python from collections.abc import Callable @@ -128,6 +153,20 @@ async def tools_reloaded() -> None: await bus.publish(ToolsListChanged()) # from a lifespan task, a webhook, anywhere ``` +## Abonelikleri kapatma {#turning-it-off} + +Kataloğu hiç değişmeyen bir sunucunun yayımlayacak bir şeyi yoktur. Bunu sunucuyu oluştururken belirtin: + +```python title="server.py" hl_lines="3" +--8<-- "docs_src/subscriptions/tutorial007.py" +``` + +* Bir `2026-07-28` istemcisi duyurulan hiçbir değişiklik bildirimi görmez; `subscriptions/listen` isteği de açık bir akış yerine *Method not found* hatasını alır. +* `ctx.notify_*` çalışmaya devam eder ve kimseye ulaşmaz; bu yüzden işleyicileriniz değişmez. +* Daha eski protokol sürümlerindeki istemciler hiçbir fark görmez. + +Açık bir akış, hiç bitmeyen bir istektir; bu yüzden bu ayar, istek süresine göre ücretlendiren bir barındırma hizmetinde de önemlidir. + ## Düşük düzeyli bileşim {#the-low-level-composition} Düşük düzeyli `Server`'da önceden bağlanmış hiçbir şey yoktur; aynı parçalar üç satırda bir araya gelir: @@ -148,5 +187,6 @@ Düşük düzeyli `Server`'da önceden bağlanmış hiçbir şey yoktur; aynı p * İstemci tarafı `async with client.listen(...)` bloğudur: ayrıntıları *İstemciler* altındaki **[Abonelikler](../client/subscriptions.md)** sayfasında. * Düşük düzeyli `Server`'da aynı parçaları kendiniz birleştirirsiniz: bir veri yolu, `ListenHandler(bus)`, `on_subscriptions_listen` yuvası. * Yatay ölçekleme, `SubscriptionBus`'ı (iki metot) uygulamak ve onu `MCPServer(subscriptions=...)` olarak geçirmek demektir. +* Yayımlayacak bir şey yoksa ya da replikaların paylaşılan bir veri yolu yoksa: `MCPServer(subscriptions=False)` hiçbir değişiklik bildirimi duyurmaz ve hiçbir akışı açık tutmaz. Tüm bunları sunan sunucuyu ister tek replikanın ister yirmisinin arkasında çalıştırma konusu **[Dağıtım ve ölçekleme](../run/deploy.md)** sayfasında. diff --git a/i18n/tr/pages/run/deploy.md b/i18n/tr/pages/run/deploy.md index 29a6eaf491..1f60542df6 100644 --- a/i18n/tr/pages/run/deploy.md +++ b/i18n/tr/pages/run/deploy.md @@ -1,6 +1,6 @@ --- translation: - sections: [28221886b198784f, f88ea1f1614f3a1d, 2e76f5cb9df15042, ce926d686730b6d0, 3be24f8ad8bb5ab9, 3fad24032b2224ff, f25a7f860e579ecb, 697b01d95080880d] + sections: [28221886b198784f, f88ea1f1614f3a1d, 2e76f5cb9df15042, ce926d686730b6d0, 3be24f8ad8bb5ab9, aaf489e944ecf5d1, f25a7f860e579ecb, 697b01d95080880d] tool: 1 --- # Dağıtım ve ölçekleme {#deploy-scale} @@ -174,6 +174,7 @@ Dağıtım (fan-out) tarafında hiçbir şey, bir akışın hangi sunucu nesnesi * Gerçek süreçler arasında **SDK size yardımcı olabilecek hiçbir bus sunmaz.** `SubscriptionBus`, kendi pub/sub altyapınız (Redis, NATS, zaten çalıştırdığınız her neyse) üzerinde gerçeklediğiniz ve `MCPServer(subscriptions=...)` olarak geçirdiğiniz iki metotlu bir `Protocol`'dür (`publish` ve `subscribe`). Taslak ve sözleşme **[Abonelikler](../handlers/subscriptions.md#scaling-past-one-process)** sayfasında. * Bus dört küçük tipli olay taşır, asla JSON-RPC taşımaz. Onaylama, filtreleme ve akış yaşam döngüsü SDK'da kalır; bu yüzden bus'ınız protokolü bozamaz, yalnızca olayları süreçler arasında taşıyabilir. * Akışlar devam ettirilebilir **değildir** ve olaylar yeniden **oynatılmaz**. Bir replikayı kaybetmek akışlarını düşürür; istemciler yeniden dinler ve yeniden getirir. Paylaşılacak bir olay deposu ve yapılandırılacak başka bir şey yoktur. Ölçeklemenin gerçekten yalnızca aynısının fazlası olduğu tek yer burası. +* Değişiklik bildirimlerine ihtiyaç duymayan bir sunucu bus'ı atlar: **[bildirimleri kapatın](../handlers/subscriptions.md#turning-it-off)**. ## SDK'nın size vermedikleri {#what-the-sdk-does-not-give-you} diff --git a/i18n/tr/pages/servers/structured-output.md b/i18n/tr/pages/servers/structured-output.md index e14d7e571c..37c126b67d 100644 --- a/i18n/tr/pages/servers/structured-output.md +++ b/i18n/tr/pages/servers/structured-output.md @@ -1,6 +1,6 @@ --- translation: - sections: [a838d57f003aed44, 857d03886a0137ed, 42d9efcb9f542867, 2290ff08435b5573, 91be9b73602abcf1, 6cdbad079f7b47f0, d4b607372fb28b51, 18dbf726ac45e0b7, c7eff2a5698225fa, c851964bb3301907, 8f296f1f09e4c400, d715db6f8dccc9cc, a0c344a48450dbe4] + sections: [a838d57f003aed44, 857d03886a0137ed, 42d9efcb9f542867, 2290ff08435b5573, 91be9b73602abcf1, 6cdbad079f7b47f0, d4b607372fb28b51, 7608fc5ebc31d6ea, c7eff2a5698225fa, c851964bb3301907, 8f296f1f09e4c400, d715db6f8dccc9cc, a0c344a48450dbe4] tool: 1 --- # Yapılandırılmış çıktı {#structured-output} @@ -174,6 +174,8 @@ result.structured_content # {"London": 16.2, "Reykjavik": 4.4} Anahtarlar `str` olmalıdır. Bir `dict[int, float]` JSON nesnesi olamaz, bu yüzden `{"result": ...}` sarmalayıcısına geri düşer. +Sözlük sonuçları, doğrulama ve serileştirme için Pydantic'in `TypeAdapter` sınıfını kullanır. Bir aracın `FuncMetadata.output_model` değerini incelerseniz, bu değer şema başlığıyla birlikte sözlük türü açıklamasını tutar. + ## Doğrulama {#validation} `output_schema` belgeleme değildir. Fonksiyonunuz ne döndürürse döndürsün, sunucudan çıkmadan önce **ona göre doğrulanır**. diff --git a/i18n/tr/pages/servers/tools.md b/i18n/tr/pages/servers/tools.md index 3ae9b8842d..27aa9c7a37 100644 --- a/i18n/tr/pages/servers/tools.md +++ b/i18n/tr/pages/servers/tools.md @@ -1,6 +1,6 @@ --- translation: - sections: [e4cc390d56573409, f30cf8103a6e918c, 2c97b9f888398951, 048e5471dfa71aea, 3076b1e16ad95950, edbedf2a16e71311, 3d8ef8da89fa87c1, f6c0e02e6ea5a363] + sections: [e4cc390d56573409, f30cf8103a6e918c, 2c97b9f888398951, 048e5471dfa71aea, 3076b1e16ad95950, c1115cd005b81e8f, 3d8ef8da89fa87c1, f6c0e02e6ea5a363] tool: 1 --- # Araçlar {#tools} @@ -141,7 +141,7 @@ Dilediğiniz gibi karıştırabilirsiniz: model parametrelerinin yanında sırad Bir araç G/Ç yapıyorsa (bir API çağırıyor, dosya okuyor, veritabanı sorguluyorsa) onu `async def` olarak bildirin ve içinde `await` kullanın. SDK onu await eder. -Sıradan bir `def` araç da çalışır: SDK onu bir iş parçacığında çalıştırır, böylece sunucuyu asla engellemez. +Sıradan bir `def` araç da çalışır: SDK onu bir iş parçacığında çalıştırır, böylece sunucuyu asla engellemez. Uzun süren bir araç, istemcinin hâlâ bekleyip beklemediğini kontrol edebilir; **[İptal](../handlers/cancellation.md)** sayfasına bakın. Yapılandırılacak başka bir şey yok. diff --git a/i18n/tr/pages/troubleshooting.md b/i18n/tr/pages/troubleshooting.md index 5933595a30..d17cd3f3a5 100644 --- a/i18n/tr/pages/troubleshooting.md +++ b/i18n/tr/pages/troubleshooting.md @@ -1,6 +1,6 @@ --- translation: - sections: [3d58228e81b99543, 170514ce901c4139, 17d61fad0a50d62b, 8a6e351ec756904d, 137454d469c867f5, 6392596bd6df54f0, 41126fa9c4fe432f, 480b6d7897e30ab4, d83bb682e708dde0, ebbed3449c499db4, 525cdf1755e29d4c, 30fd31be74169d9a, d2e88333d4f7841f, c2dc3b1007d2e987, d6eabf60cc366341, f798e815252852c2, 0cba47bae78d04eb, 2c218ba829abf74e] + sections: [3d58228e81b99543, 170514ce901c4139, 17d61fad0a50d62b, 8a6e351ec756904d, 137454d469c867f5, fcf984fa0615ed11, 6392596bd6df54f0, 41126fa9c4fe432f, 480b6d7897e30ab4, d83bb682e708dde0, ebbed3449c499db4, 525cdf1755e29d4c, 30fd31be74169d9a, d2e88333d4f7841f, c2dc3b1007d2e987, d6eabf60cc366341, f798e815252852c2, 0cba47bae78d04eb, cdc6d86a4dae8a34] tool: 1 --- # Sorun giderme {#troubleshooting} @@ -129,6 +129,14 @@ Parantezleri ekleyin. `@mcp.resource(...)` ve `@mcp.prompt()` de aynı sürçme traceback'i okuyun. Bir tür denetleyicisi de bunu yakalar: bir fonksiyon geçerli bir `name=` değildir. +## `InvalidSignature: Tool '' has an invalid x-mcp-header annotation: ` {#invalidsignature-tool-name-has-an-invalid-x-mcp-header-annotation-reason} + +Bir araç argümanı `x-mcp-header` ile spesifikasyonun izin vermediği bir biçimde işaretlenmiş; hangi kuralı çiğnediğini `` söyler. `2026-07-28` üzerindeki istemciler böyle bir aracı listelerinin dışında bırakırdı; bu yüzden SDK onu kaydetmeyi reddeder. + +Yalnızca `str`, `int` ve `bool` argümanlar işaretlenebilir ve `str | None` bunların hiçbiri değildir. İsteğe bağlı bir argümanın nasıl yazılacağı **[Başlık parametreleri](advanced/header-parameters.md)** sayfasında. + +Yukarıdaki girdi gibi bu da, herhangi bir istemci bağlanmadan önce, modül **içe aktarıldığında** fırlatılır. + ## `Tool already exists: ` {#tool-already-exists-name} İki kayıt aynı araç adını kullandı. **İlki** kazanır, ikincisi sessizce düşürülür ve *sunucu log'undaki* bu uyarı tek işarettir: @@ -426,6 +434,7 @@ mcp = MCPServer("Weather", request_state_security=RequestStateSecurity(keys=[key * `ExceptionGroup: unhandled errors in a TaskGroup` hiçbir zaman asıl hata değildir. **Son satırı** okuyun; `MCPError`'ı `async with Client(...)` bloğunun *içinde* yakalamak sarmalamayı tamamen atlar. * `call_tool` başarısız olan bir araç için istisna fırlatmaz. `Error executing tool ...` ve `Unknown tool: ...` birer sonuçtur: `result.is_error`'ı kontrol edin. Araç adından sonra mesaj yoksa araç çökmüş demektir ve traceback sunucu log'undadır. * `Client must be used within an async context manager` -> `async with` kullanın. `Use @tool() instead of @tool` -> parantezleri ekleyin. +* `has an invalid x-mcp-header annotation` -> yalnızca `str`, `int` ve `bool` argümanlar işaretlenebilir. * 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. diff --git a/i18n/tr/pages/whats-new.md b/i18n/tr/pages/whats-new.md index 0429738618..10e4bab9bd 100644 --- a/i18n/tr/pages/whats-new.md +++ b/i18n/tr/pages/whats-new.md @@ -1,6 +1,6 @@ --- translation: - sections: [cfe01c0c5863dfa2, 0dbb68d8b210177b, 80cf193023af3ed4, 875eb2889263424e] + sections: [cfe01c0c5863dfa2, 0dbb68d8b210177b, bd42ee3a268f6ea4, 875eb2889263424e] tool: 1 --- # v2'deki yenilikler {#whats-new-in-v2} @@ -204,7 +204,7 @@ Yayımlama ve sunma **[Abonelikler](handlers/subscriptions.md)** sayfasında, iz ### Geri kalanlar, kısaca {#the-rest-quickly} * **Kimlik isteğe bağlı, mesaj başına bir üstveridir.** İstek tarafındaki `clientInfo` `_meta` anahtarı isteğe bağlıdır (zorunlu ikili `protocolVersion` + `clientCapabilities`) ve `serverInfo`, `server/discover` sonuç gövdesinden çıktı: sunucular artık onu 2026 neslinden her sonucun `_meta`'sına damgalar ([spec #3002](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/3002)). SDK her zaman damgalar; bir sunucu kendini tanıtmadığında (örneğin bir middleware anahtarı çıkardığında) `client.server_info` `None` olur. Damganın iletilen verideki hâlini **[Düşük düzey Server](advanced/low-level-server.md)** gösterir. -* **İstekler gövde ayrıştırılmadan yönlendirilebilir.** Modern HTTP istekleri `Mcp-Method` taşır (ve araç benzeri üç çağrı için `Mcp-Name`); `x-mcp-header` ile işaretlenmiş bir araç girdi şeması özelliği bir `Mcp-Param-*` başlığına yansıtılır ve sunucu bunu çapraz denetler ([SEP-2243](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2243)). Ağ geçitleri ve hız sınırlayıcılar yalnızca başlıklara bakarak yönlendirebilir; kurallar **[Geçiş kılavuzu](migration.md#servers-validate-mcp-param-headers-against-the-request-body-sep-2243)** sayfasında. +* **İstekler gövde ayrıştırılmadan yönlendirilebilir.** Modern HTTP istekleri `Mcp-Method` taşır (ve araç benzeri üç çağrı için `Mcp-Name`); `x-mcp-header` ile işaretlenmiş bir araç girdi şeması özelliği bir `Mcp-Param-*` başlığına yansıtılır ve sunucu bunu çapraz denetler ([SEP-2243](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2243)). Ağ geçitleri ve hız sınırlayıcılar yalnızca başlıklara bakarak yönlendirebilir. Bir argümanın nasıl işaretleneceğini **[Başlık parametreleri](advanced/header-parameters.md)** gösterir. * **Sonuçlar önbellek ipuçları taşır.** Listeleme ve okuma sonuçları `ttlMs` ve `cacheScope` bildirir ([SEP-2549](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2549)); bunları metot başına `cache_hints=` ile ayarlarsınız, `Client` da yerleşik bir yanıt önbelleğiyle onlara uyar. Hiç ipucu göndermeyen bir sunucu (2026 öncesi her sunucu) birebir aynı, önbelleksiz trafik görür. **[Önbellek ipuçları](client/caching.md)**. * **Uzantılar birinci sınıf.** Sunucular ve istemciler ters DNS tanımlayıcıları altında isteğe bağlı yetenek paketleri bildirir ([SEP-2133](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2133)); yerleşik `Apps` uzantısı (MCP Apps) referans örnektir. **[Uzantılar](advanced/extensions.md)** ve **[MCP Apps](advanced/apps.md)**. * **Hata kodları standartlaştı.** Eksik bir kaynak, URI `error.data` içinde olmak üzere `-32602`'dir; spesifikasyonun ayırdığı yeni kodlar da `-32020` (başlık uyuşmazlığı), `-32021` (gerekli yetenek eksik) ve `-32022` (desteklenmeyen protokol sürümü) olarak görünür. **[Sorun giderme](troubleshooting.md)** tam mesaj metinlerine göre düzenlenmiştir. diff --git a/i18n/uk/pages/advanced/header-parameters.md b/i18n/uk/pages/advanced/header-parameters.md new file mode 100644 index 0000000000..b32ae7afbe --- /dev/null +++ b/i18n/uk/pages/advanced/header-parameters.md @@ -0,0 +1,65 @@ +--- +translation: + sections: [81862a209b483d27, b76e8073487afa03, bc214b2fc2bcdae4, d5835477c0b60163, a5d9786f902ad8e1] + tool: 1 +--- +# Параметри в заголовках {#header-parameters} + +Більшості серверів це ніколи не знадобиться. + +Шлюз або балансувальник навантаження перед сервером може маршрутизувати запити лише за тим, що здатен прочитати, не розбираючи тіло. Позначте аргумент інструмента ключем `x-mcp-header`, і клієнти на **[версії протоколу](../protocol-versions.md)** `2026-07-28` надсилатимуть його значення ще й як HTTP-заголовок. + +## Позначення аргументу {#mark-an-argument} + +Позначка — це один додатковий ключ у JSON-схемі аргументу. У `MCPServer` його туди додає `Field`: + +```python title="server.py" hl_lines="13" +--8<-- "docs_src/header_parameters/tutorial001.py" +``` + +* Через Streamable HTTP на версії `2026-07-28` клієнт надсилає заголовок `Mcp-Param-Region` разом із тілом, а сервер відхиляє виклик, у якому вони розходяться. +* Клієнт, який не отримував цей інструмент у списку, позначки ніколи не бачив: заголовка він не надсилає, і такий виклик буде відхилено. Клас `Client` із цього SDK після цього отримує список інструментів і один раз надсилає виклик повторно, тож якщо спершу отримати список, це лише заощадить один раунд обміну. +* Усі інші з'єднання ігнорують цю анотацію. + +Сама функція не змінюється: `region`, як і раніше, надходить як аргумент. + +## Що можна позначати {#what-can-be-marked} + +Аргументи типів `str`, `int` і `bool`. Для всього іншого реєстрація інструмента завершується винятком `InvalidSignature`. + +Це стосується й `str | None`, що не має єдиного типу. Для необов'язкового аргументу схему потрібно прописати явно — за допомогою `WithJsonSchema` з Pydantic: + +```python +region: Annotated[str | None, WithJsonSchema({"type": "string", "x-mcp-header": "Region"})] = None +``` + +## У низькорівневому класі `Server` {#on-the-low-level-server} + +Там `input_schema` ви пишете вручну, тож ключ просто вписуєте в схему: + +```python title="server.py" hl_lines="18" +--8<-- "docs_src/header_parameters/tutorial002.py" +``` + +* Анотацію за вас ніхто не перевіряє: некоректну сервер теж віддає, а клієнти на `2026-07-28` не включають такий інструмент до свого списку. + +### Схеми за назвою {#schemas-by-name} + +Щоб перевірити заголовок, SDK потребує вхідної схеми інструмента ще до того, як передасть виклик на виконання. Без `get_tool_input_schema` SDK отримує її, запускаючи обробник `on_list_tools` під час кожного виклику з аргументами, незалежно від того, чи позначено хоч один інструмент. + +```python title="server.py" hl_lines="26 39-41 48" +--8<-- "docs_src/header_parameters/tutorial003.py" +``` + +* Передайте функцію, щоб відповідати з того, що вже маєте. +* Для інструмента, в якому нічого перевіряти, поверніть `None`. + +## Підсумки {#recap} + +* Ключ `x-mcp-header` на аргументі інструмента змушує клієнтів на `2026-07-28` дублювати цей аргумент у HTTP-заголовку `Mcp-Param-*`. +* Сервер відхиляє виклик, у якому заголовок і тіло розходяться. +* Позначати можна лише аргументи типів `str`, `int` і `bool`. Для всього іншого `MCPServer` викидає виняток `InvalidSignature`. +* Низькорівневий клас `Server` нічого не перевіряє, а клієнти відкидають інструмент із некоректною анотацією. +* Завдяки `get_tool_input_schema` низькорівневий клас `Server` не запускає `on_list_tools` під час кожного виклику. + +Решту API класу `Server`, де все пишеться вручну, описано на сторінці **[Низькорівневий Server](low-level-server.md)**. diff --git a/i18n/uk/pages/advanced/index.md b/i18n/uk/pages/advanced/index.md index d2d78e6174..5db0f1fe5c 100644 --- a/i18n/uk/pages/advanced/index.md +++ b/i18n/uk/pages/advanced/index.md @@ -1,6 +1,6 @@ --- translation: - sections: [ca6988b7503cd2d3] + sections: [348f8697c6b12cd0] tool: 1 --- # Розширені можливості {#advanced} @@ -14,6 +14,8 @@ translation: JSON-RPC-методи. * **[Пагінація](pagination.md)** і **[Middleware](middleware.md)**: дві речі, які можна зробити *лише* на низькорівневому `Server`. +* **[Параметри заголовків](header-parameters.md)**: дають шлюзу змогу маршрутизувати виклик + інструмента за одним із його аргументів. * **[Розширення](extensions.md)** і **[Застосунки MCP](apps.md)**: поверхня розширень протоколу. Скомпонуйте пакети розширень у сервер або напишіть власні. diff --git a/i18n/uk/pages/advanced/low-level-server.md b/i18n/uk/pages/advanced/low-level-server.md index 2e9859f886..7cf71bb9f3 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, 9d5d10a5f0405d0a, 1086e77ce561cd7f, a3f71823df5efc31, 9fc7109f72201cae, d50fe7faead8cf68, 7bf25983df655b66, 6330e1f4c6029683, 2f1749c8c133fa1c, 8db7116fc8ddd0ee, ebc33704fbd74262, 0fde3bcea081ba3a] + sections: [2c79b6338e09b7ac, 9d5d10a5f0405d0a, 1086e77ce561cd7f, a3f71823df5efc31, 9fc7109f72201cae, d50fe7faead8cf68, 7bf25983df655b66, 6330e1f4c6029683, 2f1749c8c133fa1c, 8db7116fc8ddd0ee, 2090d99b355bc2c7, 0fde3bcea081ba3a] tool: 1 --- # Низькорівневий Server {#the-low-level-server} @@ -209,6 +209,7 @@ use Server.middleware to observe or wrap initialization * `on_call_tool`, `on_get_prompt` і `on_read_resource` можуть повернути `InputRequiredResult` замість звичайного результату, щоб призупинити виклик і попросити клієнта про введення; див. **[Багатораундові запити](../handlers/multi-round-trip.md)** (multi-round-trip). Як і годиться цьому рівню, нічого не встановлюється за вас: якщо `MCPServer` за замовчуванням запечатує `requestState`, то тут заданий вами `request_state` передається мережею точно так, як написано, доки ви не ввімкнете захист через `server.middleware.append(RequestStateBoundary(RequestStateSecurity(keys=[...]), default_audience=server.name))`: один рядок (обидва імені імпортуються з `mcp.server.request_state`) — і отримуєте те саме запечатування й перевірку, що їх виконує `MCPServer` (**[Захист `requestState`](../handlers/multi-round-trip.md#protecting-requeststate)**). * `on_list_resources`, `on_read_resource`, `on_list_prompts`, `on_get_prompt`, `on_completion` — та сама форма `(ctx, params) -> result` для інших примітивів. * `on_subscriptions_listen` обслуговує потік `subscriptions/listen` версії 2026-07-28. Передайте `ListenHandler`, побудований поверх `SubscriptionBus`, і публікуйте події в шину з інших обробників; повну композицію див. на сторінці **[Підписки](../handlers/subscriptions.md)**. +* `get_tool_input_schema` прибирає `on_list_tools` зі шляху виклику; див. **[Параметри в заголовках](header-parameters.md#schemas-by-name)**. * `server.streamable_http_app()` повертає той самий Starlette-застосунок, що й у `MCPServer`; розгортайте його так, як **[Запуск сервера](../run/index.md)** розгортає будь-який інший ASGI-застосунок. `server.run(transport=...)` тут немає: `server.run(read_stream, write_stream, server.create_initialization_options())` веде одне з'єднання через пару потоків, і цей один рядок — оце й усе. ## Підсумки {#recap} diff --git a/i18n/uk/pages/advanced/middleware.md b/i18n/uk/pages/advanced/middleware.md index b53eeb6f73..ab1dac90c1 100644 --- a/i18n/uk/pages/advanced/middleware.md +++ b/i18n/uk/pages/advanced/middleware.md @@ -1,6 +1,6 @@ --- translation: - sections: [6048b4f308edbb8c, 46056f318ef205e4, c3e565b61acd75c5, c62422b159c6ed09, 420968f514138f43] + sections: [58e1103d9a323ccf, 46056f318ef205e4, 812b414557fb0c35, 4df162eea2518d38, c62422b159c6ed09, 420968f514138f43] tool: 1 --- # Middleware {#middleware} @@ -17,7 +17,7 @@ translation: `MCPServer` приймає список у конструкторі (`MCPServer(name, middleware=[...])`) і надає доступ до нього як `mcp.middleware`; низькорівневий `Server` надає той самий список як -`server.middleware`. Приклад нижче використовує низькорівневий `Server`; якщо +`server.middleware`. Приклади нижче використовують низькорівневий `Server`; якщо `Server(name, on_call_tool=...)` вам незнайомий, спершу прочитайте сторінку **[Низькорівневий Server](low-level-server.md)**. @@ -63,14 +63,36 @@ tools/call took 0.1 ms * Навіть метод, для якого сервер не має обробника: `call_next` викидає `MCPError(-32601, "Method not found")` *крізь* ваш middleware на шляху до клієнта. +## Обмеження одночасних викликів {#a-concurrency-cap} + +Middleware не мусить викликати `call_next(ctx)`. Викиньте натомість `MCPError` — і саме це +повідомлення буде **відхилено**: з'єднання лишається живим, а наступне повідомлення проходить. + +Припустімо, кожен пошук займає з'єднання з пулу на чотири. Цей middleware дає чотирьом викликам +інструментів виконуватися одночасно, а п'ятий відхиляє: + +```python title="server.py" hl_lines="15-16 40-55 59" +--8<-- "docs_src/middleware/tutorial002.py" +``` + +* Рахується лише `tools/call`, тож сервер і далі відповідає на `server/discover` і `tools/list`, + поки відхиляє виклики інструментів. +* MCP не визначає коду помилки «сервер зайнятий», тож `SERVER_BUSY` — власний код цього сервера. +* Відмова одразу повідомляє клієнтові, що сервер перевантажено. Якщо краще, щоб викликачі + чекали, натомість утримуйте `anyio.CapacityLimiter` довкола `call_next(ctx)`. + +Викинутий `MCPError` потрапляє до клієнтського застосунку, а не до моделі. Якщо повідомлення має +прочитати модель, поверніть натомість результат інструмента з `is_error=True`: це пункт +**Відповідати** нижче. + ## Що можна робити всередині {#what-you-can-do-inside-one} У порядку зростання того, наскільки варто вагатися: -* **Спостерігати.** Заміряти час, рахувати, логувати. Приклад вище. +* **Спостерігати.** Заміряти час, рахувати, логувати. Middleware для вимірювання часу вище. * **Відхиляти.** Викиньте `MCPError` *замість* виклику `call_next(ctx)` — і саме на це повідомлення клієнт отримає помилку JSON-RPC. З'єднання лишається живим; наступне - повідомлення проходить. Саме так сервер обмежує `subscriptions/listen` для окремих + повідомлення проходить. Обмеження одночасних викликів вище. Так само сервер обмежує `subscriptions/listen` для окремих викликачів: розділ **[Хто має право стежити](../handlers/subscriptions.md#deciding-who-may-watch)** на сторінці про підписки показує це крок за кроком. * **Переписувати.** `ctx` — це dataclass: `await call_next(dataclasses.replace(ctx, params=...))` diff --git a/i18n/uk/pages/client/identity-assertion.md b/i18n/uk/pages/client/identity-assertion.md index 1820d1e610..ef52cfa307 100644 --- a/i18n/uk/pages/client/identity-assertion.md +++ b/i18n/uk/pages/client/identity-assertion.md @@ -1,6 +1,6 @@ --- translation: - sections: [a91322c46111d16d, 8e6fd6d6f59bb568, e7828fd2729b2c9d, a03ec26bfc678b65, 1034c653c0bcf1b0] + sections: [a91322c46111d16d, 8e6fd6d6f59bb568, 7cf38181f6c99fd5, 37804d4fb36d6302, 1034c653c0bcf1b0] tool: 1 --- # Твердження про ідентичність {#identity-assertion} @@ -64,7 +64,7 @@ translation: ### Конфіденційний клієнт {#a-confidential-client} -`client_secret` обов'язковий; без нього конструктор викидає `ValueError`. Профіль IETF, на якому ґрунтується [SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990), відводить цей грант лише конфіденційним клієнтам, SEP-990 вимагає, щоб клієнт автентифікувався, а цей SDK забезпечує і те, і те, наполягаючи на спільному секреті. `token_endpoint_auth_method` визначає, де він передається: `client_secret_post` (за замовчуванням, у тілі форми) або `client_secret_basic` (заголовок HTTP Basic). Профіль також дозволяє `private_key_jwt`; цей провайдер його не підтримує. +`client_secret` обов'язковий; без нього конструктор викидає `ValueError`. Профіль IETF, на якому ґрунтується [SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990), рекомендує цей грант лише для конфіденційних клієнтів, а [RFC 7521](https://datatracker.ietf.org/doc/html/rfc7521) залишає цю політику на розсуд сервера авторизації. Цей SDK з обох боків обирає консервативне прочитання: вбудований сервер авторизації відмовляє клієнту, який не має спільного секрету, а цей провайдер наполягає на ньому. `token_endpoint_auth_method` визначає, де він передається: `client_secret_post` (за замовчуванням, у тілі форми) або `client_secret_basic` (заголовок HTTP Basic). Профіль також дозволяє `private_key_jwt`; цей провайдер його не підтримує. !!! tip Читайте `client_secret` із середовища або менеджера секретів, ніколи — із системи керування версіями. @@ -90,7 +90,8 @@ SDK також може *бути* сервером авторизації: `cre ``` * `identity_assertion_enabled=True` керує всім. Якщо вимкнено (а це за замовчуванням), `/token` відповідає на цей грант `unsupported_grant_type`, навіть якщо хук реалізовано, а метадані про нього не згадують. Якщо ввімкнено, метадані отримують тип гранту `jwt-bearer` і вказують `urn:ietf:params:oauth:grant-profile:id-jag` у `authorization_grant_profiles_supported` — полі, яким розширення оголошує підтримку. (Клієнт цього SDK його ніколи не читає: він налаштований на одного емітента й просто запитує.) -* **`exchange_identity_assertion`** — це хук. До його запуску SDK вже автентифікував клієнта, відмовив публічним клієнтам і відмовив клієнтам, чия реєстрація не містить цього гранту. Повертається `IdentityAssertionParams` (сире `assertion`, запитані `scopes` і `resource`), а ви повертаєте звичайний `OAuthToken`. +* **`exchange_identity_assertion`** — це хук. До його запуску SDK вже автентифікував клієнта, відмовив публічним клієнтам і відмовив клієнтам, чия реєстрація не містить цього гранту. Надходить `IdentityAssertionParams` (сире `assertion`, запитані `scopes` і `resource`), а ви повертаєте звичайний `OAuthToken`. +* Відмова публічним клієнтам — це політика SDK, а не вимога специфікації. Вбудований сервер автентифікує клієнтів лише за спільним секретом: він не підтримує `private_key_jwt` і поки що не розв'язує документи Client ID Metadata Document ([#1801](https://github.com/modelcontextprotocol/python-sdk/issues/1801)), тож клієнт, ідентифікований таким документом, не може тут скористатися цим грантом. Розгортання, якому потрібна інша політика, може замінити маршрут `/token`, який повертає `create_auth_routes`, власним. * Динамічна реєстрація клієнтів відхиляє цей грант безумовно, тож `get_client` тут обслуговує клієнта, заведеного вручну. ID-JAG-клієнт не може зареєструвати сам себе з нічого. * Половина класу — відмови. `OAuthAuthorizationServerProvider` — це *весь* сервер авторизації, тож він вимагає й потоку з кодом авторизації; сервер, який також виконує вхід користувачів, реалізує його по-справжньому, а в цього рівно одні двері. diff --git a/i18n/uk/pages/client/transports.md b/i18n/uk/pages/client/transports.md index 17aab3a8a3..3919d44b74 100644 --- a/i18n/uk/pages/client/transports.md +++ b/i18n/uk/pages/client/transports.md @@ -1,6 +1,6 @@ --- translation: - sections: [9cac816674181eb0, 7c157764133fea1f, 40b4916d82eaf1d4, 10d151f2cc75317f, 3d0832f39b0d7059, 92742ba36533633d, 0aeca6145e7bd302] + sections: [9cac816674181eb0, 5619e950d206e6c8, 40b4916d82eaf1d4, 10d151f2cc75317f, 3d0832f39b0d7059, 92742ba36533633d, 991c10e47fda2636] tool: 1 --- # Транспорти клієнта {#client-transports} @@ -44,6 +44,8 @@ translation: * `httpx2.AsyncClient` належить вам, тож саме **ви** входите в нього й виходите з нього. SDK ніколи не закриває клієнт, якого не створював. * `streamable_http_client(url, http_client=...)` повертає транспорт, а `Client(transport)` приймає його, як і будь-що інше. +Залиште `timeout=`. Це той самий тайм-аут, який використовує власний клієнт SDK (30 секунд, 300 на читання); `httpx2.AsyncClient`, створений без нього, отримує типовий для `httpx2` 5-секундний тайм-аут, і виклик інструмента, що триває довше, завершується помилкою тайм-ауту читання. + Одне зауваження щодо TLS: `httpx2` перевіряє сертифікати за сховищем довіри операційної системи (через [`truststore`](https://pypi.org/project/truststore/)), а не за вбудованим списком CA. У середовищі без придатного системного сховища CA (деякі мінімальні контейнери) задайте стандартні змінні середовища `SSL_CERT_FILE`/`SSL_CERT_DIR` @@ -51,16 +53,32 @@ translation: (подробиці — у розділі [`httpx` і `httpx-sse` замінено на `httpx2`](../migration.md#httpx-and-httpx-sse-replaced-by-httpx2)). +### Більші SSE-події {#larger-sse-events} + +Передайте `max_sse_event_size`, якщо сервер надсилає великий результат інструмента чи сповіщення в одній SSE-події: + +```python title="client.py" hl_lines="6-9" +--8<-- "docs_src/client_transports/tutorial005.py" +``` + +За замовчуванням ліміт становить 1 МіБ на подію й вимірюється в байтах до розбору події. Ліміт діє для +POST-відповідей, GET-потоку й відновлених потоків. Завелика подія в POST-відповіді чи відновленому +потоці завершує цей запит помилкою SSE. У фоновому GET-потоці клієнт записує +помилку в лог і пробує відкрити потік знову. Задайте `max_sse_event_size=None`, щоб вимкнути обмеження, якщо довіряєте +серверу й потрібні більші події. JSON-відповідей це не стосується. Якщо ви використовуєте `ClientSessionGroup`, задайте +той самий параметр у `StreamableHttpParameters`. + !!! warning Раніше `streamable_http_client` приймав `headers=` і `timeout=` напряму. Більше ні: - його єдині параметри — `url`, `http_client` і `terminate_on_close`. Напишете `headers=` + його параметри — `url`, `http_client`, `terminate_on_close` і `max_sse_event_size`. Напишете `headers=` за звичкою — і отримаєте: ```text TypeError: streamable_http_client() got an unexpected keyword argument 'headers' ``` - Усе, що стосується HTTP, тепер живе на тому єдиному `httpx2.AsyncClient`, який ви передаєте. + Заголовки, автентифікація, проксі й тайм-аути живуть на тому єдиному `httpx2.AsyncClient`, який ви передаєте. + `max_sse_event_size` натомість застосовується до зчитувачів SSE самого MCP-транспорту. !!! info `httpx2` зберігає знайомий API `httpx`, тож якщо ви знаєте `httpx`, то вже вмієте робити тут автентифікацію, @@ -137,6 +155,7 @@ stderr дочірнього процесу йде у ваш. Щоб спряму * `Client("http://.../mcp")` (URL) під'єднується через Streamable HTTP, продакшен-транспорт. * Заголовки, автентифікація, проксі й тайм-аути належать `httpx2.AsyncClient`, який ви передаєте в `streamable_http_client(url, http_client=...)`. Іменованого аргументу `headers=` немає. +* Щоб змінити ліміт у байтах для кожної SSE-події, використовуйте `streamable_http_client(url, max_sse_event_size=...)`. * Перенаправлення виконуються лише в межах власного origin цього URL (`307`/`308` із кінцевою скісною рискою), плюс `http`→`https` на тому самому хості. Усе інше завершується помилкою `Redirect to … not followed`; пропишіть у конфігурації кінцевий URL. * stdio — це `Client(StdioServerParameters(...))`. Загортайте його в `stdio_client(...)` самостійно лише для того, щоб перенаправити stderr дочірнього процесу. * Підпроцес отримує середовище зі списку дозволених, а не ваше; `env=` його доповнює. diff --git a/i18n/uk/pages/handlers/cancellation.md b/i18n/uk/pages/handlers/cancellation.md new file mode 100644 index 0000000000..3993d5e1db --- /dev/null +++ b/i18n/uk/pages/handlers/cancellation.md @@ -0,0 +1,60 @@ +--- +translation: + sections: [07968345fdc0b84e, 4ea8416db9efa0dc, 336a7b4c5d0a4578, 18392e805dde6717, c30d50df43f9b55c] + tool: 1 +--- +# Скасування {#cancellation} + +Клієнт може відмовитися від виклику: користувач натиснув кнопку зупинки або сплив тайм-аут. + +Тоді SDK **скасовує ваш обробник**. Вираз `await`, на якому він чекає, викидає виняток, функція згортається, і ніщо з того, що вона повертає, не надсилається. Більшості обробників нічого з цим робити не треба. + +А от двом видам треба: обробнику, якому є що прибрати за собою, і обробнику, оголошеному як звичайний `def`. + +## Очищення в інструменті з `async def` {#clean-up-in-an-async-def-tool} + +Помістіть очищення в блок `finally`: + +```python title="server.py" hl_lines="23 26-28" +--8<-- "docs_src/cancellation/tutorial001.py" +``` + +* Блок `finally` виконується, хоч би як завершився інструмент: повернув результат, викинув виняток чи був скасований. +* Очищенню, якому потрібен `await`, необхідний `shield=True`. У скасованому обробнику кожен наступний `await` теж викидає виняток, тож без цього захисту функція `release_hold` зупинилася б на першому ж рядку. +* Захищений блок ніщо не може скасувати, тому обмежте його в часі. Тут це `5` секунд. + +!!! tip + Використовуйте `finally`, а не `except`. Після очищення скасування має поширюватися далі вгору, + і `finally` це дозволяє. + +## Дострокова зупинка в інструменті зі звичайним `def` {#stop-early-in-a-plain-def-tool} + +Інструмент зі звичайним `def` виконується в потоці, а перервати потік ззовні неможливо. Інструмент має запитати сам: + +```python title="server.py" hl_lines="22 25-26" +--8<-- "docs_src/cancellation/tutorial002.py" +``` + +* Функція `anyio.from_thread.check_cancelled()` нічого не робить, поки виклик активний, і викидає виняток, щойно його скасовано. Викликайте її між порціями роботи. +* Очищення і тут розміщують у `finally`. У потоці немає асинхронних очікувань, тож захист не потрібен. +* Інструмент `def`, який жодного разу не запитує, виконується до кінця, а його результат відкидається. + +## Де це діє {#where-it-applies} + +Функції промптів і ресурсів скасовуються так само, як інструменти. + +Через stdio і Streamable HTTP це працює однаково. Для класу `Client` із цього SDK відмовитися від виклику означає скасувати завдання, яке очікує на `call_tool`, або дати спливти його `read_timeout_seconds`. + +!!! warning + З двома параметрами Streamable HTTP обробник про скасування не дізнається: `json_response=True` + на з'єднанні `2026-07-28` і `stateless_http=True` на з'єднанні старого покоління. Там обробник + виконується до кінця, хоч би що зробив клієнт. + +## Підсумки {#recap} + +* Коли клієнт відмовляється від виклику, SDK скасовує обробник: інструмент, промпт чи ресурс. +* `async def`: виконуйте очищення у `finally`, а очищення, якому потрібне асинхронне очікування, помістіть усередину `anyio.move_on_after(seconds, shield=True)`. +* Звичайний `def`: викликайте `anyio.from_thread.check_cancelled()` між порціями роботи, інакше інструмент виконається до кінця. Для очищення достатньо звичайного `finally`. +* `json_response=True` (сучасні з'єднання) і `stateless_http=True` (з'єднання старого покоління) вимикають скасування. + +Перебіг виконання і скасування стосуються інструмента, що виконується, і *того, хто його викликав*. Рядки, які він записує в лог для *вас*, людини, яка керує сервером, — це інший канал: **[Логування](logging.md)**. diff --git a/i18n/uk/pages/handlers/index.md b/i18n/uk/pages/handlers/index.md index de4ba53fbf..5d728a0fca 100644 --- a/i18n/uk/pages/handlers/index.md +++ b/i18n/uk/pages/handlers/index.md @@ -1,6 +1,6 @@ --- translation: - sections: [424930166c4bc6f3] + sections: [22ca41e50cc1b536] tool: 1 --- # Усередині обробника {#inside-your-handler} @@ -28,6 +28,8 @@ translation: простору — **[Семплювання та кореневі каталоги](sampling-and-roots.md)** (sampling і roots); ці можливості застарілі, але досі обслуговуються. * Звітувати про **[Перебіг виконання](progress.md)** повільної операції. +* Прибрати за собою або зупинитися достроково, коли клієнт відмовляється від + виклику, — **[Скасування](cancellation.md)**. * Писати логи (у стандартний потік помилок, для того, хто експлуатує сервер) — **[Логування](logging.md)**. * Повідомляти підписаним клієнтам, що щось змінилося, — diff --git a/i18n/uk/pages/handlers/lifespan.md b/i18n/uk/pages/handlers/lifespan.md index 33d38a46cd..0bd007d354 100644 --- a/i18n/uk/pages/handlers/lifespan.md +++ b/i18n/uk/pages/handlers/lifespan.md @@ -1,6 +1,6 @@ --- translation: - sections: [f3ca8ac5f90f2dfa, 85a1ef3588ba0736, 563346d4d5804933, 9e3528340d0bab53] + sections: [f3ca8ac5f90f2dfa, 48e478ef7bd688b1, 563346d4d5804933, 52ac6a7734d6f581] tool: 1 --- # Життєвий цикл {#lifespan} @@ -46,7 +46,7 @@ translation: `genre` — єдиний аргумент, який модель може передати. Життєвий цикл — це справа самого сервера. -Функції `@mcp.resource()` і `@mcp.prompt()` теж можуть приймати параметр `ctx`, записаний як простий `Context` — з причини, до якої дійде наступний розділ. Усе, що несе в собі `ctx`, описано на сторінці **[Об'єкт Context](context.md)**. +Функції `@mcp.resource()` і `@mcp.prompt()` теж можуть приймати параметр `ctx`. Усе, що несе в собі `ctx`, описано на сторінці **[Об'єкт Context](context.md)**. ### Він справді типізований {#it-really-is-typed} @@ -56,19 +56,6 @@ translation: Напишіть натомість простий `Context` — і `lifespan_context` отримає тип `dict[str, Any]`: засіб перевірки типів ніяк не може дізнатися, що віддав ваш життєвий цикл. Під час виконання об'єкт нікуди не зникає; ви лише втрачаєте допомогу. -!!! warning - `Context[AppContext]` — запис **лише для інструментів**. Поставте його на функцію `@mcp.resource()` чи - `@mcp.prompt()` — і кожен виклик цього обробника завершиться помилкою. Клієнт отримає помилку у відповідь, - а лог сервера покаже причину: - - ```text - Context is not available outside of a request - ``` - - У ресурсах і промптах пишіть простий `ctx: Context`. Об'єкт, який віддав ваш життєвий цикл, - під час виконання так само доступний як `ctx.request_context.lifespan_context`; ви відмовляєтеся від параметра типу, а не - від об'єкта. - !!! tip Життєвий цикл є завжди. Якщо його не передати, типовий варіант від SDK віддає порожній `dict`, тож `ctx.request_context.lifespan_context` дорівнює `{}` і ніколи не буває `None`. Саме через цей типовий варіант @@ -101,7 +88,7 @@ translation: * Код до `yield` — це запуск. Блок `finally` після нього — зупинка. * Він виконується один раз, навколо всього життя сервера, а не на кожен запит. * Усе, що ви віддаєте через `yield`, стає `ctx.request_context.lifespan_context` у кожному інструменті, ресурсі та промпті. -* `ctx: Context[AppContext]` робить цей доступ повністю типізованим в інструментах. Ресурси й промпти приймають простий `Context`. +* `ctx: Context[AppContext]` робить цей доступ повністю типізованим. * Без `lifespan=` буде порожній `dict`, і ніколи не `None`. Обробник, що зупиняється посеред виклику, аби запитати в користувача щось відоме лише йому, — це **[Еліцитація](elicitation.md)**. diff --git a/i18n/uk/pages/handlers/progress.md b/i18n/uk/pages/handlers/progress.md index 7e4a22db55..df3c8ec0f2 100644 --- a/i18n/uk/pages/handlers/progress.md +++ b/i18n/uk/pages/handlers/progress.md @@ -1,6 +1,6 @@ --- translation: - sections: [5315262fe26b33e1, 9d8e98840f1b78f0, 52d6009a07e770ea, 8534d8dbb4053a70, 2966fac6fe697007] + sections: [5315262fe26b33e1, 9d8e98840f1b78f0, 52d6009a07e770ea, 8534d8dbb4053a70, 2e9aff14d3a882c0] tool: 1 --- # Перебіг виконання {#progress} @@ -120,4 +120,4 @@ Imported https://example.com/b.json (2.0/2.0) * Немає колбека у виклику — `report_progress` нічого не робить. Звітуйте безумовно. * Не вказуйте `total`, коли він невідомий; колбек отримає `None`. -Перебіг виконання — це те, що інструмент під час роботи показує *користувачеві*. Рядки, які він записує в лог для *вас*, людини, що експлуатує сервер, — це інший канал: **[Логування](logging.md)**. +Перебіг виконання — для клієнта, який іще чекає. А що бачить інструмент, коли клієнт перестає чекати, — це **[Скасування](cancellation.md)**. diff --git a/i18n/uk/pages/handlers/subscriptions.md b/i18n/uk/pages/handlers/subscriptions.md index f98d01ff19..61e55ce719 100644 --- a/i18n/uk/pages/handlers/subscriptions.md +++ b/i18n/uk/pages/handlers/subscriptions.md @@ -1,6 +1,6 @@ --- translation: - sections: [60a9de8a0bdaa531, 317bbe7e4355cdcc, a61d660c8029e04a, 8f7e82fcb88df8a9, b165db51249ff8ed, 266f56fb798068a4, 7c0e57030b622139, df18d7c2417a9883] + sections: [60a9de8a0bdaa531, 6693607ea56d8bd6, a61d660c8029e04a, 8f7e82fcb88df8a9, b165db51249ff8ed, b8bc624a627ead9b, 2139e68e36d9e621, 7c0e57030b622139, 34ab1af2b9ab5b45] tool: 1 --- # Підписки {#subscriptions} @@ -22,7 +22,7 @@ translation: * Споріднені методи — `notify_prompts_changed()` і `notify_resources_changed()`. * Немає підписників — немає роботи. Публікація на сервері без слухачів нічого не робить, тож перевіряти, чи хтось слухає, ніколи не потрібно. Ви лише повідомляєте, що змінилося. -`MCPServer` обслуговує `subscriptions/listen` за вас. Зобов'язання на рівні протоколу (підтвердження першим кадром, фільтрація для кожного потоку, ідентифікатор підписки в кожному кадрі) — справа SDK. +`MCPServer` обслуговує `subscriptions/listen` за вас, якщо ви цього не [вимкнули](#turning-it-off). Зобов'язання на рівні протоколу (підтвердження першим кадром, фільтрація для кожного потоку, ідентифікатор підписки в кожному кадрі) — справа SDK. !!! check У переданих даних потік, у фільтрі якого вказано `board://sprint`, після виконання `complete_task` має такий вигляд: @@ -79,7 +79,32 @@ translation: Публікації йдуть від обробника до відкритих потоків через `SubscriptionBus`. Типова шина живе в пам'яті: один процес і всі потоки в ньому. Це правильна відповідь, доки ви не запускаєте репліки за балансувальником навантаження, бо тоді потік клієнта прив'язаний до однієї репліки, а публікація на іншій репліці має до нього дійти. -Цей стик реалізуєте ви: два методи поверх вашого бекенда pub/sub. +З типовою шиною це неможливо, бо кожна репліка має свою: + +```mermaid +flowchart LR + client[Client] --> lb[Load balancer] + lb --> stream + lb ~~~~ gap + lb --> tool + subgraph B [Replica B] + tool[tools/call] -- publishes --> busB[(bus B)] + end + gap[(no shared bus)] + subgraph A [Replica A] + stream[listen stream] -- subscribed --> busA[(bus A)] + end + style A fill:none + style B fill:none + style gap fill:none,stroke-dasharray:4 4 +``` + +Нічого не ламається: виклик завершується успішно, а потік мовчить. Тож за балансувальником навантаження виберіть одне з двох: + +* **Сповіщення про зміни потрібні.** Дайте кожній репліці ту саму шину, як показано нижче. +* **Не потрібні.** [Вимкніть їх](#turning-it-off), щоб жодному клієнту не обіцяли подій, які він пропустить, і щоб жоден не тримав заради них відкритий потік. + +Спільну шину реалізуєте ви: два методи поверх вашого бекенда pub/sub. ```python from collections.abc import Callable @@ -128,6 +153,20 @@ async def tools_reloaded() -> None: await bus.publish(ToolsListChanged()) # from a lifespan task, a webhook, anywhere ``` +## Вимкнення підписок {#turning-it-off} + +Серверу, каталог якого ніколи не змінюється, нічого публікувати. Скажіть про це, коли його створюєте: + +```python title="server.py" hl_lines="3" +--8<-- "docs_src/subscriptions/tutorial007.py" +``` + +* Клієнт версії `2026-07-28` бачить, що сповіщення про зміни не оголошено, а запит `subscriptions/listen` отримує *Method not found* замість відкритого потоку. +* `ctx.notify_*` і далі працює й ні до кого не доходить, тож обробники не змінюються. +* Клієнти на раніших версіях протоколу різниці не помічають. + +Відкритий потік — це запит, який ніколи не завершується, тож це важливо й на хості, що тарифікує за тривалістю запиту. + ## Низькорівнева композиція {#the-low-level-composition} На низькорівневому `Server` нічого заздалегідь не під'єднано, і ті самі частини збираються в три рядки: @@ -148,5 +187,6 @@ async def tools_reloaded() -> None: * Клієнтський бік — це `async with client.listen(...)`: докладніше — на сторінці **[Підписки](../client/subscriptions.md)** у розділі *Клієнти*. * На низькорівневому `Server` ті самі частини ви збираєте самі: шина, `ListenHandler(bus)`, слот `on_subscriptions_listen`. * Горизонтальне масштабування — це реалізація `SubscriptionBus` (два методи) і передавання її як `MCPServer(subscriptions=...)`. +* Нічого публікувати або репліки без спільної шини: `MCPServer(subscriptions=False)` не оголошує сповіщень про зміни й не тримає жодного потоку. Як запустити сервер, що все це обслуговує, за однією реплікою чи за двадцятьма, — на сторінці **[Розгортання й масштабування](../run/deploy.md)**. diff --git a/i18n/uk/pages/run/deploy.md b/i18n/uk/pages/run/deploy.md index a09c492800..d22425e296 100644 --- a/i18n/uk/pages/run/deploy.md +++ b/i18n/uk/pages/run/deploy.md @@ -1,6 +1,6 @@ --- translation: - sections: [28221886b198784f, f88ea1f1614f3a1d, 2e76f5cb9df15042, ce926d686730b6d0, 3be24f8ad8bb5ab9, 3fad24032b2224ff, f25a7f860e579ecb, 697b01d95080880d] + sections: [28221886b198784f, f88ea1f1614f3a1d, 2e76f5cb9df15042, ce926d686730b6d0, 3be24f8ad8bb5ab9, aaf489e944ecf5d1, f25a7f860e579ecb, 697b01d95080880d] tool: 1 --- # Розгортання та масштабування {#deploy-scale} @@ -160,7 +160,7 @@ python -c "import secrets; print(secrets.token_hex(32))" ## Сповіщення про зміни між репліками {#change-notifications-across-replicas} -Потік `subscriptions/listen` клієнта — це одна довготривала відповідь, тож він прив'язаний до однієї репліки на все своє життя. `ctx.notify_resource_updated(...)`, опублікований на **іншій** репліці, має до нього дійти. +Потік `subscriptions/listen` клієнта — це одна довготривала відповідь, тож він прив'язаний до однієї репліки на все своє життя. Сповіщення `ctx.notify_resource_updated(...)`, опубліковане на **іншій** репліці, має до нього дійти. Шов між ними — `SubscriptionBus`. Яку б шину ви не дали серверу, саме в неї йде кожна публікація і саме її слухає кожен відкритий потік, тож передайте ту саму шину кожній репліці: @@ -173,6 +173,7 @@ python -c "import secrets; print(secrets.token_hex(32))" * Між справжніми процесами **SDK не постачає жодної шини, яка могла б допомогти.** `SubscriptionBus` — це `Protocol` із двох методів (`publish` і `subscribe`), який ви реалізуєте поверх власного pub/sub-бекенда (Redis, NATS, що завгодно, що у вас уже працює) і передаєте як `MCPServer(subscriptions=...)`. Начерк і контракт — на сторінці **[Підписки](../handlers/subscriptions.md#scaling-past-one-process)**. * Шина переносить чотири невеликі типізовані події, ніколи не JSON-RPC. Підтвердження, фільтрація та життєвий цикл потоку залишаються в SDK, тож ваша шина не може зламати протокол; вона може лише переміщувати події між процесами. * Потоки **не** відновлювані, а події **не** відтворюються повторно. Втрата репліки обриває її потоки; клієнти знову підписуються на прослуховування і знову отримують дані. Немає сховища подій, яке треба поділяти, і більше нічого налаштовувати. Це єдине місце, де горизонтальне масштабування — справді просто більше того самого. +* Сервер, якому сповіщення про зміни не потрібні, обходиться без шини: **[вимкніть їх](../handlers/subscriptions.md#turning-it-off)**. ## Чого SDK вам не дає {#what-the-sdk-does-not-give-you} diff --git a/i18n/uk/pages/servers/structured-output.md b/i18n/uk/pages/servers/structured-output.md index 3f897661e7..1d405ad08c 100644 --- a/i18n/uk/pages/servers/structured-output.md +++ b/i18n/uk/pages/servers/structured-output.md @@ -1,6 +1,6 @@ --- translation: - sections: [a838d57f003aed44, 857d03886a0137ed, 42d9efcb9f542867, 2290ff08435b5573, 91be9b73602abcf1, 6cdbad079f7b47f0, d4b607372fb28b51, 18dbf726ac45e0b7, c7eff2a5698225fa, c851964bb3301907, 8f296f1f09e4c400, d715db6f8dccc9cc, a0c344a48450dbe4] + sections: [a838d57f003aed44, 857d03886a0137ed, 42d9efcb9f542867, 2290ff08435b5573, 91be9b73602abcf1, 6cdbad079f7b47f0, d4b607372fb28b51, 7608fc5ebc31d6ea, c7eff2a5698225fa, c851964bb3301907, 8f296f1f09e4c400, d715db6f8dccc9cc, a0c344a48450dbe4] tool: 1 --- # Структурований вивід {#structured-output} @@ -174,6 +174,8 @@ result.structured_content # {"London": 16.2, "Reykjavik": 4.4} Ключі мають бути `str`. `dict[int, float]` не може бути JSON-об'єктом, тож він повертається до обгортки `{"result": ...}`. +Для валідації й серіалізації словникових результатів використовується `TypeAdapter` з Pydantic. Якщо зазирнути в `FuncMetadata.output_model` інструмента, там зберігається анотація типу словника разом із назвою схеми. + ## Валідація {#validation} `output_schema` — це не документація. Усе, що повертає функція, **перевіряється на відповідність їй** перед тим, як залишити сервер. diff --git a/i18n/uk/pages/servers/tools.md b/i18n/uk/pages/servers/tools.md index dba6d743fd..d07334869e 100644 --- a/i18n/uk/pages/servers/tools.md +++ b/i18n/uk/pages/servers/tools.md @@ -1,6 +1,6 @@ --- translation: - sections: [e4cc390d56573409, f30cf8103a6e918c, 2c97b9f888398951, 048e5471dfa71aea, 3076b1e16ad95950, edbedf2a16e71311, 3d8ef8da89fa87c1, f6c0e02e6ea5a363] + sections: [e4cc390d56573409, f30cf8103a6e918c, 2c97b9f888398951, 048e5471dfa71aea, 3076b1e16ad95950, c1115cd005b81e8f, 3d8ef8da89fa87c1, f6c0e02e6ea5a363] tool: 1 --- # Інструменти {#tools} @@ -141,7 +141,7 @@ Inspector покаже форму з обов'язковим текстовим Якщо інструмент виконує ввід-вивід (викликає API, читає файл, робить запит до бази даних), оголосіть його як `async def` і використовуйте `await` всередині. SDK його дочекається. -Інструмент зі звичайним `def` теж працює: SDK запускає його в окремому потоці, тож він ніколи не блокує сервер. +Інструмент зі звичайним `def` теж працює: SDK запускає його в окремому потоці, тож він ніколи не блокує сервер. Довготривалий інструмент може перевірити, чи клієнт досі чекає; див. **[Скасування](../handlers/cancellation.md)**. Більше нічого налаштовувати не потрібно. diff --git a/i18n/uk/pages/troubleshooting.md b/i18n/uk/pages/troubleshooting.md index 1b631ed4a1..77e35a8550 100644 --- a/i18n/uk/pages/troubleshooting.md +++ b/i18n/uk/pages/troubleshooting.md @@ -1,6 +1,6 @@ --- translation: - sections: [3d58228e81b99543, 170514ce901c4139, 17d61fad0a50d62b, 8a6e351ec756904d, 137454d469c867f5, 6392596bd6df54f0, 41126fa9c4fe432f, 480b6d7897e30ab4, d83bb682e708dde0, ebbed3449c499db4, 525cdf1755e29d4c, 30fd31be74169d9a, d2e88333d4f7841f, c2dc3b1007d2e987, d6eabf60cc366341, f798e815252852c2, 0cba47bae78d04eb, 2c218ba829abf74e] + sections: [3d58228e81b99543, 170514ce901c4139, 17d61fad0a50d62b, 8a6e351ec756904d, 137454d469c867f5, fcf984fa0615ed11, 6392596bd6df54f0, 41126fa9c4fe432f, 480b6d7897e30ab4, d83bb682e708dde0, ebbed3449c499db4, 525cdf1755e29d4c, 30fd31be74169d9a, d2e88333d4f7841f, c2dc3b1007d2e987, d6eabf60cc366341, f798e815252852c2, 0cba47bae78d04eb, cdc6d86a4dae8a34] tool: 1 --- # Усунення несправностей {#troubleshooting} @@ -129,6 +129,14 @@ TypeError: The @tool decorator was used incorrectly. Did you forget to call it? самі й прочитайте трасування. Перевірка типів теж це ловить: функція — не дійсне значення для `name=`. +## `InvalidSignature: Tool '' has an invalid x-mcp-header annotation: ` {#invalidsignature-tool-name-has-an-invalid-x-mcp-header-annotation-reason} + +Аргумент інструмента позначено `x-mcp-header` у спосіб, якого специфікація не дозволяє, а `` каже, яке саме правило порушено. Клієнти на `2026-07-28` не включили б такий інструмент до свого списку, тож SDK відмовляється його реєструвати. + +Позначати можна лише аргументи типів `str`, `int` і `bool`, а `str | None` не є жодним із них. Як записати необов'язковий аргумент — на сторінці **[Параметри в заголовках](advanced/header-parameters.md)**. + +Як і в пункті вище, цей виняток викидається під час **імпорту** модуля, до того як під'єднається будь-який клієнт. + ## `Tool already exists: ` {#tool-already-exists-name} Дві реєстрації використали те саме ім'я інструмента. Перемагає **перша**, другу мовчки відкидають, і це попередження в *лозі сервера* — єдиний сигнал: @@ -426,6 +434,7 @@ mcp = MCPServer("Weather", request_state_security=RequestStateSecurity(keys=[key * `ExceptionGroup: unhandled errors in a TaskGroup` ніколи не є самою помилкою. Читайте **останній рядок**; перехоплення `MCPError` *всередині* блоку `async with Client(...)` повністю оминає обгортання. * `call_tool` не викидає виняток для інструмента, що завершився збоєм. `Error executing tool ...` і `Unknown tool: ...` — це результати: перевіряйте `result.is_error`. Якщо після імені інструмента немає повідомлення, він упав, а трасування — у лозі сервера. * `Client must be used within an async context manager` -> використовуйте `async with`. `Use @tool() instead of @tool` -> додайте дужки. +* `has an invalid x-mcp-header annotation` -> позначати можна лише аргументи типів `str`, `int` і `bool`. * `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()`. diff --git a/i18n/uk/pages/whats-new.md b/i18n/uk/pages/whats-new.md index 132967224b..7980836365 100644 --- a/i18n/uk/pages/whats-new.md +++ b/i18n/uk/pages/whats-new.md @@ -1,6 +1,6 @@ --- translation: - sections: [cfe01c0c5863dfa2, 0dbb68d8b210177b, 80cf193023af3ed4, 875eb2889263424e] + sections: [cfe01c0c5863dfa2, 0dbb68d8b210177b, bd42ee3a268f6ea4, 875eb2889263424e] tool: 1 --- # Що нового у v2 {#whats-new-in-v2} @@ -204,7 +204,7 @@ v2 реалізує редакцію 2026-07-28 і обслуговує **оби ### Решта, коротко {#the-rest-quickly} * **Ідентичність — необов'язкові метадані кожного повідомлення.** Ключ `_meta` `clientInfo` на боці запиту необов'язковий (обов'язкова пара — `protocolVersion` + `clientCapabilities`), а `serverInfo` переїхав із тіла результату `server/discover`: натомість сервери проставляють його в `_meta` кожного результату покоління 2026 ([spec #3002](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/3002)). SDK проставляє завжди; `client.server_info` дорівнює `None`, коли сервер себе не ідентифікує (наприклад, middleware прибрав ключ). **[Низькорівневий Server](advanced/low-level-server.md)** показує цю позначку в переданих даних. -* **Запити можна маршрутизувати, не розбираючи тіл.** Сучасні HTTP-запити несуть `Mcp-Method` (а для трьох викликів на кшталт інструментів — ще й `Mcp-Name`); властивість вхідної схеми інструмента, анотована `x-mcp-header`, дублюється в заголовок `Mcp-Param-*` і звіряється сервером ([SEP-2243](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2243)). Шлюзи й обмежувачі частоти можуть маршрутизувати лише за заголовками; правила — у **[Посібнику з міграції](migration.md#servers-validate-mcp-param-headers-against-the-request-body-sep-2243)**. +* **Запити можна маршрутизувати, не розбираючи тіл.** Сучасні HTTP-запити несуть `Mcp-Method` (а для трьох викликів на кшталт інструментів — ще й `Mcp-Name`); властивість вхідної схеми інструмента, анотована `x-mcp-header`, дублюється в заголовок `Mcp-Param-*` і звіряється сервером ([SEP-2243](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2243)). Шлюзи й обмежувачі частоти можуть маршрутизувати лише за заголовками. **[Параметри в заголовках](advanced/header-parameters.md)** показує, як позначити аргумент. * **Результати несуть підказки кешування.** Результати списків і читання оголошують `ttlMs` і `cacheScope` ([SEP-2549](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2549)); ви задаєте їх для кожного методу через `cache_hints=`, а `Client` дотримується їх завдяки вбудованому кешу відповідей. Сервер, який не надсилає підказок (тобто будь-який сервер до 2026), бачить ідентичний, некешований трафік. **[Підказки кешування](client/caching.md)**. * **Розширення стали повноцінними.** Сервери й клієнти оголошують необов'язкові набори можливостей під ідентифікаторами у форматі зворотного 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)** упорядковано за точними повідомленнями. diff --git a/i18n/zh-hant/pages/advanced/header-parameters.md b/i18n/zh-hant/pages/advanced/header-parameters.md new file mode 100644 index 0000000000..53636fabeb --- /dev/null +++ b/i18n/zh-hant/pages/advanced/header-parameters.md @@ -0,0 +1,65 @@ +--- +translation: + sections: [81862a209b483d27, b76e8073487afa03, bc214b2fc2bcdae4, d5835477c0b60163, a5d9786f902ad8e1] + tool: 1 +--- +# 標頭參數 {#header-parameters} + +大多數伺服器都用不到這項功能。 + +伺服器前面的閘道或負載平衡器,只能根據不必解析主體就讀得到的內容來路由。用 `x-mcp-header` 標記某個工具引數,使用 `2026-07-28` **[協定版本](../protocol-versions.md)** 的用戶端就會把它的值也當成 HTTP 標頭送出。 + +## 標記引數 {#mark-an-argument} + +這個標記就是引數的 JSON Schema 裡多出的一個鍵。在 `MCPServer` 上,由 `Field` 把它放進去: + +```python title="server.py" hl_lines="13" +--8<-- "docs_src/header_parameters/tutorial001.py" +``` + +* 在 `2026-07-28` 的 Streamable HTTP 上,用戶端會連同主體一起送出 `Mcp-Param-Region`;兩者不一致時,伺服器會拒絕這次呼叫。 +* 還沒列出過工具的用戶端從沒看過這個標記:它不會送出標頭,伺服器就會拒絕這次呼叫。這時本 SDK 的 `Client` 會列出工具並重送一次呼叫,所以先列出工具只是省下一次往返。 +* 其他連線都會忽略這個註記。 + +函式不用改:`region` 仍然以引數的形式傳入。 + +## 哪些引數可以標記 {#what-can-be-marked} + +`str`、`int` 和 `bool` 引數。其他型別在註冊工具時就會遭到拒絕,並引發 `InvalidSignature`。 + +這也包括 `str | None`,因為它沒有單一型別。選用引數需要用 Pydantic 的 `WithJsonSchema` 把 schema 明確寫出來: + +```python +region: Annotated[str | None, WithJsonSchema({"type": "string", "x-mcp-header": "Region"})] = None +``` + +## 在低階 `Server` 上 {#on-the-low-level-server} + +在那裡 `input_schema` 是手寫的,所以直接把這個鍵寫進去: + +```python title="server.py" hl_lines="18" +--8<-- "docs_src/header_parameters/tutorial002.py" +``` + +* 沒有任何機制會替你檢查註記:無效的註記會照樣送出,而 `2026-07-28` 用戶端會把這個工具排除在清單之外。 + +### 依名稱取得 schema {#schemas-by-name} + +要檢查標頭,SDK 必須在分派呼叫之前拿到工具的輸入 schema。沒有 `get_tool_input_schema` 時,只要呼叫帶有引數,SDK 每次都會執行 `on_list_tools` 處理函式來取得 schema,不論有沒有任何工具帶有標記。 + +```python title="server.py" hl_lines="26 39-41 48" +--8<-- "docs_src/header_parameters/tutorial003.py" +``` + +* 傳入這個函式,就能用手邊已有的資料來回答。 +* 工具沒有需要檢查的內容時,回傳 `None`。 + +## 重點回顧 {#recap} + +* 在工具引數上加 `x-mcp-header`,`2026-07-28` 用戶端就會把它再以 `Mcp-Param-*` HTTP 標頭送一次。 +* 標頭與主體不一致的呼叫,伺服器會拒絕。 +* 只有 `str`、`int` 和 `bool` 引數可以標記。其他型別 `MCPServer` 會引發 `InvalidSignature`。 +* 低階 `Server` 什麼都不檢查,而用戶端會捨棄註記無效的工具。 +* 有了 `get_tool_input_schema`,低階 `Server` 就不必在每次呼叫時執行 `on_list_tools`。 + +手寫 `Server` API 的其餘部分請見 **[低階 Server](low-level-server.md)**。 diff --git a/i18n/zh-hant/pages/advanced/index.md b/i18n/zh-hant/pages/advanced/index.md index bef6cd1cde..0aefa257d4 100644 --- a/i18n/zh-hant/pages/advanced/index.md +++ b/i18n/zh-hant/pages/advanced/index.md @@ -1,6 +1,6 @@ --- translation: - sections: [ca6988b7503cd2d3] + sections: [348f8697c6b12cd0] tool: 1 --- # 進階 {#advanced} @@ -9,6 +9,7 @@ translation: * **[低階 Server](low-level-server.md)**:`MCPServer` 建構於其上的類別。手寫的 schema、`on_*` 處理函式、沒有任何東西會幫你檢查,還可以加上自訂的 JSON-RPC 方法。 * **[分頁](pagination.md)** 和 **[中介軟體](middleware.md)**:兩件**只**能在低階 `Server` 上做的事。 +* **[標頭參數](header-parameters.md)**:讓閘道根據工具呼叫的其中一個引數來路由這次呼叫。 * **[擴充功能](extensions.md)** 和 **[MCP Apps](apps.md)**:協定的擴充介面。把擴充功能套件組合進伺服器,或自己寫一個。 有幾樣東西你可能理所當然會來這裡找,但它們其實放在實際會用到的地方: diff --git a/i18n/zh-hant/pages/advanced/low-level-server.md b/i18n/zh-hant/pages/advanced/low-level-server.md index 62bcfd4d36..9f47de3f03 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, 9d5d10a5f0405d0a, 1086e77ce561cd7f, a3f71823df5efc31, 9fc7109f72201cae, d50fe7faead8cf68, 7bf25983df655b66, 6330e1f4c6029683, 2f1749c8c133fa1c, 8db7116fc8ddd0ee, ebc33704fbd74262, 0fde3bcea081ba3a] + sections: [2c79b6338e09b7ac, 9d5d10a5f0405d0a, 1086e77ce561cd7f, a3f71823df5efc31, 9fc7109f72201cae, d50fe7faead8cf68, 7bf25983df655b66, 6330e1f4c6029683, 2f1749c8c133fa1c, 8db7116fc8ddd0ee, 2090d99b355bc2c7, 0fde3bcea081ba3a] tool: 1 --- # 低階 Server {#the-low-level-server} @@ -208,6 +208,7 @@ use Server.middleware to observe or wrap initialization * `on_call_tool`、`on_get_prompt` 和 `on_read_resource` 可以回傳 `InputRequiredResult` 取代正常結果,暫停呼叫並向用戶端要求輸入;請見 **[多輪往返(multi-round-trip)請求](../handlers/multi-round-trip.md)**。忠於這一層的風格,沒有任何東西會替你裝好:`MCPServer` 預設會封裝 `requestState`,在這裡你設定的 `request_state` 會一字不差地跨過線路,直到你用 `server.middleware.append(RequestStateBoundary(RequestStateSecurity(keys=[...]), default_audience=server.name))` 選擇加入:一行(兩個名稱都從 `mcp.server.request_state` 匯入)就能得到和 `MCPServer` 完全相同的封裝與驗證(**[保護 `requestState`](../handlers/multi-round-trip.md#protecting-requeststate)**)。 * `on_list_resources`、`on_read_resource`、`on_list_prompts`、`on_get_prompt`、`on_completion` 是其他基本元件的同一個 `(ctx, params) -> result` 形狀。 * `on_subscriptions_listen` 負責 2026-07-28 的 `subscriptions/listen` 串流。傳入一個建構在 `SubscriptionBus` 之上的 `ListenHandler`,並從其他處理函式把事件發佈到 bus;完整的組合方式請見 **[訂閱](../handlers/subscriptions.md)**。 +* `get_tool_input_schema` 讓 `on_list_tools` 不必出現在呼叫路徑上;請見 **[標頭參數](header-parameters.md#schemas-by-name)**。 * `server.streamable_http_app()` 回傳的 Starlette 應用程式和 `MCPServer` 的一樣;照 **[執行伺服器](../run/index.md)** 部署其他 ASGI 應用程式的方式部署它。這一層沒有 `server.run(transport=...)`:`server.run(read_stream, write_stream, server.create_initialization_options())` 透過一對串流驅動一條連線,而這一行就是全部。 ## 重點回顧 {#recap} diff --git a/i18n/zh-hant/pages/advanced/middleware.md b/i18n/zh-hant/pages/advanced/middleware.md index 80b83d9363..f32e9db80a 100644 --- a/i18n/zh-hant/pages/advanced/middleware.md +++ b/i18n/zh-hant/pages/advanced/middleware.md @@ -1,6 +1,6 @@ --- translation: - sections: [6048b4f308edbb8c, 46056f318ef205e4, c3e565b61acd75c5, c62422b159c6ed09, 420968f514138f43] + sections: [58e1103d9a323ccf, 46056f318ef205e4, 812b414557fb0c35, 4df162eea2518d38, c62422b159c6ed09, 420968f514138f43] tool: 1 --- # 中介軟體 {#middleware} @@ -45,12 +45,28 @@ tools/call took 0.1 ms * 每一個抵達伺服器的請求和每一則通知。對通知而言,`ctx.request_id is None`,`call_next(ctx)` 回傳 `None`,而你回傳的任何東西都會被丟棄。(在 `2026-07-28` 的 Streamable HTTP 路徑上,用戶端以 POST 送出的通知會在傳輸層直接以 `202` 確認收到、從不分派,所以也不會抵達中介軟體;該修訂版沒有定義任何透過 HTTP 由用戶端送往伺服器的通知。) * 連伺服器沒有處理函式的方法也一樣:`call_next` 會引發 `MCPError(-32601, "Method not found")`,**穿過**你的中介軟體一路送到用戶端。 +## 並行上限 {#a-concurrency-cap} + +中介軟體不一定要呼叫 `call_next(ctx)`。改為引發 `MCPError`,就等於**拒絕**了那一則訊息:連線不會斷,下一則訊息照常通過。 + +假設每次搜尋都會佔用連線池裡的一條連線,而池子裡只有 4 條。這個中介軟體讓 4 個工具呼叫同時執行,第 5 個就拒絕: + +```python title="server.py" hl_lines="15-16 40-55 59" +--8<-- "docs_src/middleware/tutorial002.py" +``` + +* 只計算 `tools/call`,所以伺服器在拒絕工具呼叫的同時,仍會繼續回應 `server/discover` 和 `tools/list`。 +* MCP 沒有定義「伺服器忙碌」的錯誤碼,所以 `SERVER_BUSY` 是這個伺服器自己定義的。 +* 拒絕能讓用戶端立刻知道伺服器已經過載。如果寧可讓呼叫端等待,就改在 `call_next(ctx)` 外面包一層 `anyio.CapacityLimiter`。 + +引發的 `MCPError` 會送到用戶端應用程式,而不是模型。如果要讓模型讀到這則訊息,就改為回傳一個帶有 `is_error=True` 的工具結果:這就是下面的**回答**。 + ## 在裡面能做什麼 {#what-you-can-do-inside-one} 依照該有的猶豫程度,由低到高排列: -* **觀察。**計時、計數、記錄。就是上面的範例。 -* **拒絕。**不呼叫 `call_next(ctx)`,**改為**引發 `MCPError`,那一則訊息就會以 JSON-RPC 錯誤回應。連線不會斷;下一則訊息照常通過。伺服器就是這樣依呼叫端控管 `subscriptions/listen` 的:訂閱頁面的 **[決定誰可以觀看](../handlers/subscriptions.md#deciding-who-may-watch)** 有逐步說明。 +* **觀察。**計時、計數、記錄。就是上面的計時中介軟體。 +* **拒絕。**不呼叫 `call_next(ctx)`,**改為**引發 `MCPError`,那一則訊息就會以 JSON-RPC 錯誤回應。連線不會斷;下一則訊息照常通過。就是上面的並行上限。伺服器也是這樣依呼叫端控管 `subscriptions/listen` 的:訂閱頁面的 **[決定誰可以觀看](../handlers/subscriptions.md#deciding-who-may-watch)** 有逐步說明。 * **改寫。**`ctx` 是一個 dataclass:`await call_next(dataclasses.replace(ctx, params=...))` 會把和用戶端送來的不同的參數交給鏈上剩下的部分。絕對不要對 `initialize` 這麼做:用戶端拿到的結果是根據你改寫後的參數建立的,但伺服器提交連線狀態時用的是線路上原本的參數。雙方可能在交握結束時,對彼此協商出的內容認知不一致。 * **回答。**不呼叫 `call_next(ctx)` 就直接回傳一個結果,它會作為你的回應送到用戶端。`call_next` 交給你的是完成的線路格式,而管線絕不會修補你回傳的東西,所以整個封包都由你負責:在 2026 世代的連線上,這包括 `serverInfo` 的 `_meta` 戳記,SDK 會替處理函式的結果加上它,但不會替你的加。 diff --git a/i18n/zh-hant/pages/client/identity-assertion.md b/i18n/zh-hant/pages/client/identity-assertion.md index 4a47926efa..cb56eb0732 100644 --- a/i18n/zh-hant/pages/client/identity-assertion.md +++ b/i18n/zh-hant/pages/client/identity-assertion.md @@ -1,6 +1,6 @@ --- translation: - sections: [a91322c46111d16d, 8e6fd6d6f59bb568, e7828fd2729b2c9d, a03ec26bfc678b65, 1034c653c0bcf1b0] + sections: [a91322c46111d16d, 8e6fd6d6f59bb568, 7cf38181f6c99fd5, 37804d4fb36d6302, 1034c653c0bcf1b0] tool: 1 --- # 身分斷言 {#identity-assertion} @@ -57,7 +57,7 @@ translation: ### 機密用戶端 {#a-confidential-client} -`client_secret` 是必填;沒有它,建構子會引發 `ValueError`。[SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990) 底下的 IETF profile 把這種授權類型保留給機密用戶端,SEP-990 要求用戶端必須驗證身分,而這個 SDK 以堅持要有共享密鑰的方式同時強制這兩點。`token_endpoint_auth_method` 決定它走哪裡:`client_secret_post`(預設,放在表單主體)或 `client_secret_basic`(HTTP Basic 標頭)。profile 也允許 `private_key_jwt`;這個 provider 不支援。 +`client_secret` 是必填;沒有它,建構子會引發 `ValueError`。[SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990) 底下的 IETF profile 建議只讓機密用戶端使用這種授權類型,而 [RFC 7521](https://datatracker.ietf.org/doc/html/rfc7521) 把這項政策交給授權伺服器決定。這個 SDK 在兩端都採取保守的解讀:內建的授權伺服器會拒絕沒有共享密鑰的用戶端,這個 provider 也堅持一定要有。`token_endpoint_auth_method` 決定它走哪裡:`client_secret_post`(預設,放在表單主體)或 `client_secret_basic`(HTTP Basic 標頭)。profile 也允許 `private_key_jwt`;這個 provider 不支援。 !!! tip 從環境變數或密鑰管理服務讀取 `client_secret`,永遠不要從版本控制裡讀。 @@ -84,6 +84,7 @@ SDK 也可以自己**當**授權伺服器:`create_auth_routes` 以任何 Starl * `identity_assertion_enabled=True` 管控一切。關閉時(這是預設),即使你實作了 hook,`/token` 也會以 `unsupported_grant_type` 回應這種授權類型,中繼資料也不會提到它。開啟時,中繼資料會多出 `jwt-bearer` 授權類型,並在 `authorization_grant_profiles_supported` 裡列出 `urn:ietf:params:oauth:grant-profile:id-jag`,也就是擴充功能用來宣傳支援的欄位。(這個 SDK 的用戶端從不讀它:它只為一個 issuer 佈建,直接開口問就是了。) * **`exchange_identity_assertion`** 就是那個 hook。在它執行之前,SDK 已經驗證了用戶端、拒絕了公開用戶端,也拒絕了註冊資料裡沒列出這種授權類型的用戶端。你會拿到一個 `IdentityAssertionParams`(原始的 `assertion`、請求的 `scopes` 和 `resource`),回傳一個普通的 `OAuthToken`。 +* 拒絕公開用戶端是 SDK 的政策,不是規格的要求。內建的伺服器只用共享密鑰驗證用戶端:它不支援 `private_key_jwt`,也還不會解析 Client ID Metadata Document([#1801](https://github.com/modelcontextprotocol/python-sdk/issues/1801)),所以以這種文件識別的用戶端在這裡無法使用這種授權類型。想採用不同政策的部署,可以把 `create_auth_routes` 回傳的 `/token` 路由換成自己的。 * 動態用戶端註冊無條件拒絕這種授權類型,所以這裡的 `get_client` 提供的是手動佈建的用戶端。ID-JAG 用戶端沒辦法靠自己註冊而存在。 * 這個類別有一半是拒絕。`OAuthAuthorizationServerProvider` 是**整個**授權伺服器,所以它也要求授權碼流程;同時讓使用者登入的伺服器會真的實作那些,而這一台只有一扇門。 diff --git a/i18n/zh-hant/pages/client/transports.md b/i18n/zh-hant/pages/client/transports.md index 5d626ba6b0..335d15799f 100644 --- a/i18n/zh-hant/pages/client/transports.md +++ b/i18n/zh-hant/pages/client/transports.md @@ -1,6 +1,6 @@ --- translation: - sections: [9cac816674181eb0, 7c157764133fea1f, 40b4916d82eaf1d4, 10d151f2cc75317f, 3d0832f39b0d7059, 92742ba36533633d, 0aeca6145e7bd302] + sections: [9cac816674181eb0, 5619e950d206e6c8, 40b4916d82eaf1d4, 10d151f2cc75317f, 3d0832f39b0d7059, 92742ba36533633d, 991c10e47fda2636] tool: 1 --- # 用戶端傳輸方式 {#client-transports} @@ -43,16 +43,28 @@ translation: * `httpx2.AsyncClient` 是你的,所以由**你**負責進入和離開它。SDK 永遠不會關閉不是它自己建立的用戶端。 * `streamable_http_client(url, http_client=...)` 回傳的是一個傳輸,而 `Client(transport)` 和接受其他東西一樣接受它。 +保留 `timeout=`。這就是 SDK 自己的用戶端所用的設定(30 秒,read 為 300 秒);沒有設定逾時就建立的 `httpx2.AsyncClient` 會套用 `httpx2` 的 5 秒預設值,執行時間超過這個長度的工具呼叫就會因為 read 逾時而失敗。 + 關於 TLS 有一點要提:`httpx2` 是對照作業系統的信任存放區驗證憑證(透過 [`truststore`](https://pypi.org/project/truststore/)),而不是內建的 CA 清單。在沒有可用系統 CA 存放區的環境(某些精簡容器)裡,請設定標準的 `SSL_CERT_FILE`/`SSL_CERT_DIR` 環境變數,或明確傳入 `verify=ssl_context` 給你的 `httpx2.AsyncClient`(背景說明請見 [`httpx` 和 `httpx-sse` 已由 `httpx2` 取代](../migration.md#httpx-and-httpx-sse-replaced-by-httpx2))。 +### 更大的 SSE 事件 {#larger-sse-events} + +伺服器在單一 SSE 事件裡送出很大的工具結果或通知時,傳入 `max_sse_event_size`: + +```python title="client.py" hl_lines="6-9" +--8<-- "docs_src/client_transports/tutorial005.py" +``` + +預設值是每個事件 1 MiB,以事件解析前的位元組數計算。這個上限適用於 POST 回應、GET 串流和恢復的串流。POST 回應或恢復的串流裡出現過大的事件時,該請求會失敗並回報 SSE 錯誤。在背景的 GET 串流上,用戶端會記錄這個錯誤並重試串流。信任伺服器、又需要更大的事件時,設定 `max_sse_event_size=None` 即可停用上限。JSON 回應不受影響。如果使用 `ClientSessionGroup`,請在 `StreamableHttpParameters` 上設定同一個選項。 + !!! warning - `streamable_http_client` 以前可以直接接受 `headers=` 和 `timeout=`。現在不行了:它僅有的參數是 `url`、`http_client` 和 `terminate_on_close`。如果習慣性地寫了 `headers=`,會得到: + `streamable_http_client` 以前可以直接接受 `headers=` 和 `timeout=`。現在不行了:它的參數是 `url`、`http_client`、`terminate_on_close` 和 `max_sse_event_size`。如果習慣性地寫了 `headers=`,會得到: ```text TypeError: streamable_http_client() got an unexpected keyword argument 'headers' ``` - 所有跟 HTTP 有關的設定,現在都放在你傳入的那一個 `httpx2.AsyncClient` 上。 + 標頭、驗證、proxy 和逾時都放在你傳入的那一個 `httpx2.AsyncClient` 上。`max_sse_event_size` 則是套用在 MCP 傳輸的 SSE 讀取器上。 !!! info `httpx2` 保留了熟悉的 `httpx` API,所以只要會用 `httpx`,就已經知道這裡的驗證、proxy、事件掛鉤、重試和連線數限制該怎麼做。SDK 沒有在上面加任何東西,也沒有拿掉任何東西,[重新導向的處理](#redirects)除外。OAuth 也是從這裡接上的:`httpx2.AsyncClient(auth=OAuthClientProvider(...))`。整個流程請見 **[OAuth 用戶端](oauth-clients.md)**。 @@ -120,6 +132,7 @@ translation: * `Client("http://.../mcp")`(URL)透過 Streamable HTTP 連線,也就是正式環境用的傳輸方式。 * 標頭、驗證、proxy 和逾時都放在你傳給 `streamable_http_client(url, http_client=...)` 的 `httpx2.AsyncClient` 上。沒有 `headers=` 這個關鍵字引數。 +* 用 `streamable_http_client(url, max_sse_event_size=...)` 調整每個 SSE 事件的位元組上限。 * 重新導向只在 URL 自己的來源內跟隨(結尾斜線的 `307`/`308`),外加同一台主機上的 `http`→`https`。其他的一律失敗並出現 `Redirect to … not followed`;把最終的 URL 寫進設定即可。 * stdio 是 `Client(StdioServerParameters(...))`。只有要把子處理程序的 stderr 導到別處時,才需要自己用 `stdio_client(...)` 包起來。 * 子處理程序拿到的是允許清單上的環境,不是你的環境;`env=` 會往上加。 diff --git a/i18n/zh-hant/pages/handlers/cancellation.md b/i18n/zh-hant/pages/handlers/cancellation.md new file mode 100644 index 0000000000..30cb5f3468 --- /dev/null +++ b/i18n/zh-hant/pages/handlers/cancellation.md @@ -0,0 +1,57 @@ +--- +translation: + sections: [07968345fdc0b84e, 4ea8416db9efa0dc, 336a7b4c5d0a4578, 18392e805dde6717, c30d50df43f9b55c] + tool: 1 +--- +# 取消 {#cancellation} + +用戶端可以放棄一次呼叫:使用者按了停止,或是逾時時間到了。 + +這時 SDK 會**取消你的處理函式**。它正在等待的那個 `await` 會引發例外,函式逐層退出,它回傳的任何東西都不會送出。大多數處理函式不需要為此做任何事。 + +有兩種需要:有東西要清理的處理函式,以及用普通 `def` 寫的處理函式。 + +## 在 `async def` 工具中清理 {#clean-up-in-an-async-def-tool} + +把清理工作放進 `finally`: + +```python title="server.py" hl_lines="23 26-28" +--8<-- "docs_src/cancellation/tutorial001.py" +``` + +* 不論工具怎麼結束,`finally` 都會執行:正常回傳、引發例外,或是被取消。 +* 需要 `await` 的清理工作要加上 `shield=True`。在已取消的處理函式裡,之後的每個 `await` 也都會引發例外,所以少了這層保護,`release_hold` 會在第一行就停住。 +* 受保護的區塊無法被任何東西取消,所以要給它一個時間限制。這裡是 `5` 秒。 + +!!! tip + 用 `finally`,不要用 `except`。清理完成後,取消必須繼續往上傳遞,而 `finally` 會放行。 + +## 在普通 `def` 工具中提早停止 {#stop-early-in-a-plain-def-tool} + +普通的 `def` 工具在執行緒中執行,而執行緒無法從外部中斷。工具必須自己詢問: + +```python title="server.py" hl_lines="22 25-26" +--8<-- "docs_src/cancellation/tutorial002.py" +``` + +* `anyio.from_thread.check_cancelled()` 在呼叫仍有效時什麼都不做,一旦呼叫被取消就會引發例外。在每個工作單元之間呼叫它。 +* 這裡的清理工作同樣放在 `finally` 裡。執行緒裡不會有任何 await,所以不需要保護。 +* 從不詢問的 `def` 工具會一路執行到結束,結果則被丟棄。 + +## 適用範圍 {#where-it-applies} + +提示詞和資源函式被取消的方式和工具完全相同。 + +在 stdio 和 Streamable HTTP 上的運作方式相同。使用這個 SDK 的 `Client` 時,放棄指的是取消正在等待 `call_tool` 的任務,或是讓它的 `read_timeout_seconds` 時間用完。 + +!!! warning + 有兩個 Streamable HTTP 選項會讓處理函式無從得知取消:`2026-07-28` 連線上的 `json_response=True`,以及舊版連線上的 `stateless_http=True`。這時不論用戶端做了什麼,處理函式都會執行到結束。 + +## 重點回顧 {#recap} + +* 用戶端放棄呼叫時,SDK 會取消處理函式:工具、提示詞或資源都一樣。 +* `async def`:在 `finally` 裡清理,需要 await 的清理工作放進 `anyio.move_on_after(seconds, shield=True)`。 +* 普通 `def`:在工作單元之間呼叫 `anyio.from_thread.check_cancelled()`,否則工具會執行到結束。清理用一般的 `finally` 即可。 +* `json_response=True`(新版連線)和 `stateless_http=True`(舊版連線)會關閉取消功能。 + +進度與取消是執行中的工具和它的**呼叫端**之間的事。它為**你**這個伺服器操作者寫下的記錄,走的是另一個管道:**[記錄](logging.md)**。 diff --git a/i18n/zh-hant/pages/handlers/index.md b/i18n/zh-hant/pages/handlers/index.md index defab844ca..9e4b4e5651 100644 --- a/i18n/zh-hant/pages/handlers/index.md +++ b/i18n/zh-hant/pages/handlers/index.md @@ -1,6 +1,6 @@ --- translation: - sections: [424930166c4bc6f3] + sections: [22ca41e50cc1b536] tool: 1 --- # 在處理函式內部 {#inside-your-handler} @@ -18,6 +18,7 @@ translation: * 用 **[徵詢](elicitation.md)**(elicitation)向使用者要求更多輸入,以及承載它的 2026-07-28 模式 **[多輪往返請求](multi-round-trip.md)**(multi-round-trip)。 * 用 **[取樣與根目錄](sampling-and-roots.md)**(sampling 與 roots)向用戶端要求 LLM 生成結果或它的工作區資料夾,這兩者已棄用但仍然提供。 * 對耗時的工作回報 **[進度](progress.md)**。 +* 用戶端放棄這次呼叫時,用 **[取消](cancellation.md)** 清理善後,或提早停止。 * 用 **[記錄](logging.md)** 寫入記錄(寫到標準錯誤輸出,給負責維運伺服器的人看)。 * 用 **[訂閱](subscriptions.md)** 告訴已訂閱的用戶端有東西變了。 diff --git a/i18n/zh-hant/pages/handlers/lifespan.md b/i18n/zh-hant/pages/handlers/lifespan.md index 42ef56b8d8..e943122230 100644 --- a/i18n/zh-hant/pages/handlers/lifespan.md +++ b/i18n/zh-hant/pages/handlers/lifespan.md @@ -1,6 +1,6 @@ --- translation: - sections: [f3ca8ac5f90f2dfa, 85a1ef3588ba0736, 563346d4d5804933, 9e3528340d0bab53] + sections: [f3ca8ac5f90f2dfa, 48e478ef7bd688b1, 563346d4d5804933, 52ac6a7734d6f581] tool: 1 --- # 生命週期 {#lifespan} @@ -46,7 +46,7 @@ translation: `genre` 是模型唯一能傳入的引數。生命週期是伺服器自己的事。 -`@mcp.resource()` 和 `@mcp.prompt()` 函式也可以接收 `ctx` 參數,寫成不帶型別參數的 `Context`,原因下一節會說明。`ctx` 所攜帶的一切,請見 **[Context](context.md)**。 +`@mcp.resource()` 和 `@mcp.prompt()` 函式也可以接收 `ctx` 參數。`ctx` 所攜帶的一切,請見 **[Context](context.md)**。 ### 它真的有型別 {#it-really-is-typed} @@ -56,15 +56,6 @@ translation: 如果改寫成不帶型別參數的 `Context`,`lifespan_context` 的型別就是 `dict[str, Any]`:型別檢查器無從得知你的生命週期 yield 了什麼。執行時物件還在;你失去的是協助。 -!!! warning - `Context[AppContext]` 是**只限工具**的寫法。把它放在 `@mcp.resource()` 或 `@mcp.prompt()` 函式上,對該處理函式的每次呼叫都會失敗。用戶端會收到錯誤,伺服器記錄會顯示原因: - - ```text - Context is not available outside of a request - ``` - - 在資源和提示詞裡,寫不帶型別參數的 `ctx: Context`。生命週期 yield 出來的物件在執行時仍然是 `ctx.request_context.lifespan_context`;放棄的只是型別參數,不是物件。 - !!! tip 生命週期永遠存在。如果不傳入,SDK 的預設會 yield 一個空的 `dict`,所以 `ctx.request_context.lifespan_context` 是 `{}`,永遠不會是 `None`。這個預設也是為什麼不帶型別參數的 `Context` 會把它的型別定為 `dict[str, Any]`。 @@ -95,7 +86,7 @@ translation: * `yield` 之前的程式碼是啟動。之後的 `finally` 是關閉。 * 它只執行一次,涵蓋伺服器的整個生命,而不是每個請求一次。 * 不論 `yield` 什麼,在每個工具、資源和提示詞裡都是 `ctx.request_context.lifespan_context`。 -* `ctx: Context[AppContext]` 讓工具裡的這種存取完全有型別。資源和提示詞則用不帶型別參數的 `Context`。 +* `ctx: Context[AppContext]` 讓這種存取完全有型別。 * 沒有 `lifespan=` 代表一個空的 `dict`,永遠不會是 `None`。 在呼叫途中停下來、向使用者詢問只有他們才知道的事的處理函式,就是 **[徵詢(elicitation)](elicitation.md)**。 diff --git a/i18n/zh-hant/pages/handlers/progress.md b/i18n/zh-hant/pages/handlers/progress.md index 7f1d96d509..1bd3c8ef7e 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, 52d6009a07e770ea, 8534d8dbb4053a70, 2966fac6fe697007] + sections: [5315262fe26b33e1, 9d8e98840f1b78f0, 52d6009a07e770ea, 8534d8dbb4053a70, 2e9aff14d3a882c0] tool: 1 --- # 進度 {#progress} @@ -111,4 +111,4 @@ Imported https://example.com/b.json (2.0/2.0) * 呼叫時沒有回呼,`report_progress` 就什麼都不做。無條件回報就好。 * 不知道 `total` 就省略;回呼會收到 `None`。 -進度是執行中的工具給**使用者**看的。它為**你**(操作伺服器的人)記下的那些行,是另一條通道:**[記錄](logging.md)**。 +進度是給還在等待的用戶端看的。用戶端不再等待時,工具會看到的則是**[取消](cancellation.md)**。 diff --git a/i18n/zh-hant/pages/handlers/subscriptions.md b/i18n/zh-hant/pages/handlers/subscriptions.md index 24449c645d..3bc395a49e 100644 --- a/i18n/zh-hant/pages/handlers/subscriptions.md +++ b/i18n/zh-hant/pages/handlers/subscriptions.md @@ -1,6 +1,6 @@ --- translation: - sections: [60a9de8a0bdaa531, 317bbe7e4355cdcc, a61d660c8029e04a, 8f7e82fcb88df8a9, b165db51249ff8ed, 266f56fb798068a4, 7c0e57030b622139, df18d7c2417a9883] + sections: [60a9de8a0bdaa531, 6693607ea56d8bd6, a61d660c8029e04a, 8f7e82fcb88df8a9, b165db51249ff8ed, b8bc624a627ead9b, 2139e68e36d9e621, 7c0e57030b622139, 34ab1af2b9ab5b45] tool: 1 --- # 訂閱 {#subscriptions} @@ -22,7 +22,7 @@ translation: * 同系列的還有 `notify_prompts_changed()` 和 `notify_resources_changed()`。 * 沒有訂閱者,就沒有工作。對閒置的伺服器發布是空操作,所以永遠不必檢查有沒有人在聽,只要說明什麼變了。 -`MCPServer` 會替你服務 `subscriptions/listen`。線路上的義務(第一個訊框是確認、逐串流過濾、每個訊框都帶訂閱 id)是 SDK 的工作。 +`MCPServer` 會替你服務 `subscriptions/listen`,除非你[把它關閉](#turning-it-off)。線路上的義務(第一個訊框是確認、逐串流過濾、每個訊框都帶訂閱 id)是 SDK 的工作。 !!! check 在線路上,一個過濾條件指名 `board://sprint` 的串流,在 `complete_task` 執行之後看起來像這樣: @@ -79,7 +79,32 @@ translation: 發布的內容透過 `SubscriptionBus` 從處理函式送到開啟中的串流。預設是記憶體內的:一個處理程序,所有串流都在裡面。在你於負載平衡器後面執行多個副本之前,這都是正確答案;因為到那時,用戶端的串流會固定在某一個副本上,而另一個副本上的發布必須送得到它。 -那個接縫由你實作:在你的 pub/sub 後端上實作兩個方法。 +用預設的匯流排就送不到,因為每個副本都有自己的匯流排: + +```mermaid +flowchart LR + client[Client] --> lb[Load balancer] + lb --> stream + lb ~~~~ gap + lb --> tool + subgraph B [Replica B] + tool[tools/call] -- publishes --> busB[(bus B)] + end + gap[(no shared bus)] + subgraph A [Replica A] + stream[listen stream] -- subscribed --> busA[(bus A)] + end + style A fill:none + style B fill:none + style gap fill:none,stroke-dasharray:4 4 +``` + +不會有任何東西失敗:呼叫成功,串流卻保持安靜。所以在負載平衡器後面,二選一: + +* **需要變更通知。** 讓每個副本都用同一個匯流排,做法見下方。 +* **不需要。** [把它關閉](#turning-it-off),這樣就不會有用戶端被承諾一些它收不到的事件,也不會為了等這些事件而讓串流一直開著。 + +共用的匯流排由你實作:在你的 pub/sub 後端上實作兩個方法。 ```python from collections.abc import Callable @@ -128,6 +153,20 @@ async def tools_reloaded() -> None: await bus.publish(ToolsListChanged()) # from a lifespan task, a webhook, anywhere ``` +## 關閉訂閱 {#turning-it-off} + +目錄永遠不變的伺服器沒有東西可以發布。建立伺服器時就直接說明: + +```python title="server.py" hl_lines="3" +--8<-- "docs_src/subscriptions/tutorial007.py" +``` + +* `2026-07-28` 用戶端不會看到伺服器宣告任何變更通知,而 `subscriptions/listen` 請求會得到 *Method not found*,而不是一個開啟的串流。 +* `ctx.notify_*` 照樣能用,只是不會送達任何人,所以處理函式不用改。 +* 使用較早協定版本的用戶端看不出任何差別。 + +開啟的串流是一個永遠不會結束的請求,所以在依請求持續時間計費的主機上,這一點也很重要。 + ## 低階組合方式 {#the-low-level-composition} 在低階的 `Server` 上沒有任何預先接好的東西,同樣的零件三行就能組起來: @@ -148,5 +187,6 @@ async def tools_reloaded() -> None: * 用戶端那一端是 `async with client.listen(...)`:完整說明請見「用戶端」章節下的 **[訂閱](../client/subscriptions.md)**。 * 在低階的 `Server` 上,同樣的零件自己組:一個匯流排、`ListenHandler(bus)`、`on_subscriptions_listen` 插槽。 * 橫向擴展代表實作 `SubscriptionBus`(兩個方法),然後以 `MCPServer(subscriptions=...)` 傳入。 +* 沒有東西要發布,或是副本之間沒有共用的匯流排:`MCPServer(subscriptions=False)` 不會宣告任何變更通知,也不會保持任何串流開啟。 執行提供這一切的伺服器,不管是一個副本還是 20 個,請見 **[部署與擴展](../run/deploy.md)**。 diff --git a/i18n/zh-hant/pages/run/deploy.md b/i18n/zh-hant/pages/run/deploy.md index c500dcaa08..6b6f4e9558 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, 2e76f5cb9df15042, ce926d686730b6d0, 3be24f8ad8bb5ab9, 3fad24032b2224ff, f25a7f860e579ecb, 697b01d95080880d] + sections: [28221886b198784f, f88ea1f1614f3a1d, 2e76f5cb9df15042, ce926d686730b6d0, 3be24f8ad8bb5ab9, aaf489e944ecf5d1, f25a7f860e579ecb, 697b01d95080880d] tool: 1 --- # 部署與擴展 {#deploy-scale} @@ -157,6 +157,7 @@ python -c "import secrets; print(secrets.token_hex(32))" * 跨真正的處理程序時,**SDK 沒有附任何幫得上忙的 bus。**`SubscriptionBus` 是一個只有兩個方法的 `Protocol`(`publish` 和 `subscribe`),由你在自己的 pub/sub 後端(Redis、NATS,或你已經在跑的任何東西)上實作,再以 `MCPServer(subscriptions=...)` 傳入。草稿與契約請見 **[訂閱](../handlers/subscriptions.md#scaling-past-one-process)**。 * bus 載的是四種小型的有型別事件,從來不是 JSON-RPC。確認、過濾和串流生命週期都留在 SDK 裡,所以你的 bus 不可能破壞協定;它只能在處理程序之間搬運事件。 * 串流**不能**續傳,事件也**不會**重播。失去一個副本就丟掉它的串流;用戶端會重新 listen、重新抓取。沒有要共用的事件儲存區,也沒有別的要設定。這是唯一一個向外擴展真的只是「多幾台一樣的」的地方。 +* 不需要變更通知的伺服器可以略過 bus:**[把通知關掉](../handlers/subscriptions.md#turning-it-off)**。 ## SDK 不提供的東西 {#what-the-sdk-does-not-give-you} diff --git a/i18n/zh-hant/pages/servers/structured-output.md b/i18n/zh-hant/pages/servers/structured-output.md index d9359726a7..5057287516 100644 --- a/i18n/zh-hant/pages/servers/structured-output.md +++ b/i18n/zh-hant/pages/servers/structured-output.md @@ -1,6 +1,6 @@ --- translation: - sections: [a838d57f003aed44, 857d03886a0137ed, 42d9efcb9f542867, 2290ff08435b5573, 91be9b73602abcf1, 6cdbad079f7b47f0, d4b607372fb28b51, 18dbf726ac45e0b7, c7eff2a5698225fa, c851964bb3301907, 8f296f1f09e4c400, d715db6f8dccc9cc, a0c344a48450dbe4] + sections: [a838d57f003aed44, 857d03886a0137ed, 42d9efcb9f542867, 2290ff08435b5573, 91be9b73602abcf1, 6cdbad079f7b47f0, d4b607372fb28b51, 7608fc5ebc31d6ea, c7eff2a5698225fa, c851964bb3301907, 8f296f1f09e4c400, d715db6f8dccc9cc, a0c344a48450dbe4] tool: 1 --- # 結構化輸出 {#structured-output} @@ -172,6 +172,8 @@ result.structured_content # {"London": 16.2, "Reykjavik": 4.4} 鍵必須是 `str`。`dict[int, float]` 沒辦法成為 JSON 物件,所以會退回 `{"result": ...}` 包裝。 +字典結果使用 Pydantic 的 `TypeAdapter` 來驗證和序列化。如果檢視工具的 `FuncMetadata.output_model`,裡面放的是字典的型別註記,以及它的 schema 標題。 + ## 驗證 {#validation} `output_schema` 不是寫來看的說明文件。函式回傳的任何東西,在離開伺服器之前都會**拿它來驗證**。 diff --git a/i18n/zh-hant/pages/servers/tools.md b/i18n/zh-hant/pages/servers/tools.md index 1e3b48e4e7..cdb38ed166 100644 --- a/i18n/zh-hant/pages/servers/tools.md +++ b/i18n/zh-hant/pages/servers/tools.md @@ -1,6 +1,6 @@ --- translation: - sections: [e4cc390d56573409, f30cf8103a6e918c, 2c97b9f888398951, 048e5471dfa71aea, 3076b1e16ad95950, edbedf2a16e71311, 3d8ef8da89fa87c1, f6c0e02e6ea5a363] + sections: [e4cc390d56573409, f30cf8103a6e918c, 2c97b9f888398951, 048e5471dfa71aea, 3076b1e16ad95950, c1115cd005b81e8f, 3d8ef8da89fa87c1, f6c0e02e6ea5a363] tool: 1 --- # 工具 {#tools} @@ -137,7 +137,7 @@ schema 跟著變: 如果工具會做 I/O(呼叫 API、讀檔案、查資料庫),就宣告成 `async def`,在裡面 `await`。SDK 會 await 它。 -一般的 `def` 工具也可以:SDK 會在執行緒裡執行它,所以永遠不會阻塞伺服器。 +一般的 `def` 工具也可以:SDK 會在執行緒裡執行它,所以永遠不會阻塞伺服器。執行時間長的工具可以檢查用戶端是否還在等待;請見 **[取消](../handlers/cancellation.md)**。 沒有其他要設定的東西。 diff --git a/i18n/zh-hant/pages/troubleshooting.md b/i18n/zh-hant/pages/troubleshooting.md index e7bc47178a..62f834c0cc 100644 --- a/i18n/zh-hant/pages/troubleshooting.md +++ b/i18n/zh-hant/pages/troubleshooting.md @@ -1,6 +1,6 @@ --- translation: - sections: [3d58228e81b99543, 170514ce901c4139, 17d61fad0a50d62b, 8a6e351ec756904d, 137454d469c867f5, 6392596bd6df54f0, 41126fa9c4fe432f, 480b6d7897e30ab4, d83bb682e708dde0, ebbed3449c499db4, 525cdf1755e29d4c, 30fd31be74169d9a, d2e88333d4f7841f, c2dc3b1007d2e987, d6eabf60cc366341, f798e815252852c2, 0cba47bae78d04eb, 2c218ba829abf74e] + sections: [3d58228e81b99543, 170514ce901c4139, 17d61fad0a50d62b, 8a6e351ec756904d, 137454d469c867f5, fcf984fa0615ed11, 6392596bd6df54f0, 41126fa9c4fe432f, 480b6d7897e30ab4, d83bb682e708dde0, ebbed3449c499db4, 525cdf1755e29d4c, 30fd31be74169d9a, d2e88333d4f7841f, c2dc3b1007d2e987, d6eabf60cc366341, f798e815252852c2, 0cba47bae78d04eb, cdc6d86a4dae8a34] tool: 1 --- # 疑難排解 {#troubleshooting} @@ -123,6 +123,14 @@ TypeError: The @tool decorator was used incorrectly. Did you forget to call it? !!! note 這在模組**匯入**時就會引發,早於任何用戶端連線。所以如果主機(host)把伺服器顯示成「failed to start」(或「disconnected」),而不是已連線但零個工具,就是這種情況:自己執行 `python server.py`,讀 traceback。型別檢查器也抓得到:函式不是合法的 `name=`。 +## `InvalidSignature: Tool '' has an invalid x-mcp-header annotation: ` {#invalidsignature-tool-name-has-an-invalid-x-mcp-header-annotation-reason} + +某個工具引數標記 `x-mcp-header` 的方式是規格不允許的,`` 會說明違反了哪一條規則。`2026-07-28` 的用戶端會把這樣的工具排除在清單之外,所以 SDK 拒絕註冊它。 + +只有 `str`、`int` 和 `bool` 引數可以標記,而 `str | None` 不屬於其中任何一種。選用引數的寫法請見 **[標頭參數](advanced/header-parameters.md)**。 + +和上面那一則一樣,這在模組**匯入**時就會引發,早於任何用戶端連線。 + ## `Tool already exists: ` {#tool-already-exists-name} 兩次註冊用了同一個工具名稱。**第一個**勝出,第二個會被默默丟掉,而**伺服器記錄**裡的這則警告是唯一的訊號: @@ -409,6 +417,7 @@ mcp = MCPServer("Weather", request_state_security=RequestStateSecurity(keys=[key * `ExceptionGroup: unhandled errors in a TaskGroup` 永遠不是錯誤本身。讀**最後一行**;在 `async with Client(...)` 區塊**裡面**攔截 `MCPError` 就完全跳過包裝。 * `call_tool` 不會因為工具失敗而引發例外。`Error executing tool ...` 和 `Unknown tool: ...` 是結果:檢查 `result.is_error`。工具名稱後面沒有訊息表示它當掉了,traceback 在伺服器記錄裡。 * `Client must be used within an async context manager` -> 用 `async with`。`Use @tool() instead of @tool` -> 加上括號。 +* `has an invalid x-mcp-header annotation` -> 只有 `str`、`int` 和 `bool` 引數可以標記。 * 伺服器記錄裡的 `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()`。 diff --git a/i18n/zh-hant/pages/whats-new.md b/i18n/zh-hant/pages/whats-new.md index bcdfc7feb8..ce1ceb4736 100644 --- a/i18n/zh-hant/pages/whats-new.md +++ b/i18n/zh-hant/pages/whats-new.md @@ -1,6 +1,6 @@ --- translation: - sections: [cfe01c0c5863dfa2, 0dbb68d8b210177b, 80cf193023af3ed4, 875eb2889263424e] + sections: [cfe01c0c5863dfa2, 0dbb68d8b210177b, bd42ee3a268f6ea4, 875eb2889263424e] tool: 1 --- # v2 的新功能 {#whats-new-in-v2} @@ -196,7 +196,7 @@ v2 實作 2026-07-28 修訂版,而且**兩個**修訂版同時服務:同一 ### 其餘的,快速帶過 {#the-rest-quickly} * **身分是選用的、逐訊息的中繼資料。** 請求端的 `clientInfo` `_meta` 鍵是選用的(必要的一對是 `protocolVersion` + `clientCapabilities`),而 `serverInfo` 搬出了 `server/discover` 的結果本體:伺服器改為把它蓋進每個 2026 世代結果的 `_meta`([spec #3002](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/3002))。SDK 一定會蓋;伺服器沒有表明身分時(例如中介軟體拿掉了那個鍵),`client.server_info` 是 `None`。**[低階 Server](advanced/low-level-server.md)** 展示線路上的這個戳記。 -* **請求不必解析本體就能路由。** 現代 HTTP 請求帶有 `Mcp-Method`(三個類似工具的呼叫還帶 `Mcp-Name`);標註了 `x-mcp-header` 的工具輸入 schema 屬性會鏡射到 `Mcp-Param-*` 標頭,並由伺服器交叉核對([SEP-2243](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2243))。閘道和限流器光靠標頭就能路由;規則請見 **[遷移指南](migration.md#servers-validate-mcp-param-headers-against-the-request-body-sep-2243)**。 +* **請求不必解析本體就能路由。** 現代 HTTP 請求帶有 `Mcp-Method`(三個類似工具的呼叫還帶 `Mcp-Name`);標註了 `x-mcp-header` 的工具輸入 schema 屬性會鏡射到 `Mcp-Param-*` 標頭,並由伺服器交叉核對([SEP-2243](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2243))。閘道和限流器光靠標頭就能路由。**[標頭參數](advanced/header-parameters.md)** 示範怎麼標記引數。 * **結果帶有快取提示。** 列表和讀取結果會宣告 `ttlMs` 和 `cacheScope`([SEP-2549](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2549));用 `cache_hints=` 逐方法設定,`Client` 則用內建的回應快取遵守它們。不送提示的伺服器(所有 2026 以前的伺服器)看到的是一模一樣、沒有快取的流量。**[快取提示](client/caching.md)**。 * **擴充功能是一等公民。** 伺服器和用戶端在反向 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)** 以確切的訊息為索引。 diff --git a/i18n/zh/pages/advanced/header-parameters.md b/i18n/zh/pages/advanced/header-parameters.md new file mode 100644 index 0000000000..fec83f4c84 --- /dev/null +++ b/i18n/zh/pages/advanced/header-parameters.md @@ -0,0 +1,65 @@ +--- +translation: + sections: [81862a209b483d27, b76e8073487afa03, bc214b2fc2bcdae4, d5835477c0b60163, a5d9786f902ad8e1] + tool: 1 +--- +# 请求头参数 {#header-parameters} + +大多数服务器都用不到这个。 + +服务器前面的网关或负载均衡器只能根据不解析请求体就能读到的内容来路由。用 `x-mcp-header` 标记一个工具参数,使用 `2026-07-28` **[协议版本](../protocol-versions.md)** 的客户端就会把它的值同时作为 HTTP 请求头发送。 + +## 标记参数 {#mark-an-argument} + +这个标记就是参数的 JSON Schema 里多出的一个键。在 `MCPServer` 上,由 `Field` 把它放进去: + +```python title="server.py" hl_lines="13" +--8<-- "docs_src/header_parameters/tutorial001.py" +``` + +* 在 `2026-07-28` 的 Streamable HTTP 上,客户端会在请求体之外同时发送 `Mcp-Param-Region`。两者不一致时,服务器会拒绝这次调用。 +* 没有列出过该工具的客户端从未见过这个标记:它不会发送请求头,调用会遭到拒绝。这时本 SDK 的 `Client` 会列出工具并重发一次调用,所以先列出工具只是省去一次往返。 +* 其他所有连接都会忽略这个注解。 + +函数不用改:`region` 仍然作为参数传入。 + +## 哪些参数可以标记 {#what-can-be-marked} + +`str`、`int` 和 `bool` 类型的参数。其他类型在注册工具时都会被拒绝,并抛出 `InvalidSignature`。 + +这也包括 `str | None`,因为它没有单一的类型。可选参数需要用 Pydantic 的 `WithJsonSchema` 把模式明确写出来: + +```python +region: Annotated[str | None, WithJsonSchema({"type": "string", "x-mcp-header": "Region"})] = None +``` + +## 在底层 `Server` 上 {#on-the-low-level-server} + +在那里 `input_schema` 是手写的,所以直接把这个键写进去: + +```python title="server.py" hl_lines="18" +--8<-- "docs_src/header_parameters/tutorial002.py" +``` + +* 没有任何东西替你检查这个注解:无效的注解会照样提供出去,`2026-07-28` 客户端则会把这个工具从列表中剔除。 + +### 按名称获取模式 {#schemas-by-name} + +要检查请求头,SDK 需要在分发调用之前拿到工具的输入模式。没有 `get_tool_input_schema` 时,每次调用只要带有参数,SDK 就会运行你的 `on_list_tools` 处理函数来获取它,不管有没有工具被标记。 + +```python title="server.py" hl_lines="26 39-41 48" +--8<-- "docs_src/header_parameters/tutorial003.py" +``` + +* 传入这个函数,就能直接用你手头已有的数据来回答。 +* 对于没有需要检查内容的工具,返回 `None`。 + +## 回顾 {#recap} + +* 给工具参数加上 `x-mcp-header`,`2026-07-28` 客户端就会把它再作为 `Mcp-Param-*` HTTP 请求头发送一遍。 +* 请求头与请求体不一致的调用,服务器会拒绝。 +* 只有 `str`、`int` 和 `bool` 参数可以标记。遇到其他类型,`MCPServer` 会抛出 `InvalidSignature`。 +* 底层 `Server` 什么都不检查,而客户端会丢弃注解无效的工具。 +* `get_tool_input_schema` 让底层 `Server` 不必在每次调用时都运行 `on_list_tools`。 + +手写 `Server` API 的其余内容见 **[底层 Server](low-level-server.md)**。 diff --git a/i18n/zh/pages/advanced/index.md b/i18n/zh/pages/advanced/index.md index f21e83f298..4c3448ece3 100644 --- a/i18n/zh/pages/advanced/index.md +++ b/i18n/zh/pages/advanced/index.md @@ -1,6 +1,6 @@ --- translation: - sections: [ca6988b7503cd2d3] + sections: [348f8697c6b12cd0] tool: 1 --- # 进阶 {#advanced} @@ -9,6 +9,7 @@ translation: * **[底层 Server](low-level-server.md)**:`MCPServer` 构建于其上的类。手写模式、`on_*` 处理函数、没有任何替你做的检查,还可以定义你自己的 JSON-RPC 方法。 * **[分页](pagination.md)** 和 **[中间件](middleware.md)**:两件**只能**在底层 `Server` 上做的事。 +* **[请求头参数](header-parameters.md)**:让网关根据工具调用的某个参数来路由这次调用。 * **[扩展](extensions.md)** 和 **[MCP Apps](apps.md)**:协议的扩展面。把扩展包组合进服务器,或者自己写一个。 有几样东西你可能理所当然地想在这里找,但它们其实放在实际用到它们的地方: diff --git a/i18n/zh/pages/advanced/low-level-server.md b/i18n/zh/pages/advanced/low-level-server.md index 437905d376..cba97002f3 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, 9d5d10a5f0405d0a, 1086e77ce561cd7f, a3f71823df5efc31, 9fc7109f72201cae, d50fe7faead8cf68, 7bf25983df655b66, 6330e1f4c6029683, 2f1749c8c133fa1c, 8db7116fc8ddd0ee, ebc33704fbd74262, 0fde3bcea081ba3a] + sections: [2c79b6338e09b7ac, 9d5d10a5f0405d0a, 1086e77ce561cd7f, a3f71823df5efc31, 9fc7109f72201cae, d50fe7faead8cf68, 7bf25983df655b66, 6330e1f4c6029683, 2f1749c8c133fa1c, 8db7116fc8ddd0ee, 2090d99b355bc2c7, 0fde3bcea081ba3a] tool: 1 --- # 底层 Server {#the-low-level-server} @@ -208,6 +208,7 @@ use Server.middleware to observe or wrap initialization * `on_call_tool`、`on_get_prompt` 和 `on_read_resource` 可以返回 `InputRequiredResult` 而不是正常结果,来暂停调用并向客户端索要输入;见 **[多轮往返(multi-round-trip)请求](../handlers/multi-round-trip.md)**。符合这一层的风格,没有任何东西替你装好:`MCPServer` 默认会密封 `requestState`,而在这里,你设置的 `request_state` 按原样穿过线路,直到你用 `server.middleware.append(RequestStateBoundary(RequestStateSecurity(keys=[...]), default_audience=server.name))` 主动启用:一行代码(两个名字都从 `mcp.server.request_state` 导入),得到和 `MCPServer` 完全相同的密封与验证(**[保护 `requestState`](../handlers/multi-round-trip.md#protecting-requeststate)**)。 * `on_list_resources`、`on_read_resource`、`on_list_prompts`、`on_get_prompt`、`on_completion` 是针对其他原语的同样 `(ctx, params) -> result` 形状。 * `on_subscriptions_listen` 提供 2026-07-28 的 `subscriptions/listen` 流。传入一个构建在 `SubscriptionBus` 之上的 `ListenHandler`,并从其他处理函数向总线发布事件;完整的组合方式见 **[订阅](../handlers/subscriptions.md)**。 +* `get_tool_input_schema` 让 `on_list_tools` 不出现在调用路径上;见 **[请求头参数](header-parameters.md#schemas-by-name)**。 * `server.streamable_http_app()` 返回的 Starlette 应用和 `MCPServer` 的一样;按 **[运行你的服务器](../run/index.md)** 部署任何其他 ASGI 应用的方式部署它。这一层没有 `server.run(transport=...)`:`server.run(read_stream, write_stream, server.create_initialization_options())` 在一对流上驱动一个连接,整件事就是这一行。 ## 回顾 {#recap} diff --git a/i18n/zh/pages/advanced/middleware.md b/i18n/zh/pages/advanced/middleware.md index e23d55a3e2..87e8b511d1 100644 --- a/i18n/zh/pages/advanced/middleware.md +++ b/i18n/zh/pages/advanced/middleware.md @@ -1,6 +1,6 @@ --- translation: - sections: [6048b4f308edbb8c, 46056f318ef205e4, c3e565b61acd75c5, c62422b159c6ed09, 420968f514138f43] + sections: [58e1103d9a323ccf, 46056f318ef205e4, 812b414557fb0c35, 4df162eea2518d38, c62422b159c6ed09, 420968f514138f43] tool: 1 --- # 中间件 {#middleware} @@ -45,12 +45,28 @@ tools/call took 0.1 ms * 每一个到达服务器的请求和通知。对于通知,`ctx.request_id is None`,`call_next(ctx)` 返回 `None`,而你返回的任何东西都会被丢弃。(在 `2026-07-28` 的 Streamable HTTP 路径上,客户端的通知 POST 在传输层就以 `202` 确认,从不分发,所以也到不了中间件;该修订版本没有定义任何经由 HTTP 的客户端到服务器通知。) * 甚至包括服务器没有处理函数的方法:`call_next` 会抛出 `MCPError(-32601, "Method not found")`,**穿过**你的中间件送往客户端。 +## 并发上限 {#a-concurrency-cap} + +中间件不是非得调用 `call_next(ctx)`。改为抛出一个 `MCPError`,这一条消息就会被**拒绝**:连接保持不断,下一条消息照常通过。 + +假设每次搜索都要占用连接池里的一个连接,而池里一共只有四个。这个中间件允许四个工具调用同时运行,第五个则拒绝: + +```python title="server.py" hl_lines="15-16 40-55 59" +--8<-- "docs_src/middleware/tutorial002.py" +``` + +* 只统计 `tools/call`,所以服务器在拒绝工具调用的同时,仍然会响应 `server/discover` 和 `tools/list`。 +* MCP 没有定义“服务器繁忙”错误码,所以 `SERVER_BUSY` 是这个服务器自己定义的。 +* 拒绝能让客户端立刻知道服务器已经过载。如果更希望让调用方等待,就改用 `anyio.CapacityLimiter` 把 `call_next(ctx)` 包起来。 + +抛出的 `MCPError` 会交给客户端应用,而不是模型。如果想让模型读到这条消息,就改为返回一个带 `is_error=True` 的工具结果:这就是下面的**作答**。 + ## 在中间件里能做什么 {#what-you-can-do-inside-one} 按你应当犹豫的程度递增排列: -* **观察。**计时、计数、记录日志。就是上面的例子。 -* **拒绝。**抛出一个 `MCPError` 来**代替**调用 `call_next(ctx)`,这一条消息就会以 JSON-RPC 错误作答。连接保持不断;下一条消息照常通过。服务器就是这样按调用方对 `subscriptions/listen` 设限的:订阅页面的 **[决定谁可以监听](../handlers/subscriptions.md#deciding-who-may-watch)** 一节有完整的讲解。 +* **观察。**计时、计数、记录日志。就是上面的计时中间件。 +* **拒绝。**抛出一个 `MCPError` 来**代替**调用 `call_next(ctx)`,这一条消息就会以 JSON-RPC 错误作答。连接保持不断;下一条消息照常通过。就是上面的并发上限。服务器也是这样按调用方对 `subscriptions/listen` 设限的:订阅页面的 **[决定谁可以监听](../handlers/subscriptions.md#deciding-who-may-watch)** 一节有完整的讲解。 * **改写。**`ctx` 是一个 dataclass:`await call_next(dataclasses.replace(ctx, params=...))` 会把与客户端所发不同的参数交给链条剩下的部分。永远不要对 `initialize` 这样做:客户端拿到的结果是根据你改写后的参数构建的,但服务器提交连接状态时依据的是线路上的原始参数。两端可能在握手结束时对协商结果各执一词。 * **作答。**不调用 `call_next(ctx)` 而直接返回一个结果,它就会作为你的响应发给客户端。`call_next` 交给你的是最终的线路形式,而流水线从不修补你返回的内容,所以整个信封都由你负责:在 2026 年代的连接上,这包括 `serverInfo` 的 `_meta` 戳记——SDK 会给处理函数的结果加上它,但不会给你的结果加。 diff --git a/i18n/zh/pages/client/identity-assertion.md b/i18n/zh/pages/client/identity-assertion.md index db91643eb5..5bb977133e 100644 --- a/i18n/zh/pages/client/identity-assertion.md +++ b/i18n/zh/pages/client/identity-assertion.md @@ -1,6 +1,6 @@ --- translation: - sections: [a91322c46111d16d, 8e6fd6d6f59bb568, e7828fd2729b2c9d, a03ec26bfc678b65, 1034c653c0bcf1b0] + sections: [a91322c46111d16d, 8e6fd6d6f59bb568, 7cf38181f6c99fd5, 37804d4fb36d6302, 1034c653c0bcf1b0] tool: 1 --- # 身份断言 {#identity-assertion} @@ -57,7 +57,7 @@ translation: ### 机密客户端 {#a-confidential-client} -`client_secret` 是必填的;没有它,构造函数会抛出 `ValueError`。[SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990) 底下的 IETF profile 把这种授权许可留给机密客户端,SEP-990 要求客户端进行身份认证,而这个 SDK 通过坚持要求共享密钥来同时落实这两点。`token_endpoint_auth_method` 决定它走哪条路:`client_secret_post`(默认,放在表单体里)或 `client_secret_basic`(HTTP Basic 头)。该 profile 还允许 `private_key_jwt`;这个 provider 不支持。 +`client_secret` 是必填的;没有它,构造函数会抛出 `ValueError`。[SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990) 底下的 IETF profile 建议只让机密客户端使用这种授权许可,而 [RFC 7521](https://datatracker.ietf.org/doc/html/rfc7521) 把这项策略留给授权服务器决定。这个 SDK 在两端都取保守的解读:内置的授权服务器拒绝没有共享密钥的客户端,这个 provider 也坚持要求提供一个。`token_endpoint_auth_method` 决定它走哪条路:`client_secret_post`(默认,放在表单体里)或 `client_secret_basic`(HTTP Basic 头)。该 profile 还允许 `private_key_jwt`;这个 provider 不支持。 !!! tip 从环境变量或密钥管理器读取 `client_secret`,永远不要从源码仓库里读。 @@ -84,6 +84,7 @@ SDK 也可以自己**充当**授权服务器:`create_auth_routes` 以列表形 * `identity_assertion_enabled=True` 是总开关。关闭时(这是默认),即使你实现了钩子,`/token` 对这种授权许可也回答 `unsupported_grant_type`,元数据里也不会提到它。打开后,元数据会多出 `jwt-bearer` 授权类型,并在 `authorization_grant_profiles_supported` 里列出 `urn:ietf:params:oauth:grant-profile:id-jag`,这是扩展用来宣告支持的字段。(这个 SDK 的客户端从不读它:它只为一个 issuer 配置,直接发请求就是了。) * **`exchange_identity_assertion`** 就是那个钩子。它运行之前,SDK 已经认证了客户端,拒绝了公开客户端,也拒绝了注册信息里没有列出该授权许可的客户端。你拿到一个 `IdentityAssertionParams`(原始的 `assertion`、请求的 `scopes` 和 `resource`),返回一个普通的 `OAuthToken`。 +* 拒绝公开客户端是 SDK 的策略,不是规范的要求。内置服务器只通过共享密钥认证客户端:它不支持 `private_key_jwt`,目前也不解析 Client ID Metadata Document([#1801](https://github.com/modelcontextprotocol/python-sdk/issues/1801)),所以靠这种文档标识的客户端在这里用不了这种授权许可。想采用别的策略的部署,可以把 `create_auth_routes` 返回的 `/token` 路由换成自己的。 * 动态客户端注册无条件拒绝这种授权许可,所以这里的 `get_client` 提供的是一个手工配置的客户端。ID-JAG 客户端没法靠自我注册凭空出现。 * 这个类有一半是拒绝。`OAuthAuthorizationServerProvider` 是**整个**授权服务器,所以它也要求实现授权码流程;一个同时让用户登录的服务器会真正实现那些方法,而这一个只开一扇门。 diff --git a/i18n/zh/pages/client/transports.md b/i18n/zh/pages/client/transports.md index d505e94503..41fc083753 100644 --- a/i18n/zh/pages/client/transports.md +++ b/i18n/zh/pages/client/transports.md @@ -1,6 +1,6 @@ --- translation: - sections: [9cac816674181eb0, 7c157764133fea1f, 40b4916d82eaf1d4, 10d151f2cc75317f, 3d0832f39b0d7059, 92742ba36533633d, 0aeca6145e7bd302] + sections: [9cac816674181eb0, 5619e950d206e6c8, 40b4916d82eaf1d4, 10d151f2cc75317f, 3d0832f39b0d7059, 92742ba36533633d, 991c10e47fda2636] tool: 1 --- # 客户端传输 {#client-transports} @@ -43,16 +43,28 @@ translation: * `httpx2.AsyncClient` 归你所有,所以由**你**进入和退出它。SDK 从不关闭不是它自己创建的客户端。 * `streamable_http_client(url, http_client=...)` 返回一个传输,`Client(transport)` 像接受其他任何东西一样接受它。 +保留 `timeout=`。它就是 SDK 自己的客户端所用的那个超时(30 秒,读超时 300 秒);不带超时构建的 `httpx2.AsyncClient` 用的是 `httpx2` 的 5 秒默认值,运行时间超过它的工具调用会因读超时而失败。 + 关于 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))。 +### 更大的 SSE 事件 {#larger-sse-events} + +服务器在一个 SSE 事件里发送很大的工具结果或通知时,传入 `max_sse_event_size`: + +```python title="client.py" hl_lines="6-9" +--8<-- "docs_src/client_transports/tutorial005.py" +``` + +默认上限是每个事件 1 MiB,按事件解析之前的字节数计算。这个限制适用于 POST 响应、GET 流和恢复的流。POST 响应或恢复的流中出现超限事件时,该请求会以 SSE 错误失败。在后台 GET 流上,客户端会记录错误并重试这个流。如果信任服务器又需要更大的事件,设置 `max_sse_event_size=None` 来取消上限。JSON 响应不受影响。如果使用 `ClientSessionGroup`,在 `StreamableHttpParameters` 上设置同样的选项。 + !!! warning - `streamable_http_client` 过去可以直接接受 `headers=` 和 `timeout=`。现在不行了:它只有 `url`、`http_client` 和 `terminate_on_close` 三个参数。习惯性地去用 `headers=`,会得到: + `streamable_http_client` 过去可以直接接受 `headers=` 和 `timeout=`。现在不行了:它的参数是 `url`、`http_client`、`terminate_on_close` 和 `max_sse_event_size`。习惯性地去用 `headers=`,会得到: ```text TypeError: streamable_http_client() got an unexpected keyword argument 'headers' ``` - 所有 HTTP 层面的东西现在都放在你传入的那一个 `httpx2.AsyncClient` 上。 + 请求头、认证、代理和超时都放在你传入的那一个 `httpx2.AsyncClient` 上。`max_sse_event_size` 则作用于 MCP 传输的 SSE 读取器。 !!! info `httpx2` 保留了熟悉的 `httpx` API,所以只要会 `httpx`,就已经知道在这里怎么做认证、代理、事件钩子、重试和连接限制。SDK 既不在上面加东西,也不拿走什么,[重定向处理](#redirects)除外。OAuth 也是在这里接入的:`httpx2.AsyncClient(auth=OAuthClientProvider(...))`。整个流程见 **[OAuth 客户端](oauth-clients.md)**。 @@ -120,6 +132,7 @@ translation: * `Client("http://.../mcp")`(URL)通过 Streamable HTTP 连接,即生产环境的传输方式。 * 请求头、认证、代理和超时应放在 `httpx2.AsyncClient` 上,再传给 `streamable_http_client(url, http_client=...)`。没有 `headers=` 关键字参数。 +* 用 `streamable_http_client(url, max_sse_event_size=...)` 修改每个 SSE 事件的字节上限。 * 重定向只在 URL 自己的源之内被跟随(尾部斜杠的 `307`/`308`),外加同一主机上的 `http`→`https`。其他情况都会以 `Redirect to … not followed` 失败;把最终的 URL 写进配置。 * stdio 是 `Client(StdioServerParameters(...))`。只有在需要重定向子进程的 stderr 时,才自己用 `stdio_client(...)` 包一层。 * 子进程拿到的是允许列表里的环境,不是你的环境;`env=` 往里添加。 diff --git a/i18n/zh/pages/handlers/cancellation.md b/i18n/zh/pages/handlers/cancellation.md new file mode 100644 index 0000000000..6342942485 --- /dev/null +++ b/i18n/zh/pages/handlers/cancellation.md @@ -0,0 +1,57 @@ +--- +translation: + sections: [07968345fdc0b84e, 4ea8416db9efa0dc, 336a7b4c5d0a4578, 18392e805dde6717, c30d50df43f9b55c] + tool: 1 +--- +# 取消 {#cancellation} + +客户端可以放弃一次调用:用户按了停止,或者超时时间到了。 + +这时,SDK 会**取消你的处理函数**。它正在等待的 `await` 会抛出异常,函数逐层退出,它返回的任何内容都不会发送出去。大多数处理函数不需要为此做任何事。 + +有两类需要:有东西要清理的处理函数,以及用普通 `def` 定义的处理函数。 + +## 在 `async def` 工具中清理 {#clean-up-in-an-async-def-tool} + +把清理代码放进 `finally`: + +```python title="server.py" hl_lines="23 26-28" +--8<-- "docs_src/cancellation/tutorial001.py" +``` + +* 无论工具以哪种方式结束,`finally` 都会运行:正常返回、抛出异常,或者被取消。 +* 需要 `await` 的清理代码要加 `shield=True`。在已取消的处理函数里,之后的每个 `await` 也都会抛出异常,所以没有这层屏蔽,`release_hold` 在第一行就会停下。 +* 被屏蔽的代码块无法被取消,所以要给它设一个时间限制。这里是 `5` 秒。 + +!!! tip + 用 `finally`,不要用 `except`。清理完成后,取消必须继续向上传播,而 `finally` 会放行。 + +## 在普通 `def` 工具中提前停止 {#stop-early-in-a-plain-def-tool} + +普通 `def` 工具在线程中运行,而线程无法从外部中断。工具得自己去问: + +```python title="server.py" hl_lines="22 25-26" +--8<-- "docs_src/cancellation/tutorial002.py" +``` + +* `anyio.from_thread.check_cancelled()` 在调用仍有效时什么也不做,调用被取消后则会抛出异常。在工作单元之间调用它。 +* 这里的清理代码同样放进 `finally`。线程里没有任何 await,所以不需要屏蔽。 +* 从不询问的 `def` 工具会一直运行到结束,结果则被丢弃。 + +## 适用范围 {#where-it-applies} + +提示词函数和资源函数的取消方式与工具完全相同。 + +在 stdio 和 Streamable HTTP 上行为一致。使用这个 SDK 的 `Client` 时,放弃调用就是取消正在等待 `call_tool` 的任务,或者让它的 `read_timeout_seconds` 耗尽。 + +!!! warning + 有两个 Streamable HTTP 选项会让处理函数收不到取消的消息:`2026-07-28` 连接上的 `json_response=True`,以及旧版连接上的 `stateless_http=True`。这两种情况下,无论客户端做了什么,处理函数都会运行到结束。 + +## 回顾 {#recap} + +* 客户端放弃一次调用时,SDK 会取消处理函数:工具、提示词或资源都一样。 +* `async def`:在 `finally` 中清理,需要 await 的清理代码放进 `anyio.move_on_after(seconds, shield=True)`。 +* 普通 `def`:在工作单元之间调用 `anyio.from_thread.check_cancelled()`,否则工具会运行到结束。清理用普通的 `finally` 就行。 +* `json_response=True`(新版连接)和 `stateless_http=True`(旧版连接)会关闭取消。 + +进度和取消发生在运行中的工具和它的**调用方**之间。它为**你**(运维这台服务器的人)记录的日志走的是另一条通道:**[日志](logging.md)**。 diff --git a/i18n/zh/pages/handlers/index.md b/i18n/zh/pages/handlers/index.md index e788547e9c..fd598a8f8b 100644 --- a/i18n/zh/pages/handlers/index.md +++ b/i18n/zh/pages/handlers/index.md @@ -1,6 +1,6 @@ --- translation: - sections: [424930166c4bc6f3] + sections: [22ca41e50cc1b536] tool: 1 --- # 在处理函数内部 {#inside-your-handler} @@ -18,6 +18,7 @@ translation: * 用 **[征询(elicitation)](elicitation.md)** 向用户请求更多输入,以及承载它的 2026-07-28 模式 **[多轮往返请求](multi-round-trip.md)**(multi-round-trip)。 * 用 **[采样(sampling)与根目录(roots)](sampling-and-roots.md)** 向客户端请求一次 LLM 补全或它的工作区文件夹——已弃用,但仍然提供。 * 对耗时的操作报告 **[进度](progress.md)**。 +* 客户端放弃这次调用时,用 **[取消](cancellation.md)** 做清理,或者提前停止。 * 用 **[日志](logging.md)** 写日志(写到标准错误,给运维服务器的人看)。 * 用 **[订阅](subscriptions.md)** 告诉已订阅的客户端有东西变了。 diff --git a/i18n/zh/pages/handlers/lifespan.md b/i18n/zh/pages/handlers/lifespan.md index 155d578df6..e9d8a86466 100644 --- a/i18n/zh/pages/handlers/lifespan.md +++ b/i18n/zh/pages/handlers/lifespan.md @@ -1,6 +1,6 @@ --- translation: - sections: [f3ca8ac5f90f2dfa, 85a1ef3588ba0736, 563346d4d5804933, 9e3528340d0bab53] + sections: [f3ca8ac5f90f2dfa, 48e478ef7bd688b1, 563346d4d5804933, 52ac6a7734d6f581] tool: 1 --- # 生命周期 {#lifespan} @@ -46,7 +46,7 @@ translation: `genre` 是模型唯一能传入的参数。生命周期是服务器自己的事。 -`@mcp.resource()` 和 `@mcp.prompt()` 函数也可以接收 `ctx` 参数,只是要写成裸的 `Context`,原因下一节会讲到。`ctx` 携带的所有内容详见 **[Context](context.md)**。 +`@mcp.resource()` 和 `@mcp.prompt()` 函数也可以接收 `ctx` 参数。`ctx` 携带的所有内容详见 **[Context](context.md)**。 ### 它确实带类型 {#it-really-is-typed} @@ -56,15 +56,6 @@ translation: 如果改写成裸的 `Context`,`lifespan_context` 的类型就是 `dict[str, Any]`:类型检查器无从知道你的生命周期 yield 了什么。运行时对象还在,只是失去了类型上的帮助。 -!!! warning - `Context[AppContext]` 是**仅限工具**的写法。把它放在 `@mcp.resource()` 或 `@mcp.prompt()` 函数上,对该处理函数的每次调用都会失败。客户端会收到一个错误,服务器日志会说明原因: - - ```text - Context is not available outside of a request - ``` - - 在资源和提示词里,写裸的 `ctx: Context`。生命周期 yield 出的对象在运行时仍然是 `ctx.request_context.lifespan_context`;你放弃的是类型参数,不是对象。 - !!! tip 生命周期总是存在。如果你不传,SDK 的默认实现会 yield 一个空 `dict`,所以 `ctx.request_context.lifespan_context` 是 `{}`,绝不会是 `None`。也正是因为这个默认值,裸的 `Context` 才把它的类型定为 `dict[str, Any]`。 @@ -95,7 +86,7 @@ translation: * `yield` 之前的代码是启动。之后的 `finally` 是关闭。 * 它只运行一次,围绕服务器的整个生命,而不是每个请求一次。 * 无论 `yield` 出什么,它在每个工具、资源和提示词里都是 `ctx.request_context.lifespan_context`。 -* `ctx: Context[AppContext]` 让这种访问在工具里完全带类型。资源和提示词用裸的 `Context`。 +* `ctx: Context[AppContext]` 让这种访问完全带类型。 * 不传 `lifespan=` 意味着一个空 `dict`,绝不会是 `None`。 在调用中途停下来,向用户询问只有他们知道的事情的处理函数,详见 **[征询(elicitation)](elicitation.md)**。 diff --git a/i18n/zh/pages/handlers/progress.md b/i18n/zh/pages/handlers/progress.md index 1461f326af..b28656c0eb 100644 --- a/i18n/zh/pages/handlers/progress.md +++ b/i18n/zh/pages/handlers/progress.md @@ -1,6 +1,6 @@ --- translation: - sections: [5315262fe26b33e1, 9d8e98840f1b78f0, 52d6009a07e770ea, 8534d8dbb4053a70, 2966fac6fe697007] + sections: [5315262fe26b33e1, 9d8e98840f1b78f0, 52d6009a07e770ea, 8534d8dbb4053a70, 2e9aff14d3a882c0] tool: 1 --- # 进度 {#progress} @@ -111,4 +111,4 @@ Imported https://example.com/b.json (2.0/2.0) * 调用上没有回调,`report_progress` 就什么都不做。无条件地报告即可。 * 不知道 `total` 就省略;回调拿到的是 `None`。 -进度是运行中的工具展示给**用户**看的。它为**你**——运维这台服务器的人——记录的那些日志行走的是另一条通道:**[日志](logging.md)**。 +进度是给还在等待的客户端看的。客户端不再等待时,你的工具看到的是 **[取消](cancellation.md)**。 diff --git a/i18n/zh/pages/handlers/subscriptions.md b/i18n/zh/pages/handlers/subscriptions.md index 2932921fba..5f4a8eb108 100644 --- a/i18n/zh/pages/handlers/subscriptions.md +++ b/i18n/zh/pages/handlers/subscriptions.md @@ -1,6 +1,6 @@ --- translation: - sections: [60a9de8a0bdaa531, 317bbe7e4355cdcc, a61d660c8029e04a, 8f7e82fcb88df8a9, b165db51249ff8ed, 266f56fb798068a4, 7c0e57030b622139, df18d7c2417a9883] + sections: [60a9de8a0bdaa531, 6693607ea56d8bd6, a61d660c8029e04a, 8f7e82fcb88df8a9, b165db51249ff8ed, b8bc624a627ead9b, 2139e68e36d9e621, 7c0e57030b622139, 34ab1af2b9ab5b45] tool: 1 --- # 订阅 {#subscriptions} @@ -22,7 +22,7 @@ translation: * 同类方法还有 `notify_prompts_changed()` 和 `notify_resources_changed()`。 * 没有订阅者,就没有开销。向空闲的服务器发布是空操作,所以永远不需要检查有没有人在听。只管声明什么变了。 -`MCPServer` 替你处理 `subscriptions/listen`。线路上的义务(第一帧是确认、按流过滤、每一帧都带订阅 id)是 SDK 的事。 +`MCPServer` 替你处理 `subscriptions/listen`,除非你[把它关掉](#turning-it-off)。线路上的义务(第一帧是确认、按流过滤、每一帧都带订阅 id)是 SDK 的事。 !!! check 在线路上,一个过滤器里指定了 `board://sprint` 的流,在 `complete_task` 运行之后是这样的: @@ -79,7 +79,32 @@ translation: 发布通过一个 `SubscriptionBus` 从你的处理函数传到打开的流。默认是内存内的:一个进程,里面的每一个流。在你把多个副本放到负载均衡器后面之前,这就是正确答案;因为到那时,客户端的流被固定在一个副本上,而另一个副本上的发布必须能到达它。 -这个接缝由你来实现:在你的 pub/sub 后端之上写两个方法。 +用默认总线做不到,因为每个副本都有自己的总线: + +```mermaid +flowchart LR + client[Client] --> lb[Load balancer] + lb --> stream + lb ~~~~ gap + lb --> tool + subgraph B [Replica B] + tool[tools/call] -- publishes --> busB[(bus B)] + end + gap[(no shared bus)] + subgraph A [Replica A] + stream[listen stream] -- subscribed --> busA[(bus A)] + end + style A fill:none + style B fill:none + style gap fill:none,stroke-dasharray:4 4 +``` + +什么都不会报错:调用成功,流却保持沉默。所以在负载均衡器后面,二选一: + +* **需要变更通知。** 给每个副本同一个总线,做法见下文。 +* **不需要。** [关掉变更通知](#turning-it-off),这样就不会有客户端被许诺了注定收不到的事件,也不会有客户端为这些事件一直开着流。 + +共享总线由你来实现:在你的 pub/sub 后端之上写两个方法。 ```python from collections.abc import Callable @@ -128,6 +153,20 @@ async def tools_reloaded() -> None: await bus.publish(ToolsListChanged()) # from a lifespan task, a webhook, anywhere ``` +## 关闭订阅 {#turning-it-off} + +目录从不变化的服务器没有什么可发布的。构建它的时候就说明这一点: + +```python title="server.py" hl_lines="3" +--8<-- "docs_src/subscriptions/tutorial007.py" +``` + +* `2026-07-28` 客户端看不到任何变更通知的声明,`subscriptions/listen` 请求得到的是“Method not found”,而不是一个打开的流。 +* `ctx.notify_*` 仍然可用,只是谁也送达不到,所以处理函数不用改。 +* 使用更早协议版本的客户端看不出任何差别。 + +打开的流是一个永远不会结束的请求,所以在按请求时长计费的托管平台上,这一点同样重要。 + ## 低层组合 {#the-low-level-composition} 在低层的 `Server` 上没有任何预先接好的东西,同样的部件三行就能组装起来: @@ -148,5 +187,6 @@ async def tools_reloaded() -> None: * 客户端这一端是 `async with client.listen(...)`:详见“客户端”下的 **[订阅](../client/subscriptions.md)**。 * 在低层的 `Server` 上你自己组装同样的部件:一个总线、`ListenHandler(bus)`、`on_subscriptions_listen` 槽位。 * 横向扩展意味着实现 `SubscriptionBus`,两个方法,然后作为 `MCPServer(subscriptions=...)` 传入。 +* 没有东西可发布,或者多个副本之间没有共享总线:`MCPServer(subscriptions=False)` 不声明任何变更通知,也不保持任何流。 运行提供这一切的服务器,不管是一个副本还是二十个,见 **[部署与扩展](../run/deploy.md)**。 diff --git a/i18n/zh/pages/run/deploy.md b/i18n/zh/pages/run/deploy.md index d6dc95d0dd..c04d1edb7a 100644 --- a/i18n/zh/pages/run/deploy.md +++ b/i18n/zh/pages/run/deploy.md @@ -1,6 +1,6 @@ --- translation: - sections: [28221886b198784f, f88ea1f1614f3a1d, 2e76f5cb9df15042, ce926d686730b6d0, 3be24f8ad8bb5ab9, 3fad24032b2224ff, f25a7f860e579ecb, 697b01d95080880d] + sections: [28221886b198784f, f88ea1f1614f3a1d, 2e76f5cb9df15042, ce926d686730b6d0, 3be24f8ad8bb5ab9, aaf489e944ecf5d1, f25a7f860e579ecb, 697b01d95080880d] tool: 1 --- # 部署与扩展 {#deploy-scale} @@ -156,7 +156,8 @@ python -c "import secrets; print(secrets.token_hex(32))" * 跨真正的进程时,**SDK 没有提供任何能帮上忙的总线。** `SubscriptionBus` 是一个两方法的 `Protocol`(`publish` 和 `subscribe`),你在自己的 pub/sub 后端(Redis、NATS,或任何你已经在跑的东西)上实现它,并作为 `MCPServer(subscriptions=...)` 传入。**[订阅](../handlers/subscriptions.md#scaling-past-one-process)** 有示意代码和契约。 * 总线承载的是四种小的有类型事件,从来不是 JSON-RPC。确认、过滤和流的生命周期都留在 SDK 里,所以你的总线不可能破坏协议;它只能在进程之间搬运事件。 -* 流**不可**恢复,事件**不会**重放。丢失一个副本就丢掉它的流;客户端重新监听、重新获取。没有需要共享的事件存储,也没有别的需要配置。这是横向扩展真正只是"多来几份"的唯一一处。 +* 流**不可**恢复,事件**不会**重放。丢失一个副本就丢掉它的流;客户端重新监听、重新获取。没有需要共享的事件存储,也没有别的需要配置。这是横向扩展真正只是“多来几份”的唯一一处。 +* 不需要变更通知的服务器可以跳过总线:**[把它们关掉](../handlers/subscriptions.md#turning-it-off)**。 ## SDK 不提供什么 {#what-the-sdk-does-not-give-you} diff --git a/i18n/zh/pages/servers/structured-output.md b/i18n/zh/pages/servers/structured-output.md index ededb26119..706aac22f7 100644 --- a/i18n/zh/pages/servers/structured-output.md +++ b/i18n/zh/pages/servers/structured-output.md @@ -1,6 +1,6 @@ --- translation: - sections: [a838d57f003aed44, 857d03886a0137ed, 42d9efcb9f542867, 2290ff08435b5573, 91be9b73602abcf1, 6cdbad079f7b47f0, d4b607372fb28b51, 18dbf726ac45e0b7, c7eff2a5698225fa, c851964bb3301907, 8f296f1f09e4c400, d715db6f8dccc9cc, a0c344a48450dbe4] + sections: [a838d57f003aed44, 857d03886a0137ed, 42d9efcb9f542867, 2290ff08435b5573, 91be9b73602abcf1, 6cdbad079f7b47f0, d4b607372fb28b51, 7608fc5ebc31d6ea, c7eff2a5698225fa, c851964bb3301907, 8f296f1f09e4c400, d715db6f8dccc9cc, a0c344a48450dbe4] tool: 1 --- # 结构化输出 {#structured-output} @@ -172,6 +172,8 @@ result.structured_content # {"London": 16.2, "Reykjavik": 4.4} 键必须是 `str`。`dict[int, float]` 成不了 JSON 对象,所以会退回到 `{"result": ...}` 包装。 +字典结果使用 Pydantic 的 `TypeAdapter` 做校验和序列化。如果查看某个工具的 `FuncMetadata.output_model`,会发现它保存的是带有模式标题的字典类型注解。 + ## 校验 {#validation} `output_schema` 并非只是文档。函数返回的任何内容,在离开服务器之前都会**对照它校验**。 diff --git a/i18n/zh/pages/servers/tools.md b/i18n/zh/pages/servers/tools.md index f21f8cdc4c..eb7b330b9f 100644 --- a/i18n/zh/pages/servers/tools.md +++ b/i18n/zh/pages/servers/tools.md @@ -1,6 +1,6 @@ --- translation: - sections: [e4cc390d56573409, f30cf8103a6e918c, 2c97b9f888398951, 048e5471dfa71aea, 3076b1e16ad95950, edbedf2a16e71311, 3d8ef8da89fa87c1, f6c0e02e6ea5a363] + sections: [e4cc390d56573409, f30cf8103a6e918c, 2c97b9f888398951, 048e5471dfa71aea, 3076b1e16ad95950, c1115cd005b81e8f, 3d8ef8da89fa87c1, f6c0e02e6ea5a363] tool: 1 --- # 工具 {#tools} @@ -137,7 +137,7 @@ Inspector 会渲染出一个表单,里面有一个必填的 `query` 文本字 如果工具要做 I/O(调用 API、读文件、查数据库),就把它声明为 `async def`,并在里面 `await`。SDK 会 await 它。 -普通的 `def` 工具也可以:SDK 会在线程里运行它,所以它永远不会阻塞服务器。 +普通的 `def` 工具也可以:SDK 会在线程里运行它,所以它永远不会阻塞服务器。运行时间长的工具可以检查客户端是否还在等待;参见 **[取消](../handlers/cancellation.md)**。 没有别的需要配置。 diff --git a/i18n/zh/pages/troubleshooting.md b/i18n/zh/pages/troubleshooting.md index ee6f7a6b0f..217e976904 100644 --- a/i18n/zh/pages/troubleshooting.md +++ b/i18n/zh/pages/troubleshooting.md @@ -1,6 +1,6 @@ --- translation: - sections: [3d58228e81b99543, 170514ce901c4139, 17d61fad0a50d62b, 8a6e351ec756904d, 137454d469c867f5, 6392596bd6df54f0, 41126fa9c4fe432f, 480b6d7897e30ab4, d83bb682e708dde0, ebbed3449c499db4, 525cdf1755e29d4c, 30fd31be74169d9a, d2e88333d4f7841f, c2dc3b1007d2e987, d6eabf60cc366341, f798e815252852c2, 0cba47bae78d04eb, 2c218ba829abf74e] + sections: [3d58228e81b99543, 170514ce901c4139, 17d61fad0a50d62b, 8a6e351ec756904d, 137454d469c867f5, fcf984fa0615ed11, 6392596bd6df54f0, 41126fa9c4fe432f, 480b6d7897e30ab4, d83bb682e708dde0, ebbed3449c499db4, 525cdf1755e29d4c, 30fd31be74169d9a, d2e88333d4f7841f, c2dc3b1007d2e987, d6eabf60cc366341, f798e815252852c2, 0cba47bae78d04eb, cdc6d86a4dae8a34] tool: 1 --- # 故障排查 {#troubleshooting} @@ -123,6 +123,14 @@ TypeError: The @tool decorator was used incorrectly. Did you forget to call it? !!! note 这个异常在模块**被导入**时抛出,早于任何客户端连接。所以如果宿主把你的服务器显示为“启动失败”(或“已断开”),而不是已连接但零个工具,就是这种情形:自己运行 `python server.py`,读 traceback。类型检查器也能抓到它:函数不是合法的 `name=`。 +## `InvalidSignature: Tool '' has an invalid x-mcp-header annotation: ` {#invalidsignature-tool-name-has-an-invalid-x-mcp-header-annotation-reason} + +某个工具参数标记 `x-mcp-header` 的方式不被规范允许,`` 会说明它违反了哪条规则。`2026-07-28` 上的客户端会把这样的工具从列表里漏掉,所以 SDK 拒绝注册它。 + +只有 `str`、`int` 和 `bool` 参数可以标记,而 `str | None` 不属于其中任何一种。可选参数的写法见 **[请求头参数](advanced/header-parameters.md)**。 + +和上一条一样,这个异常在模块**被导入**时抛出,早于任何客户端连接。 + ## `Tool already exists: ` {#tool-already-exists-name} 两次注册用了同一个工具名。**第一个**胜出,第二个被悄悄丢弃,**服务器日志**里的这条警告是唯一的信号: @@ -409,6 +417,7 @@ mcp = MCPServer("Weather", request_state_security=RequestStateSecurity(keys=[key * `ExceptionGroup: unhandled errors in a TaskGroup` 从来都不是真正的错误。读**最后一行**;在 `async with Client(...)` 块**内部**捕获 `MCPError` 可以完全跳过这层包装。 * `call_tool` 不会因为工具失败而抛异常。`Error executing tool ...` 和 `Unknown tool: ...` 是结果:检查 `result.is_error`。工具名后面没有消息表示它崩溃了,traceback 在服务器日志里。 * `Client must be used within an async context manager` -> 用 `async with`。`Use @tool() instead of @tool` -> 加上括号。 +* `has an invalid x-mcp-header annotation` -> 只有 `str`、`int` 和 `bool` 参数可以标记。 * 服务器日志里的 `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()`。 diff --git a/i18n/zh/pages/whats-new.md b/i18n/zh/pages/whats-new.md index d3445d690a..dc53761fca 100644 --- a/i18n/zh/pages/whats-new.md +++ b/i18n/zh/pages/whats-new.md @@ -1,6 +1,6 @@ --- translation: - sections: [cfe01c0c5863dfa2, 0dbb68d8b210177b, 80cf193023af3ed4, 875eb2889263424e] + sections: [cfe01c0c5863dfa2, 0dbb68d8b210177b, bd42ee3a268f6ea4, 875eb2889263424e] tool: 1 --- # v2 的新变化 {#whats-new-in-v2} @@ -196,7 +196,7 @@ v2 实现了 2026-07-28 修订版,并且同时服务 **两个** 修订版: ### 其余变化速览 {#the-rest-quickly} * **身份信息是可选的、按消息携带的元数据。** 请求侧的 `clientInfo` `_meta` 键是可选的(必需的一对是 `protocolVersion` + `clientCapabilities`),`serverInfo` 则从 `server/discover` 的结果体里搬了出来:服务器改为把它盖进每个 2026 版结果的 `_meta` 里([规范 #3002](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/3002))。SDK 总是会盖;服务器不表明身份时(比如某个中间件剥掉了这个键),`client.server_info` 就是 `None`。**[底层 Server](advanced/low-level-server.md)** 展示了线路上的这个印记。 -* **请求不用解析请求体就能路由。** 新版 HTTP 请求带有 `Mcp-Method`(对三个类似工具的调用,还有 `Mcp-Name`);用 `x-mcp-header` 注解的工具输入模式属性会被镜像成一个 `Mcp-Param-*` 头,并由服务器交叉核对([SEP-2243](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2243))。网关和限流器单凭请求头就能路由;规则见 **[迁移指南](migration.md#servers-validate-mcp-param-headers-against-the-request-body-sep-2243)**。 +* **请求不用解析请求体就能路由。** 新版 HTTP 请求带有 `Mcp-Method`(对三个类似工具的调用,还有 `Mcp-Name`);用 `x-mcp-header` 注解的工具输入模式属性会被镜像成一个 `Mcp-Param-*` 头,并由服务器交叉核对([SEP-2243](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2243))。网关和限流器单凭请求头就能路由。**[请求头参数](advanced/header-parameters.md)** 展示了如何标记参数。 * **结果带有缓存提示。** 列表和读取结果声明 `ttlMs` 和 `cacheScope`([SEP-2549](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2549));用 `cache_hints=` 按方法设置它们,`Client` 则用内置的响应缓存来遵守它们。不发送提示的服务器(所有 2026 之前的服务器)看到的是完全相同、未经缓存的流量。**[缓存提示](client/caching.md)**。 * **扩展是一等公民。** 服务器和客户端在反向 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)** 按确切的消息文本编排。