docs: publish translated docs in twelve languages and the tool that maintains them (#3280)

This commit is contained in:
Max 2026-08-14 17:07:34 +01:00 • committed by GitHub
commit 52ad0a8876
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 89737 additions and 54 deletions

View file

@ -0,0 +1,130 @@
---
translation:
sections: [72f9c964769076dd, 9a2c14e10935b515, 235299eb78ab12d7, 8aee1e78c8237fb8, 9bd86acd4112138f, 55343cb7f250dc7b]
tool: 1
---
# Vervollständigungen {#completions}
Ein Client, der eine UI auf deinem Server aufbaut, möchte Argumentwerte automatisch vervollständigen, während die Person tippt: Sprachnamen, Repository-Namen, Dateipfade.
Mit **Vervollständigungen** liefert dein Server diese Vorschläge.
## Etwas zum Vervollständigen {#something-worth-completing}
Vervollständigungen gibt es für genau zwei Dinge: die Argumente eines **Prompts** und die Parameter eines **Ressourcen-Templates**. Beginne also mit einem Server, der von beidem eines hat:
```python title="server.py" hl_lines="6 12"
--8<-- "docs_src/completions/tutorial001.py"
```
Noch hat hier nichts mit Vervollständigungen zu tun.
* `review_code` nimmt eine `language` entgegen. Niemand sollte raten müssen, welche Schreibweisen du akzeptierst.
* `github_repo` nimmt einen `owner` und ein `repo` entgegen. Freitextfelder für beide ergeben ein schlechtes Formular.
## Der Vervollständigungs-Handler {#the-completion-handler}
Füge **eine** mit `@mcp.completion()` dekorierte Funktion hinzu:
```python title="server.py" hl_lines="21-29"
--8<-- "docs_src/completions/tutorial002.py"
```
* Es gibt einen Handler pro Server. Jeder Vervollständigungs-Request landet hier, und du verzweigst danach, was gerade vervollständigt wird.
* Er muss mit `async def` definiert sein: Das SDK wartet per await auf ihn.
* Er erhält drei Argumente:
* `ref`: um *welchen* Prompt oder welches Ressourcen-Template es geht, als `PromptReference` oder `ResourceTemplateReference`. Mit `isinstance` unterscheidest du die beiden.
* `argument`: `argument.name` ist das Argument, das vervollständigt wird, `argument.value` das, was die Person bisher getippt hat.
* `context`: die bereits aufgelösten Argumente. Ignoriere es vorerst.
* Du gibst eine `Completion(values=[...])` zurück, oder `None`, wenn du nichts anzubieten hast.
!!! tip
`argument.value` ist das Präfix, das die Person getippt hat. Das SDK filtert **nicht** für dich: Was
immer du in `values` packst, zeigt die UI an. Das `startswith` schreibst du selbst.
### Ausprobieren {#try-it}
Steuere ihn mit dem In-Memory-`Client` aus **[Testen](../get-started/testing.md)** an. Rufe
`client.complete()` mit `ref=PromptReference(name="review_code")` und
`argument={"name": "language", "value": "py"}` auf:
```python
result.completion.values # ['python']
```
* `ref` ist derselbe Referenztyp, den dein Handler erhält.
* `argument` ist ein einfaches dict mit genau zwei Schlüsseln, `name` und `value`.
Schickst du ein leeres `value`, bekommst du die ganze Liste zurück. `lang.startswith("")` ist für jede Sprache wahr:
```python
result.completion.values # ['go', 'javascript', 'python', 'rust', 'typescript']
```
Fragst du nach `code` (einem Argument, das dein Handler nicht kennt), gibt er `None` zurück, was das SDK in eine leere Liste verwandelt:
```python
result.completion.values # []
```
`None` bedeutet *„keine Vorschläge“*, nie einen Fehler. Eine UI fällt auf ein einfaches Textfeld zurück.
## Eine Capability, die du nie deklariert hast {#a-capability-you-never-declared}
Den Handler zu registrieren ist die Deklaration. Verbinde einen Client und sieh nach:
```python
client.server_capabilities.completions # CompletionsCapability()
```
Du hast `completions` nirgends aufgeführt. Das SDK hat den Handler gesehen und die Capability für dich deklariert. Jede *optionale* Capability funktioniert so: Der Handler ist die Deklaration. (Die drei Primitive sind nicht optional: `MCPServer` deklariert sie immer, mit oder ohne Handler.)
!!! check
Geh zurück zur ersten `server.py` (der ohne Handler) und frage trotzdem. Der Aufruf schlägt
mit einem JSON-RPC-Fehler fehl:
```text
Method not found
```
Und `client.server_capabilities.completions` ist `None`. Genau dafür ist die Capability da: Ein
Client, der sich korrekt verhält, prüft sie und schickt den Request, den du nicht beantworten kannst, gar nicht erst.
## Abhängige Argumente {#dependent-arguments}
`github://repos/{owner}/{repo}` hat zwei Parameter, und die sinnvollen Werte für `repo` hängen davon ab, welcher `owner` zuerst gewählt wurde.
Dafür ist `context` da. Es trägt die Argumente, die die Person **bereits aufgelöst** hat:
```python title="server.py" hl_lines="8-11 34-38"
--8<-- "docs_src/completions/tutorial003.py"
```
* Der neue Zweig greift beim Parameter `repo` des Templates.
* `context.arguments` ist ein `dict[str, str] | None` mit den bisher gewählten Werten (hier `owner`).
* Noch kein `owner` bedeutet keine sinnvollen Vorschläge, also gibt der Handler `None` zurück.
Der Client schickt diese aufgelösten Werte mit `context_arguments=`. Diesmal ist `ref` eine
`ResourceTemplateReference(uri="github://repos/{owner}/{repo}")`. Frage mit leerem
`value` nach `repo` und übergib `context_arguments={"owner": "modelcontextprotocol"}`:
```python
result.completion.values # ['python-sdk', 'typescript-sdk', 'inspector']
```
Lässt du `context_arguments=` weg, gibt derselbe Aufruf `[]` zurück. Der Handler kann nicht wissen, welche Repos er anbieten soll, solange er den Owner nicht kennt.
!!! info
`Completion` nimmt außerdem `total=` und `has_more=` entgegen. Setze sie, wenn `values` ein Ausschnitt einer
längeren Liste ist, damit eine UI *„und 200 weitere“* anzeigen kann. Die meisten Handler brauchen sie nie.
## Zusammenfassung {#recap}
* Vervollständigungen sind Vorschläge für **Prompt-Argumente** und **Parameter von Ressourcen-Templates**. Sonst nichts.
* `@mcp.completion()` registriert den einen Handler. Er ist `async def (ref, argument, context) -> Completion | None`.
* Verzweige nach `isinstance(ref, ...)` und nach `argument.name`. Filtere selbst nach `argument.value`.
* `None` wird zu einer leeren Liste. Es ist nie ein Fehler.
* `context.arguments` enthält die bereits aufgelösten Werte; der Client liefert sie als `context_arguments=`.
* Die Capability `completions` erscheint, sobald du den Handler registrierst. Ohne ihn endet der Request mit `Method not found`.
Vorschläge helfen, solange die Person einen Prompt oder ein Template noch *ausfüllt*; um ihr *mitten* in einem Tool-Aufruf eine Frage zu stellen, brauchst du **[Elicitation](../handlers/elicitation.md)** (Rückfrage bei der Person am Host). Alles, was ein Tool außer Text zurückgeben kann, steht in **[Bilder, Audio und Icons](media.md)**.

View file

@ -0,0 +1,139 @@
---
translation:
sections: [e33d441f12d50535, 7099694c603e0f5f, c1df4cf9673433e6, c9cd294541422e6e, 6cec073617bfd037, efa92b8f99e908c8, 6a22a29e27fb4601]
tool: 1
---
# Fehler behandeln {#handling-errors}
Ein Tool kann auf zwei Arten scheitern, und das SDK behandelt sie sehr unterschiedlich.
Löse eine gewöhnliche Exception aus, und das **Modell** sieht sie. Löse `MCPError` aus, und das **Protokoll** sieht sie.
Auf dieser Seite geht es um die Wahl zwischen beiden.
## Ein Fehler, den das Modell beheben kann {#an-error-the-model-can-fix}
Nimm ein Tool, das etwas nachschlägt, und lass das Nachschlagen ins Leere laufen:
```python title="server.py" hl_lines="11-12"
--8<-- "docs_src/handling_errors/tutorial001.py"
```
An diesen zwei Zeilen ist nichts MCP-Spezifisches. `get_author` löst einen schlichten `ValueError` aus, so wie es jede Python-Funktion täte.
Ruf es mit einem Titel auf, der nicht im Katalog steht, und sieh dir das Ergebnis an:
```python
result.is_error # True
result.content # [TextContent(text="Error executing tool get_author: No book titled 'Nothing' in the catalog.")]
result.structured_content # None
```
* Der Request war **erfolgreich**. Es gibt ein Ergebnis; beim Aufrufer wurde nichts ausgelöst.
* `is_error` ist `True`, und die Meldung deiner Exception (mit dem Tool-Namen als Präfix) steht in `content` – genau dort, wo das Modell liest.
* `structured_content` ist `None`. Ein fehlgeschlagener Aufruf hat keinen Rückgabewert, den man strukturieren könnte.
Das ist ein **Tool-Fehler**, und er ist der Standard für *jede* Exception, die dein Tool auslöst. Fast immer ist es auch genau das, was du willst.
Das Modell ist es, das dein Tool aufruft. Es hat die Argumente gewählt. Ein Tool-Fehler ist also ein Zug im Gespräch: Das Modell liest *„No book titled 'Nothing' in the catalog.“*, merkt, dass es den Titel falsch geraten hat, und ruft erneut mit einem besseren auf. Du hast ein einziges `raise` geschrieben und einen sich selbst korrigierenden Agenten bekommen.
!!! tip
Gib aus einem Tool nie eine Fehlermeldung per `return` zurück. Ein zurückgegebener String hat `is_error=False`;
für das Modell (und für jede Client-UI) sieht es also aus, als hätte das Tool funktioniert und dieser String
wäre die Antwort. `raise`. Das Flag ist das Signal.
## Ein Fehler, den das Modell nicht beheben kann {#an-error-the-model-cannot-fix}
Tausche jetzt `ValueError` gegen `MCPError`.
```python title="server.py" hl_lines="1 3 14"
--8<-- "docs_src/handling_errors/tutorial002.py"
```
`MCPError` ist der **Protokollfehler** des SDK. Es ist die eine Exception, die der Tool-Wrapper *nicht* abfängt: Sie wird weitergereicht, und der ganze `tools/call`-Request schlägt mit einem JSON-RPC-Fehler fehl statt mit einem Ergebnis zu enden.
```json
{
"code": -32602,
"message": "No book titled 'Nothing' in the catalog."
}
```
* Es gibt **kein Ergebnis**. Kein `content`, kein `is_error`: nichts, was das Modell lesen könnte.
* Stattdessen bekommt die **Host**-Anwendung den Fehler – genauso, als gäbe es das Tool gar nicht.
* `code`, `message` und `data` kommen unverändert an. `INVALID_PARAMS` ist `-32602`; `mcp.types` exportiert ihn und die anderen JSON-RPC-Fehlercodes (`INVALID_REQUEST`, `INTERNAL_ERROR`, ...) als Konstanten, sodass du nie eine magische Zahl tippen musst.
!!! check
Dasselbe Nachschlagen, derselbe Fehlschlag, aber jetzt *löst* der Aufruf auf der Client-Seite eine Exception *aus*, statt zurückzukehren:
```text
mcp.shared.exceptions.MCPError: No book titled 'Nothing' in the catalog.
```
Die erste Version gab dem Modell einen Satz, auf den es reagieren konnte. Diese hier gibt ihm nichts.
Für `get_author` ist das eindeutig schlechter – und genau darum geht es im nächsten Abschnitt.
## Welche der beiden auslösen {#which-one-to-raise}
Die beiden Wege beantworten zwei verschiedene Fragen.
* **Löse irgendeine Exception aus** bei einem Fehlschlag der *Ausführung*: Das, was dein Tool versucht hat, hat nicht geklappt. Das Modell hat den Aufruf gewählt, also sollte das Modell die Folge sehen und die Chance bekommen, sich zu fangen. Ein falsch geschriebener Titel, eine vorgelagerte API mit Timeout, eine Zeile, die es nicht gibt: alles Tool-Fehler.
* **Löse `MCPError` aus**, wenn der *Request selbst* abgelehnt werden soll: Dem Client fehlt eine Capability, auf die dein Tool angewiesen ist, der Server ist nicht in einem Zustand, irgendwen zu bedienen, der Aufrufer hat einen erforderlichen Schritt übersprungen. Kein erneuter Versuch des Modells behebt irgendetwas davon, also bringt es nichts, ihm die Meldung zu geben.
Eine Frage entscheidet: **Hätte ein klügeres Modell das vermeiden können?** Ja -> gewöhnliche Exception. Nein -> `MCPError`.
Nach diesem Test hat die zweite Version von `get_author` die falsche Wahl getroffen: Ein besserer Titel behebt das Problem, also hätte das Modell die Meldung sehen sollen. Sie soll dir den Mechanismus zeigen, nicht ihn empfehlen.
!!! info
`MCPError` findest du unter `from mcp import MCPError`; sie nimmt `code`, `message` und eine optionale
`data`-Payload entgegen. Was immer du hineinlegst, bekommt der Client: Das SDK leitet eine ausgelöste
`MCPError` wortwörtlich weiter, statt sie zu bereinigen.
## Eine Ressource, die es nicht gibt {#a-resource-that-doesnt-exist}
Ressourcen ziehen dieselbe Grenze und bringen für den häufigen Fall eine benannte Exception mit.
```python title="server.py" hl_lines="2 13"
--8<-- "docs_src/handling_errors/tutorial003.py"
```
`books://{title}` ist ein **Template**. Es passt auf *jeden* Titel, also sind „der URI ist wohlgeformt“ und „das Buch existiert“ zwei verschiedene Fragen, und nur deine Funktion kann die zweite beantworten.
Wenn sie das nicht kann, löse `ResourceNotFoundError` aus. Das SDK macht daraus den Protokollfehler, den die Spezifikation einer fehlenden Ressource zuordnet: `-32602` mit dem angeforderten URI in `data`, damit der Client weiß, *welcher* Lesevorgang fehlgeschlagen ist.
```json
{
"code": -32602,
"message": "No book titled 'Nothing' in the catalog.",
"data": {"uri": "books://Nothing"}
}
```
Beachte, dass es hier kein halbes Ergebnis mit `is_error=True` gibt. Das Lesen einer Ressource liefert entweder Inhalte oder schlägt fehl: Ressourcen haben nur den Protokollweg. Templates und alles Weitere zu Ressourcen stehen in **[Ressourcen](resources.md)**.
## Fehler, die du nie auslöst {#errors-you-never-raise}
Ein ungültiges Argument erreicht deine Funktion nie.
Schick `get_author` einen `title`, der kein String ist, und das SDK weist ihn anhand des Eingabeschemas ab, **bevor** es dich aufruft – als dieselbe Art Tool-Fehler mit `is_error=True`, den das Modell lesen und korrigieren kann. **[Tools](tools.md)** zeigt dieselbe Ablehnung mit einer `Field(le=50)`-Einschränkung.
Das bedeutet eine ganze Klasse von `raise`-Anweisungen, die du nicht schreibst: Validiere deine eigenen Type Hints nicht noch einmal.
!!! info
Alles auf dieser Seite ist das, was ein **Client** sieht, und der In-Memory-`Client`, mit dem du
Tests schreibst, sieht exakt dasselbe. Selbst `raise_exceptions=True` macht aus einem Tool-Fehler
keinen Traceback mehr: Bis dieses Flag greifen könnte, ist deine Exception längst das
Ergebnis mit `is_error=True`. Prüfe das Ergebnis mit Assertions. **[Testen](../get-started/testing.md)** beschreibt das Muster.
## Zusammenfassung {#recap}
* Löse **irgendeine Exception** in einem Tool aus -> der Aufruf gibt `is_error=True` mit deiner Meldung in `content` zurück. Das Modell liest sie und kann es erneut versuchen. Das ist der Standard.
* Löse **`MCPError`** aus -> der Aufruf selbst schlägt mit einem JSON-RPC-Fehler fehl. Das Modell sieht nichts; der Host kümmert sich darum. `code`, `message` und `data` kommen unverändert durch.
* Die entscheidende Frage: *Hätte ein klügeres Modell das vermeiden können?* Ja -> Exception. Nein -> `MCPError`.
* `ResourceNotFoundError` aus einem Ressourcen-Handler -> das `-32602` des Protokolls, mit dem URI in `data`.
* Ungültige Argumente werden anhand des Schemas abgewiesen, bevor deine Funktion läuft; dafür schreibst du kein `raise`.
* `from mcp import MCPError`; die Fehlercode-Konstanten kommen aus `mcp.types`.
Fehler behandelt. Das ist alles, was ein Server *nach außen anbietet*. Was jeder Handler lesen und während der Ausführung zurück an den Client tun kann, ist der nächste Abschnitt: **[Im Handler](../handlers/index.md)**.
Den genauen Wortlaut der SDK-Fehler, denen du am ehesten begegnest, was jeder bedeutet und wie du ihn jeweils mit einem Handgriff behebst, findest du unter **[Fehlerbehebung](../troubleshooting.md)**.

View file

@ -0,0 +1,39 @@
---
translation:
sections: [09defc170a0da89d]
tool: 1
---
# Server {#servers}
Ein `MCPServer` stellt einem verbundenen Client drei Primitive bereit. Sie
unterscheiden sich darin, wer über ihren Einsatz entscheidet:
* Ein **[Tool](tools.md)** ist eine Aktion, die das *Modell* auswählt und
aufruft. Diese Seite wollen die meisten zuerst lesen, und
**[Strukturierte Ausgabe](structured-output.md)** ist die zugehörige
Referenz: alles über die Form dessen, was ein Tool zurückgibt.
* Eine **[Ressource](resources.md)** sind schreibgeschützte Daten, die die
*Anwendung* zu lesen beschließt. **[URI-Templates](uri-templates.md)** ist
die zugehörige Referenz: die vollständige Adressierungssyntax und die Regeln
zur Pfadsicherheit.
* Ein **[Prompt](prompts.md)** ist eine Nachrichtenvorlage, die eine *Person*
beim Namen aufruft – über ein Menü oder einen Slash-Befehl.
Rund um die drei Primitive liegt der Rest dessen, was ein Server deklariert:
* **[Vervollständigungen](completions.md)** ist serverseitige
Autovervollständigung für Argumente von Prompts und Ressourcen-Templates.
* **[Bilder, Audio und Icons](media.md)** behandelt alles, was ein Tool
außer Text zurückgeben kann, sowie die Icons, die ein Client neben deinem
Server anzeigt.
* **[Fehler behandeln](handling-errors.md)** erklärt den Unterschied zwischen
einem Fehler, von dem sich das Modell erholen kann, und einem, den es nie zu
sehen bekommen darf.
Jede Seite hier steht für sich; spring direkt zu der, die du brauchst. Hast du
noch keinen Server gebaut, beginne stattdessen mit
**[Erste Schritte](../get-started/first-steps.md)**.
Was *innerhalb* der Funktionen passiert, die du registrierst (der `Context`,
Dependency Injection, die Person mitten im Aufruf um weitere Eingaben bitten),
ist Thema des nächsten Abschnitts, **[Im Handler](../handlers/index.md)**.

View file

@ -0,0 +1,122 @@
---
translation:
sections: [496394d24d221bf1, 4ceb4591180dc6c3, 0fd63e4682d02e0c, 969ede0bd3686a16, 043f526230dd243d, 6ee3e9bcfd24047a]
tool: 1
---
# Medien {#media}
Text ist nicht das Einzige, was ein Tool zurückgeben kann.
Das SDK bringt zwei Helfer für binäre Ergebnisse mit (**`Image`** und **`Audio`**) sowie einen Typ **`Icon`**, mit dem dein Server, deine Tools, Ressourcen und Prompts im UI des Clients ein Gesicht bekommen.
## Ein Bild zurückgeben {#returning-an-image}
Annotiere den Rückgabetyp als `Image`, zeige auf eine Datei und gib sie zurück:
```python title="server.py" hl_lines="8 12 14"
--8<-- "docs_src/media/tutorial001.py"
```
* `Image` nimmt genau eines von `path` (eine Datei, die gelesen wird) oder `data` (rohe Bytes).
* Den MIME-Typ, den der Client sieht, errät das SDK aus der Dateiendung: `logo.png` wird als `image/png` angekündigt.
* Nichts hiervon ist speziell für Logos. Jedes PNG neben `server.py` funktioniert: ein Diagramm, das dein Code gerendert hat, eine Skizze, ein Foto.
`Image` ist eine Bequemlichkeit des SDK, kein Protokolltyp. Auf der Leitung wird dein Rückgabewert zu einem **`ImageContent`**-Block (die Bytes der Datei base64-kodiert, dazu der MIME-Typ):
```python
result.content # [ImageContent(type="image", data="iVBORw0KGgoAAAANSUhEUg...", mime_type="image/png")]
result.structured_content # None
```
Zwei Dinge fallen auf:
* `data` ist base64. Du hast die Bytes nie angefasst; das SDK hat die Datei gelesen und kodiert.
* `structured_content` ist `None`. Ein `Image` ist Inhalt, den das Modell anschaut, keine Daten, die die Anwendung parst: Es gibt kein Output-Schema. (Vergleiche **[Strukturierte Ausgabe](structured-output.md)**, wo die Rückgabeannotation das Schema *ist*.)
!!! info
`ImageContent` und `AudioContent` liegen in `mcp.types`, direkt neben dem `TextContent`,
zu dem ein einfaches `str`-Ergebnis wird (**[Tools](tools.md)**). Ein Tool-Ergebnis ist eine Liste von Content-Blöcken; `Image` und `Audio` sind
der kürzeste Weg, die beiden binären Arten zu erzeugen.
### Ausprobieren {#try-it}
Lege ein beliebiges PNG neben `server.py`, nenne es `logo.png` und starte:
```console
uv run mcp dev server.py
```
Öffne den Tab **Tools** und rufe `logo` auf. Das Ergebnis ist kein String: Es ist ein Content-Block vom Typ `image`, und der Inspector rendert dein Bild. Alles zwischen der Datei auf der Platte und den Pixeln auf dem Bildschirm hat das SDK erledigt.
## Audio zurückgeben {#returning-audio}
`Audio` hat dieselbe Form. Lass `logo.png`, wo es war, und lege eine beliebige WAV-Datei als `chime.wav` daneben:
```python title="server.py" hl_lines="18-21"
--8<-- "docs_src/media/tutorial002.py"
```
Das Ergebnis ist ein **`AudioContent`**-Block:
```python
result.content # [AudioContent(type="audio", data="UklGR...", mime_type="audio/wav")]
result.structured_content # None
```
Dasselbe Prinzip: eine Datei auf der Platte hinein, base64 und ein MIME-Typ heraus, kein Output-Schema.
## Bytes oder eine Datei {#bytes-or-a-file}
Beide Helfer akzeptieren auch `data=` (rohe Bytes) statt `path=`. Das ist der Modus für Bytes, die nie aus einer eigenen Datei kamen – eine Datenbankspalte, eine HTTP-Response, etwas, das Pillow gerade gezeichnet hat:
```python title="server.py" hl_lines="14 15"
--8<-- "docs_src/media/tutorial003.py"
```
Mit `path=` gibt es nichts zu deklarieren: Die Datei wird gelesen, wenn das Ergebnis gebaut wird, und der MIME-Typ wird aus der Endung erraten:
* `Image`: `.png`, `.jpg`, `.jpeg`, `.gif`, `.webp`.
* `Audio`: `.wav`, `.mp3`, `.ogg`, `.flac`, `.aac`, `.m4a`.
Eine Endung, die nicht erkannt wird, fällt auf `application/octet-stream` zurück.
!!! check
Mit `data=` gibt es keinen Dateinamen, also nichts, woraus sich etwas erraten ließe. Vergisst du `format=`,
fällt das SDK auf einen Standardwert zurück: `image/png` für Bilder, `audio/wav` für Audio. Baust du so ein
`Audio` aus MP3-Bytes, bekommt der Client `mime_type="audio/wav"` mitgeteilt und scheitert dann
folgerichtig am Dekodieren. Wenn du `data=` übergibst, übergib auch `format=`.
## Icons {#icons}
Ein `Icon` ist Metadaten, kein Inhalt. Es trägt das Bild nicht; es zeigt per URI auf eines, und ein Client kann es abrufen und neben dem Namen deines Servers, einem Tool, einer Ressource oder einem Prompt anzeigen.
```python title="server.py" hl_lines="4-5 7 10 16"
--8<-- "docs_src/media/tutorial004.py"
```
* `src` ist ein URI, den der Client auflösen kann: `https:` oder ein `data:`-URI, wenn du das Icon ohne zusätzlichen Abruf einbetten willst.
* Mit `mime_type` und `sizes` (`"48x48"` oder `"any"` für ein skalierbares Format) kann der Client das passende auswählen, wenn du mehrere anbietest.
* `theme="light"` oder `theme="dark"` markiert ein Icon für ein Farbschema.
Dasselbe Keyword `icons=[...]` akzeptieren `MCPServer(...)`, `@mcp.tool()`, `@mcp.resource()` und `@mcp.prompt()`.
### Wo ein Client sie sieht {#where-a-client-sees-them}
Icons reisen mit dem, was sie schmücken. Die des Servers kommen an, wenn sich der Client verbindet, auf `client.server_info` (auf Verbindungen der 2026er-Generation optional, also grenze es zuerst ein):
```python
assert client.server_info is not None # python-sdk servers identify themselves by default
client.server_info.icons # [Icon(src="https://example.com/brand-kit.png", mime_type="image/png", sizes=["48x48"])]
```
Die Icons eines Tools liegen auf dem `Tool`-Objekt aus `tools/list`, die einer Ressource auf der `Resource` aus `resources/list`, die eines Prompts auf dem `Prompt` aus `prompts/list`. Das Feld heißt immer `icons`.
## Zusammenfassung {#recap}
* Gib ein `Image` oder `Audio` aus einem Tool zurück, und der Client empfängt einen `ImageContent`- bzw. `AudioContent`-Block: deine Bytes base64-kodiert, mit einem MIME-Typ.
* Baue eines aus einem `path=` und lass die Endung den MIME-Typ bestimmen, oder aus `data=` im Speicher plus einem expliziten `format=`.
* Medien-Ergebnisse tragen kein `structured_content` und kein Output-Schema.
* Ein `Icon` ist ein Zeiger: ein `src`-URI plus optional `mime_type`, `sizes` und `theme`.
* `icons=[...]` funktioniert auf dem Server, auf Tools, auf Ressourcen und auf Prompts, und Clients finden sie auf den passenden Objekten.
Das ist alles, was ein Tool *in* ein Ergebnis packen kann. Was passiert, wenn ein Tool *fehlschlägt* (und wer davon erfahren sollte), steht in **[Fehler behandeln](handling-errors.md)**.

View file

@ -0,0 +1,156 @@
---
translation:
sections: [d65c098f37f5b6c3, dd0c2724d6f2877e, 6835bb3570c6714c, ffe823cb0fedd488, f33651add1b59094]
tool: 1
---
# Prompts {#prompts}
Ein **Prompt** ist eine Nachrichtenvorlage, die die Person am Host auswählt.
Tools sind für das Modell gedacht. Ein Prompt ist das Gegenteil: Die Person wählt einen aus einem Menü in ihrem Client (ein Slash-Command, ein Button), füllt die Argumente aus, und die gerenderten Nachrichten landen in der Unterhaltung, als hätte sie sie selbst getippt.
Du deklarierst einen, indem du `@mcp.prompt()` auf eine Funktion setzt, die den Text zurückgibt.
## Dein erster Prompt {#your-first-prompt}
```python title="server.py" hl_lines="6-9"
--8<-- "docs_src/prompts/tutorial001.py"
```
Das SDK liest dieselben drei Dinge wie bei einem Tool:
* Der **Name** ist der Funktionsname: `review_code`.
* Die **Beschreibung**, die der Client anzeigt, ist der Docstring: `Review a piece of code.`
* Die **Argumente** stammen aus den Parametern. `code` hat keinen Standardwert, also ist es erforderlich.
Das bekommt ein Client von `prompts/list` zurück:
```json
{
"name": "review_code",
"description": "Review a piece of code.",
"arguments": [
{"name": "code", "required": true}
]
}
```
Hier gibt es kein JSON Schema. Prompt-Argumente sind eine flache Liste **benannter String-Werte**: ein Formular, das eine Person ausfüllt, keine Payload, die ein Modell zusammenbaut.
### Rendern {#rendering-it}
Der Client rendert die Vorlage mit `prompts/get` und übergibt dabei die Argumente. Deine Funktion läuft, und der `str`, den du zurückgibst, wird zu **einer einzigen User-Nachricht**:
```json
{
"description": "Review a piece of code.",
"messages": [
{
"role": "user",
"content": {
"type": "text",
"text": "Please review this code:\n\ndef add(a, b): return a + b"
}
}
],
"resultType": "complete"
}
```
Das ist der ganze Lebenslauf eines Prompts: unter seinem Namen aufgelistet, bei Bedarf gerendert, in den Chat eingefügt.
!!! check
`required` wird durchgesetzt, bevor deine Funktion läuft. Renderst du `review_code` ohne `code`,
schlägt der Request selbst mit einem JSON-RPC-Fehler (Code `-32603`) fehl:
```text
mcp.shared.exceptions.MCPError: Internal server error
```
Es gibt kein Fehlerergebnis im Stil eines Tools, das man einem Modell zurückgeben könnte, denn es ist
kein Modell beteiligt: Der Aufruf löst eine Exception aus. Der Grund (`Missing required arguments: {'code'}`)
landet im Log deines Servers.
### Ausprobieren {#try-it}
Starte den Server mit dem MCP Inspector:
```console
uv run mcp dev server.py
```
Öffne den Tab **Prompts** und wähle `review_code`. Der Inspector zeichnet ein Formular mit einem erforderlichen Feld `code`. Fülle es aus, rendere es, und du bekommst genau die User-Nachricht von oben zurück.
## Mehr als eine Nachricht {#more-than-one-message}
Ein Code-Review ist eine Nachricht. Eine Debugging-Sitzung ist eine Unterhaltung, und ein Prompt kann sie komplett anstoßen.
Gib eine Liste von Nachrichten statt eines `str` zurück:
```python title="server.py" hl_lines="2 13-20"
--8<-- "docs_src/prompts/tutorial002.py"
```
* `UserMessage` und `AssistantMessage` kommen aus `mcp.server.mcpserver.prompts.base`. Übergib ihnen einen `str`, und sie verpacken ihn für dich in `TextContent`. Die Rolle ist der Klassenname.
* `Message` ist ihre gemeinsame Basisklasse. Verwende sie als Rückgabeannotation.
Das Rendern von `debug_error` erzeugt jetzt drei Nachrichten, in dieser Reihenfolge:
```json
{
"description": "Start a debugging conversation.",
"messages": [
{"role": "user", "content": {"type": "text", "text": "I'm seeing this error:"}},
{"role": "user", "content": {"type": "text", "text": "TypeError: 'int' object is not iterable"}},
{
"role": "assistant",
"content": {"type": "text", "text": "I'll help debug that. What have you tried so far?"}
}
],
"resultType": "complete"
}
```
Beachte die letzte. Einen `assistant`-Beitrag vorzubelegen ist der Weg, die *nächste* Antwort des Modells zu lenken, ohne dass die Person die Lenkung selbst tippen muss.
## Titel und Argumentbeschreibungen {#titles-and-argument-descriptions}
`review_code` ist ein Funktionsname, keine Beschriftung. Gib dem Client etwas Besseres für den Button und beschreibe jedes Argument, damit sich das Formular von selbst erklärt:
```python title="server.py" hl_lines="10-13"
--8<-- "docs_src/prompts/tutorial003.py"
```
* `title="Code review"` ist der menschenlesbare Name, genau wie das `title` eines Tools.
* `Annotated[str, Field(description=...)]` ist dasselbe Muster, mit dem **[Tools](tools.md)** die Parameter eines Tools beschreibt. Hier landet die Beschreibung am Argument statt in einem Schema.
* `language` hat einen Standardwert und ist damit nicht mehr erforderlich.
Der `prompts/list`-Eintrag enthält jetzt alles, was ein Client braucht, um ein gutes Formular zu zeichnen:
```json
{
"name": "review_code",
"title": "Code review",
"description": "Review a piece of code.",
"arguments": [
{"name": "code", "description": "The code to review.", "required": true},
{"name": "language", "description": "The language the code is written in.", "required": false}
]
}
```
!!! info
Wenn du **[Tools](tools.md)** gelesen hast, kennst du schon alles auf dieser Seite. Derselbe Dekorator, derselbe
Docstring als Beschreibung, dasselbe `Annotated`/`Field`. Das Einzige, was sich ändert: wer
ihn auslöst (die Person) und wohin das Ergebnis geht (in die Unterhaltung).
## Zusammenfassung {#recap}
* `@mcp.prompt()` auf einer Funktion macht sie zu einem Prompt. Der Name kommt von der Funktion, die Beschreibung vom Docstring.
* Prompts sind **von der Person gesteuert**: Der Client listet sie auf, die Person wählt einen und füllt die Argumente aus.
* Argumente sind eine flache Liste benannter Strings (kein Schema). Ein Parameter mit Standardwert ist optional.
* Gibst du einen `str` zurück, wird daraus eine User-Nachricht. Gib eine Liste von `UserMessage` / `AssistantMessage` zurück, um eine mehrteilige Unterhaltung anzustoßen.
* `title=` und `Field(description=...)` sind das, was ein Client in seiner Oberfläche anzeigt.
* Ein fehlendes erforderliches Argument lässt den ganzen Request fehlschlagen. Es gibt kein Fehlerergebnis pro Prompt.
Serverseitige Autovervollständigung für die Argumente eines Prompts (oder eines Ressourcen-Templates) ist **[Vervollständigungen](completions.md)**.

View file

@ -0,0 +1,146 @@
---
translation:
sections: [09df998c2a799f78, 0cf131146d16d4f9, 4e6b91e3f8025346, 8fe4eef576db17ed, 0d0d1ed43e3d0a53]
tool: 1
---
# Ressourcen {#resources}
Eine **Ressource** sind Daten, die du bereitstellst, damit die Anwendung sie lesen kann.
Das ist die Trennlinie. Ein Tool ist etwas, das das **Modell** aufzurufen beschließt. Eine Ressource ist etwas, das die **Anwendung** zu laden beschließt (eine Konfigurationsdatei, einen Datensatz, ein Dokument) und dem Modell als Kontext vorlegt.
Du deklarierst eine, indem du `@mcp.resource(uri)` auf eine ganz normale Python-Funktion setzt.
## Deine erste Ressource {#your-first-resource}
```python title="server.py" hl_lines="6-8"
--8<-- "docs_src/resources/tutorial001.py"
```
Sie hat dieselbe Form wie ein Tool, plus eine Sache: den **URI**. Ressourcen werden adressiert, nicht benannt. Ein Client fragt nach `config://app`, nie nach `get_config`.
Den Rest liest das SDK weiterhin aus der Funktion:
* Der **Name** ist der Funktionsname: `get_config`.
* Die **Beschreibung**, die der Client sieht, ist der Docstring.
* Der **Inhalt** ist das, was du zurückgibst.
Bei `resources/list` bekommt der Client das hier:
```json
{
"name": "get_config",
"uri": "config://app",
"description": "The active shop configuration.",
"mimeType": "text/plain"
}
```
Und wenn er `config://app` liest, läuft deine Funktion, und der Rückgabewert kommt als Text zurück:
```python
result.contents # [TextResourceContents(uri="config://app", mime_type="text/plain", text="theme=dark\nlanguage=en")]
```
!!! tip
Auflisten ist billig. Deine Funktion wird bei `resources/list` **nicht** aufgerufen, nur bei
`resources/read`, und nur für den URI, nach dem gefragt wurde. Stelle tausend Ressourcen
bereit, und du zahlst nur für die, die jemand öffnet.
### Ausprobieren {#try-it}
Starte den Server mit dem MCP Inspector:
```console
uv run mcp dev server.py
```
Öffne die URL, die er ausgibt, und wechsle zum Tab **Resources**. `config://app` steht mit seiner Beschreibung in der Liste. Klicke darauf, und der Inspector liest es: Da sind deine zwei Zeilen Konfiguration.
## Ressourcen-Templates {#resource-templates}
Ein URI pro Datensatz skaliert nicht. Setze einen **Platzhalter** in den URI und einen passenden Parameter auf die Funktion:
```python title="server.py" hl_lines="12-13"
--8<-- "docs_src/resources/tutorial002.py"
```
`{user_id}` im URI, `user_id: str` an der Funktion. Das ist der ganze Vertrag.
Das ist jetzt ein **Ressourcen-Template**, und es zieht um: Es verlässt `resources/list` und taucht stattdessen in `resources/templates/list` auf – als Muster statt als Adresse:
```json
{
"name": "get_user_profile",
"uriTemplate": "users://{user_id}/profile",
"description": "A customer's profile.",
"mimeType": "text/plain"
}
```
Der Client füllt den Platzhalter aus und liest einen konkreten URI: `users://42/profile`, `users://ada/profile`. Eine einzige Funktion beantwortet sie alle, wobei der erkannte Wert als `user_id` übergeben wird:
```python
result.contents # [TextResourceContents(uri="users://42/profile", text="User 42: 12 orders since 2021.")]
```
Beachte den `uri` im Ergebnis. Es ist der **konkrete** URI, nach dem der Client gefragt hat, nicht das Template.
!!! check
Platzhalter und Parameter müssen übereinstimmen. Benenne den Funktionsparameter in
`user` um, während im URI noch `{user_id}` steht, und der Dekorator verweigert sich **beim Import**,
bevor irgendein Client in die Nähe kommt:
```text
ValueError: Mismatch between URI parameters {'user_id'} and function parameters {'user'}
```
Eine Abweichung kann immer nur ein Bug sein, also macht das SDK es unmöglich, den Server damit zu starten.
Die Platzhalter-Syntax ist [RFC 6570](https://datatracker.ietf.org/doc/html/rfc6570): `{+path}` für Werte über mehrere Segmente, `{?q,lang}` für optionale Query-Parameter und mehr. Außerdem wendet das SDK standardmäßig Pfadsicherheitsprüfungen auf die extrahierten Werte an. Die vollständige Referenz steht in **[URI-Templates und Pfadsicherheit](uri-templates.md)**.
`get_user_profile` kann auch einen Parameter mit der Annotation `Context` entgegennehmen. Das SDK injiziert ihn, ohne ihn je als URI-Parameter zu behandeln, und die Seite **[Der Context](../handlers/context.md)** beschreibt, was er dir bietet.
## Was du zurückgibst {#what-you-return}
Du bist nicht auf `str` beschränkt. Gib jeder Ressource einen `mime_type` und gib zurück, was passt:
```python title="server.py" hl_lines="8-9 14-15 20-21"
--8<-- "docs_src/resources/tutorial003.py"
```
* `readme` gibt einen `str` zurück, also wird er unverändert gesendet. Das ist der Normalfall.
* `catalog_stats` gibt ein `dict` zurück, also serialisiert das SDK es für dich zu **JSON-Text**:
```json
{
"books": 1204,
"authors": 391
}
```
* `placeholder_cover` gibt `bytes` zurück, also bekommt der Client ein `BlobResourceContents` statt eines `TextResourceContents`, mit deinen Bytes base64-kodiert im Feld `blob`.
Dieselbe Regel gilt für alles andere, was JSON-serialisierbar ist: eine Liste, ein Pydantic-Modell, eine Dataclass. Ist es kein `str` und kein `bytes`, wird es zu JSON.
`mime_type` deklarierst du selbst, und der Standardwert ist `text/plain`. Das SDK untersucht nie, was du zurückgibst, um ihn zu erraten – eine `dict`-Ressource, die du nicht kennzeichnest, wird also weiterhin als Plain Text angekündigt.
!!! tip
`@mcp.resource()` akzeptiert auch `name=`, `title=` und `description=`, wenn du sie nicht
aus der Funktion ableiten willst. Und wenn es gar keine Funktion zu schreiben gibt,
hält `mcp.server.mcpserver.resources` fertige `Resource`-Klassen bereit (`TextResource`,
`BinaryResource`, `FileResource`, `HttpResource`, `DirectoryResource`), die du
mit `mcp.add_resource(...)` registrierst.
Ein Client kann eine Ressource außerdem **abonnieren** und benachrichtigt werden, wenn sie sich ändert; das ist die Client-Hälfte der Geschichte, und sie steht in **[Der Client](../client/index.md)**.
## Zusammenfassung {#recap}
* `@mcp.resource(uri)` auf einer Funktion macht sie zur Ressource. Der URI ist die Adresse, der Rückgabewert ist der Inhalt, der Docstring ist die Beschreibung.
* Ein `{placeholder}` im URI macht sie zum **Template**: Es wird unter `resources/templates/list` aufgeführt, und eine einzige Funktion bedient jeden URI, der passt.
* Die Platzhalternamen müssen den Parameternamen der Funktion entsprechen. Machst du es falsch, erfährst du es beim Import, nicht in Produktion.
* Deine Funktion läuft, wenn die Ressource **gelesen** wird, nicht wenn sie aufgelistet wird.
* `str` wird zu Text, `bytes` zu einem base64-Blob, alles andere zu JSON-Text. Mit `mime_type=` kennzeichnest du es.
* Tools sind dafür da, dass das Modell handelt. Ressourcen sind dafür da, dass die Anwendung liest.
Das dritte Primitiv – das, das eine Person aus einem Menü auswählt – sind **[Prompts](prompts.md)**.

View file

@ -0,0 +1,250 @@
---
translation:
sections: [a838d57f003aed44, 857d03886a0137ed, 42d9efcb9f542867, 2290ff08435b5573, e866c192e11d1c14, 6cdbad079f7b47f0, d4b607372fb28b51, 18dbf726ac45e0b7, c6f7d2a148aa49f4, c851964bb3301907, d715db6f8dccc9cc, ef86634aa70498a7]
tool: 1
---
# Strukturierte Ausgabe {#structured-output}
Ein Tool, das einen einfachen `str` zurückgibt, liefert das Ergebnis doppelt: als Text in `content` und als `{"result": "..."}` in `structured_content`.
Auf dieser Seite geht es um diesen zweiten Kanal: woher er kommt, welche Formen er annehmen kann und wie das SDK dafür sorgt, dass er hält, was er verspricht.
Die Kurzfassung: **Die Annotation des Rückgabetyps ist das Ausgabeschema**. Du hast sie schon geschrieben.
## Das Ausgabeschema {#the-output-schema}
```python title="server.py" hl_lines="9"
--8<-- "docs_src/structured_output/tutorial001.py"
```
Die entscheidende Zeile ist die Signatur: `-> int`.
Ihretwegen trägt das Tool, das das SDK bei `tools/list` sendet, ein `output_schema` neben dem Eingabeschema, das es aus deinen Parametern baut (darum kümmert sich **[Tools](tools.md)**):
```json
{
"properties": {
"result": {"title": "Result", "type": "integer"}
},
"required": ["result"],
"title": "get_temperatureOutput",
"type": "object"
}
```
Ein nackter `int` ist kein JSON-Objekt, also **verpackt** das SDK ihn in `{"result": ...}`. Ruf das Tool auf, und beide Kanäle sind gefüllt:
```python
result.content # [TextContent(text="17")]
result.structured_content # {"result": 17}
```
Jeder skalare Wert bekommt dieselbe Hülle: `str`, `int`, `float`, `bool`, `bytes`, `None`.
## Zwei Kanäle {#two-channels}
Warum denselben Wert zweimal senden?
* `content` ist für das **Modell**. Ein Sprachmodell liest Text; das ist der einzige Teil des Ergebnisses, den es sieht.
* `structured_content` ist für die **Anwendung**, in der das Modell läuft: Code, der `17` will und keinen Satz, in dem „17“ vorkommt.
* `output_schema` ist der Vertrag zwischen beiden, veröffentlicht, bevor das Tool überhaupt aufgerufen wird.
Du gibst einen einzigen Python-Wert zurück. Das SDK füllt alle drei.
## Ein Modell zurückgeben {#return-a-model}
Deklariere die Form als Pydantic-`BaseModel` und gib eine Instanz zurück:
```python title="server.py" hl_lines="8-11 15"
--8<-- "docs_src/structured_output/tutorial002.py"
```
`WeatherData` **ist** jetzt das Schema. Keine Hülle, kein `result`-Schlüssel:
```json
{
"properties": {
"temperature": {"description": "Degrees Celsius.", "title": "Temperature", "type": "number"},
"humidity": {"description": "Relative humidity, 0 to 1.", "title": "Humidity", "type": "number"},
"conditions": {"title": "Conditions", "type": "string"}
},
"required": ["temperature", "humidity", "conditions"],
"title": "WeatherData",
"type": "object"
}
```
`structured_content` ist das Objekt, Feld für Feld:
```python
result.structured_content # {"temperature": 16.2, "humidity": 0.83, "conditions": "Overcast"}
```
Und das Modell geht nicht leer aus. Das SDK serialisiert dasselbe Objekt für `content` zu JSON-Text:
```json
{
"temperature": 16.2,
"humidity": 0.83,
"conditions": "Overcast"
}
```
Beachte, dass das `Field(description=...)` an `temperature` und `humidity` im Schema gelandet ist. Dasselbe `Field`, das deine **Eingaben** beschrieben hat, beschreibt auch deine Ausgaben.
!!! info
Wenn du FastAPIs `response_model` kennst, kennst du das hier schon: ein Pydantic-Modell als deklarierte
Response, für dich serialisiert und dokumentiert. Der einzige Unterschied: Hier ist die Annotation des
Rückgabetyps die ganze Deklaration.
## Ein `TypedDict` {#a-typeddict}
Nicht jede Form verdient eine Klasse. Ein `TypedDict` erzeugt dasselbe Schema:
```python title="server.py" hl_lines="8"
--8<-- "docs_src/structured_output/tutorial003.py"
```
Ein `TypedDict` ist zur Laufzeit ein einfaches `dict`, also baust du genau das und gibst es zurück. Das Schema, die Validierung und `structured_content` sind identisch mit der `BaseModel`-Variante (abgesehen von den Beschreibungen, für die ein `TypedDict` keinen Platz hat).
## Eine Dataclass {#a-dataclass}
Dataclasses funktionieren auch, genauso wie jede gewöhnliche Klasse, deren Attribute Type Hints tragen. Das SDK baut unter der Haube aus den Annotationen ein Pydantic-Modell.
```python title="server.py" hl_lines="8-9"
--8<-- "docs_src/structured_output/tutorial004.py"
```
Drei Schreibweisen, ein Schema. Nimm die, die deine Codebasis ohnehin schon verwendet.
## Listen {#lists}
Eine `list[...]` ist ebenfalls kein JSON-Objekt, also bekommt sie die `{"result": ...}`-Hülle, mit deinem Elementtyp als `$defs`-Referenz darin:
```python title="server.py" hl_lines="15"
--8<-- "docs_src/structured_output/tutorial005.py"
```
```json
{
"$defs": {
"WeatherData": {
"properties": {
"temperature": {"title": "Temperature", "type": "number"},
"humidity": {"title": "Humidity", "type": "number"},
"conditions": {"title": "Conditions", "type": "string"}
},
"required": ["temperature", "humidity", "conditions"],
"title": "WeatherData",
"type": "object"
}
},
"properties": {
"result": {"items": {"$ref": "#/$defs/WeatherData"}, "title": "Result", "type": "array"}
},
"required": ["result"],
"title": "get_forecastOutput",
"type": "object"
}
```
Fordere eine Zwei-Tage-Vorhersage an, und `structured_content` ist `{"result": [{...}, {...}]}`. `content` wird zu **zwei** `TextContent`-Blöcken, einer pro Element: Eine Liste wird für das Modell aufgefächert, statt als ein einziger String ausgegeben zu werden.
`tuple[...]`, Unions und `Optional[...]` werden genauso verpackt.
## Dictionaries {#dictionaries}
`dict[str, ...]` ist der eine generische Typ, der bereits ein JSON-Objekt *ist*, und wird deshalb nicht verpackt:
```python title="server.py" hl_lines="9"
--8<-- "docs_src/structured_output/tutorial006.py"
```
```json
{
"additionalProperties": {"type": "number"},
"title": "get_temperaturesDictOutput",
"type": "object"
}
```
```python
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.
## Validierung {#validation}
`output_schema` ist keine Dokumentation. Was auch immer deine Funktion zurückgibt, wird **dagegen validiert**, bevor es den Server verlässt.
Solange du den Wert von Hand baust, merkst du davon nichts: Pydantic hat schon sichergestellt, dass dein `WeatherData` ein `WeatherData` ist. Du merkst es an dem Tag, an dem die Daten von irgendwo kommen, das du nicht kontrollierst:
```python title="server.py" hl_lines="9 21"
--8<-- "docs_src/structured_output/tutorial007.py"
```
Die Annotation verspricht `WeatherData`. Die Upstream-Response liefert `humidity` nicht mehr mit.
!!! check
Ruf `get_weather` auf, und es reicht dem Client nicht stillschweigend ein halb leeres Objekt weiter. Der Aufruf schlägt fehl,
und die ersten Zeilen des Fehlers nennen das Feld:
```text
Error executing tool get_weather: 1 validation error for WeatherData
humidity
Field required [type=missing, input_value={'temperature': 16.2, 'conditions': 'Overcast'}, input_type=dict]
```
Dieser Text kommt als Tool-Ergebnis mit `is_error=True` zurück. So weiß das Modell, dass der Aufruf fehlgeschlagen ist,
statt selbstbewusst Wetterdaten abzulesen, die gar nicht da sind.
Ein einfaches `dict` aus einem `-> WeatherData`-Tool zurückzugeben ist übrigens in Ordnung. Genau das hat `json.loads` erzeugt. Validiert wird der Wert, nicht der Python-Typ.
## Abschalten {#opting-out}
Manchmal ist die Annotation des Rückgabetyps für den Type Checker da, nicht für das Protokoll. Übergib `structured_output=False`, und das Tool liefert nur Text:
```python title="server.py" hl_lines="6"
--8<-- "docs_src/structured_output/tutorial008.py"
```
Kein `output_schema`, keine Hülle, keine Validierung. `structured_content` ist `None`, und `content` ist der String, den du zurückgegeben hast.
Das Gegenteil, `structured_output=True`, macht aus der automatischen Erkennung eine Anforderung: Ein Tool, dessen Rückgabetyp kein Schema erzeugen kann, löst beim Import eine Exception aus, statt auf Text zurückzufallen.
## Eine Klasse ohne Type Hints {#a-class-without-type-hints}
Es gibt einen Weg, unstrukturiert zu enden, ohne es gewollt zu haben: eine Klasse zurückzugeben, die **keine Annotationen im Klassenrumpf** hat.
```python title="server.py" hl_lines="6-9"
--8<-- "docs_src/structured_output/tutorial009.py"
```
`Station` setzt `name` und `online` in `__init__`, aber die *Klasse* deklariert nichts. Das SDK liest die Klassenannotationen, findet keine und gibt auf.
!!! warning
Es gibt **stillschweigend** auf. `output_schema` ist `None`, `structured_content` ist `None`, und der Text,
den das Modell liest, ist das `repr` des Objekts:
```text
"<server.Station object at 0x7f539d75b230>"
```
Kein Fehler, keine Warnung, ein nutzloses Tool. Verschiebe die Annotationen in den Klassenrumpf oder übergib
`structured_output=True`. Das macht daraus einen harten Fehler, sobald das Modul importiert wird:
`Function get_station: return type <class 'server.Station'> is not serializable for structured output`.
!!! tip
Brauchst du die volle Kontrolle (das `CallToolResult` selbst bauen oder `_meta` anhängen, das die
Anwendung sieht, das Modell aber nicht)? Das ist **[Der Low-Level-Server](../advanced/low-level-server.md)**.
## Zusammenfassung {#recap}
* Die **Annotation des Rückgabetyps** ist das Ausgabeschema. Sie wird in `tools/list` als `output_schema` veröffentlicht.
* Skalare, Listen, Tupel und Unions werden in `{"result": ...}` verpackt. Modelle, `TypedDict`s, Dataclasses, annotierte Klassen und `dict[str, ...]` sind schon Objekte und bleiben, wie sie sind.
* Jedes Ergebnis trägt `content` (Text, für das Modell) **und** `structured_content` (Daten, für die Anwendung).
* Was du zurückgibst, wird gegen das Schema validiert. Eine Abweichung ist ein Tool-Fehler, kein kaputtes Ergebnis.
* `structured_output=False` nimmt ein Tool davon aus. Eine Klasse ohne Type Hints nimmt sich stillschweigend aus; achte darauf.
Damit hast du alles in der Hand, was ein Tool zurückmelden kann. Als Nächstes das zweite Primitiv: **[Ressourcen](resources.md)**.

View file

@ -0,0 +1,177 @@
---
translation:
sections: [e4cc390d56573409, 8566e2b68594e9ad, 2c97b9f888398951, 048e5471dfa71aea, 3076b1e16ad95950, edbedf2a16e71311, 3d8ef8da89fa87c1, f6c0e02e6ea5a363]
tool: 1
---
# Tools {#tools}
Ein **Tool** ist eine Funktion, die das Modell aufrufen kann.
Du deklarierst eines, indem du `@mcp.tool()` auf eine ganz normale Python-Funktion setzt. Das ist die ganze API.
## Dein erstes Tool {#your-first-tool}
```python title="server.py" hl_lines="6-8"
--8<-- "docs_src/tools/tutorial001.py"
```
Sieh dir an, was du geschrieben hast. Keine Schemas, kein JSON, kein Protokoll, nur eine Funktion. Das SDK liest drei Dinge daraus:
* Der **Name** des Tools ist der Name der Funktion: `search_books`.
* Die **Beschreibung**, die das Modell sieht, ist der Docstring: `Search the catalog by title or author.`
* Die **Argumente**, die das Modell übergeben darf, ergeben sich aus den Type Hints: `query: str` und `limit: int`.
### Das Eingabeschema {#the-input-schema}
Aus diesen Type Hints erzeugt das SDK ein JSON Schema und sendet es während `tools/list` an den Client:
```json
{
"type": "object",
"properties": {
"query": {"title": "Query", "type": "string"},
"limit": {"title": "Limit", "type": "integer"}
},
"required": ["query", "limit"],
"title": "search_booksArguments"
}
```
Beide Argumente stehen in `required`, weil keines einen Standardwert hat. Das änderst du gleich. (Die `title`-Schlüssel sind Pydantic-Artefakte; die Properties, ihre Typen und `required` sind der Vertrag.)
!!! tip
Type Hints sind hier keine Dokumentation. Sie sind **der Vertrag**. Sendet ein Client `"limit": "ten"`,
weist das SDK das zurück, bevor deine Funktion überhaupt läuft.
### Was das Modell zurückbekommt {#what-the-model-gets-back}
Ruf das Tool mit `{"query": "dune", "limit": 5}` auf, und das Ergebnis hat zwei Teile:
```python
result.content # [TextContent(text="Found 3 books matching 'dune' (showing up to 5).")]
result.structured_content # {'result': "Found 3 books matching 'dune' (showing up to 5)."}
```
`content` ist der Text, den das **Modell** liest. `structured_content` sind typisierte Daten für die **Client-Anwendung**. Es ist da, weil du den Rückgabetyp als `-> str` deklariert hast.
Kümmere dich noch nicht um `structured_content`. Gib aus deinen Tools echte Python-Objekte zurück, und es passiert das Richtige; die Seite **[Strukturierte Ausgabe](structured-output.md)** dreht sich genau darum.
### Ausprobieren {#try-it}
Starte den Server mit dem MCP Inspector:
```console
uv run mcp dev server.py
```
Öffne die URL, die er ausgibt, geh zum Tab **Tools** und ruf `search_books` auf.
Der Inspector zeigt ein Formular mit einem erforderlichen Textfeld `query` und einem erforderlichen Zahlenfeld `limit`. Dieses Formular hat er aus deinen Type Hints gebaut. Das macht jeder andere MCP-Client genauso.
## Optionale Argumente {#optional-arguments}
Gib einem Parameter einen Standardwert, und er ist nicht mehr erforderlich. Das ist alles. Ganz normales Python.
```python title="server.py" hl_lines="7"
--8<-- "docs_src/tools/tutorial002.py"
```
Das Schema zieht mit:
```json
{
"type": "object",
"properties": {
"query": {"title": "Query", "type": "string"},
"limit": {"default": 10, "title": "Limit", "type": "integer"}
},
"required": ["query"],
"title": "search_booksArguments"
}
```
`limit` ist aus `required` verschwunden und hat `"default": 10` bekommen. Ein Client, der es weglässt, bekommt `10` – genau wie in Python.
## Reichere Schemas mit `Field` {#richer-schemas-with-field}
Type Hints bringen dich weit, aber manchmal willst du ein Argument *beschreiben* oder einschränken.
Verpacke den Typ in `Annotated` und füge ein Pydantic-`Field` hinzu:
```python title="server.py" hl_lines="12-14"
--8<-- "docs_src/tools/tutorial003.py"
```
Drei neue Dinge, alle an den Parametern:
* `Field(description=...)`: eine Beschreibung pro Argument, die das Modell zusätzlich zum Docstring liest.
* `Field(ge=1, le=50)`: numerische Grenzen. Sie landen im Schema als `"minimum": 1, "maximum": 50`.
* `Literal["fiction", "non-fiction", "poetry"]`: ein Enum. Das Modell kann nur einen dieser Werte wählen.
!!! check
Constraints sind keine Dekoration. Ruf das Tool mit `limit=999` auf, und das SDK antwortet mit einem
Tool-Fehler, **bevor deine Funktion läuft**:
```text
Input should be less than or equal to 50
```
Dieser Fehler geht als Tool-Ergebnis zurück an das Modell, das Modell liest ihn und versucht es mit
einem gültigen Wert erneut. Du hast einmal `le=50` geschrieben und bekommst selbstkorrigierende Agenten umsonst dazu.
!!! info
Wenn du FastAPI oder Pydantic schon benutzt hast, kennst du das alles bereits. Es ist dasselbe `Field`,
dasselbe `Annotated`, dieselbe Validierung. Es gibt hier nichts MCP-Spezifisches zu lernen.
## Ein Modell als Parameter {#a-model-as-a-parameter}
Nimmt ein Tool mehr als ein paar Argumente entgegen, fasse sie in einem Pydantic-Modell zusammen:
```python title="server.py" hl_lines="8-11 15"
--8<-- "docs_src/tools/tutorial004.py"
```
Das `Book`-Schema wird in das Eingabeschema des Tools eingebettet (als `$defs`-Referenz), das Modell füllt es als JSON-Objekt aus, und deine Funktion erhält eine **echte `Book`-Instanz**, bereits validiert, mit den Attributen `.title`, `.author` und `.year`.
Du kannst frei kombinieren: einfache Parameter neben Modell-Parametern, verschachtelte Modelle, Listen von Modellen. Es ist Pydantic bis ganz nach unten.
## `async def` {#async-def}
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.
Mehr gibt es nicht zu konfigurieren.
## Namen, Titel und Annotationen {#names-titles-and-annotations}
Alles, was das SDK ableitet, kannst du im Dekorator überschreiben:
```python title="server.py" hl_lines="7-10"
--8<-- "docs_src/tools/tutorial005.py"
```
* `title` ist ein menschenlesbarer Name für UIs. Clients zeigen *„Search the catalog“* statt `search_books`.
* `annotations` sind **Hinweise** zum Verhalten für den Client:
* `read_only_hint=True`: Dieses Tool ändert nichts.
* `open_world_hint=False`: Es arbeitet auf einer geschlossenen Menge von Dingen (diesem Katalog), nicht im offenen Web.
* Die beiden anderen, `destructive_hint` und `idempotent_hint`, beschreiben ein Tool, das *schreibt*: Darf es
etwas löschen, und ist zweimal aufrufen dasselbe wie einmal aufrufen? Die Spezifikation definiert beide
nur für Tools, die nicht read-only sind, deshalb würden sie bei `search_books` nichts aussagen.
Ein gut erzogener Client nutzt sie, um Dinge zu entscheiden wie *„Muss ich die Person fragen, bevor ich das ausführe?“*. Es sind Hinweise, keine Sicherheit. Verlass dich nie darauf, dass ein Client sie beachtet.
!!! tip
`@mcp.tool()` akzeptiert auch `name=` und `description=`, falls du sie nicht aus dem Funktionsnamen
und dem Docstring ableiten lassen willst. Meistens willst du das aber.
## Zusammenfassung {#recap}
* `@mcp.tool()` auf einer Funktion macht sie zum Tool. Name aus der Funktion, Beschreibung aus dem Docstring.
* Type Hints **sind** das Eingabeschema. Standardwerte machen Argumente optional.
* `Annotated[..., Field(...)]` fügt Beschreibungen und Constraints hinzu; `Literal` fügt Enums hinzu.
* Über einen Pydantic-Modell-Parameter nimmst du einen strukturierten „Body“ entgegen.
* Ungültige Argumente werden für dich abgewiesen, mit einem Fehler, den das Modell lesen und aus dem es sich erholen kann.
* `async def` für I/O, einfaches `def` für alles andere.
**[Strukturierte Ausgabe](structured-output.md)** beschreibt, was mit dem Wert passiert, den du mit `return` zurückgibst.

View file

@ -0,0 +1,290 @@
---
translation:
sections: [4a7033e1ed8ad602, 55dcbfff0c6271bf, 101ef9d14bf4ec46, 4b6c4a845438abc7, f98b46bafbee4acd]
tool: 1
---
# URI-Templates und Pfadsicherheit {#uri-templates-and-path-safety}
Dies ist die Referenz für die URI-Template-Syntax, die
[`@mcp.resource`](resources.md) akzeptiert, und für die
Pfadsicherheitsrichtlinie, die das SDK auf extrahierte Werte anwendet. Eine
Einführung, was Ressourcen sind und wann du sie einsetzt, findest du in
**[Ressourcen](resources.md)**; diese Seite setzt voraus, dass du bereits
sicher im Deklarieren einer Ressource bist und den vollständigen
Operatorsatz, die Sicherheitseinstellungen oder die Low-Level-Verdrahtung
suchst.
Die Template-Syntax ist [RFC 6570](https://datatracker.ietf.org/doc/html/rfc6570).
Das SDK unterstützt eine Teilmenge, die für das Matching eingehender
`resources/read`-URIs ausgewählt wurde, plus eine Sicherheitsschicht, die
Werte ablehnt, die außerhalb des Verzeichnisses landen würden, das du
bereitstellen willst. Die Details auf Protokollebene (Nachrichtenformate,
Lebenszyklus, Paginierung) stehen in der
[MCP-Ressourcen-Spezifikation](https://modelcontextprotocol.io/specification/latest/server/resources).
## Der vollständige Operatorsatz {#the-full-operator-set}
Der einfache Platzhalter `{user_id}` ist der, den **[Ressourcen](resources.md)** einführt. Es gibt vier weitere
Operatorformen; hier stehen sie alle auf einem Server, damit du sie
nebeneinander siehst:
```python title="server.py" hl_lines="16-17 22-23 28-29 34-35 40-41"
--8<-- "docs_src/uri_templates/tutorial001.py"
```
Jeder hervorgehobene Dekorator zerlegt den URI auf eine andere Weise.
Die folgenden Abschnitte gehen sie von oben nach unten durch.
### Einfache Expansion: `{name}` {#simple-expansion-name}
`books://{isbn}` ist die schlichte Alltagsform. Der Platzhalter wird auf
den Parameter `isbn` abgebildet, sodass ein Client, der
`books://978-0441172719` liest, `get_book("978-0441172719")` aufruft.
Ein einfaches `{name}` endet am ersten `/`. `books://978/extra` passt
nicht, weil der Schrägstrich nach `978` die Erfassung beendet und `/extra`
übrig bleibt.
### Typkonvertierung {#type-conversion}
Extrahierte Werte kommen als Strings an, aber du kannst einen genaueren
Typ deklarieren, und das SDK konvertiert. `orders://{order_id}` landet in
einer Funktion, deren Parameter `order_id: int` ist, sodass das Lesen von
`orders://12345` `get_order(12345)` aufruft, nicht `get_order("12345")`. Der
Handler rechnet damit (`order_id + 1`), ohne zu casten.
### Mehrteilige Pfade: `{+name}` {#multi-segment-paths-name}
Um einen Wert zu erfassen, der Schrägstriche enthält, verwende `{+name}`. Mit
`manuals://{+path}`:
* `manuals://returns.md` ergibt `path = "returns.md"`
* `manuals://printing/setup.md` ergibt `path = "printing/setup.md"`
Greif zu `{+name}`, wann immer der Wert hierarchisch ist: Dateisystempfade,
verschachtelte Objektschlüssel, URL-Pfade, die du als Proxy weiterreichst.
### Query-Parameter: `{?a,b,c}` {#query-parameters-abc}
`reviews://{isbn}{?limit,sort}` setzt `limit` und `sort` hinter das `?`.
Der Pfad bestimmt, *welches* Buch; die Query steuert, *wie* du es liest.
Query-Parameter werden nachsichtig abgeglichen: Die Reihenfolge spielt keine
Rolle, zusätzliche werden ignoriert, und weggelassene fallen auf die
Standardwerte deiner Funktion zurück. `reviews://978-0441172719` verwendet
also `limit=10, sort="newest"`, und
`reviews://978-0441172719?sort=top` überschreibt nur `sort`.
### Pfadsegmente als Liste: `{/name*}` {#path-segments-as-a-list-name}
Wenn du jedes Pfadsegment als eigenes Listenelement haben willst statt als
einen String mit Schrägstrichen, verwende `{/name*}`. Mit
`shelves://browse{/path*}` ruft ein Client, der
`shelves://browse/fiction/sci-fi` liest,
`browse_shelf(["fiction", "sci-fi"])` auf.
### Template-Referenz {#template-reference}
Die häufigsten Muster:
| Muster | Beispieleingabe | Du bekommst |
|--------------|-----------------------|-------------------------|
| `{name}` | `alice` | `"alice"` |
| `{name}` | `docs/intro.md` | *kein Treffer* (endet am `/`) |
| `{+path}` | `docs/intro.md` | `"docs/intro.md"` |
| `{.ext}` | `.json` | `"json"` |
| `{/segment}` | `/v2` | `"v2"` |
| `{?key}` | `?key=value` | `"value"` |
| `{?a,b}` | `?a=1&b=2` | `"1"`, `"2"` |
| `{/path*}` | `/a/b/c` | `["a", "b", "c"]` |
### Was der Parser ablehnt {#what-the-parser-rejects}
Einige Template-Formen werden vorab abgefangen, statt beim ersten Request
zu scheitern. `@mcp.resource` parst das Template, wenn der Dekorator läuft,
sodass keine davon je einen laufenden Server erreicht.
`UriTemplate.parse()` löst `InvalidUriTemplate` aus bei:
* **Zwei Variablen ohne etwas dazwischen.** `manuals://{+path}{ext}`
wird abgelehnt: Das Matching kann nicht erkennen, wo `path` endet und `ext`
beginnt. Setze ein Literal dazwischen (`manuals://{+path}/{ext}`) oder
verwende einen Operator, der seinen eigenen Trenner mitbringt.
`manuals://{+path}{.ext}` wird akzeptiert, weil `{.ext}` den `.` selbst
beisteuert.
* **Mehr als eine mehrteilige Variable.** Höchstens eines von `{+var}`,
`{#var}` oder einer explodierten Variable (`{/var*}`, `{.var*}`, `{;var*}`)
pro Template. Zwei sind grundsätzlich mehrdeutig: Es gibt keinen
begründbaren Weg zu entscheiden, welche ein zusätzliches Segment aufnimmt.
* **Den üblichen Syntaxfehlern**: eine nicht geschlossene geschweifte
Klammer, ein doppelt verwendeter Variablenname oder ein RFC-6570-Feature,
das das SDK nicht unterstützt, etwa der Präfix-Modifikator `{var:3}` oder
die Query-Explosion `{?vars*}`.
Darüber hinaus löst `@mcp.resource` einen `ValueError` aus, wenn ein
Handler-Parameter an eine Query-Variable im abschließenden
`{?...}`/`{&...}`-Lauf des Templates gebunden ist, aber keinen
Python-Standardwert hat. Diese Variablen werden nachsichtig abgeglichen
(ein Client darf jede davon weglassen), sodass ein Parameter ohne
Standardwert erst beim ersten Request, der ihn weglässt, als
undurchsichtiger interner Fehler auftauchen würde.
`reviews://{isbn}{?limit,sort}` im Server oben ist die wohlgeformte
Variante: `limit` und `sort` tragen beide Standardwerte.
## Sicherheit {#security}
Template-Parameter kommen vom Client. Fließen sie ungeprüft in
Dateisystem- oder Datenbankoperationen, können Werte wie
`../../etc/passwd` außerhalb des Verzeichnisses landen, das du
bereitstellen wolltest.
### Was das SDK standardmäßig prüft {#what-the-sdk-checks-by-default}
Bevor dein Handler läuft, lehnt das SDK jeden Parameter ab, der:
* sein Ausgangsverzeichnis über `..`-Komponenten verlassen würde
* wie ein absoluter Pfad aussieht (`/etc/passwd`, `C:\Windows`) oder wie
ein laufwerksrelativer Windows-Pfad (`C:foo`). Ein laufwerksrelativer
Wert und ein Bezeichner mit Namensraum wie `x:y` sind als Strings nicht
zu unterscheiden, daher wird standardmäßig jeder Wert aus einem einzelnen
Buchstaben plus Doppelpunkt abgelehnt; nimm den Parameter aus, wenn er
solche Werte legitim erhält
* ein Nullbyte (`\x00`) enthält
Die `..`-Prüfung arbeitet komponentenbasiert, nicht als Teilstringsuche.
Werte wie `v1.0..v2.0` oder `HEAD~3..HEAD` kommen durch, weil `..` dort
kein eigenständiges Pfadsegment ist.
Diese Prüfungen gelten für den dekodierten Wert, sie fangen Traversal also
unabhängig davon ab, wie es im URI kodiert war (`../etc`, `..%2Fetc`,
`%2E%2E/etc`, `..%5Cetc`, `%00` werden alle abgefangen).
!!! check
Lies `manuals://../etc/passwd` vom Server oben, und der Request wird
rundweg abgelehnt: Das Template-Matching stoppt beim ersten Fehlschlag,
sodass kein späteres (womöglich großzügigeres) Template als Fallback
probiert wird. Der Client sieht denselben `-32602`-Fehler „Unknown
resource“ wie bei einem URI, der auf gar kein Template passt, und
`read_manual` läuft nie.
### Dateisystem-Handler: safe_join verwenden {#filesystem-handlers-use-safe_join}
Die eingebauten Prüfungen stoppen die häufigen Fälle, können aber deine
Sandbox-Grenze nicht kennen. Für Dateisystemzugriffe verwende `safe_join`,
um den Pfad aufzulösen und zu verifizieren, dass er innerhalb deines
Basisverzeichnisses bleibt:
```python title="server.py" hl_lines="4 14"
--8<-- "docs_src/uri_templates/tutorial002.py"
```
`safe_join` fängt Symlink-Ausbrüche, `..`-Sequenzen und Tricks mit
absoluten Pfaden ab, die eine einfache Stringprüfung übersehen würde.
Verlässt der aufgelöste Pfad `DOCS_ROOT`, löst es `PathEscapeError` aus,
der beim Client als `ResourceError` ankommt.
### Wenn die Standardwerte im Weg stehen {#when-the-defaults-get-in-the-way}
Manchmal blockieren die Prüfungen legitime Werte. Ein Tool für den
Katalogimport könnte absichtlich einen absoluten Pfad erhalten, oder ein
Parameter könnte eine relative Referenz wie `../sibling` sein, die dein
Handler sicher interpretiert, ohne das Dateisystem anzufassen. Nimm diesen
Parameter aus oder lockere die Richtlinie für den ganzen Server:
```python title="server.py" hl_lines="9 16-19"
--8<-- "docs_src/uri_templates/tutorial003.py"
```
* `security=ResourceSecurity(exempt_params={"source"})` am Dekorator
überspringt die Prüfungen für diesen einen Parameter auf dieser einen
Ressource. Der Rest des Servers behält die Standardrichtlinie.
* `resource_security=` am `MCPServer`-Konstruktor setzt den Standard
für jede Ressource. Hier schaltet `relaxed` die `..`-Prüfung ganz ab.
Die konfigurierbaren Prüfungen:
| Einstellung | Standardwert | Was sie tut |
|-------------------------|---------|-------------------------------------|
| `reject_path_traversal` | `True` | Lehnt `..`-Sequenzen ab, die das Ausgangsverzeichnis verlassen |
| `reject_absolute_paths` | `True` | Lehnt `/foo`, `C:\foo`, UNC-Pfade und laufwerksrelatives `C:foo` ab (fängt auch `x:y` ab) |
| `reject_null_bytes` | `True` | Lehnt Werte ab, die `\x00` enthalten |
| `exempt_params` | leer | Parameternamen, für die Prüfungen übersprungen werden |
Diese Prüfungen sind ein heuristischer Vorfilter; für Dateisystemzugriffe
bleibt `safe_join` die Eindämmungsgrenze.
!!! tip
Kann dein Handler den Request nicht erfüllen (die Datei existiert nicht,
die ID ist unbekannt), löse eine Exception aus. Das SDK macht daraus eine
Fehler-Response. Den Unterschied zwischen einem Protokollfehler und einem
Tool-Fehler erklärt **[Fehler behandeln](handling-errors.md)**.
## Ressourcen auf dem Low-Level-Server {#resources-on-the-low-level-server}
Wenn du auf dem Low-Level-`Server` aufbaust (siehe **[Der
Low-Level-Server](../advanced/low-level-server.md)**), registrierst du Handler für die
Protokollmethoden `resources/list` und `resources/read` direkt. Es gibt
keinen Dekorator; du gibst die Protokolltypen selbst zurück.
### Statische Ressourcen {#static-resources}
Für feste URIs führe eine Registry und verteile anhand exakter
Übereinstimmung:
```python title="server.py" hl_lines="17 21 27"
--8<-- "docs_src/uri_templates/tutorial004.py"
```
Der List-Handler teilt Clients mit, was verfügbar ist; der Read-Handler
liefert den Inhalt. Prüfe zuerst deine Registry, falle auf Templates
(unten) zurück, falls du welche hast, und löse für alles andere eine
Exception aus.
### Templates {#templates}
Die Template-Engine, die `MCPServer` verwendet, liegt in
`mcp.shared.uri_template` und funktioniert eigenständig. Du bekommst
dasselbe Parsing und Matching; Routing und Sicherheitsrichtlinie
verdrahtest du selbst.
```python title="server.py" hl_lines="13-16 22-25 29 33 45"
--8<-- "docs_src/uri_templates/tutorial005.py"
```
In den hervorgehobenen Zeilen passieren drei Dinge:
* **Einmal parsen, pro Request matchen.** `UriTemplate.parse()` baut das
Template; `template.match(uri)` gibt die extrahierten Variablen als
`dict` zurück, oder `None`, wenn der URI nicht passt. Die URL-Dekodierung
geschieht innerhalb von `match()`; die dekodierten Werte werden unverändert
zurückgegeben, ohne Pfadsicherheitsprüfung. Die Werte kommen als Strings
heraus: Konvertiere sie selbst
(`int(matched["id"])`, `Path(matched["path"])`).
* **Die Sicherheitsprüfungen selbst anwenden.** Die `..`- und
Absolutpfad-Prüfungen, die `MCPServer` standardmäßig ausführt, liegen in
`mcp.shared.path_security`. `read_manual_safely` ruft sie auf, bevor es
`MANUALS` anfasst. Ist ein Parameter kein Dateisystempfad (eine ISBN, eine
Suchanfrage), überspring die Prüfungen für diesen Wert: Du steuerst die
Richtlinie pro Handler statt über ein Konfigurationsobjekt.
* **Die Templates aus derselben Quelle auflisten.** Clients entdecken
Templates über `resources/templates/list`. `str(template)` gibt den
ursprünglichen Template-String zurück, sodass Auflistung und Matcher
eine einzige Quelle der Wahrheit teilen.
## Zusammenfassung {#recap}
* `{name}` passt auf ein Segment; `{+name}` behält die Schrägstriche;
`{?a,b}` zieht aus dem Query-String; `{/name*}` teilt Segmente in eine
Liste auf.
* Zwei Variablen ohne etwas dazwischen oder eine zweite mehrteilige
Variable werden beim Parsen abgelehnt. Ein Parameter, der an eine
abschließende `{?...}`/`{&...}`-Query-Variable gebunden ist, muss einen
Python-Standardwert deklarieren.
* Annotiere den Parameter (`order_id: int`), und das SDK konvertiert.
* Die Standard-Sicherheitsrichtlinie lehnt `..`, absolute Pfade und
Nullbytes ab, bevor dein Handler läuft; überschreibe sie pro Ressource
mit `security=ResourceSecurity(...)` oder serverweit mit
`resource_security=`.
* Für Dateisystemzugriffe ist `safe_join` die Eindämmungsgrenze.
* Auf dem Low-Level-`Server` parst du mit `UriTemplate.parse()`, matchst
mit `.match()` und wendest `mcp.shared.path_security` selbst an.