You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
@@ -116,6 +116,17 @@ Der `_meta`-Block ist der Identitätsstempel des Servers: Das SDK fügt ihn jede
116
116
117
117
Der Server vergleicht die beiden Felder nie. Der `Client` dieses SDK schon: Gibst du `structured_content` zurück, das das von dir deklarierte `output_schema` nicht erfüllt, löst `call_tool` einen `RuntimeError` aus, der mit `Invalid structured content returned by tool search_books` beginnt und dann den `jsonschema`-Fehler zitiert. Ein Schema zu versprechen ist billig; es einzuhalten liegt bei dir. Die ganze Stufenleiter der Rückgabetypen und Schemas steht in **[Strukturierte Ausgabe](../servers/structured-output.md)**.
118
118
119
+
## Der Dialekt ist JSON Schema 2020-12 {#the-dialect-is-json-schema-2020-12}
120
+
121
+
`input_schema` und `output_schema` sind JSON Schema, und die [MCP-Spezifikation](https://modelcontextprotocol.io/specification/latest/basic#json-schema-usage) legt den Dialekt fest: Ein Schema ohne `$schema`-Schlüssel ist **JSON Schema 2020-12**. Die Schemas, die `MCPServer` generiert, verlassen sich auf diesen Standardwert (Pydantic schreibt 2020-12 und lässt den Schlüssel weg), und ein von Hand geschriebenes dict wird ebenfalls daran gemessen. Das volle 2020-12-Vokabular steht also zur Verfügung:
122
+
123
+
```python title="server.py" hl_lines="8 14-15"
124
+
--8<--"docs_src/lowlevel/tutorial007.py"
125
+
```
126
+
127
+
* Die Wurzel von `input_schema` muss `"type": "object"` sein. Daneben erreichen `oneOf`, `additionalProperties`, `anyOf`, `if`/`then`/`else`, `prefixItems`, `$defs` mit lokalen `$ref`s und die übrigen 2020-12-Schlüsselwörter den Client genau so, wie du sie geschrieben hast.
128
+
* Ein `$schema`-Schlüssel ist nicht nötig. Füge einen nur hinzu, um einen älteren Draft zu wählen: Der `Client` dieses SDK, der `structured_content` gegen das `output_schema` eines Tools validiert, wählt seinen Validator anhand von `$schema` und verwendet 2020-12, wenn keiner vorhanden ist.
129
+
119
130
## `_meta`: für die Anwendung, nicht für das Modell {#\_meta-for-the-application-not-the-model}
120
131
121
132
`content` ist der Teil der Antwort, den das Modell liest. `structured_content` ist dieselbe Antwort als typisierte Daten. `_meta` ist der dritte Kanal: Daten, die mit dem Ergebnis für die **Client-Anwendung** mitreisen, ohne überhaupt Teil der Antwort zu sein.
@@ -167,7 +178,7 @@ Der Konstruktor deckt die Methoden ab, die MCP definiert. `add_request_handler`
167
178
--8<--"docs_src/lowlevel/tutorial006.py"
168
179
```
169
180
170
-
* Das erste Argument ist der Methoden-String. Benachrichtigungen haben ein Gegenstück, `add_notification_handler`.
181
+
* Das erste Argument ist der Methoden-String. Benachrichtigungen haben ein Gegenstück, `add_notification_handler`. Dessen Handler feuern auf stdio und auf HTTP-Verbindungen der Handshake-Generation; auf dem Streamable-HTTP-Pfad von `2026-07-28` wird der Benachrichtigungs-POST eines Clients mit `202` quittiert und nicht zugestellt, weil diese Revision keine Benachrichtigungen vom Client zum Server über HTTP definiert.
171
182
*`params_type` ist das Modell, gegen das die eingehenden `params` validiert werden, **bevor** dein Handler läuft – eigene Methoden bekommen also *doch* die Validierung, die Tools nicht bekommen. Leite von `RequestParams` ab, damit das Feld `_meta` so geparst wird wie bei jeder anderen Methode.
172
183
* Der Handler gibt ein `BaseModel`, ein `dict` oder `None` zurück. Das SDK serialisiert es in das JSON-RPC-Ergebnis.
@@ -27,9 +27,10 @@ Der Server oben ist nur da, damit du etwas hast, womit du dich verbinden kannst.
27
27
28
28
* Eine Instanz von `MCPServer` (oder des Low-Level-`Server`): Verbindung **im selben Prozess**.
29
29
* Ein URL-String (`Client("http://localhost:8000/mcp")`): Streamable HTTP, der Weg für die Produktion.
30
-
* Ein **Transport**: alles, was sich mit `async with ... as (read, write)` verwenden lässt, etwa `stdio_client(...)` um einen Subprozess herum.
30
+
* Ein `StdioServerParameters`: der Befehl, der als **Subprozess** gestartet wird und mit dem über dessen stdin und stdout gesprochen wird.
31
+
* Ein **Transport**: alles, was sich mit `async with ... as (read, write)` verwenden lässt, etwa `streamable_http_client(url, http_client=...)` um deinen eigenen HTTP-Client herum.
31
32
32
-
Alles Übrige auf dieser Seite ist in allen drei Fällen identisch. Header, Subprozesse, Timeouts und das `Transport`-Protokoll haben ihre eigene Seite: **[Client-Transporte](transports.md)**.
33
+
Alles Übrige auf dieser Seite ist in allen vier Fällen identisch. Header, Subprozesse, Timeouts und das `Transport`-Protokoll haben ihre eigene Seite: **[Client-Transporte](transports.md)**.
33
34
34
35
### Was ein verbundener Client mitbringt {#whats-on-a-connected-client}
35
36
@@ -85,7 +86,7 @@ Dieses Schema ist alles, was eine UI braucht, um ein Argumentformular zu rendern
85
86
86
87
`call_tool(name, arguments)` führt das Tool aus und gibt dir ein `CallToolResult` zurück.
87
88
88
-
```python title="client.py" hl_lines="26-33"
89
+
```python title="client.py" hl_lines="27-34"
89
90
--8<--"docs_src/client/tutorial003.py"
90
91
```
91
92
@@ -117,17 +118,18 @@ Ein Tool, das eine Exception auslöst, löst in deinem Client **keine** aus. Es
117
118
118
119
!!! check
119
120
Frag `lookup_book` nach `"Solaris"` (einem Titel, der nicht im Katalog steht), und die Funktion löst
120
-
`ValueError` aus. Der Aufruf kehrt trotzdem normal zurück:
121
+
`ToolError` aus. Der Aufruf kehrt trotzdem normal zurück:
121
122
122
123
```python
123
124
result.is_error # True
124
125
result.content # [TextContent(type='text', text="Error executing tool lookup_book: No book titled 'Solaris' in the catalog.")]
125
126
result.structured_content # None
126
127
```
127
128
128
-
Die Meldung der Exception ist in `content` gelandet, wo das **Modell** sie lesen und es erneut versuchen kann. Das
129
-
ist Absicht: Ein Tool-Fehler ist Teil des Gesprächs, kein Absturz. Sieh dir immer `is_error` an,
130
-
bevor du `structured_content` vertraust.
129
+
Die Meldung des `ToolError` ist in `content` gelandet, wo das **Modell** sie lesen und es erneut versuchen kann. Das
130
+
ist Absicht: Ein Tool-Fehler ist Teil des Gesprächs, kein Absturz. (Wäre das Tool mit einer
131
+
anderen Exception abgestürzt, stünde in `content` nur `Error executing tool lookup_book`.) Sieh dir immer
132
+
`is_error` an, bevor du `structured_content` vertraust.
131
133
132
134
!!! warning
133
135
`is_error=True` deckt mehr ab als dein eigenes `raise`. Frag nach einem Tool, das der Server gar nicht hat
@@ -87,15 +87,15 @@ oder übergibst deinem `httpx2.AsyncClient` ein explizites `verify=ssl_context`
87
87
88
88
Ein **stdio**-Server ist ein Subprozess. Der Client startet ihn, schreibt JSON-RPC in seine stdin und liest JSON-RPC aus seiner stdout. So betreibt ein Desktop-Host einen Server auf deinem Rechner: Ein Host *ist* dieser Code plus eine UI, und **[Mit einem echten Host verbinden](../get-started/real-host.md)** zeigt dieselbe Beziehung von der Seite des Hosts, als Konfigurationsdatei.
89
89
90
-
Beschreibe den Prozess mit `StdioServerParameters`, mach daraus mit `stdio_client` einen Transport und übergib *den* an `Client`:
90
+
Beschreibe den Prozess mit `StdioServerParameters`und übergib das Objekt an `Client`:
91
91
92
-
```python title="client.py" hl_lines="4-8 12"
92
+
```python title="client.py" hl_lines="3-7 11"
93
93
--8<--"docs_src/client_transports/tutorial004.py"
94
94
```
95
95
96
-
`Client` akzeptiert das Parameter-Objekt allein nicht. `StdioServerParameters` ist Konfiguration; `stdio_client(server)` ist der Transport, der weiß, wie er daraus einen Prozess startet. Immer einpacken.
96
+
Beim Eintreten in den Block wird der Prozess gestartet. Beim Verlassen wird der Subprozess beendet: stdin schließen, warten, abschießen, falls er hängen bleibt. Du räumst ihn nie selbst auf.
97
97
98
-
Beim Verlassen des `async with`-Blocks wird auch der Subprozess beendet: stdin schließen, warten, abschießen, falls er hängen bleibt. Du räumst ihn nie selbst auf.
98
+
Die stderr des Kindprozesses landet in deiner. Um sie woandershin zu leiten, baust du den Transport selbst mit `stdio_client` (aus `mcp`) und übergibst stattdessen diesen: `Client(stdio_client(server, errlog=log_file))`.
99
99
100
100
!!! warning
101
101
Der Kindprozess erbt **nicht** deine Umgebung. Er bekommt eine minimale Allow-List (`HOME`, `LOGNAME`,
@@ -113,16 +113,16 @@ Beim Verlassen des `async with`-Blocks wird auch der Subprozess beendet: stdin s
113
113
114
114
Für `Client` ist alles oben Genannte dasselbe.
115
115
116
-
Ein **Transport** ist ein beliebiger asynchroner Kontextmanager, der ein `(read, write)`-Paar von Nachrichten-Streams liefert: formal das `Transport`-Protokoll in `mcp.client`. `Client` löst sein Argument nach Typ auf: Ein Server-Objekt verbindet im Prozess, ein `str` wird zu `streamable_http_client(url)`, und alles andere wird direkt als Transport betreten. Diese letzte Regel ist der Grund, warum `stdio_client(...)`, `streamable_http_client(...)` und `sse_client(...)` alle in denselben Platz passen – und warum du deinen eigenen schreiben kannst.
116
+
Ein **Transport** ist ein beliebiger asynchroner Kontextmanager, der ein `(read, write)`-Paar von Nachrichten-Streams liefert: formal das `Transport`-Protokoll in `mcp.client`. `Client` löst sein Argument nach Typ auf: Ein Server-Objekt verbindet im Prozess, ein `str` wird zu `streamable_http_client(url)`, ein `StdioServerParameters` wird zu `stdio_client(params)`, und alles andere wird direkt als Transport betreten. Diese letzte Regel ist der Grund, warum `stdio_client(...)`, `streamable_http_client(...)` und `sse_client(...)` alle in denselben Platz passen – und warum du deinen eigenen schreiben kannst.
117
117
118
118
## Zusammenfassung {#recap}
119
119
120
120
*`Client(mcp)` (das Server-Objekt) verbindet im Speicher. Nutze es für Tests und zum Einbetten.
121
121
*`Client("http://.../mcp")` (eine URL) verbindet über Streamable HTTP, den Produktions-Transport.
122
122
* 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=`.
123
-
* stdio ist `Client(stdio_client(StdioServerParameters(...)))`, nie das Parameter-Objekt allein.
123
+
* stdio ist `Client(StdioServerParameters(...))`. Pack es nur dann selbst in `stdio_client(...)` ein, wenn du die stderr des Kindprozesses umleiten willst.
124
124
* Der Subprozess bekommt eine Umgebung per Allow-List, nicht deine; `env=` ergänzt sie.
125
-
* Ein Transport ist alles, womit du `async with x as (read, write)` schreiben kannst. Alles, was weder Server-Objekt noch URL ist, reicht `Client` direkt an dieses Protokoll weiter.
125
+
* Ein Transport ist alles, womit du `async with x as (read, write)` schreiben kannst. Alles, was weder Server-Objekt noch URL noch `StdioServerParameters`ist, reicht `Client` direkt an dieses Protokoll weiter.
126
126
* Das Erzeugen eines `Client` wählt den Transport. `async with` öffnet ihn.
127
127
128
128
Sobald der Transport offen ist, müssen sich beide Seiten auf eine Protokollversion einigen. Normalerweise denkst du nie darüber nach; wenn doch, ist **[Protokollversionen](../protocol-versions.md)** die richtige Seite.
0 commit comments