docs: publish translated docs in twelve languages and the tool that maintains them (#3280)
This commit is contained in:
parent
14aa889992
commit
52ad0a8876
9 changed files with 89737 additions and 54 deletions
130
i18n/de/pages/servers/completions.md
Normal file
130
i18n/de/pages/servers/completions.md
Normal 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)**.
|
||||
139
i18n/de/pages/servers/handling-errors.md
Normal file
139
i18n/de/pages/servers/handling-errors.md
Normal 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)**.
|
||||
39
i18n/de/pages/servers/index.md
Normal file
39
i18n/de/pages/servers/index.md
Normal 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)**.
|
||||
122
i18n/de/pages/servers/media.md
Normal file
122
i18n/de/pages/servers/media.md
Normal 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)**.
|
||||
156
i18n/de/pages/servers/prompts.md
Normal file
156
i18n/de/pages/servers/prompts.md
Normal 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)**.
|
||||
146
i18n/de/pages/servers/resources.md
Normal file
146
i18n/de/pages/servers/resources.md
Normal 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)**.
|
||||
250
i18n/de/pages/servers/structured-output.md
Normal file
250
i18n/de/pages/servers/structured-output.md
Normal 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)**.
|
||||
177
i18n/de/pages/servers/tools.md
Normal file
177
i18n/de/pages/servers/tools.md
Normal 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.
|
||||
290
i18n/de/pages/servers/uri-templates.md
Normal file
290
i18n/de/pages/servers/uri-templates.md
Normal 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.
|
||||
Loading…
Reference in a new issue