docs: publish translated docs in twelve languages and the tool that maintains them (#3280)
This commit is contained in:
parent
14aa889992
commit
52ad0a8876
658 changed files with 89737 additions and 54 deletions
2
.github/workflows/deploy-docs.yml
vendored
2
.github/workflows/deploy-docs.yml
vendored
|
|
@ -12,6 +12,8 @@ on:
|
|||
# docs pages include their code blocks from these files via `--8<--`, so a
|
||||
# change here changes the rendered site even when no .md file moves.
|
||||
- docs_src/**
|
||||
# translated pages and the language registry feed the site/<code>/ sites
|
||||
- i18n/**
|
||||
- mkdocs.yml
|
||||
- src/mcp/**
|
||||
- src/mcp-types/**
|
||||
|
|
|
|||
1
.github/workflows/docs-preview.yml
vendored
1
.github/workflows/docs-preview.yml
vendored
|
|
@ -21,6 +21,7 @@ on:
|
|||
paths:
|
||||
- docs/**
|
||||
- docs_src/**
|
||||
- i18n/**
|
||||
- mkdocs.yml
|
||||
- scripts/docs/**
|
||||
- pyproject.toml
|
||||
|
|
|
|||
7
.gitignore
vendored
7
.gitignore
vendored
|
|
@ -144,10 +144,13 @@ venv.bak/
|
|||
# documentation
|
||||
/site
|
||||
/.worktrees/
|
||||
# Generated at build time by scripts/docs/ (the API reference tree and the
|
||||
# concrete Zensical config spliced from mkdocs.yml).
|
||||
# Generated at build time by scripts/docs/ (the API reference tree, the
|
||||
# concrete Zensical configs spliced from mkdocs.yml, and the staged docs tree
|
||||
# of each translated site).
|
||||
/docs/api/
|
||||
/mkdocs.gen.yml
|
||||
/mkdocs.*.gen.yml
|
||||
/.build/
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
|
|
|
|||
|
|
@ -126,6 +126,10 @@ pre-commit run --all-files
|
|||
- Add type hints to all functions
|
||||
- Include docstrings for public APIs
|
||||
|
||||
## Documentation and Translations
|
||||
|
||||
Documentation contributions are English only: the pages under `docs/` are the source of truth, and the translated documentation sites are generated from them, guided by the per-language style guides and glossaries under `i18n/<lang>/`. Never edit the generated pages under `i18n/<lang>/pages/`—the tool can't tell a hand edit from its own output, so the edit persists unchecked, is carried forward into future runs, and hides the real fix. To fix a translation, change that language's `instructions.md` or `glossary.json` (or the English page, if that's where the problem is) and re-run `translate --lang <code> --pages <page> …` for the affected pages; the fix then carries into every future run. See [`i18n/README.md`](i18n/README.md) for the details.
|
||||
|
||||
## Pull Requests
|
||||
|
||||
By the time you open a PR, the "what" and "why" should already be settled in an issue. This keeps reviews focused on implementation.
|
||||
|
|
|
|||
19
docs/js/language-switch.js
Normal file
19
docs/js/language-switch.js
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
// The theme links each language-switcher entry to that language's home page.
|
||||
// Point the entries at the current page on each language's site instead: every
|
||||
// prose page exists at the same path on all of them. The API reference is
|
||||
// English-only, so from there the entries keep pointing at the site roots.
|
||||
// Instant navigation swaps the page but keeps the header, so re-run on every
|
||||
// page the theme loads (`document$`) rather than once.
|
||||
const base = JSON.parse(document.getElementById("__config").textContent).base;
|
||||
// The site root as a directory path; `base` lacks the trailing slash on 404 pages.
|
||||
const site = new URL(base.replace(/\/?$/, "/"), location).pathname;
|
||||
|
||||
document$.subscribe(() => {
|
||||
let page = location.pathname.slice(site.length);
|
||||
if (page.startsWith("api/")) page = "";
|
||||
for (const entry of document.querySelectorAll(".md-select__link[hreflang]")) {
|
||||
// The language root the theme rendered, relative to the page first shown.
|
||||
entry.dataset.site ??= new URL(entry.getAttribute("href"), location).pathname;
|
||||
entry.href = entry.dataset.site + page;
|
||||
}
|
||||
});
|
||||
25
docs/translations.md
Normal file
25
docs/translations.md
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
# Translations
|
||||
|
||||
This documentation is written in English. To make it useful to more people, we also publish machine-translated editions of it, and this page explains what that means for you and how to help improve them.
|
||||
|
||||
## What's available
|
||||
|
||||
Translated documentation is currently a **preview** in twelve languages: Deutsch, español, français, हिन्दी, 日本語, 한국어, português (Brasil), русский язык, Türkçe, українська мова, 简体中文 and 繁體中文. Pick one from the language switcher at the top of any page. More languages may follow once these have proved themselves.
|
||||
|
||||
The API reference is not translated: the translated site links to the single English one.
|
||||
|
||||
## English is the source of truth
|
||||
|
||||
If a translated page and its English original disagree, the English page is correct. Every page of a translated site opens with one of three notes saying where it stands:
|
||||
|
||||
- **Machine translation** — the page was translated automatically and links to its English original.
|
||||
- **Translation behind the English page** — the English original changed after the page was translated. You are still reading that translation, so parts of it may be out of date until it catches up; the note links to the current English page.
|
||||
- **Shown in English** — the page has not been translated yet, so you are reading the English text.
|
||||
|
||||
## How the translations are made
|
||||
|
||||
Translated pages are machine-generated by a tool in this repository from the English pages under `docs/`, guided by two human-written inputs per language: a style guide (register, tone, typography, how to handle jokes and idioms) and a glossary (which terms stay in English, and the required and forbidden renderings for the rest). The generated text is never edited by hand. Every improvement goes into those inputs instead, so it survives the next time the pages are regenerated.
|
||||
|
||||
## Reporting a translation problem
|
||||
|
||||
Found a wrong term, an awkward sentence, or a translation that says something the English doesn't? [Open an issue](https://github.com/modelcontextprotocol/python-sdk/issues) with the language, the page and the passage; reports from native speakers are especially valuable. If you know the fix, propose it directly as a pull request against that language's style guide (`instructions.md`) or glossary (`glossary.json`) under [`i18n/`](https://github.com/modelcontextprotocol/python-sdk/tree/main/i18n) — the correction then reaches every affected page the next time the translations are regenerated. Problems with the English text itself are fixed in the pages under `docs/`, like any other documentation change.
|
||||
20
i18n/README.md
Normal file
20
i18n/README.md
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
# Documentation translations
|
||||
|
||||
The English pages under `docs/` are the source. This directory holds what steers their machine translation and the generated result; [`docs/translations.md`](../docs/translations.md) is the reader-facing explanation.
|
||||
|
||||
- `languages.yml` — the registry: one entry per translated site (served at `/<code>/`), the model id, and the nav pages that stay in English.
|
||||
- `general-prompt.md` — translation rules shared by every language. `notices.md` — English source of the three notes staged onto the pages of a translated site.
|
||||
- `<code>/instructions.md` (register, voice, typography, terminology) and `<code>/glossary.json` (`keep`: terms that stay in English; `terms`: required renderings, each with an optional `note` and banned `avoid` renderings, which are checked) — human-authored, sent with every request.
|
||||
- `<code>/pages/**` and `<code>/notices.md` — **generated**, never edited by hand: a correction goes into that language's `instructions.md` or `glossary.json` (or the English page), and the affected pages are re-run.
|
||||
|
||||
## The tool
|
||||
|
||||
```text
|
||||
uv run --frozen python scripts/docs/translations.py status [--lang CODE]
|
||||
uv run --frozen --group translate python scripts/docs/translations.py translate --lang CODE [--pages PATH ...]
|
||||
uv run --frozen python scripts/docs/translations.py stage [--lang CODE]
|
||||
```
|
||||
|
||||
`status` is offline: per language it lists missing, outdated (with the sections that changed), current and removable pages (translations whose English page is gone — `git rm` them). `translate` calls the Claude API (`ANTHROPIC_API_KEY` in the environment; the registry's model, or `DOCS_TRANSLATE_MODEL` to trial another) for the missing and outdated pages, retranslating only the English sections that changed and keeping the rest byte for byte; `--pages` instead re-translates exactly the named pages from scratch, which is also how a glossary or instructions change reaches existing pages (each generated page records the English section hashes it reflects, so editing those inputs invalidates nothing). `stage` assembles the tree each language site is built from (every language's, or one with `--lang`): each generated page exactly as it was generated, under an "outdated" notice linking the current English page when the English has changed since, and the English page where nothing was generated yet; `scripts/docs/build.sh` runs it before building them. Commit the generated pages in an ordinary pull request.
|
||||
|
||||
To add a language, add an entry to `languages.yml`, write `<code>/instructions.md` (the sections the `pt` file has) and `<code>/glossary.json`, then run `translate --lang <code>`.
|
||||
296
i18n/de/glossary.json
Normal file
296
i18n/de/glossary.json
Normal file
|
|
@ -0,0 +1,296 @@
|
|||
{
|
||||
"keep": [
|
||||
"MCP",
|
||||
"Model Context Protocol",
|
||||
"MCPServer",
|
||||
"FastMCP",
|
||||
"ClientSession",
|
||||
"Context",
|
||||
"ctx",
|
||||
"stdio",
|
||||
"Streamable HTTP",
|
||||
"SSE",
|
||||
"JSON-RPC",
|
||||
"JSON",
|
||||
"OAuth",
|
||||
"PKCE",
|
||||
"JWT",
|
||||
"CIMD",
|
||||
"HTTP",
|
||||
"HTTPS",
|
||||
"TLS",
|
||||
"CORS",
|
||||
"URI",
|
||||
"URL",
|
||||
"ASGI",
|
||||
"WebSocket",
|
||||
"API",
|
||||
"SDK",
|
||||
"CLI",
|
||||
"IDE",
|
||||
"LLM",
|
||||
"SEP",
|
||||
"RFC",
|
||||
"Python",
|
||||
"TypeScript",
|
||||
"Node.js",
|
||||
"PyPI",
|
||||
"Pydantic",
|
||||
"Starlette",
|
||||
"FastAPI",
|
||||
"uvicorn",
|
||||
"httpx",
|
||||
"anyio",
|
||||
"asyncio",
|
||||
"trio",
|
||||
"pytest",
|
||||
"OpenTelemetry",
|
||||
"Inspector",
|
||||
"Claude",
|
||||
"GitHub",
|
||||
"VS Code",
|
||||
"Windows",
|
||||
"macOS",
|
||||
"Linux",
|
||||
"llms.txt",
|
||||
"2026-07-28",
|
||||
"2025-11-25",
|
||||
"2025-06-18",
|
||||
"2025-03-26"
|
||||
],
|
||||
"terms": [
|
||||
{
|
||||
"source": "tool",
|
||||
"target": "Tool",
|
||||
"note": "MCP protocol noun (a server exposes tools) and the everyday word for a developer utility alike; kept as the loanword German developers use: das Tool / die Tools, compounds hyphenated (der Tool-Aufruf, die Tool-Beschreibung). Not Werkzeug. Wire identifiers such as `tools/call` and the `@mcp.tool()` decorator are code and stay untouched. Provisional pending native review."
|
||||
},
|
||||
{
|
||||
"source": "resource",
|
||||
"target": "Ressource",
|
||||
"note": "MCP protocol noun (data a server exposes for reading) and the general noun (a pool acquired in a lifespan is still a Ressource). German spelling with double s: die Ressource / die Ressourcen; never the English spelling Resource in German prose — except where it is a UI label the code emits, such as the Inspector's **Resources** tab. `resources/read` and `@mcp.resource()` are code."
|
||||
},
|
||||
{
|
||||
"source": "prompt",
|
||||
"target": "Prompt",
|
||||
"note": "The MCP feature (a reusable prompt template a server exposes) and the general AI sense; kept in English as German AI writing does. Masculine: der Prompt / die Prompts. Never Eingabeaufforderung, which is the command-line prompt and the wrong sense. `prompts/get` and `@mcp.prompt()` are code."
|
||||
},
|
||||
{
|
||||
"source": "sampling",
|
||||
"target": "Sampling",
|
||||
"note": "The (deprecated) client feature that lets a server borrow the client's model for a completion. Kept in English: das Sampling. Not Stichprobe / Stichprobenverfahren, which is statistical sampling and the wrong sense. The `sampling` capability key and `sampling/createMessage` stay Latin in code font. Provisional pending native review."
|
||||
},
|
||||
{
|
||||
"source": "roots",
|
||||
"target": "Roots",
|
||||
"note": "The (deprecated) client feature listing the workspace directories a client exposes; kept in English because readers meet it as the identifier `roots/list`. Plural: die Roots. May take the gloss \"Roots (freigegebene Arbeitsverzeichnisse)\" on first mention. Not Wurzeln (the botanical/mathematical sense); a root directory elsewhere is das Wurzelverzeichnis or Stammverzeichnis. A `Root` object in code font stays Latin. Provisional pending native review."
|
||||
},
|
||||
{
|
||||
"source": "elicitation",
|
||||
"target": "Elicitation",
|
||||
"note": "OPEN QUESTION for native review: there is no established German term for the server asking the person at the host a question mid-request. Provisionally kept in English — die Elicitation — glossed on its first appearance per page as \"Elicitation (Rückfrage bei der Person am Host)\", or, when that first appearance already sits inside parentheses, with a spaced en dash instead — \"Elicitation – Rückfrage bei der Person am Host\" — never a nested parenthesis; in running prose the act itself may be described with Rückfrage / zurückfragen. Do not coin Erhebung or Abfrage for it. `elicitation/create`, `ctx.elicit()` and the `Elicit` class stay Latin."
|
||||
},
|
||||
{
|
||||
"source": "capability",
|
||||
"target": "Capability",
|
||||
"note": "A negotiated protocol capability (what a client or server declared it supports during initialization). Provisional pending native review: kept as the term of art — die Capability / die Capabilities, \"capability negotiation\" → das Aushandeln der Capabilities — because it names the `capabilities` field; Fähigkeit is the open alternative and Funktion (a feature) is a different thing. Keys such as `sampling.tools` stay Latin."
|
||||
},
|
||||
{
|
||||
"source": "transport",
|
||||
"target": "Transport",
|
||||
"note": "The connection mechanism: der Transport / die Transporte (\"every standard transport\" → jeder Standard-Transport). The transport names stdio, Streamable HTTP and SSE are on the keep list and stay in English (der stdio-Transport, der Streamable-HTTP-Transport). Provisional pending native review."
|
||||
},
|
||||
{
|
||||
"source": "session",
|
||||
"target": "Session",
|
||||
"note": "An MCP session (the negotiated connection state): die Session / die Sessions, die Session-ID. Sitzung is understood too, but pin Session throughout rather than alternating; it matches the `session` objects and the `Mcp-Session-Id` header, which are code. Provisional pending native review."
|
||||
},
|
||||
{
|
||||
"source": "handler",
|
||||
"target": "Handler",
|
||||
"note": "The tool, resource or prompt function you register (nav section \"Inside your handler\" → Im Handler). Kept as everyday German developer usage: der Handler / die Handler (no plural -s). The verb is behandeln or verarbeiten, never handlen. Provisional pending native review."
|
||||
},
|
||||
{
|
||||
"source": "dependency",
|
||||
"target": "Abhängigkeit",
|
||||
"note": "Both package dependencies and the SDK's parameter-injection feature (the \"Dependencies\" page → Abhängigkeiten): die Abhängigkeit / die Abhängigkeiten. The pattern name stays English and open — Dependency Injection — as German developers write it. The `Resolve` marker class stays Latin. Provisional pending native review."
|
||||
},
|
||||
{
|
||||
"source": "resolver",
|
||||
"target": "Resolver",
|
||||
"note": "The plain function attached to a parameter with `Resolve(...)` that computes or asks for its value before the tool runs: der Resolver / die Resolver. Not Auflöser. The `Resolve` class stays Latin. Provisional pending native review."
|
||||
},
|
||||
{
|
||||
"source": "client",
|
||||
"target": "Client",
|
||||
"note": "An MCP client, and the client side of a connection: der Client / die Clients. Not Kunde (a customer). The `Client` class and the `mcp.client` module are code and stay untouched."
|
||||
},
|
||||
{
|
||||
"source": "server",
|
||||
"target": "Server",
|
||||
"note": "An MCP server (the program you build): der Server / die Server (no plural -s); ein MCP-Server with a hyphen. The `MCPServer`, `Server` and `ServerSession` classes are code and stay untouched."
|
||||
},
|
||||
{
|
||||
"source": "host",
|
||||
"target": "Host",
|
||||
"note": "The MCP host — the application the person talks to, which embeds the client and drives the model (Claude Desktop, an IDE) — and also a network host: der Host / die Hosts in both senses. Never Gastgeber.",
|
||||
"avoid": ["Gastgeber"]
|
||||
},
|
||||
{
|
||||
"source": "context",
|
||||
"target": "Kontext",
|
||||
"note": "The generic lower-case word (\"provide context to LLMs\" → LLMs Kontext bereitstellen): der Kontext. The capitalised `Context` is the SDK object injected as `ctx`; it is on the keep list, stays Latin in prose and is masculine by analogy (\"The Context\" → Der Context, das `Context`-Objekt)."
|
||||
},
|
||||
{
|
||||
"source": "request",
|
||||
"target": "Request",
|
||||
"note": "A JSON-RPC or HTTP request (\"the initialize request\" → der initialize-Request, \"request body\" → der Request-Body): der Request / die Requests. Anfrage is correct German too, but pin Request throughout rather than alternating. The verb \"to request\" is anfordern. `Request` types in code font stay Latin. Provisional pending native review."
|
||||
},
|
||||
{
|
||||
"source": "response",
|
||||
"target": "Response",
|
||||
"note": "A JSON-RPC or HTTP response message: die Response / die Responses. Antwort remains the word for an answer a person, model or tool gives (\"the user's answer\" → die Antwort), so both may appear on one page in their own senses. `Response` types in code font stay Latin. Provisional pending native review."
|
||||
},
|
||||
{
|
||||
"source": "notification",
|
||||
"target": "Benachrichtigung",
|
||||
"note": "A JSON-RPC notification (a message that expects no response) and change notifications alike: die Benachrichtigung / die Benachrichtigungen, \"change notification\" → Änderungsbenachrichtigung. Method strings such as `notifications/tools/list_changed` are code. Provisional pending native review; Notification is the open alternative."
|
||||
},
|
||||
{
|
||||
"source": "callback",
|
||||
"target": "Callback",
|
||||
"note": "Client callbacks (`sampling_callback`, `elicitation_callback`) and OAuth redirect callbacks alike: der Callback / die Callbacks, die Callback-URL. Not Rückruf (a return phone call); the dated Rückruffunktion is not used either. Parameter names stay Latin. Provisional pending native review."
|
||||
},
|
||||
{
|
||||
"source": "decorator",
|
||||
"target": "Dekorator",
|
||||
"note": "The Python decorators the SDK is built on, in the German spelling Python literature uses: der Dekorator / die Dekoratoren. `@mcp.tool()` and its siblings are code and stay untouched. Provisional pending native review; the English spelling Decorator is the open alternative."
|
||||
},
|
||||
{
|
||||
"source": "type hint",
|
||||
"target": "Type Hint",
|
||||
"note": "Python type hints (\"from your type hints\" → aus deinen Type Hints): der Type Hint / die Type Hints, kept as German Python developers say it; \"type annotation\" is die Typannotation. Provisional pending native review; Typ-Hinweis is the open alternative."
|
||||
},
|
||||
{
|
||||
"source": "round trip",
|
||||
"target": "Roundtrip",
|
||||
"note": "One request/response exchange: der Roundtrip / die Roundtrips (\"zero negotiation round trips\" → kein einziger Roundtrip für die Aushandlung). Written closed as German developers do, not Round Trip and not Rundreise (a tour). Provisional pending native review."
|
||||
},
|
||||
{
|
||||
"source": "multi-round-trip",
|
||||
"target": "Multi-Roundtrip",
|
||||
"note": "The 2026-07-28 request pattern (\"Multi-round-trip requests\" → Multi-Roundtrip-Requests, hyphenated through). Provisional coinage pending native review: gloss the English on first use per page — Multi-Roundtrip-Requests (multi-round-trip requests). The abbreviation MRTR stays Latin."
|
||||
},
|
||||
{
|
||||
"source": "lifespan",
|
||||
"target": "Lifespan",
|
||||
"note": "The server's startup/shutdown scope (the \"Lifespan\" page, as in the ASGI lifespan); kept in English so the prose matches the `lifespan=` parameter: der Lifespan. May take the gloss \"Lifespan (Start- und Stopp-Phase des Servers)\" on first mention. Lebensdauer is the word for the neighbouring \"lifetime\" (\"for the lifetime of the app\" → für die Lebensdauer der App) and is not banned; Lebenserwartung is always wrong.",
|
||||
"avoid": ["Lebenserwartung"]
|
||||
},
|
||||
{
|
||||
"source": "back-channel",
|
||||
"target": "Rückkanal",
|
||||
"note": "This documentation's term for the server calling back into the client during a request, which exists only on legacy connections: der Rückkanal. First mention on a page reads \"Rückkanal (back-channel)\" so the reader can connect it to the `NoBackChannelError` exception, which is code. Provisional pending native review."
|
||||
},
|
||||
{
|
||||
"source": "deprecated",
|
||||
"target": "veraltet",
|
||||
"note": "Advisory status: still works, scheduled for removal later — veraltet (\"Deprecated features\" → Veraltete Features, \"is deprecated\" → ist veraltet / gilt als veraltet); \"removed\" is entfernt, a different state. \"Deprecation warning\" → Deprecation-Warnung, tied to the `MCPDeprecationWarning` class, which stays Latin. Not the coinage deprecatet. Provisional pending native review."
|
||||
},
|
||||
{
|
||||
"source": "legacy",
|
||||
"target": "Legacy-",
|
||||
"note": "\"A legacy connection / client / session\" = one negotiated at spec version 2025-11-25 or earlier → die Legacy-Verbindung, der Legacy-Client, die Legacy-Session (prefix compound with hyphen); \"Serving legacy clients\" → Legacy-Clients unterstützen. Keep it distinct from veraltet, which renders \"deprecated\". Provisional pending native review; Alt- (Alt-Client) is the open alternative."
|
||||
},
|
||||
{
|
||||
"source": "era",
|
||||
"target": "Generation",
|
||||
"note": "\"Protocol era\" (\"a 2025-era client\", \"whatever era the client speaks\", \"any era of server\") → die Protokollgeneration, ein Client der 2025er-Generation, Server jeder Generation. Provisional pending native review; not Ära or Zeitalter, which read grandiose."
|
||||
},
|
||||
{
|
||||
"source": "wire",
|
||||
"target": "Leitung",
|
||||
"note": "The corpus's light metaphor for the byte stream between client and server, rendered with the idiom German networking prose uses: \"on the wire\" → auf der Leitung, \"what goes over the wire\" → was über die Leitung geht, \"stdout is the wire\" → stdout ist die Leitung, \"the JSON on the wire\" → das JSON auf der Leitung. Not Draht or Kabel. Provisional pending native review."
|
||||
},
|
||||
{
|
||||
"source": "handshake",
|
||||
"target": "Handshake",
|
||||
"note": "The initialization handshake (\"the classic handshake\" → der klassische Handshake): der Handshake / die Handshakes, as German protocol descriptions say. Provisional pending native review; not Handschlag in this corpus."
|
||||
},
|
||||
{
|
||||
"source": "escape hatch",
|
||||
"target": "Notausgang",
|
||||
"note": "The API-design metaphor for the lower-level mechanism you drop to when the convenience layer is in the way (`client.session`, `add_request_handler()`, the low-level `Server`): der Notausgang / die Notausgänge. Pinned so every page uses one rendering; Hintertür (a backdoor, with its security connotation) is wrong here. Provisional pending native review."
|
||||
},
|
||||
{
|
||||
"source": "library",
|
||||
"target": "Bibliothek",
|
||||
"note": "A software library: die Bibliothek / die Bibliotheken (Library is heard in speech but pin the German word). Bücherei is a lending library and never right here.",
|
||||
"avoid": ["Bücherei"]
|
||||
},
|
||||
{
|
||||
"source": "deploy",
|
||||
"target": "bereitstellen",
|
||||
"note": "The verb: bereitstellen / in Produktion bringen (\"deploy it the way you deploy any ASGI app\" → stelle es bereit wie jede andere ASGI-App); the noun is das Deployment; the nav title \"Deploy & scale\" → Bereitstellen und skalieren. Never the coinage deployen / deployt (see instructions §5). Provisional pending native review."
|
||||
},
|
||||
{
|
||||
"source": "default value",
|
||||
"target": "Standardwert",
|
||||
"note": "A parameter's default: der Standardwert; \"by default\" → standardmäßig; Default- is fine inside an established compound (die Default-Konfiguration) but do not alternate Standardwert and Defaultwert. Provisional pending native review."
|
||||
},
|
||||
{
|
||||
"source": "exception",
|
||||
"target": "Exception",
|
||||
"note": "A raised Python exception: die Exception / die Exceptions, \"raises an exception\" → löst eine Exception aus / wirft eine Exception. Ausnahme is correct German too; pin Exception so prose matches the class names, which stay Latin, and never alternate. An \"error\" is der Fehler. Provisional pending native review."
|
||||
},
|
||||
{
|
||||
"source": "return value",
|
||||
"target": "Rückgabewert",
|
||||
"note": "A function's return value: der Rückgabewert; \"returns X\" → gibt X zurück / liefert X. The `return` keyword and annotations are code."
|
||||
},
|
||||
{
|
||||
"source": "subscription",
|
||||
"target": "Abonnement",
|
||||
"note": "Resource and list-change subscriptions (the two \"Subscriptions\" pages → Abonnements): das Abonnement / die Abonnements, \"subscribe\" → abonnieren, \"subscriber\" → der Abonnent (a piece of software here, so no gendering question arises). `subscriptions/listen` and `resources/subscribe` are code. Provisional pending native review."
|
||||
},
|
||||
{
|
||||
"source": "completion",
|
||||
"target": "Vervollständigung",
|
||||
"note": "Two senses. The MCP feature that autocompletes prompt and resource-template arguments (the \"Completions\" page, `completion/complete`) → die Vervollständigung / Vervollständigungen. The text a model produces in the sampling pages (\"ask the client for an LLM completion\") is a different thing → die Antwort des Modells or die Completion; never Vervollständigung there. Provisional pending native review."
|
||||
},
|
||||
{
|
||||
"source": "structured output",
|
||||
"target": "strukturierte Ausgabe",
|
||||
"note": "The tools feature and its page title (\"Structured Output\" → Strukturierte Ausgabe): die strukturierte Ausgabe. `structured_output` and `outputSchema` are code. Provisional pending native review."
|
||||
},
|
||||
{
|
||||
"source": "troubleshooting",
|
||||
"target": "Fehlerbehebung",
|
||||
"note": "The page title and the activity: die Fehlerbehebung. Not Problembehandlung and not the loan Troubleshooting in a heading. Provisional pending native review."
|
||||
},
|
||||
{
|
||||
"source": "authorization",
|
||||
"target": "Autorisierung",
|
||||
"note": "The OAuth sense and the page title (\"Authorization\" → Autorisierung); \"authentication\" is Authentifizierung — keep the two apart as the English does. The `Authorization` header is code. Provisional pending native review."
|
||||
},
|
||||
{
|
||||
"source": "Get started",
|
||||
"target": "Einstieg",
|
||||
"note": "The nav section that opens the guide, and the title of its index page. \"First steps\" is a separate page inside that section (Erste Schritte), so the two need distinct renderings or the sidebar shows the same title twice — never Erste Schritte for this one. Provisional pending native review; Loslegen is the open alternative."
|
||||
},
|
||||
{
|
||||
"source": "First steps",
|
||||
"target": "Erste Schritte",
|
||||
"note": "The tutorial page inside the \"Get started\" section; never reuse this rendering for the section itself (see that entry). Provisional pending native review."
|
||||
},
|
||||
{
|
||||
"source": "Recap",
|
||||
"target": "Zusammenfassung",
|
||||
"note": "Recurring section heading that closes most pages; one rendering everywhere, not Fazit on some pages and Zusammenfassung on others. Provisional pending native review."
|
||||
},
|
||||
{
|
||||
"source": "Try it",
|
||||
"target": "Ausprobieren",
|
||||
"note": "Recurring section heading above a runnable example; one rendering everywhere (the infinitive, per the heading rule), not Probier es aus on some pages. Provisional pending native review."
|
||||
}
|
||||
]
|
||||
}
|
||||
173
i18n/de/instructions.md
Normal file
173
i18n/de/instructions.md
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
# German (de) — translation instructions
|
||||
|
||||
Target language: German in Germany's standard orthography (Deutsch, de-DE),
|
||||
directory and URL code `de`, page language tag `de`. This file is sent verbatim
|
||||
with every translation request for this language, on top of the shared rules
|
||||
in `../general-prompt.md`. The termbase in `glossary.json` is sent alongside it
|
||||
and wins any terminology conflict with this file.
|
||||
|
||||
## 1. Register
|
||||
|
||||
Address the reader as **du**, consistently — the register of modern open-source
|
||||
and developer-tool documentation; Sie would read like vendor docs.
|
||||
|
||||
- du, dich, dir, dein are lower-case mid-sentence. Never address the reader as
|
||||
Sie / Ihnen / Ihr, never capitalised Du / Dein, never a mix — a page that
|
||||
drifts between du and Sie, or between direct imperatives and impersonal
|
||||
officialese, is wrong even when each sentence is acceptable on its own.
|
||||
(Third-person sie and a sentence-initial Sie are ordinary German and fine.)
|
||||
- Steps are bare du imperatives: "Install the SDK, then run the server" →
|
||||
Installiere das SDK und starte dann den Server — not Installieren Sie …, not
|
||||
the infinitive SDK installieren in running prose, not Du solltest … (needless
|
||||
modal), no bitte per step. Impersonal man only for truly general statements.
|
||||
Where English says "your", German often uses the article: öffne das Terminal.
|
||||
- Headings, table headers, tab labels and admonition titles are noun phrases or
|
||||
infinitive constructions, never imperatives: "Declare a tool" → Ein Tool
|
||||
deklarieren, "Handling errors" → Fehler behandeln, "Running your server" →
|
||||
Den Server betreiben, "The Context" → Der Context. A question heading may
|
||||
stay a question (Wohin damit?). No full stop after a heading.
|
||||
- Requirement strength stays exact: must → muss, should → sollte, may / can →
|
||||
kann or darf, must not → darf nicht (muss nicht means "need not").
|
||||
- Gender-neutral wording by phrasing, never by typography. Sentences about the
|
||||
reader (du) or about software (der Client, der Server) need nothing. For
|
||||
people use plurals and neutral nouns — alle, die den Host bedienen; wer das
|
||||
SDK einsetzt; das Team — and for the single human in front of the host ("the
|
||||
user") die Person, or die Person am Host where the role needs naming, then
|
||||
sie. Never Nutzer*innen, Nutzer:innen, NutzerInnen or Nutzer/-innen, and no
|
||||
bare generic masculine (der Nutzer, der Entwickler) either. Provisional;
|
||||
apply it uniformly.
|
||||
|
||||
## 2. Voice
|
||||
|
||||
The English source is warm, direct and confident: short sentences, the
|
||||
occasional one-line payoff. Carry that — sachlich, direkt, freundlich.
|
||||
|
||||
- Keep the payoff sentences short: "That's the whole API." → Das ist die ganze
|
||||
API. — not a formal summary sentence. Split long English sentences: two main
|
||||
clauses read better than one nested period with the verb parked at the end.
|
||||
Never merge, drop or reorder the technical claims themselves.
|
||||
- Verbs, not Nominalstil: die Durchführung der Installation erfolgt →
|
||||
installiere; eine Überprüfung vornehmen → prüfen. Active where German allows
|
||||
it: "The tool is called by the model" → Das Modell ruft das Tool auf.
|
||||
- No officialese (seitens, mittels, im Rahmen von, es ist darauf zu achten,
|
||||
dass, erfolgt as an all-purpose verb), no hype or softeners (leistungsstark,
|
||||
nahtlos, im Handumdrehen; du könntest eventuell → du kannst), no
|
||||
English-shaped German (Sinn machen → sinnvoll sein, Python's → Pythons, ist
|
||||
am Laufen → läuft). Nor the over-correction: no buddy tone (mega, easy).
|
||||
- Example — "You don't construct it and you don't configure it. You ask for
|
||||
it." → Du erzeugst ihn nicht selbst und konfigurierst ihn auch nicht. Du
|
||||
forderst ihn einfach an. (ihn: der Context.) Not the Nominalstil Eine
|
||||
Instanziierung sowie Konfiguration ist nicht erforderlich; es genügt eine
|
||||
Anforderung. — nor the slangy calque Du baust es nicht … fragst danach, easy!
|
||||
|
||||
## 3. Humour and idioms
|
||||
|
||||
- The English is friendly and dry rather than jokey; the warmth carries over
|
||||
into the du register unchanged, the idioms do not. Never translate a pun,
|
||||
idiom or aside literally: say what it means as a short, natural German
|
||||
sentence in the same register; a German idiom at home in technical prose is
|
||||
welcome (unter der Haube for "under the hood"). An aside with no information
|
||||
may go — a technical caveat phrased lightly never does.
|
||||
- Recurring English tags get fixed renderings: "**[X](…)** has the whole story"
|
||||
/ "The whole story is in **[X](…)**" → Alles Weitere steht in **[X](…)**;
|
||||
"That's the whole API." / "That's the whole protocol." → Das ist die ganze
|
||||
API. / Das ist das ganze Protokoll.; "That's it. It's just Python." → Das ist
|
||||
alles. Ganz normales Python. (not Das ist es. Es ist nur Python!); "You get
|
||||
`3` back. ✨" → Du bekommst `3` zurück. ✨ (not Du erhältst 3 zurück! ✨ —
|
||||
lost code span, added exclamation mark).
|
||||
- Idioms take the plain meaning, not the picture: "Out of the box the app
|
||||
answers **only** requests addressed to localhost." → Ohne weitere
|
||||
Konfiguration beantwortet die App **nur** Requests an localhost — not aus der
|
||||
Box heraus. "it stops being required" → er ist nicht mehr erforderlich, not
|
||||
er stoppt, required zu sein.
|
||||
- Exclamation marks: keep one only where the English carries genuine emphasis;
|
||||
never add, never double, never in a heading. Emoji: keep the source's rare,
|
||||
deliberately placed emoji exactly where they are; never add new ones.
|
||||
|
||||
## 4. Typography
|
||||
|
||||
- Quotation marks in prose are German „…“ (U+201E, U+201C), with ‚…‘ for a
|
||||
quote inside a quote. Straight "…" and English “…” in the source prose become
|
||||
„…“, scare quotes and example utterances included. Quotes inside code spans
|
||||
and code blocks stay exactly as they are, and a code span is never wrapped in
|
||||
quotation marks.
|
||||
- Dashes: an English em-dash aside becomes a Gedankenstrich — an en dash with a
|
||||
space on each side (Text – Einschub – Text) — or commas, parentheses or a
|
||||
second sentence; never an em dash (—) in German text. Ranges: 3.10 bis 3.14,
|
||||
or 3.10–3.14 with an en dash and no spaces.
|
||||
- Compounds are closed or hyphenated, never spaced. A compound with an English,
|
||||
abbreviated or code-font part is hyphenated through every joint: der
|
||||
MCP-Server, das JSON-RPC-Format, der Streamable-HTTP-Transport, das
|
||||
`Context`-Objekt, die `PATH`-Umgebungsvariable. Never MCP Server with a space
|
||||
(and `MCPServer` is a class, not ein MCP-Server). A multi-word English term
|
||||
standing alone stays open: Streamable HTTP, Dependency Injection.
|
||||
- Every noun is capitalised, borrowed ones included (der Request, das Tool);
|
||||
borrowed adjectives and verbs are not (optional, gecacht). Orthography is
|
||||
de-DE: dass, muss, schließen, außerdem — never Swiss ss.
|
||||
- Digits stay ASCII. Protocol revision strings such as `2026-07-28`, version
|
||||
numbers, ports, status and error codes, RFC and SEP numbers are identifiers,
|
||||
copied byte for byte — never 28.07.2026, never 28. Juli 2026. Prose
|
||||
quantities take the decimal comma only when nothing but the separator changes
|
||||
(2,5 Sekunden), never inside code; a space before units and % (30 s, 100 %).
|
||||
- Abbreviations: e.g. → z. B., i.e. → d. h., etc. → usw. (inner space kept);
|
||||
vs → oder / gegenüber; & in prose → und. Commas follow German grammar, not
|
||||
the source (before dass, weil, wenn, ob and relative clauses).
|
||||
- Bold and italics land on the words that carry the source's emphasis; a bolded
|
||||
negation ("**not**" → **nicht** / **kein**) stays bold. English words kept in
|
||||
German text are set in normal type.
|
||||
|
||||
## 5. Terminology pointer
|
||||
|
||||
The termbase is `glossary.json` next to this file. It is injected into the
|
||||
prompt separately and its renderings override anything written here. This
|
||||
section only fixes the conventions the glossary assumes:
|
||||
|
||||
- Terms in the glossary's `keep` list are copied exactly — same spelling and
|
||||
casing, not translated, italicised or quoted. They take an article by gender
|
||||
(das SDK, die API, das JSON, die URL, der URI, das CLI, das LLM, der SEP, der
|
||||
RFC) and the English plural where the source is plural (die SDKs).
|
||||
- Everything in code font — class, function, parameter and module names,
|
||||
protocol method strings (`tools/call`), header names, error text, config keys
|
||||
— stays byte-identical. Name the kind of thing in front where it helps (die
|
||||
Klasse `Context`, der Parameter `lifespan=`); compounds take a hyphen outside
|
||||
the backticks (der `Resolve`-Marker). A glossary term used as a code-font
|
||||
identifier stays English: "the `sampling` capability" → die Capability
|
||||
`sampling`.
|
||||
- Text quoted from what the example code prints or displays — an output line, a
|
||||
log message, an error string, a UI label such as the Inspector's **Tools**
|
||||
and **Resources** tabs — stays exactly as the code emits it (usually
|
||||
English), in or out of code font. The quotation marks around it may become
|
||||
„…“; the text inside does not change.
|
||||
- Nouns are borrowed, verbs are not. German developers keep many English nouns
|
||||
— capitalised, with a fixed gender, declined: der Request, die Response, der
|
||||
Client, der Server, der Host, der Handler, der Callback, das Tool, der
|
||||
Prompt, das Token, der String, der Header, die Payload, der Stream, das
|
||||
Schema, die Middleware, die Session, der Commit, der Build, das Deployment.
|
||||
Plurals take -s (die Requests, die Tools) except nouns in -er, which stay
|
||||
unchanged (die Server, die Handler, die Parameter). Verbs are German wherever
|
||||
a plain German verb exists: bereitstellen (not deployen), einen Commit
|
||||
anlegen (not committen), zusammenführen (not mergen), aktualisieren (not
|
||||
updaten). Fully naturalised verbs are fine: debuggen, parsen, loggen, cachen.
|
||||
- Translate where German developers use the German word themselves — a forced
|
||||
purism is as wrong as needless English: Ressource, Abhängigkeit, Fehler,
|
||||
Rückgabewert, Standardwert, Umgebungsvariable, Bibliothek, Verzeichnis,
|
||||
Verbindung, Benachrichtigung; but never Zeichenkette for String.
|
||||
- First-use gloss: a rendering the reader may need to map back to the English
|
||||
specification carries the English in parentheses on its first occurrence on
|
||||
a page — der Rückkanal (back-channel) — where the glossary note says so.
|
||||
- One rendering per term per page: the glossary target, every time. Where an
|
||||
entry's note marks the choice as open or provisional, still use the listed
|
||||
target consistently — never Request in one paragraph and Anfrage in the next.
|
||||
|
||||
## 6. Provisional note
|
||||
|
||||
The register, voice and terminology decisions above, and every entry in
|
||||
`glossary.json`, are provisional pending review by native German-speaking
|
||||
readers — in particular the du address, the gender-neutral phrasing convention
|
||||
and the keep-versus-translate line for individual nouns. To propose a change,
|
||||
edit this file or `glossary.json` in a pull request, ideally with a short
|
||||
good/bad example; never edit the generated `pages/` or `notices.md` next to
|
||||
this file. The tool cannot tell a hand edit from its own output, so one would
|
||||
persist unchecked and be carried forward into later runs; a correction made
|
||||
here reaches the pages when they are regenerated with
|
||||
`translate --lang de --pages …`.
|
||||
20
i18n/de/notices.md
Normal file
20
i18n/de/notices.md
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
---
|
||||
translation:
|
||||
sections: [aff1b3e872b7876a, 4d80558ad052d586, 0bb81f1e62062d26, d5c35dcec50156bc]
|
||||
tool: 1
|
||||
---
|
||||
# Übersetzungshinweise {#translation-notices}
|
||||
|
||||
Einer dieser Hinweise steht oben auf jeder Seite einer übersetzten Dokumentationsseite.
|
||||
|
||||
## Maschinelle Übersetzung {#translated}
|
||||
|
||||
Diese Seite wurde automatisch aus der englischen Dokumentation übersetzt, und die [englische Seite](ENGLISH_PAGE) ist die maßgebliche Fassung. Wenn sich etwas falsch liest, erklärt [Übersetzungen](TRANSLATIONS_PAGE), wie du es melden kannst.
|
||||
|
||||
## Übersetzung hinter der englischen Seite zurück {#outdated}
|
||||
|
||||
Die englische Seite hat sich geändert, nachdem diese Übersetzung entstanden ist, daher können Teile davon veraltet sein. Lies im Zweifel die [englische Seite](ENGLISH_PAGE); [Übersetzungen](TRANSLATIONS_PAGE) erklärt, wie die übersetzte Dokumentation funktioniert.
|
||||
|
||||
## Auf Englisch angezeigt {#english}
|
||||
|
||||
Für diese Seite gibt es keine aktuelle Übersetzung, deshalb liest du sie auf Englisch. [Übersetzungen](TRANSLATIONS_PAGE) erklärt, wie die übersetzte Dokumentation funktioniert.
|
||||
123
i18n/de/pages/advanced/apps.md
Normal file
123
i18n/de/pages/advanced/apps.md
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
---
|
||||
translation:
|
||||
sections: [0355618e5f4d5fe4, 1821eaf50f2d0b64, 82e0b28ebd3abf5a, 8ac39614c094f2d0, dab6ff945501ab2a, bd5565c3b2d4f959, 96819ce3d63a0487]
|
||||
tool: 1
|
||||
---
|
||||
# MCP Apps {#mcp-apps}
|
||||
|
||||
Eine **MCP App** ist ein Tool mit Gesicht: Neben seinen Daten verweist das Tool auf ein HTML-Dokument, das der Host als interaktive Oberfläche rendert.
|
||||
|
||||
Zwei Teile, immer zwei Teile:
|
||||
|
||||
1. **Ein Tool**, das die Arbeit macht und Daten zurückgibt, wie jedes andere Tool auch.
|
||||
2. **Eine `ui://`-Ressource** mit dem HTML, das der Host dafür anzeigt.
|
||||
|
||||
Das Tool trägt eine `_meta.ui.resourceUri`-Referenz auf die Ressource. Der Host holt sie mit `resources/read`, rendert sie in einem **Sandbox-iframe** und schiebt das Ergebnis des Tools per `postMessage` in diesen iframe. Dein Server sendet oder empfängt niemals `ui/*`-Nachrichten: Dieser Verkehr läuft zwischen Host und iframe. Du lieferst ein Tool und ein HTML-Dokument; das Theater übernimmt der Host.
|
||||
|
||||
Das SDK liefert das als eingebaute Extension `Apps` (`io.modelcontextprotocol/ui`) mit. Falls [Extensions](extensions.md) neu für dich sind, überfliege zuerst jene Seite. Eine Minute, dann komm zurück.
|
||||
|
||||
## Eine Uhr mit Gesicht {#a-clock-with-a-face}
|
||||
|
||||
```python title="server.py" hl_lines="19 22 30 32"
|
||||
--8<-- "docs_src/apps/tutorial001.py"
|
||||
```
|
||||
|
||||
Vier Schritte:
|
||||
|
||||
* `Apps()`: Eine Instanz hält deine UI-gebundenen Tools und ihre Ressourcen.
|
||||
* `@apps.tool(resource_uri="ui://clock/app.html")`: ein normales Tool plus der `_meta.ui.resourceUri`-Stempel. Alles, was `@mcp.tool()` akzeptiert (name, title, description, ...), wird durchgereicht.
|
||||
* `apps.add_html_resource("ui://clock/app.html", CLOCK_HTML)`: die passende Ressource, ausgeliefert als `text/html;profile=mcp-app`. Genau dieser MIME-Typ sagt einem Host „das ist eine App, rendere sie“.
|
||||
* `MCPServer("clock", extensions=[apps])`: die Anmeldung. Der Server bewirbt jetzt `io.modelcontextprotocol/ui` unter `capabilities.extensions`.
|
||||
|
||||
Das HTML selbst lauscht auf das `postMessage` des Hosts und zeigt das Ergebnis an. Für echte Apps verwende das offizielle Browser-SDK [`@modelcontextprotocol/ext-apps`](https://github.com/modelcontextprotocol/ext-apps) in deinem HTML. Es gibt dir `ontoolresult`, `callServerTool`, `getHostContext` und `onhostcontextchanged` statt roher Message-Events.
|
||||
|
||||
## Graceful Degradation {#graceful-degradation}
|
||||
|
||||
Nicht jeder Client rendert Apps. Die Spezifikation sagt unverblümt, was das für dich bedeutet:
|
||||
|
||||
> Tools **MÜSSEN** ein sinnvolles `content`-Array zurückgeben, auch wenn eine UI verfügbar ist.
|
||||
|
||||
Das Modell liest `content`; der iframe ist für Menschen. Ein UI-fähiger Host füttert das Modell trotzdem mit dem Textergebnis, und ein reiner Text-Client bekommt *nur* das. Das kanonische Muster ist also: ein Tool, zwei Antworten. Sieh dir `get_time` noch einmal an:
|
||||
|
||||
```python title="server.py" hl_lines="23-27"
|
||||
--8<-- "docs_src/apps/tutorial001.py"
|
||||
```
|
||||
|
||||
`client_supports_apps(ctx)` ist nur dann `True`, wenn der Client die Extension `io.modelcontextprotocol/ui` deklariert **und** `text/html;profile=mcp-app` in seinen `mimeTypes`-Einstellungen aufgeführt hat. Das Feld ist Pflicht, ein Client, der es weglässt, zählt also nicht. Genau das deklariert `main()` in derselben Datei: die Client-Hälfte der Aushandlung – und die reichhaltige Antwort kommt zurück.
|
||||
|
||||
!!! warning
|
||||
Gib niemals einen Platzhalter wie `"[Rendered UI]"` als einzigen Inhalt zurück. Wenn der Fallback-Text nutzlos ist, ist das Tool für jeden reinen Text-Client und für das Modell selbst nutzlos. Schreib den Satz.
|
||||
|
||||
## Den iframe abriegeln {#locking-the-iframe-down}
|
||||
|
||||
Die Ressourcenseite trägt die Sicherheitsmetadaten: was der iframe laden darf, welche Browser-Berechtigungen er möchte, wie er eingebettet werden will:
|
||||
|
||||
```python title="server.py" hl_lines="9 19-22"
|
||||
--8<-- "docs_src/apps/tutorial002.py"
|
||||
```
|
||||
|
||||
`csp` und `permissions` sind **Anfragen an den Host**, kein Serververhalten. Der Host baut daraus die Content-Security-Policy und die Permissions-Policy des iframes, und er darf ablehnen. Prüfe in deinem JS per Feature Detection, statt eine Zusage vorauszusetzen.
|
||||
|
||||
`ResourceCsp`, Feld für Feld (Python-Name, Schlüssel auf der Leitung, was der Host damit macht):
|
||||
|
||||
| Python | Leitung (`_meta.ui.csp`) | Steuert |
|
||||
|---|---|---|
|
||||
| `connect_domains` | `connectDomains` | `connect-src`: wohin `fetch`/XHR gehen darf |
|
||||
| `resource_domains` | `resourceDomains` | `img-src`, `style-src`, ...: statische Assets |
|
||||
| `frame_domains` | `frameDomains` | `frame-src`: verschachtelte iframes |
|
||||
| `base_uri_domains` | `baseUriDomains` | `base-uri`: worauf `<base>` zeigen darf |
|
||||
|
||||
`ResourcePermissions`: Jedes Feld fordert eine Browser-Berechtigung für den iframe an.
|
||||
|
||||
| Python | Leitung (`_meta.ui.permissions`) |
|
||||
|---|---|
|
||||
| `camera` | `camera` |
|
||||
| `microphone` | `microphone` |
|
||||
| `geolocation` | `geolocation` |
|
||||
| `clipboard_write` | `clipboardWrite` |
|
||||
|
||||
!!! note
|
||||
CSP und Berechtigungen liegen auf der **Ressource**, nie auf dem Tool. Die Tool-Metadaten der Spezifikation haben keinen Platz dafür, und Hosts ignorieren sie dort. Das SDK macht den Fehler unmöglich: `@apps.tool()` hat schlicht keinen Parameter `csp`.
|
||||
|
||||
### Sichtbarkeit {#visibility}
|
||||
|
||||
`visibility=["app"]` an einem Tool sagt „das existiert für den iframe, nicht für das Modell“:
|
||||
|
||||
* `"model"`: Das Modell darf es aufrufen.
|
||||
* `"app"`: Der iframe darf es aufrufen (über `callServerTool`).
|
||||
* Weggelassen: beide, das ist der Standardwert.
|
||||
|
||||
Filtern ist Aufgabe des **Hosts**. Dein Server listet reine App-Tools in `tools/list` wie alle anderen; der Host verbirgt sie vor dem Modell. Filtere nicht serverseitig.
|
||||
|
||||
## Die Regeln, die das SDK durchsetzt {#the-rules-the-sdk-enforces}
|
||||
|
||||
All das schlägt beim Start fehl, nicht in Produktion:
|
||||
|
||||
* Ein `resource_uri` oder ein Ressourcen-URI, der nicht `ui://...` ist, ist ein `ValueError` zum Zeitpunkt der Dekoration bzw. Registrierung.
|
||||
* Ein Tool, das an einen URI **ohne passende registrierte Ressource** gebunden ist, ist ein `ValueError`, wenn `MCPServer(extensions=[apps])` die Extension übernimmt. Ein Tool, das HTML bewirbt, das bei `resources/read` mit 404 antwortet, ist eine Fehlkonfiguration, also verweigert der Server die Konstruktion.
|
||||
* `meta={"ui": ...}` an `@apps.tool()` ist ein `ValueError`. `_meta["ui"]` gehört dem Dekorator; sag es mit `resource_uri=` und `visibility=`. Andere `meta=`-Schlüssel werden daneben problemlos zusammengeführt.
|
||||
|
||||
Weder das TypeScript-ext-apps-SDK noch FastMCP fängt heute irgendetwas davon ab; uns ist lieber, du erfährst es, bevor ein Host es tut.
|
||||
|
||||
## Über Inline-HTML hinaus {#beyond-inline-html}
|
||||
|
||||
`add_html_resource` deckt den häufigen Fall ab: einen String mit HTML. Für alles andere, HTML auf der Platte oder generierte Inhalte, baust du die Ressource selbst und reichst sie weiter:
|
||||
|
||||
```python title="server.py" hl_lines="12 18"
|
||||
--8<-- "docs_src/apps/tutorial003.py"
|
||||
```
|
||||
|
||||
`add_resource` ergänzt den MIME-Typ `text/html;profile=mcp-app`, wenn die Ressource keinen explizit setzt, und weist einen expliziten Widerspruch zurück: Eine `ui://`-Ressource unter einem anderen MIME-Typ rendert kein Host.
|
||||
|
||||
!!! tip
|
||||
Du zielst auf einen Pre-GA-Host, der noch den veralteten flachen Schlüssel `_meta["ui/resourceUri"]` liest? Führe ihn selbst zusammen:
|
||||
`@apps.tool(resource_uri="ui://x", meta={"ui/resourceUri": "ui://x"})`.
|
||||
Das verschachtelte `ui`-Objekt ist die Form der Spezifikation; der flache Schlüssel ist auf dem Weg nach draußen.
|
||||
|
||||
## Laufen sehen {#see-it-run}
|
||||
|
||||
Die Story `apps` in `examples/stories/` ist diese Seite als lauffähiges Paar: ein Server mit einem UI-gebundenen Uhr-Tool und ein Client, der Apps aushandelt, die `_meta.ui.resourceUri` des Tools liest, das HTML holt und das Tool aufruft.
|
||||
|
||||
```bash
|
||||
uv run python -m stories.apps.client
|
||||
```
|
||||
172
i18n/de/pages/advanced/extensions.md
Normal file
172
i18n/de/pages/advanced/extensions.md
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
---
|
||||
translation:
|
||||
sections: [05891e7cc1938a13, b3c01a6af28c51ee, 7ffc91f5e38bdfe0, 717d3f235a8333a7, f471a13b2fe5d737, ed6af2df4b656dff]
|
||||
tool: 1
|
||||
---
|
||||
# Extensions {#extensions}
|
||||
|
||||
Eine **Extension** ist ein optionales Bündel von MCP-Verhalten hinter einem einzigen Identifier.
|
||||
|
||||
Auf einem Server kann sie Tools, Ressourcen und neue Request-Methoden beisteuern, und sie kann `tools/call` umhüllen. Auf einem Client kann sie zusätzliche Ergebnisformen von `tools/call` für sich beanspruchen und Vendor-Benachrichtigungen beobachten. Jede Seite kündigt sie unter ihrem eigenen `capabilities.extensions` an, und für alle, die nicht darum gebeten haben, ändert sich nichts. Das ist der Vertrag ([SEP-2133](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2133)), und er hat eine goldene Regel: **Extensions sind standardmäßig aus**.
|
||||
|
||||
## Eine Extension verwenden {#using-an-extension}
|
||||
|
||||
Übergib Instanzen bei der Konstruktion:
|
||||
|
||||
```python title="server.py"
|
||||
--8<-- "docs_src/extensions/tutorial001.py"
|
||||
```
|
||||
|
||||
Fertig. Der Server kündigt jetzt `io.modelcontextprotocol/ui` unter `capabilities.extensions` an und bedient alles, was die Extension beisteuert.
|
||||
|
||||
`Apps` ist die eingebaute Referenz-Extension und bekommt eine eigene Seite: **[MCP Apps](apps.md)**.
|
||||
|
||||
!!! note
|
||||
Extensions stehen bei der Konstruktion fest. Es gibt kein `add_extension`, das du später aufrufen könntest: Die Capability-Map eines Servers sollte sich nicht ändern, während Clients mit ihm verbunden sind.
|
||||
|
||||
Die Capability-Map reist mit `server/discover`, und das ist ein Pfad von **2026-07-28**. Ein Legacy-`initialize`-Handshake hat keinen Platz dafür, also sieht ein Legacy-Client die Extension schlicht nicht. Plane das ein: Eine Extension *ergänzt* einen Server, sie darf nicht der einzige Weg sein, auf dem der Server nutzbar ist.
|
||||
|
||||
## Eine eigene Extension schreiben {#writing-your-own}
|
||||
|
||||
Leite von `Extension` ab und überschreibe nur, was du brauchst. Jede Methode hat eine Standardimplementierung.
|
||||
|
||||
### Der Identifier {#the-identifier}
|
||||
|
||||
```python
|
||||
--8<-- "docs_src/extensions/tutorial002.py"
|
||||
```
|
||||
|
||||
Der Identifier ist ein `vendor-prefix/name`-String nach der `_meta`-Schlüsselgrammatik der Spezifikation: durch Punkte getrennte Labels (jedes beginnt mit einem Buchstaben und endet mit einem Buchstaben oder einer Ziffer), ein Schrägstrich, dann der Name. Er wird **bei der Definition der Klasse** validiert, ein Tippfehler wartet also nicht darauf, dass ein Server startet:
|
||||
|
||||
```text
|
||||
TypeError: Stamps.identifier must be a `vendor-prefix/name` string
|
||||
(reverse-DNS prefix required), got 'stamps'
|
||||
```
|
||||
|
||||
Nimm als Präfix eine Domain, die du kontrollierst. `io.modelcontextprotocol/*` ist Extensions vorbehalten, die das MCP-Projekt selbst spezifiziert.
|
||||
|
||||
### Tools beisteuern {#contributing-tools}
|
||||
|
||||
Die kleinste nützliche Extension besteht aus einem Tool und einer Settings-Map:
|
||||
|
||||
```python title="server.py" hl_lines="17 19-20 22-23 26"
|
||||
--8<-- "docs_src/extensions/tutorial003.py"
|
||||
```
|
||||
|
||||
* `tools()` gibt `ToolBinding`s zurück. Der Server registriert jedes einzelne genau so, als hättest du selbst `mcp.add_tool(...)` aufgerufen: dieselbe Schema-Generierung, dieselbe `Context`-Injection, alles gleich.
|
||||
* `settings()` ist der Wert, der unter `capabilities.extensions["com.example/stamps"]` angekündigt wird. Gib `{}` zurück (den Standardwert), um die Extension ohne Settings anzukündigen.
|
||||
* Die Extension bekommt den Server nie in die Hand. Sie deklariert ihre Beiträge als Daten; `MCPServer` verarbeitet sie. Es gibt kein `self.server`, das sie verändern könnte.
|
||||
|
||||
Und `main()` ist der Beweis, ein In-Memory-Client direkt gegen `mcp`:
|
||||
|
||||
```python title="server.py" hl_lines="29-34"
|
||||
--8<-- "docs_src/extensions/tutorial003.py"
|
||||
```
|
||||
|
||||
### Eigene Methoden bedienen {#serving-your-own-methods}
|
||||
|
||||
Eine Extension kann **neue Request-Methoden** registrieren: eigene Verben, bedient neben denen der Spezifikation:
|
||||
|
||||
```python title="server.py" hl_lines="16-22 31 40-48"
|
||||
--8<-- "docs_src/extensions/tutorial004.py"
|
||||
```
|
||||
|
||||
* `SearchParams` leitet von `RequestParams` ab, sodass der `_meta`-Umschlag von 2026 einheitlich geparst wird und dein Handler validierte Parameter bekommt, nie ein rohes Dict. Begrenze, was der Client kontrolliert: `Field(ge=1, le=100)` weist ein absurdes `limit` zurück, bevor dein Code irgendetwas dafür alloziert.
|
||||
* `require_client_extension(ctx, EXTENSION_ID)` ist die Schranke: Ein Client, der die Extension nicht deklariert hat, bekommt den Fehler `-32021` (missing required client capability), samt der maschinenlesbaren `requiredCapabilities`-Payload, die die Spezifikation verlangt.
|
||||
* `protocol_versions=frozenset({"2026-07-28"})` heftet die Methode an genau eine Protokollversion auf der Leitung. Bei jeder anderen Version bekommt der Client `METHOD_NOT_FOUND`, genau so, als gäbe es die Methode dort nicht. Für diesen Client gibt es sie auch nicht.
|
||||
|
||||
Methoden sind **strikt additiv**. Das SDK erzwingt das bei der Konstruktion, nicht zur Laufzeit:
|
||||
|
||||
* Ein `MethodBinding` für eine in der Spezifikation definierte Methode (`tools/list`, `completion/complete`, ...) löst `ValueError` aus, wenn das Binding konstruiert wird. Kernverben gehören dem Server.
|
||||
* Zwei Extensions, die dieselbe Methode binden, lösen eine Exception aus, sobald sich die zweite registriert. Last-write-wins ist genau der Weg, auf dem Plugins einander beschädigen; das machen wir nicht.
|
||||
* Ein leeres `protocol_versions`-Set löst ebenfalls eine Exception aus: Eine Methode, die nie bedient werden kann, ist ein Bug, keine Konfiguration.
|
||||
|
||||
### Die Client-Seite {#the-client-side}
|
||||
|
||||
Das `main()` derselben Datei ist die ganze Client-Geschichte, beide Hälften davon:
|
||||
|
||||
```python title="server.py" hl_lines="54-58"
|
||||
--8<-- "docs_src/extensions/tutorial004.py"
|
||||
```
|
||||
|
||||
* `Client(..., extensions=[advertise(EXTENSION_ID)])` deklariert die Extension. Die Deklarationen werden zu `ClientCapabilities.extensions`: Auf einer 2026-07-28-Verbindung reist die Map im `_meta`-Umschlag jedes einzelnen Requests, der Server sieht sie also bei **jedem** Request; auf einer Legacy-Verbindung reist sie mit dem `initialize`-Handshake. Dem Server-Code ist das egal: `require_client_extension(ctx, ...)` und `ctx.session.check_client_capability(...)` lesen auf beiden Pfaden die richtige Quelle.
|
||||
* Vendor-Methoden steigen eine Schicht tiefer zu `client.session.send_request(...)` hinab; `Client` bekommt nur für Verben der Spezifikation eigene Methoden. `send_request` akzeptiert jede `Request`-Unterklasse, der Vendor-Request geht also unverändert durch.
|
||||
|
||||
### `tools/call` abfangen {#intercepting-toolscall}
|
||||
|
||||
Der eine eingreifende Hook. Überschreibe `intercept_tool_call`, um einen Tool-Aufruf zu beobachten, kurzzuschließen oder zu verhindern:
|
||||
|
||||
```python title="server.py" hl_lines="17-24"
|
||||
--8<-- "docs_src/extensions/tutorial005.py"
|
||||
```
|
||||
|
||||
* `params` sind die validierten `CallToolRequestParams`: Du bekommst `params.name` und `params.arguments`, ohne rohes JSON anzufassen. Sie entscheiden auch, welcher Tool-Aufruf läuft: Reichst du über `call_next` einen umgeschriebenen Kontext weiter, ändert das, was der Handler auf `ctx` sieht, nicht den Tool-Aufruf selbst. Das Umschreiben von Requests auf Leitungsebene gehört in die [Middleware](middleware.md).
|
||||
* `call_next(ctx)` führt den Rest der Kette aus und gibt das Ergebnis des Handlers zurück. Gib es unverändert zurück (beobachten), gib etwas anderes zurück (ersetzen) oder löse einen `MCPError` aus (ablehnen). Was immer du zurückgibst, wird wie jedes Handler-Ergebnis serialisiert, einschließlich des `serverInfo`-Identitätsstempels der 2026er-Generation, ein kurzschließender Interceptor erzeugt also nie eine anonyme oder vom Schema abweichende Response.
|
||||
* Bei mehreren Extensions schachteln sich die Interceptors in Registrierungsreihenfolge: Die erste Extension in `extensions=[...]` liegt ganz außen.
|
||||
* Die Standardimplementierung reicht einfach durch, und ein Server, dessen Extensions diesen Hook nie überschreiben, behält den nackten `tools/call`-Handler unangetastet. Du zahlst nicht für das, was du nicht nutzt.
|
||||
|
||||
Der Hook umhüllt `tools/call` und sonst nichts. Für alles, was jede Nachricht betrifft, nimm [Middleware](middleware.md). Dafür ist sie da.
|
||||
|
||||
## Eine Client-Extension verwenden {#using-a-client-extension}
|
||||
|
||||
Eine **Client-Extension** ist derselbe Vertrag von der konsumierenden Seite: ein Bündel clientseitigen Verhaltens hinter einem einzigen Identifier. Übergib Instanzen an `Client(extensions=[...])` und rufe Tools ganz normal auf:
|
||||
|
||||
```python title="client.py" hl_lines="66-68"
|
||||
--8<-- "docs_src/extensions/tutorial006.py"
|
||||
```
|
||||
|
||||
`call_tool("buy", ...)` gibt ein gewöhnliches `CallToolResult` zurück, wie jeder andere Aufruf. Was die Extension geändert hat: Der Server darf `buy` jetzt mit einer `receipt`-**Ergebnisform** statt mit einem endgültigen Ergebnis beantworten, und `Receipts` bringt sie zu Ende (hier, indem sie den Beleg mit einem Folgeaufruf einlöst), bevor `call_tool` zurückkehrt. An der Aufrufstelle bewegt sich nichts.
|
||||
|
||||
Lass die Extension weg, und nichts davon existiert: Die Schranke des Servers weist einen Client ab, der sie nicht deklariert hat (Fehler -32021), und eine beanspruchte Form von einem Server, der die Schranke überspringt, fällt durch die Validierung, genau wie die Spezifikation es für einen unbekannten `resultType` verlangt. Standardmäßig aus, an beiden Enden der Leitung.
|
||||
|
||||
Um einen Identifier **ohne** clientseitiges Verhalten anzukündigen (der Server prüft die Capability, der Client tut nichts, wie beim Search-Client oben), nimm `advertise()`:
|
||||
|
||||
```python
|
||||
from mcp.client import advertise
|
||||
|
||||
client = Client(mcp, extensions=[advertise("com.example/search")])
|
||||
```
|
||||
|
||||
## Eine Client-Extension schreiben {#writing-a-client-extension}
|
||||
|
||||
Leite von `ClientExtension` ab und überschreibe nur, was du brauchst. Drei Arten von Beiträgen, jede mit einer Standardimplementierung: `settings()`, `claims()` und `notifications()`.
|
||||
|
||||
```python title="client.py" hl_lines="17-18 43-44 46-47"
|
||||
--8<-- "docs_src/extensions/tutorial006.py"
|
||||
```
|
||||
|
||||
* Der Identifier folgt derselben Grammatik wie auf dem Server und wird validiert, wenn die Klasse definiert wird.
|
||||
* `claims()` gibt `ResultClaim`s zurück: ein Tag auf der Leitung, das Model, das es parst, und der Resolver, der es zu Ende bringt. Das Model muss das Tag mit `result_type: Literal["receipt"]` festlegen und darf nicht von den Kern-Ergebnistypen des Verbs ableiten; beides wird erzwungen, wenn der Claim konstruiert wird. Vendor-Felder wie `receipt_token` gehen unverändert über die Leitung: Eine ersetzte Form erreicht den Client wortwörtlich.
|
||||
* Der Resolver erhält das geparste Model und einen `ClaimContext`; `ctx.session` ist derselbe öffentliche Griff wie `client.session`, Folgeaufrufe sind also gewöhnliche Session-Aufrufe. Er gibt das normale `CallToolResult` des Verbs zurück.
|
||||
* `settings()` ist der Wert, der unter `ClientCapabilities.extensions[identifier]` angekündigt wird, einmal bei der Konstruktion von `Client` gelesen.
|
||||
|
||||
`notifications()` deklariert Vendor-Benachrichtigungen des Servers, die beobachtet werden sollen:
|
||||
|
||||
```python
|
||||
def notifications(self) -> Sequence[NotificationBinding[Any]]:
|
||||
return [NotificationBinding(method="notifications/receipts", params_type=ReceiptEvent, handler=self.on_receipt)]
|
||||
```
|
||||
|
||||
Der Handler erhält validierte Parameter, eine Benachrichtigung nach der anderen, in Dispatch-Reihenfolge. Er beobachtet; ein Veto einlegen oder antworten kann er nicht.
|
||||
|
||||
Zwei stille Regeln. Claims sind nur auf 2026-07-28-Verbindungen aktiv, und die Capability-Ankündigung folgt ihnen: Auf einer Legacy-Verbindung lösen sich die Claims auf, und der Identifier fällt mit ihnen aus der Ankündigung heraus, der Client kündigt also nie eine Extension an, deren Formen er zurückweisen würde. Und wenn du die beanspruchte Form selbst statt des Resolvers haben willst, rufe `client.session.call_tool(..., allow_claimed=True)` auf; ohne dieses Flag löst eine beanspruchte Form, die bei einem Aufrufer auf Session-Ebene ankommt, `UnexpectedClaimedResult` aus.
|
||||
|
||||
### Extension-Verben {#extension-verbs}
|
||||
|
||||
Die eigenen Request-Methoden einer Extension brauchen keine clientseitige Registrierung. Ein Vendor-Request-Typ leitet von `mcp.types.Request` ab und geht durch `client.session.send_request`, wie in [Eigene Methoden bedienen](#serving-your-own-methods). Eine Ergänzung: Wenn ein Params-Schlüssel im `Mcp-Name`-Header mitreisen muss (Extension-Spezifikationen wie Tasks verlangen das für ihre Verben), deklariert der Request-Typ `name_param`:
|
||||
|
||||
```python title="client.py" hl_lines="22-25 46-47"
|
||||
--8<-- "docs_src/extensions/tutorial007.py"
|
||||
```
|
||||
|
||||
Die Session spiegelt `params["jobId"]` auf jedem Sendepfad in `Mcp-Name`, und ein fehlender Wert scheitert laut, statt einen erforderlichen Header stillschweigend wegzulassen.
|
||||
|
||||
## Was eine Extension nicht kann {#what-an-extension-cannot-do}
|
||||
|
||||
Die Fläche für Beiträge ist absichtlich **geschlossen**. Auf dem Server: Settings, Tools, Ressourcen, Methoden, ein `tools/call`-Interceptor. Auf dem Client: Settings, Result-Claims, Notification-Bindings. Eine Extension kann nicht:
|
||||
|
||||
* **In den Host hineingreifen.** Sie deklariert Daten; sie hält keine Referenz auf Server oder Client.
|
||||
* **Kernverhalten ersetzen.** Methoden der Spezifikation und Kern-Ergebnistags werden bei der Konstruktion abgewiesen (`initialize` reserviert der Runner von vornherein für sich); ein Notification-Binding, das vom Kernvokabular überdeckt wird, verstummt stattdessen mit einer Warnung.
|
||||
* **Sich nachträglich registrieren.** Sobald `MCPServer(...)` oder `Client(...)` zurückgekehrt ist, ist die Menge der Extensions, wie sie ist.
|
||||
|
||||
Wenn du gegen diese Wände ankämpfst, schreibst du keine Extension. Du schreibst einen Fork. Die Wände sind das Feature: Wer `extensions=[Apps(), Stamps()]` liest, weiß *alles*, was diese beiden angefasst haben können.
|
||||
34
i18n/de/pages/advanced/index.md
Normal file
34
i18n/de/pages/advanced/index.md
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
---
|
||||
translation:
|
||||
sections: [ca6988b7503cd2d3]
|
||||
tool: 1
|
||||
---
|
||||
# Für Fortgeschrittene {#advanced}
|
||||
|
||||
Alles, was ein gewöhnlicher Server oder Client braucht, hat in den Abschnitten oben sein thematisches Zuhause.
|
||||
Dieser Abschnitt versammelt die Notausgänge, zu denen du greifst, wenn die Komfortschicht
|
||||
von `MCPServer` im Weg ist:
|
||||
|
||||
* **[Der Low-Level-Server](low-level-server.md)**: die Klasse, auf der `MCPServer` aufbaut.
|
||||
Handgeschriebene Schemas, `on_*`-Handler, keine Prüfungen, die dir abgenommen werden, und
|
||||
eigene JSON-RPC-Methoden.
|
||||
* **[Paginierung](pagination.md)** und **[Middleware](middleware.md)**: zwei Dinge, die
|
||||
*nur* auf dem Low-Level-`Server` gehen.
|
||||
* **[Erweiterungen](extensions.md)** und **[MCP Apps](apps.md)**: die
|
||||
Erweiterungsfläche des Protokolls. Kombiniere Erweiterungspakete zu einem Server oder schreibe deine eigenen.
|
||||
|
||||
Einiges, was du mit gutem Grund hier suchen könntest, steht stattdessen dort, wo du es
|
||||
tatsächlich einsetzt:
|
||||
|
||||
* **Autorisierung** steht unter **[Den Server betreiben](../run/index.md)**, weil du
|
||||
einen Server dort schützt, wo du ihn bereitstellst.
|
||||
* **OAuth**, **Identity Assertion**, die Verbindung zu **mehreren Servern** und der
|
||||
Response-**Cache** stehen alle unter **[Clients](../client/index.md)**.
|
||||
* **Multi-Roundtrip-Requests** (multi-round-trip requests) und **Abonnements** stehen unter
|
||||
**[Im Handler](../handlers/index.md)**, weil beides etwas ist, das ein
|
||||
Handler *tut*.
|
||||
* **URI-Templates** steht unter **[Server](../servers/index.md)**, neben den Ressourcen.
|
||||
* **[Protokollversionen](../protocol-versions.md)** und
|
||||
**[Veraltete Features](../deprecated.md)** haben jeweils eine eigene Seite auf oberster Ebene.
|
||||
|
||||
Wenn du nicht sicher bist, ob du diesen Abschnitt brauchst, brauchst du ihn nicht.
|
||||
207
i18n/de/pages/advanced/low-level-server.md
Normal file
207
i18n/de/pages/advanced/low-level-server.md
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
---
|
||||
translation:
|
||||
sections: [2c79b6338e09b7ac, 7edc43b3fae11314, 1086e77ce561cd7f, a3f71823df5efc31, 9fc7109f72201cae, 7bf25983df655b66, 6330e1f4c6029683, 2f1749c8c133fa1c, b3530fcf4d11fd56, ebc33704fbd74262, cd0e9c933350390e]
|
||||
tool: 1
|
||||
---
|
||||
# Der Low-Level-Server {#the-low-level-server}
|
||||
|
||||
`@mcp.tool()` ist eine Schicht. Darunter liegt eine zweite Server-Klasse, `Server`, die rohes MCP spricht: Du gibst ihr die Protokollobjekte, und sie legt sie unverändert auf die Leitung.
|
||||
|
||||
`MCPServer` ist darauf aufgebaut. Du steigst hinab, wenn die Komfortschicht im Weg ist:
|
||||
|
||||
* Du musst ein **exaktes** Schema ausgeben (aus einer Datei geladen, aus einer Datenbank generiert), nicht eines, das aus einer Python-Signatur abgeleitet ist.
|
||||
* Du brauchst die volle Kontrolle über das Ergebnis: `_meta`, `is_error`, jeden Schlüssel von `structured_content`.
|
||||
* Du musst eine Methode behandeln, die MCP nicht definiert.
|
||||
|
||||
Für alles andere bleib bei `MCPServer`.
|
||||
|
||||
## Dasselbe Tool, von Hand {#the-same-tool-by-hand}
|
||||
|
||||
Das ist das Tool `search_books`, das **[Tools](../servers/tools.md)** in neun Zeilen `@mcp.tool()` schreibt, ohne den Zucker:
|
||||
|
||||
```python title="server.py" hl_lines="22 26 32"
|
||||
--8<-- "docs_src/lowlevel/tutorial001.py"
|
||||
```
|
||||
|
||||
Drei Dinge haben sich geändert, und sie sind die ganze Low-Level-API:
|
||||
|
||||
* **Handler sind Konstruktorparameter.** `on_list_tools=` und `on_call_tool=` wandern in `Server(...)`. Hier unten gibt es keine Dekoratoren, und jeder Handler hat dieselbe Form: `async (ctx, params) -> result`.
|
||||
* **Du schreibst das Input-Schema.** `Tool.input_schema` ist ein schlichtes JSON-Schema-`dict`. Niemand leitet es aus Type Hints ab, denn es gibt keine Type Hints, aus denen man es ableiten könnte.
|
||||
* **Du baust das Ergebnis.** `CallToolResult(content=[TextContent(...)])`, von Hand. Nichts wird verpackt, konvertiert oder aus einer Rückgabeannotation abgeleitet.
|
||||
|
||||
`params` ist der geparste Request: `CallToolRequestParams` gibt dir `.name` und `.arguments`. `ctx` ist ein `ServerRequestContext`: `ctx.session` zum Zurücksprechen an den Client, `ctx.lifespan_context`, `ctx.request_id` und `ctx.meta`, das eingehende `_meta` des Requests.
|
||||
|
||||
!!! info
|
||||
Wenn du FastAPI kennst, kennst du diese Beziehung bereits. `MCPServer` ist die Schicht aus Dekoratoren und Type Hints; `Server` ist das Starlette darunter. Sie sind keine Rivalen: `MCPServer` erzeugt einen `Server` und registriert darauf genau solche Handler wie diese.
|
||||
|
||||
### Ausprobieren {#try-it}
|
||||
|
||||
Hierfür gibt es keinen Inspector: `mcp dev` und `mcp run` akzeptieren nur einen `MCPServer`. Dem In-Memory-`Client` ist das egal; er nimmt einen Low-Level-`Server` genauso wie einen `MCPServer`:
|
||||
|
||||
```python title="main.py"
|
||||
import asyncio
|
||||
|
||||
from mcp import Client
|
||||
|
||||
from server import server
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
async with Client(server) as client:
|
||||
result = await client.call_tool("search_books", {"query": "dune", "limit": 5})
|
||||
print(result.content)
|
||||
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
```text
|
||||
[TextContent(type='text', text="Found 3 books matching 'dune' (showing up to 5).", annotations=None, meta=None)]
|
||||
```
|
||||
|
||||
Derselbe Text, den die `@mcp.tool()`-Version erzeugt hat. Zwei ehrliche Unterschiede:
|
||||
|
||||
* `result.structured_content` ist `None`. Der High-Level-Server verpackt ein `-> str` für dich in `{"result": ...}`; hier baut niemand, was du nicht gebaut hast.
|
||||
* `list_tools` gibt das Schema zurück, das **du** getippt hast, Zeichen für Zeichen. Die High-Level-Version hatte `"title": "Query"` auf jeder Property und ein `"title": "search_booksArguments"` an der Wurzel: Pydantic-Artefakte. Hier unten gilt: Was auf der Leitung ist, hast du dort hingelegt.
|
||||
|
||||
## Nichts wird für dich geprüft {#nothing-is-checked-for-you}
|
||||
|
||||
`MCPServer` weist ein fehlerhaftes Argument ab, bevor deine Funktion überhaupt läuft, indem er den Aufruf gegen das generierte Schema validiert (**[Tools](../servers/tools.md)**).
|
||||
|
||||
`Server` tut das nicht. Dein `input_schema` wird dem Client *angekündigt*; es wird nie auf `params.arguments` *angewendet*.
|
||||
|
||||
!!! check
|
||||
Ruf `search_books` ohne `limit` auf, und dein `args["limit"]` löst einen `KeyError` aus. Der Client sieht:
|
||||
|
||||
```text
|
||||
MCPError: Internal server error
|
||||
```
|
||||
|
||||
Ein JSON-RPC-Fehler, Code `-32603`, mit einer bewusst generischen Meldung: Das SDK gibt deinen Traceback nicht an einen entfernten Aufrufer preis. Das Modell erfährt nie, was es falsch gemacht hat, kann es also nicht erneut versuchen. (In einem Test bringt `raise_exceptions=True` stattdessen die echte Exception zum Vorschein; siehe **[Testen](../get-started/testing.md)**.)
|
||||
|
||||
Das lässt sich verallgemeinern. Eine Exception, die ein Low-Level-Handler auslöst, ist **immer** ein Protokollfehler, nie ein Tool-Ergebnis mit `is_error=True`. Wenn das Modell den Fehlschlag lesen und sich erholen soll, validiere `params.arguments` selbst und gib `CallToolResult(content=[TextContent(...)], is_error=True)` zurück. Die beiden Arten von Fehlschlägen sind das Thema von **[Fehler behandeln](../servers/handling-errors.md)**.
|
||||
|
||||
## Zwei Tools, ein Handler {#two-tools-one-handler}
|
||||
|
||||
`on_call_tool` ist der einzige Einstiegspunkt für jedes Tool auf dem Server. Du verzweigst über `params.name`:
|
||||
|
||||
```python title="server.py" hl_lines="38-43"
|
||||
--8<-- "docs_src/lowlevel/tutorial002.py"
|
||||
```
|
||||
|
||||
* `list_tools` kündigt beide an. `call_tool` verteilt nach dem Namen.
|
||||
* Der `else`-Zweig ist wichtig: `Server` leitet ein `tools/call` für einen Namen, den du nie gelistet hast, bereitwillig direkt in deinen Handler weiter. Löst du dort eine Exception aus, wird aus dem Aufruf dasselbe `-32603` wie oben.
|
||||
|
||||
## Strukturierte Ausgabe, von Hand {#structured-output-by-hand}
|
||||
|
||||
Deklariere `output_schema` auf dem `Tool` und setze `structured_content` auf das Ergebnis. Beides liegt bei dir:
|
||||
|
||||
```python title="server.py" hl_lines="19-23 36"
|
||||
--8<-- "docs_src/lowlevel/tutorial003.py"
|
||||
```
|
||||
|
||||
Ruf es auf, und das Ergebnis trägt beide Darstellungen:
|
||||
|
||||
```json
|
||||
{
|
||||
"content": [{"type": "text", "text": "Found 3 books matching 'dune'."}],
|
||||
"structuredContent": {"matches": 3, "query": "dune"},
|
||||
"isError": false,
|
||||
"resultType": "complete",
|
||||
"_meta": {"io.modelcontextprotocol/serverInfo": {"name": "Bookshop", "version": "2.0.0"}}
|
||||
}
|
||||
```
|
||||
|
||||
Der `_meta`-Block ist der Identitätsstempel des Servers: Das SDK fügt ihn jedem Ergebnis der 2026er-Generation hinzu, mit der `version` aus dem Konstruktor (ein Server, der keine setzt, meldet einen leeren String). Ein Server, der sich nicht zu erkennen geben darf, kann den Schlüssel mit einer Middleware entfernen, der die Ergebnisse gehören, die sie zurückgibt.
|
||||
|
||||
Der Server vergleicht die beiden Felder nie. Der `Client` dieses SDK schon: Gibst du `structured_content` zurück, das das von dir deklarierte `output_schema` nicht erfüllt, löst `call_tool` einen `RuntimeError` aus, der mit `Invalid structured content returned by tool search_books` beginnt und dann den `jsonschema`-Fehler zitiert. Ein Schema zu versprechen ist billig; es einzuhalten liegt bei dir. Die ganze Stufenleiter der Rückgabetypen und Schemas steht in **[Strukturierte Ausgabe](../servers/structured-output.md)**.
|
||||
|
||||
## `_meta`: für die Anwendung, nicht für das Modell {#\_meta-for-the-application-not-the-model}
|
||||
|
||||
`content` ist der Teil der Antwort, den das Modell liest. `structured_content` ist dieselbe Antwort als typisierte Daten. `_meta` ist der dritte Kanal: Daten, die mit dem Ergebnis für die **Client-Anwendung** mitreisen, ohne überhaupt Teil der Antwort zu sein.
|
||||
|
||||
Nutze es für Datensatz-IDs, Trace-IDs, alles, was deine UI braucht und dein Prompt nicht:
|
||||
|
||||
```python title="server.py" hl_lines="37"
|
||||
--8<-- "docs_src/lowlevel/tutorial004.py"
|
||||
```
|
||||
|
||||
* Du erzeugst es als `_meta=`, dem Namen auf der Leitung. Der Client liest es als `result.meta` zurück.
|
||||
* Versieh deine Schlüssel mit einem Namensraum (`bookshop/record_ids`). Die Schlüssel `io.modelcontextprotocol/*` sind vom Protokoll reserviert.
|
||||
|
||||
!!! warning
|
||||
`_meta` ist eine Konvention zwischen dir und der Client-Anwendung, keine Garantie darüber, was
|
||||
das Modell erreicht. Der Host entscheidet, was er darstellt. Lege niemals ein Geheimnis in irgendeinen Teil eines Tool-Ergebnisses.
|
||||
|
||||
## Capabilities folgen deinen Handlern {#capabilities-follow-your-handlers}
|
||||
|
||||
Ein `Server` kündigt genau die Methodenfamilien an, für die du ihm Handler gegeben hast. Der `Bookshop` oben übergibt `on_list_tools` und `on_call_tool` und sonst nichts, also sieht ein Client, der sich mit ihm verbindet:
|
||||
|
||||
```json
|
||||
{"tools": {"listChanged": false}}
|
||||
```
|
||||
|
||||
Kein `resources`, kein `prompts`: Es gibt nichts, was dahinter stünde. Übergib `on_list_prompts`, und `prompts` erscheint; übergib `on_completion`, und `completions` erscheint.
|
||||
|
||||
`MCPServer` kündigt Tools, Ressourcen und Prompts immer an, ob du welche registriert hast oder nicht, weil seine Manager immer existieren. Hier unten *ist* die Deklaration der Konstruktoraufruf.
|
||||
|
||||
## Der Lifespan-Generic {#the-lifespan-generic}
|
||||
|
||||
`Server` ist generisch im Typ, den sein Lifespan liefert. Annotiere ihn einmal, und das Objekt ist überall typisiert, wo es auftaucht:
|
||||
|
||||
```python title="server.py" hl_lines="24-26 44-45 50"
|
||||
--8<-- "docs_src/lowlevel/tutorial005.py"
|
||||
```
|
||||
|
||||
* Der Lifespan ist ein `Callable[[Server[Catalog]], AbstractAsyncContextManager[Catalog]]`; `@asynccontextmanager` auf einem `async`-Generator gibt dir genau das.
|
||||
* Was immer er per `yield` liefert, wird zu `ctx.lifespan_context`, und weil die Handler mit `ServerRequestContext[Catalog]` annotiert sind, funktionieren Autovervollständigung und Typprüfung für `.search(...)`.
|
||||
* Er wird einmal betreten, wenn der Server startet, und einmal verlassen, wenn er stoppt. Start, Abbau und die Variante derselben Idee in `MCPServer` stehen in **[Lifespan](../handlers/lifespan.md)**.
|
||||
|
||||
Ohne ein `lifespan=` ist `ctx.lifespan_context` ein leeres `dict`.
|
||||
|
||||
## Eine eigene Methode {#a-method-of-your-own}
|
||||
|
||||
Der Konstruktor deckt die Methoden ab, die MCP definiert. `add_request_handler` deckt alles andere ab:
|
||||
|
||||
```python title="server.py" hl_lines="35-36 39-40 43-44 48"
|
||||
--8<-- "docs_src/lowlevel/tutorial006.py"
|
||||
```
|
||||
|
||||
* Das erste Argument ist der Methoden-String. Benachrichtigungen haben ein Gegenstück, `add_notification_handler`.
|
||||
* `params_type` ist das Modell, gegen das die eingehenden `params` validiert werden, **bevor** dein Handler läuft – eigene Methoden bekommen also *doch* die Validierung, die Tools nicht bekommen. Leite von `RequestParams` ab, damit das Feld `_meta` so geparst wird wie bei jeder anderen Methode.
|
||||
* Der Handler gibt ein `BaseModel`, ein `dict` oder `None` zurück. Das SDK serialisiert es in das JSON-RPC-Ergebnis.
|
||||
|
||||
Ein ehrlicher Vorbehalt: Der High-Level-`Client` hat nur Verben für die Methoden, die MCP definiert, es gibt also kein `client.reindex()`. Eine Vendor-Methode ist für eine Gegenstelle gedacht, die bereits weiß, dass es sie gibt: ein Client, den du ebenfalls auslieferst, oder ein anderer deiner Dienste, der JSON-RPC spricht.
|
||||
|
||||
Eine Methode, die du nicht beanspruchen kannst:
|
||||
|
||||
```text
|
||||
ValueError: 'initialize' is handled by the server runner and cannot be overridden;
|
||||
use Server.middleware to observe or wrap initialization
|
||||
```
|
||||
|
||||
Der Handshake gehört dem Runner. `server/discover`, `ping` und jeden anderen Built-in darfst du ersetzen.
|
||||
|
||||
!!! tip
|
||||
`Server.middleware`, in dieser Fehlermeldung erwähnt, umhüllt **jede** eingehende Nachricht, `initialize` eingeschlossen. Wenn du Verkehr beobachten oder umschreiben willst, statt eine neue Methode zu beantworten, fang bei **[Middleware](middleware.md)** an.
|
||||
|
||||
## Die übrigen Handler {#the-other-handlers}
|
||||
|
||||
Jeder davon ist eine Idee, für die du jetzt das Vokabular hast; jeder hat seine eigene Seite.
|
||||
|
||||
* `on_call_tool`, `on_get_prompt` und `on_read_resource` dürfen statt ihres normalen Ergebnisses ein `InputRequiredResult` zurückgeben, um den Aufruf anzuhalten und den Client um Eingaben zu bitten; siehe **[Multi-Roundtrip-Requests](../handlers/multi-round-trip.md)** (multi-round-trip requests). Getreu dieser Ebene wird nichts für dich installiert: Wo `MCPServer` `requestState` standardmäßig versiegelt, geht hier der `request_state`, den du setzt, genau so über die Leitung, wie du ihn geschrieben hast, bis du dich mit `server.middleware.append(RequestStateBoundary(RequestStateSecurity(keys=[...]), default_audience=server.name))` dafür entscheidest: eine Zeile (beide Namen lassen sich aus `mcp.server.request_state` importieren) für genau die Versiegelung und Verifizierung, die `MCPServer` vornimmt (**[`requestState` schützen](../handlers/multi-round-trip.md#protecting-requeststate)**).
|
||||
* `on_list_resources`, `on_read_resource`, `on_list_prompts`, `on_get_prompt`, `on_completion` haben dieselbe Form `(ctx, params) -> result` für die anderen Primitive.
|
||||
* `on_subscriptions_listen` bedient den Stream `subscriptions/listen` aus 2026-07-28. Übergib einen `ListenHandler`, der auf einem `SubscriptionBus` aufgebaut ist, und veröffentliche Ereignisse aus deinen anderen Handlern auf dem Bus; die vollständige Zusammensetzung steht in **[Abonnements](../handlers/subscriptions.md)**.
|
||||
* `server.streamable_http_app()` gibt dieselbe Starlette-App zurück wie die von `MCPServer`; stelle sie bereit, wie **[Den Server betreiben](../run/index.md)** jede andere ASGI-App bereitstellt. Hier unten gibt es kein `server.run(transport=...)`: `server.run(read_stream, write_stream, server.create_initialization_options())` treibt eine Verbindung über ein Paar Streams, und diese eine Zeile ist alles.
|
||||
|
||||
## Zusammenfassung {#recap}
|
||||
|
||||
* Der Low-Level-`Server` nimmt seine Handler als `on_*`-**Konstruktorparameter**; jeder Handler ist `async (ctx, params) -> result`.
|
||||
* Du schreibst das `input_schema`-dict und du baust das `CallToolResult`. Nichts wird für dich abgeleitet, verpackt oder validiert.
|
||||
* Eine Exception in einem Handler ist ein `-32603`-Protokollfehler. Ein Tool-Fehler, den das Modell lesen kann, ist ein `CallToolResult` mit `is_error=True`, das **du** zurückgibst.
|
||||
* `_meta` auf dem Ergebnis richtet sich an die Client-Anwendung, nicht an das Modell.
|
||||
* `Server[T]` ist generisch in dem, was sein Lifespan liefert; `ctx.lifespan_context` ist ein typisiertes `T`.
|
||||
* `add_request_handler(method, params_type, handler)` bedient jede Methode. `initialize` ist reserviert.
|
||||
* Die Capabilities, die ein `Server` ankündigt, leiten sich davon ab, welche Handler du registriert hast.
|
||||
|
||||
`Client(server)` hat beide Server identisch behandelt, weil sie dasselbe Protokoll *sind* – und genau darum geht es. Die nächste Schicht darunter ist gar keine Klasse: Es ist **[Middleware](middleware.md)**.
|
||||
127
i18n/de/pages/advanced/middleware.md
Normal file
127
i18n/de/pages/advanced/middleware.md
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
---
|
||||
translation:
|
||||
sections: [6048b4f308edbb8c, 068bda0f21ee9c1b, c3e565b61acd75c5, c62422b159c6ed09, 47204fab253cc45c]
|
||||
tool: 1
|
||||
---
|
||||
# Middleware {#middleware}
|
||||
|
||||
Eine **Middleware** ist eine einzelne async-Funktion, die jede Nachricht umschließt, die dein Server empfängt.
|
||||
|
||||
Du schreibst sie als `async (ctx, call_next)` und hängst sie an `server.middleware` an. Das ist die ganze API.
|
||||
|
||||
!!! warning
|
||||
Die Middleware-Liste ist im Quellcode als **provisorisch** markiert: Signatur und Semantik können
|
||||
sich in einem 2.x-Minor-Release ändern. Nutze sie zum *Beobachten* (Timing, Logging, Tracing) und
|
||||
zum *Ablehnen* von Nachrichten; mache sie nicht zum Fundament, auf dem dein Server steht.
|
||||
|
||||
`MCPServer` nimmt die Liste bei der Konstruktion entgegen (`MCPServer(name, middleware=[...])`) und stellt
|
||||
sie als `mcp.middleware` bereit; der Low-Level-`Server` stellt dieselbe Liste als `server.middleware`
|
||||
bereit. Das Beispiel unten verwendet den Low-Level-`Server`; wenn `Server(name, on_call_tool=...)` neu
|
||||
für dich ist, lies zuerst **[Der Low-Level-Server](low-level-server.md)**.
|
||||
|
||||
## Eine Timing-Middleware {#a-timing-middleware}
|
||||
|
||||
Ein Server, ein Tool, eine Middleware, die loggt, wie lange jede Nachricht gedauert hat:
|
||||
|
||||
```python title="server.py" hl_lines="39-45 49"
|
||||
--8<-- "docs_src/middleware/tutorial001.py"
|
||||
```
|
||||
|
||||
* `ctx` ist derselbe `ServerRequestContext`, den deine Handler erhalten. `ctx.method` ist der rohe
|
||||
Methoden-String; `ctx.params` sind die rohen Params, **vor** jeder Validierung.
|
||||
* `call_next(ctx)` führt den Rest der Kette aus: Validierung, die Handler-Suche, deinen Handler.
|
||||
Gib zurück, was es zurückgegeben hat, und die Response bleibt unverändert.
|
||||
* Das `try`/`finally` ist Absicht: Ein Handler, der eine Exception auslöst, wird trotzdem gemessen,
|
||||
denn der Fehlschlag erreicht deine Middleware als Exception aus `call_next`.
|
||||
* `server.middleware.append(...)` registriert sie. Die Liste läuft von außen nach innen, also ist
|
||||
`middleware[0]` diejenige, die am nächsten an der Leitung sitzt.
|
||||
|
||||
### Ausprobieren {#try-it}
|
||||
|
||||
Verbinde einen Client, liste die Tools auf, rufe eines auf. Dein Log hat **drei** Zeilen:
|
||||
|
||||
```text
|
||||
server/discover took 18.3 ms
|
||||
tools/list took 0.1 ms
|
||||
tools/call took 0.1 ms
|
||||
```
|
||||
|
||||
Du hast zwei Aufrufe gemacht und drei Zeilen bekommen. Die erste ist `server/discover`: der Request,
|
||||
den der Client zum Aufbau der Verbindung geschickt hat, bevor du irgendetwas angefordert hast.
|
||||
|
||||
Genau darum geht es. Middleware umschließt **jede** eingehende Nachricht:
|
||||
|
||||
* Den Verbindungsaufbau: `server/discover`, oder `initialize` und `notifications/initialized`
|
||||
in einer Legacy-Session.
|
||||
* Jeden Request und jede Benachrichtigung. Bei einer Benachrichtigung gilt `ctx.request_id is None`,
|
||||
`call_next(ctx)` gibt `None` zurück, und was immer du zurückgibst, wird verworfen.
|
||||
* Sogar eine Methode, für die der Server keinen Handler hat: `call_next` wirft den
|
||||
`MCPError(-32601, "Method not found")` *durch* deine Middleware hindurch auf dem Weg zum Client.
|
||||
|
||||
## Was du in einer Middleware tun kannst {#what-you-can-do-inside-one}
|
||||
|
||||
In aufsteigender Reihenfolge danach, wie sehr du zögern solltest:
|
||||
|
||||
* **Beobachten.** Miss es, zähle es, logge es. Das Beispiel oben.
|
||||
* **Ablehnen.** Wirf einen `MCPError` *statt* `call_next(ctx)` aufzurufen, und diese eine Nachricht
|
||||
wird mit einem JSON-RPC-Fehler beantwortet. Die Verbindung bleibt bestehen; die nächste Nachricht
|
||||
geht durch. So beschränkt ein Server `subscriptions/listen` pro Aufrufer:
|
||||
**[Entscheiden, wer zusehen darf](../handlers/subscriptions.md#deciding-who-may-watch)** auf der
|
||||
Seite Abonnements führt es Schritt für Schritt vor.
|
||||
* **Umschreiben.** `ctx` ist eine Dataclass: `await call_next(dataclasses.replace(ctx, params=...))`
|
||||
reicht dem Rest der Kette andere Params weiter, als der Client geschickt hat. Tu das nie mit
|
||||
`initialize`: Das Ergebnis, das der Client zurückbekommt, wird aus deinen umgeschriebenen Params
|
||||
gebaut, aber der Server legt seinen Verbindungszustand anhand der ursprünglichen Params von der
|
||||
Leitung fest. Beide Seiten können den Handshake beenden und sich dabei uneinig sein, was sie
|
||||
ausgehandelt haben.
|
||||
* **Antworten.** Gib ein Ergebnis zurück, ohne `call_next(ctx)` aufzurufen, und es geht als deine
|
||||
Response an den Client. `call_next` reicht dir die fertige Form für die Leitung, und die Pipeline
|
||||
bessert nie nach, was du zurückgibst – der ganze Umschlag gehört also dir: Auf einer Verbindung der
|
||||
2026er-Generation gehört dazu der `serverInfo`-Stempel in `_meta`, den das SDK an Handler-Ergebnisse
|
||||
anfügt, an deine aber nicht.
|
||||
|
||||
!!! check
|
||||
`initialize` gehört zu dem, was Middleware umschließt, und es ist der *einzige* Hook, den du
|
||||
dafür bekommst. Versuchst du, es mit `add_request_handler` zu übernehmen, weigert sich das SDK:
|
||||
|
||||
```text
|
||||
ValueError: 'initialize' is handled by the server runner and cannot be overridden;
|
||||
use Server.middleware to observe or wrap initialization
|
||||
```
|
||||
|
||||
!!! warning
|
||||
`initialize` wird inline behandelt: Der Server liest keine weiteren eingehenden Nachrichten, bis
|
||||
deine Middleware-Kette zurückkehrt. Auf einen Server-zu-Client-Request zu warten
|
||||
(`ctx.session.send_request(...)`, eine Elicitation – Rückfrage bei der Person am Host), während
|
||||
`initialize` behandelt wird, **blockiert die Verbindung** daher **dauerhaft** (Deadlock): Die
|
||||
Response, auf die du wartest, kann nie gelesen werden. Benachrichtigungen nach dem
|
||||
Fire-and-forget-Prinzip sind in Ordnung.
|
||||
|
||||
## Die eine Middleware, die standardmäßig aktiv ist {#the-one-middleware-that-ships-on-by-default}
|
||||
|
||||
Das SDK liefert genau eine Middleware mit, und sie steht bereits auf der Liste deines Servers: die,
|
||||
die für jede Nachricht einen OpenTelemetry-Span ausgibt. Du hängst sie nicht an, und meistens denkst
|
||||
du gar nicht an sie. Sie tut nichts, bis du einen Exporter installierst, und sie hat ihre eigene Seite:
|
||||
**[OpenTelemetry](../run/opentelemetry.md)**.
|
||||
|
||||
!!! info
|
||||
Wenn du schon ASGI-Middleware geschrieben hast, kennst du diese Form bereits. Aus Starlettes
|
||||
`(scope, receive, send)` wurde `(ctx, call_next)`, und sie läuft *nach* dem Transport, auf der
|
||||
dekodierten Nachricht statt auf dem rohen HTTP-Request. Beide lassen sich kombinieren:
|
||||
Starlette-Middleware auf `streamable_http_app()` sieht HTTP; diese hier sieht MCP.
|
||||
|
||||
## Zusammenfassung {#recap}
|
||||
|
||||
* Eine Middleware ist `async (ctx, call_next) -> result`, übergeben als `MCPServer(middleware=[...])`
|
||||
(oder an `mcp.middleware` angehängt) und beim Low-Level-`Server` an `server.middleware` angehängt.
|
||||
* Sie umschließt **jede** eingehende Nachricht (`server/discover`, `initialize`, Requests,
|
||||
Benachrichtigungen, unbekannte Methoden) und läuft von außen nach innen.
|
||||
* An `ctx.request_id is None` unterscheidest du eine Benachrichtigung von einem Request.
|
||||
* Wirf eine Exception, statt `call_next` aufzurufen, um eine einzelne Nachricht abzulehnen; die
|
||||
Verbindung überlebt.
|
||||
* Das OpenTelemetry-Tracing des SDK ist ebenfalls eine Middleware und steht schon auf der Liste. Siehe
|
||||
**[OpenTelemetry](../run/opentelemetry.md)**.
|
||||
* Die ganze Oberfläche ist provisorisch. Beobachte damit; baue nicht darauf.
|
||||
|
||||
Das ist alles, was einen Request umschließt. **[Autorisierung](../run/authorization.md)** entscheidet,
|
||||
ob der Request überhaupt laufen darf.
|
||||
85
i18n/de/pages/advanced/pagination.md
Normal file
85
i18n/de/pages/advanced/pagination.md
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
---
|
||||
translation:
|
||||
sections: [a9aba7a026c7bd85, ed32bda7ba9ae33a, 7e64cc5646abb91f, 22a0129ee78b3c63, d875373c06d8d2f9]
|
||||
tool: 1
|
||||
---
|
||||
# Paginierung {#pagination}
|
||||
|
||||
Die meisten Server brauchen das nie.
|
||||
|
||||
`MCPServer` beantwortet jeden `list_*`-Request mit allem, was er hat, auf einer Seite, `next_cursor=None`. Bei ein paar Dutzend Tools, Ressourcen oder Prompts ist das die richtige Antwort, und es gibt nichts zu konfigurieren.
|
||||
|
||||
Paginierung ist für den Server gedacht, dessen Ressourcenliste in Wahrheit eine Datenbank ist: Tausende Zeilen, die er nicht in einer einzigen Response serialisieren will. Die Antwort des Protokolls darauf ist ein **Cursor**: Der Server gibt eine Seite plus ein opakes Token zurück, und der Client schickt dieses Token zurück, um die nächste Seite zu bekommen.
|
||||
|
||||
`@mcp.resource()` hat dafür keinen Einstiegspunkt. Um seitenweise auszuliefern, schreibst du den List-Handler selbst, auf dem **[Low-Level-Server](low-level-server.md)**.
|
||||
|
||||
## Ein Server, der paginiert {#a-server-that-pages}
|
||||
|
||||
```python title="server.py" hl_lines="12 15-16"
|
||||
--8<-- "docs_src/pagination/tutorial001.py"
|
||||
```
|
||||
|
||||
* Auf einem Low-Level-`Server` sind Handler Konstruktorargumente, keine Dekoratoren. `on_list_resources` beantwortet jeden `resources/list`-Request; mehr Verkabelung gibt es nicht.
|
||||
* Jeder paginierte Handler ist als `params: PaginatedRequestParams | None` typisiert, und das Beispiel akzeptiert beides. Über eine Verbindung übergibt dir das SDK jedoch nie `None` (ein Request ohne `params`-Member erreicht den Handler als Modell mit seinen Standardwerten). Das Signal, auf das es ankommt, ist daher `params.cursor is None`: **von vorne beginnen**.
|
||||
* Du entscheidest, was ein Cursor *ist*. Hier ist es ein Offset, als String dargestellt. Ein Zeitstempel, ein Primärschlüssel, ein Base64-Blob: alles, was du beim Herausgeben erzeugen und beim Zurückkommen wiedererkennen kannst.
|
||||
* Mit `next_cursor=None` sagst du „das war die letzte Seite“. Es gibt keine Anzahl, keine Gesamtsumme, kein `has_more`. `None` ist das ganze Signal.
|
||||
|
||||
!!! tip
|
||||
Eine `PAGE_SIZE` von 10 macht das Beispiel lesbar. Wähle deine pro Endpunkt: Eine Liste
|
||||
einzeiliger Ressourcen verträgt eine Seite mit 500 Einträgen; eine Liste fetter Prompt-Templates nicht.
|
||||
Der Client hat dabei nichts mitzureden, und das ist Absicht.
|
||||
|
||||
### Ausprobieren {#try-it}
|
||||
|
||||
`Client(server)` verbindet sich im Speicher mit einem Low-Level-`Server` genau so, wie er sich mit einem `MCPServer` verbindet.
|
||||
|
||||
Rufe `list_resources()` ohne Argumente auf. Du bekommst zehn Ressourcen, `book-1` bis `book-10`, und `next_cursor` ist der String `"10"`.
|
||||
|
||||
Gib ihn mit `list_resources(cursor="10")` zurück, und die erste Ressource ist `book-11`, der neue `next_cursor` ist `"20"`.
|
||||
|
||||
Die zehnte Seite kommt mit `next_cursor` auf `None` zurück. Fertig.
|
||||
|
||||
## Die Client-Schleife {#the-client-loop}
|
||||
|
||||
Jede `list_*`-Methode auf `Client` (`list_tools`, `list_resources`, `list_resource_templates`, `list_prompts`) nimmt ein Keyword-Argument `cursor=`. Eine paginierte Liste leerzulesen ist ein einziges `while True`:
|
||||
|
||||
```python title="client.py" hl_lines="26-32"
|
||||
--8<-- "docs_src/pagination/tutorial002.py"
|
||||
```
|
||||
|
||||
* `cursor` beginnt als `None`, der erste Request trägt also keinen Cursor.
|
||||
* Erweitere die Liste, **bevor** du auf `next_cursor` schaust: Auch die letzte Seite enthält Ressourcen.
|
||||
* `next_cursor is None` ist der Ausstieg. Alles andere geht unverändert direkt zurück in `cursor=`.
|
||||
|
||||
Führe sein `main()` aus, und es gibt `100 resources` aus: zehn Seiten zu je zehn, zusammengefügt von einer Schleife, die nie wusste, dass es zehn Seiten waren.
|
||||
|
||||
Das ist dieselbe Schleife, die **[Der Client](../client/index.md)** für jedes `list_*`-Verb zeigt, und sie kostet nichts gegenüber einem Server, der nicht paginiert: `next_cursor` ist schon in der ersten Response `None`, und die Schleife läuft genau einmal.
|
||||
|
||||
## Die drei Regeln {#the-three-rules}
|
||||
|
||||
**Cursor sind opak.** Ein Client darf einen Cursor nie parsen, bauen oder erraten. Die einzige zulässige Quelle eines Cursors ist der `next_cursor` der vorherigen Seite, wortwörtlich.
|
||||
|
||||
**Der Server bestimmt die Seitengröße.** Es gibt kein `limit=` im Protokoll. Wenn du eine andere Seitengröße brauchst, änderst du den Server.
|
||||
|
||||
**Ein Client, der Paginierung ignoriert, funktioniert trotzdem.** Er ruft `list_resources()` einmal auf, bekommt die ersten zehn und bemerkt den `next_cursor`, den er weggeworfen hat, nie. Nichts geht kaputt; er sieht nur weniger.
|
||||
|
||||
!!! check
|
||||
Opak heißt opak. Erfinde einen Cursor (`list_resources(cursor="page-2")`), und das
|
||||
Protokoll kann nichts für dich tun. Dieser Server versucht `int("page-2")`, der Handler löst eine Exception aus,
|
||||
und beim Client kommt an:
|
||||
|
||||
```text
|
||||
MCPError(-32603, 'Internal server error', None)
|
||||
```
|
||||
|
||||
Ein Cursor, den du nicht vom Server bekommen hast, ist ein Bug, kein Feature-Wunsch.
|
||||
|
||||
## Zusammenfassung {#recap}
|
||||
|
||||
* `MCPServer` gibt alles auf einer Seite zurück. Paginierung ist optional, und du aktivierst sie auf dem Low-Level-`Server`.
|
||||
* `on_list_resources` (und `on_list_tools`, `on_list_prompts`, `on_list_resource_templates`) erhält `PaginatedRequestParams | None`; `params.cursor` ist bei der ersten Seite `None`.
|
||||
* Du gibst eine Seite plus `next_cursor` zurück: einen beliebigen String, den du später wiedererkennst, oder `None`, wenn nichts mehr übrig ist.
|
||||
* Die Client-Schleife: `cursor=` übergeben, sammeln, wiederholen, bis `next_cursor is None`.
|
||||
* Cursor sind opak, die Seitengröße gehört dem Server, und ein Client ohne Paginierung bekommt trotzdem Seite eins.
|
||||
|
||||
Der Rest der handgeschriebenen `Server`-API (`on_call_tool`, `input_schema`-Dicts, `_meta`) steht in **[Der Low-Level-Server](low-level-server.md)**.
|
||||
123
i18n/de/pages/client/caching.md
Normal file
123
i18n/de/pages/client/caching.md
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
---
|
||||
translation:
|
||||
sections: [9e7b9a1710e5aeba, b74ca4c1d2ddddee, fa8714e61bf90c5a, 04db67a886b7271c, 857690fb8f876800]
|
||||
tool: 1
|
||||
---
|
||||
# Caching-Hinweise {#caching-hints}
|
||||
|
||||
Jedes Ergebnis, das ein Server für `tools/list`, `prompts/list`, `resources/list`, `resources/templates/list`, `resources/read` und `server/discover` zurückgibt, trägt im Protokoll 2026-07-28 zwei Felder: `ttlMs`, wie viele Millisekunden ein Client das Ergebnis als frisch behandeln darf, und `cacheScope`, ob ein gecachtes Ergebnis personenübergreifend geteilt werden darf (`"public"`) oder zu genau einem Autorisierungskontext gehört (`"private"`).
|
||||
|
||||
Der Server cacht selbst nichts. Die Felder sind eine *Erklärung*: „Diese Tool-Liste ist für alle gleich und ändert sich eine Minute lang nicht.“ Ein Client (oder ein Gateway vor deinem Server) kann sich dann den Roundtrip sparen. Ob er die Hinweise beachtet, entscheidet der Client; sie auszugeben ist Aufgabe des Servers, und das übernimmt das SDK für dich.
|
||||
|
||||
Ohne weitere Konfiguration sagt jedes Ergebnis `ttlMs: 0, cacheScope: "private"`: sofort abgelaufen, nie geteilt. Das ist immer sicher und immer protokollkonform. Wenn deine Listen tatsächlich stabil und für alle Aufrufer identisch sind, gib das bei der Konstruktion an:
|
||||
|
||||
```python title="server.py" hl_lines="5-8"
|
||||
--8<-- "docs_src/caching/tutorial001.py"
|
||||
```
|
||||
|
||||
* Die Map ist nach **Methodennamen** geschlüsselt, und die sechs cachefähigen Methoden sind die einzigen zulässigen Schlüssel. Der Parameter ist als `Mapping[CacheableMethod, CacheHint]` typisiert, sodass dein Editor die Schlüssel automatisch vervollständigt und einen Tippfehler markiert, bevor du den Code ausführst; was am Typprüfer vorbeirutscht, löst bei der Konstruktion eine Exception aus.
|
||||
* Eine Methode, die du nicht erwähnst, behält die Standardwerte. Die Map ist eine Sammlung von Überschreibungen, kein Manifest.
|
||||
* `CacheHint(ttl_ms=5_000)` lässt `scope` ungesetzt, also bleibt es `"private"`: fünf Sekunden Frische, pro Aufrufer. Scope und TTL sind unabhängige Entscheidungen.
|
||||
* `"server/discover"` ist ebenfalls ein zulässiger Schlüssel, denn das Discovery-Ergebnis ist cachefähig wie jede Liste.
|
||||
|
||||
!!! warning
|
||||
`cacheScope: "public"` heißt: Deine gecachte Response darf an *alle* ausgeliefert werden. Ein
|
||||
gemeinsam genutztes Gateway reicht das Ergebnis einer Person ohne Zögern an eine andere weiter,
|
||||
selbst wenn der Request authentifiziert war. Markiere ein Ergebnis nur dann als `"public"`, wenn
|
||||
es für alle Aufrufer identisch ist, und verwende `cacheScope` nie als Zugriffskontrolle: Es ist
|
||||
ein Etikett, kein Schloss.
|
||||
|
||||
## Überschreiben pro Handler {#per-handler-override}
|
||||
|
||||
Auf dem Low-Level-`Server` bauen Handler ihre Ergebnisse von Hand, und `ttl_ms` / `cache_scope` sind einfach Felder der Ergebnismodelle. Ein Handler, der sie explizit setzt, gewinnt immer gegen die Map aus dem Konstruktor, Feld für Feld:
|
||||
|
||||
```python title="server.py" hl_lines="10 16"
|
||||
--8<-- "docs_src/caching/tutorial002.py"
|
||||
```
|
||||
|
||||
Der Handler hat `ttl_ms=1_000` gesetzt und nichts zum Scope gesagt. Auf der Leitung: `ttlMs: 1000` (vom Handler, nicht die `60_000` der Map) und `cacheScope: "public"` (aus der Map, weil der Handler es ungesetzt ließ). Explizit schlägt konfiguriert, und konfiguriert schlägt Standardwert. Das gilt pro Feld, ein Handler kann also ein Feld festlegen und das andere der serverweiten Richtlinie überlassen.
|
||||
|
||||
Das ist auch der Notausgang für Dynamik, die der Konstruktor nicht kennen kann: Ein Handler, der `resources/read` pro Person filtert, kann auf einem ansonsten öffentlichen Server für einen URI `cache_scope="private"` zurückgeben.
|
||||
|
||||
Ein Vorbehalt bei paginierten Listen: Das Protokoll verlangt **denselben `cacheScope` auf jeder Seite** einer Liste. Die Map aus dem Konstruktor erfüllt das von selbst, weil sie nach Methode geschlüsselt ist, nicht nach Seite. Ein Handler, der den Scope selbst überschreibt, ist aber auch selbst für diese Konsistenz verantwortlich: Überschreibe ihn auf *jeder* Seite, nie nur dann, wenn ein Cursor vorhanden ist, sonst widersprechen sich Seite eins und Seite zwei.
|
||||
|
||||
## Was der Client sieht {#what-the-client-sees}
|
||||
|
||||
In einer 2026-07-28-Session beachtet `Client` die Hinweise für dich: Er hat einen eingebauten Response-Cache, der standardmäßig aktiv ist. Ein Ergebnis, das mit einem `ttlMs` ankommt, wird gespeichert, und ein identischer Aufruf innerhalb dieser TTL wird ohne Roundtrip aus dem Cache bedient. Ein Ergebnis, das *keinen* Hinweis trägt, wird nicht gecacht: Ergebnisse ohne Hinweis bekommen `CacheConfig.default_ttl_ms`, dessen Standardwert `0` ist (sofort abgelaufen), sodass ein Server, der nichts deklariert, Aufruf für Aufruf genau denselben Verkehr sieht wie schon immer.
|
||||
|
||||
```python title="client.py" hl_lines="33 35 38"
|
||||
--8<-- "docs_src/caching/tutorial003.py"
|
||||
```
|
||||
|
||||
Vier Aufrufe, drei Abrufe. Der zweite Aufruf fand einen frischen Eintrag und erreichte den Server nie; die (injizierte) Uhr über die TTL hinaus vorzustellen ließ den dritten wieder abrufen; der vierte gab `cache_mode="refresh"` an. Dieses Keyword-Argument gibt es auf den fünf cachenden Verben (`list_tools`, `list_prompts`, `list_resources`, `list_resource_templates`, `read_resource`):
|
||||
|
||||
* `"use"` (der Standardwert) liefert einen frischen Eintrag, wenn es einen gibt, und speichert andernfalls den Abruf.
|
||||
* `"refresh"` liefert nie aus dem Cache: Es ruft ab und speichert das Ergebnis, wobei es ersetzt, was auch immer gecacht war.
|
||||
* `"bypass"` macht den Roundtrip, ohne den Cache überhaupt anzufassen: kein Lesen, kein Schreiben.
|
||||
|
||||
Eine Regel steht über `"use"`: **Aufrufe mit `meta` erreichen immer den Server.** Ein Request mit gesetztem `meta` (ein Progress-Token, Tracing-Felder) erwartet einen Request auf der Leitung, deshalb wird er unter `cache_mode="use"` wie `"refresh"` behandelt: Das Lesen aus dem Cache entfällt, und das abgerufene Ergebnis ersetzt trotzdem den gecachten Eintrag. `"bypass"` und ein explizites `"refresh"` verhalten sich wie immer.
|
||||
|
||||
Um das Caching ganz abzuschalten, konstruiere mit `Client(server, cache=None)`: Jeder Aufruf ist wieder ein Roundtrip, und `cache_mode` wird zwar weiter akzeptiert, bewirkt aber nichts.
|
||||
|
||||
Auch der Scope wird automatisch beachtet: `"private"`-Einträge sind an die *Partition* des Caches gebunden (siehe unten), während `"public"`-Einträge sich für breiteres Teilen entscheiden können. Und **Benachrichtigungen schlagen die TTL** für genau die Einträge, die sie benennen: Eine `list_changed`-Benachrichtigung verdrängt die passende gecachte Liste, und `resources/updated` verdrängt den gecachten Lesevorgang, der unter exakt ihrem URI gespeichert ist – egal, wie frisch sie waren. Auf einer 2026-07-28-Verbindung kommen diese Benachrichtigungen auf einem `subscriptions/listen`-Stream an, den du mit `client.listen(...)` öffnest, und die Verdrängung ist abgeschlossen, bevor dein Watcher das Ereignis sieht; alles dazu steht in **[Abonnements](subscriptions.md)**.
|
||||
|
||||
Ein Vorbehalt bei `resources/updated`: Verdrängt wird nur bei exakt gleichem URI. Der Store-Vertrag kennt keine Operation zum Aufzählen oder Scannen (wie auch die TypeScript-Referenzimplementierung), daher verdrängt eine Benachrichtigung mit dem URI einer *Unter*-Ressource keinen gecachten Lesevorgang ihrer übergeordneten Ressource. Wenn dein Server Unter-Ressourcen so signalisiert, rufe die übergeordnete Ressource mit `cache_mode="refresh"` erneut ab.
|
||||
|
||||
### Konfiguration: `CacheConfig` {#configuring-it-cacheconfig}
|
||||
|
||||
```python
|
||||
from mcp.client import CacheConfig
|
||||
|
||||
client = Client("https://api.example.com/mcp", cache=CacheConfig(default_ttl_ms=5_000))
|
||||
```
|
||||
|
||||
* `store`: wo die Einträge liegen. Standardmäßig ist das ein frischer In-Memory-Store pro Client; übergib deine eigene `ResponseCacheStore`-Implementierung (etwa mit Redis dahinter), um einen Cache über Clients oder Prozesse hinweg zu teilen. Die Vertragstypen (`ResponseCacheStore`, `CacheKey`, `CacheEntry` und der Standard-`InMemoryResponseCacheStore`) lassen sich aus `mcp.client` importieren. Ein Lookup kann bis zu zwei aufeinanderfolgende `get`s am Store auslösen (erst den privaten Zweig, dann den öffentlichen), plane die Latenzerwartungen an einen entfernten Store also entsprechend. Ein eigener Store **erfordert** eine explizite `partition`.
|
||||
* `partition`: das Label für den Autorisierungskontext, das verhindert, dass die `"private"`-Einträge eines Principals in einem gemeinsam genutzten Store an einen anderen ausgeliefert werden.
|
||||
* `target_id`: explizite Server-Identität, für eigene Transporte und In-Process-Server (siehe unten).
|
||||
* `default_ttl_ms`: TTL für Ergebnisse, die keinen `ttlMs`-Hinweis tragen. Der Standardwert `0` lässt Ergebnisse ohne Hinweis ungecacht.
|
||||
* `share_public`: vom Server als `"public"` deklarierte Einträge über Partitionen hinweg ausliefern (siehe unten). Standardmäßig aus.
|
||||
* `clock`: die Quelle für die Uhrzeit, in Epoch-Sekunden. Injiziere eine, wie es das Beispiel oben tut, und Ablauftests kommen ohne Schlafen aus.
|
||||
|
||||
!!! warning "Partition = verifizierter Principal"
|
||||
Leite `partition` aus einem **verifizierten Credential** ab, etwa dem Subject eines validierten Tokens. Leite sie nie aus Daten ab, die der Request mitliefert, und nie aus der Server-URL (die Server-Identität ist eine eigene Schlüsselachse). Das SDK ist eine Bibliothek ohne eigene Authentifizierung: Der Vertrauensanker ist, wer auch immer die `CacheConfig` konstruiert – also das Deployment, nicht der Mandant. Ein mandantenfähiges Gateway erzeugt eine `CacheConfig` pro authentifiziertem Principal.
|
||||
|
||||
Die Partition steht außerdem für die Lebensdauer des `Client` fest. Ändert sich der Autorisierungskontext der Verbindung mitten in der Session (etwa durch erneute Authentifizierung als anderer Principal), folgt der Cache nicht; konstruiere einen neuen `Client` für den neuen Principal.
|
||||
|
||||
Cache-Schlüssel tragen außerdem die **Identität des Servers**: den URL-String, den du angewählt hast, ohne etwaige `user:pass@`-Userinfo und ansonsten bytegenau. Keine Normalisierung der Groß-/Kleinschreibung, keine Umsortierung der Query, kein Bereinigen abschließender Schrägstriche. Zu wenig Normalisierung kostet nur Teilbarkeit, zu viel könnte zwei Mandanten zusammenlegen (`?tenant=a` gegenüber `?tenant=b`), deshalb teilen oberflächlich verschiedene URLs einfach keine Einträge. Gibt es keine URL (ein In-Process-Server oder eine `Transport`-Instanz), bekommt der Client stattdessen eine zufällige Identität pro Instanz; setze `CacheConfig.target_id`, um den Server zu benennen (bei einem eigenen Store ist das Pflicht, und die Konstruktion sagt dir das). Die Identität wird mit sha256 gehasht, bevor sie ins Schlüsselmaterial eingeht, sodass eine URL mit Geheimnissen im Query-String nie in Store-Schlüsseln auftaucht. Logge die Form vor dem Hashing auch selbst nicht.
|
||||
|
||||
!!! warning "`share_public` vertraut dem Server, flottenweit"
|
||||
Standardmäßig bleiben selbst `"public"`-Einträge in ihrer Partition. `share_public=True` liefert Einträge, die der Server mit `cacheScope: "public"` markiert hat, an **jede** Partition aus, die den Store nutzt, und vertraut dabei im Namen aller auf die Einstufung des Servers. Ein Server, der mandantenspezifische Daten als `"public"` stempelt (aus Versehen oder in böser Absicht), lässt dann die Response eines Mandanten zu den anderen durchsickern. Das Flag gibt es bewusst nur auf Konstruktorebene: Das `cache_mode` pro Aufruf kann das Caching einschränken, aber nichts pro Aufruf kann das Teilen ausweiten.
|
||||
|
||||
### Was der Cache nie tut {#what-the-cache-never-does}
|
||||
|
||||
* **Aufrufe auf Session-Ebene umgehen ihn.** `client.session.list_tools()` und Konsorten machen immer den Roundtrip; der Cache sitzt auf den `Client`-Verben.
|
||||
* **`server/discover` bleibt außen vor.** Das Discover-Ergebnis wird einmal geliefert, beim Verbinden, und gelangt nie in den Response-Cache, selbst wenn es ein `ttlMs` trägt. Wenn du selbst eines persistierst, um die Probe beim Wiederverbinden zu überspringen ([`prior_discover`](../protocol-versions.md#reconnecting-with-prior_discover)), ist seine Frische deine eigene Buchführung: `DiscoverResult` trägt `ttl_ms` und `cache_scope`, bereits geparst, genau zu diesem Zweck.
|
||||
* **Folgeseiten werden nie gecacht.** Nur Aufrufe ohne Cursor nehmen teil. Eine Folgeseite, die wegen eines abgelaufenen Cursors abgelehnt wird, *verdrängt* allerdings die gecachte Liste, weil sich die Liste darunter geändert hat.
|
||||
* **Multi-Roundtrip-Lesevorgänge (multi-round-trip reads) werden nie gecacht.** Ein `read_resource`, das mit `input_responses`/`request_state` gestartet wird oder das über Eingaberunden aufgelöst wird, gelangt nie in den Cache (ein MUST der Spezifikation).
|
||||
* **Verdrängung per Benachrichtigung braucht Benachrichtigungen.** Die Verdrängung ist nur so gut wie die Zustellung durch den Transport, und der moderne In-Process-Pfad (`Client(server)` mit dem Standardwert `mode="auto"`) stellt heute keine eigenständigen Benachrichtigungen zu.
|
||||
* **Verdrängung geschieht letztendlich, nicht augenblicklich.** Benachrichtigungen vom Leitungspfad werden aus eigens gestarteten Tasks verteilt, sodass ein Aufruf, der mit dem Eintreffen einer Benachrichtigung um die Wette läuft, noch einmal den Eintrag von vor der Verdrängung bekommen kann; das Fenster ist durch die Dispatch-Latenz begrenzt, und die Verdrängung kommt trotzdem an.
|
||||
* **Kein Stale-if-error.** Ein abgelaufener Eintrag wird nie deshalb ausgeliefert, weil der erneute Abruf fehlschlug; der Fehler wird weitergereicht.
|
||||
* **Kein vorzeitiger Neuabruf.** Ein gespeicherter Eintrag wird ausgeliefert, bis seine TTL abläuft, und der nächste Aufruf danach bezahlt den Roundtrip; nichts wird im Hintergrund aktualisiert.
|
||||
* **Kein Zusammenfassen.** Zwei gleichzeitige identische Aufrufe sind zwei Abrufe.
|
||||
* **Keine TTL über 24 Stunden.** Ein größeres `ttlMs`, ob vom Server gesendet oder konfiguriert, wird beim Speichern gekappt (`mcp.client.caching.MAX_TTL_MS`); das begrenzt, wie lange irgendein Eintrag ausgeliefert werden kann, egal wie großzügig der Hinweis war.
|
||||
* Auf einem **gemeinsam genutzten Store** laufen Clients gegeneinander um die Wette. Jeder Client verwirft seinen eigenen Schreibvorgang, wenn eine Verdrängung den laufenden Abruf überholt hat, aber ein Client eines *Mit-Mandanten* kann trotzdem einen Eintrag zurückschreiben, den eine Verdrängung entfernt hatte, die er nie gesehen hat; und diese Race-Buchführung ist selbst begrenzt: Jenseits von 4096 verfolgten Schlüsseln wird zuerst der Schutz des ältesten Schlüssels verworfen. Beide Fenster sind akzeptiert und werden durch die TTL-Obergrenze oben geschlossen.
|
||||
* **Kein Ausliefern über Protokollgenerationen hinweg.** Einträge sind auf die ausgehandelte Protokollversion beschränkt: Auf einem gemeinsam genutzten persistenten Store liefert eine Session nie einen Eintrag aus, der unter einer anderen ausgehandelten Version geschrieben wurde (dieselbe Liste unterscheidet sich tatsächlich je nach Generation, weil das SDK die 2026er-Felder für ältere Sessions entfernt). Verdrängung berührt ebenso nur die Einträge der aktuellen Generation; Einträge einer anderen Generation laufen einfach per TTL ab.
|
||||
|
||||
### Die Hinweise selbst lesen {#reading-the-hints-yourself}
|
||||
|
||||
Die Hinweise sind außerdem ganz normale Felder auf jedem cachefähigen Ergebnis (`result.ttl_ms` und `result.cache_scope`, bereits geparst), falls du eine eigene Buchführung über den eingebauten Cache legen willst (oder an seiner Stelle).
|
||||
|
||||
Gegenüber einem **älteren Server** (Protokoll vor 2026) fehlen die Felder auf der Leitung einfach, und die Modelle zeigen ihre konservativen Standardwerte: `ttl_ms == 0` und `cache_scope == "private"`, abgelaufen und ungeteilt – die richtige Annahme für einen Server, der nichts deklariert hat. Der Cache behandelt eine Legacy-Session genauso: Hinweise werden dort nie herangezogen (egal welche Schlüssel auf der Leitung auftauchen), es gilt nur `default_ttl_ms`, und dessen Standardwert `0` cacht nichts, sodass sich eine Verbindung vor 2026 genau so verhält wie vor der Existenz des Caches. Musst du „der Server hat 0 gesagt“ von „der Server hat nichts gesagt“ unterscheiden, prüfe `"ttl_ms" in result.model_fields_set`: Das ist nur gesetzt, wenn das Feld tatsächlich angekommen ist.
|
||||
|
||||
## Ältere Clients {#older-clients}
|
||||
|
||||
Clients mit Protokollversionen vor 2026 sehen keines der beiden Felder; das SDK entfernt sie für diese Verbindungen bei der Serialisierung. Konfiguriere deine Hinweise einmal; es gibt nichts Versionsspezifisches zu schreiben.
|
||||
|
||||
## Zusammenfassung {#recap}
|
||||
|
||||
* Sechs Methoden tragen `ttlMs`/`cacheScope`; das SDK setzt sie standardmäßig auf `0`/`"private"` – abgelaufen und ungeteilt, immer sicher.
|
||||
* `cache_hints={method: CacheHint(...)}` bei der Konstruktion (sowohl `MCPServer` als auch `Server`) setzt serverweite Werte pro Methode.
|
||||
* Ein Handler, der die Felder auf seinem Ergebnis setzt, überschreibt die Map, pro Feld.
|
||||
* `"public"` ist ein Versprechen, dass das Ergebnis für alle Aufrufer identisch ist. Es ist keine Zugriffskontrolle.
|
||||
* `Client` beachtet die Hinweise automatisch: Sein Response-Cache ist standardmäßig aktiv, liefert frische Einträge statt neu abzurufen und cacht nichts für Server (oder Sessions), die keine Hinweise liefern.
|
||||
* Pro Aufruf ruft `cache_mode="refresh"` neu ab und `"bypass"` umgeht den Cache; `cache=None` bei der Konstruktion schaltet ihn ganz ab.
|
||||
154
i18n/de/pages/client/callbacks.md
Normal file
154
i18n/de/pages/client/callbacks.md
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
---
|
||||
translation:
|
||||
sections: [adf3c545b5be46b6, 916cd3ab1c03f461, e9be7a8d0eb0a456, 565890a636288ecf, 6af7e49db9129ec3, 06b0238c174186af, 90c6043be435fcb0]
|
||||
tool: 1
|
||||
---
|
||||
# Client-Callbacks {#client-callbacks}
|
||||
|
||||
Fast jeder Request in MCP läuft in eine Richtung: vom Client zum Server.
|
||||
|
||||
Ein Server kann aber auch den **Client** um etwas bitten: der Person am Host eine Frage zu stellen, ihr Modell per Sampling zu nutzen, ihre Arbeitsverzeichnisse aufzulisten. Diese Requests beantwortest du, indem du `Client(...)` **Callbacks** übergibst.
|
||||
|
||||
## Ein Server, der fragt {#a-server-that-asks}
|
||||
|
||||
Hier ist ein Server, dessen Tool allein nicht fertig werden kann:
|
||||
|
||||
```python title="server.py" hl_lines="16"
|
||||
--8<-- "docs_src/client_callbacks/tutorial001.py"
|
||||
```
|
||||
|
||||
* `ctx.elicit(...)` sendet einen `elicitation/create`-Request **an den Client** und wartet.
|
||||
* Das Tool kehrt erst zurück, wenn jemand (eine Person in einem Formular oder dein Code) einen `name` liefert.
|
||||
|
||||
Das ist die Server-Hälfte, und die gehört der Seite **[Elicitation](../handlers/elicitation.md)** (Elicitation: Rückfrage bei der Person am Host). Diese Seite hier ist das andere Ende der Leitung.
|
||||
|
||||
## Der Elicitation-Callback {#the-elicitation-callback}
|
||||
|
||||
```python title="client.py" hl_lines="6-10 16-17"
|
||||
--8<-- "docs_src/client_callbacks/tutorial002.py"
|
||||
```
|
||||
|
||||
* Ein Elicitation-Callback ist `async (context, params) -> ElicitResult`.
|
||||
* `params.message` ist die Frage. `params.requested_schema` ist das JSON-Schema der Antwort, die der Server haben will. Ein echter Client rendert daraus ein Formular; dieser hier füllt es automatisch aus.
|
||||
* Du gibst `ElicitResult(action="accept", content={...})` zurück, oder `action="decline"`, oder `action="cancel"`. Die einzige andere Möglichkeit ist `ErrorData(...)`: Das weist den Request zurück und lässt den gesamten Aufruf fehlschlagen.
|
||||
* `context` ist ein `ClientRequestContext`: die laufende `session`, die `request_id` des Servers und alles, was er an `meta` angehängt hat.
|
||||
|
||||
!!! tip
|
||||
`params` ist eine Union der beiden Elicitation-Modi. Hier ist `params.mode` gleich `"form"`; ein `"url"`-Request
|
||||
trägt `params.url` statt eines Schemas. Ein Callback behandelt beide; verzweige anhand von `params.mode`.
|
||||
**[Elicitation](../handlers/elicitation.md)** zeigt das vollständige Muster.
|
||||
|
||||
### Ausprobieren {#try-it}
|
||||
|
||||
Rufe `issue_card` auf und beobachte beide Enden.
|
||||
|
||||
Dein Callback erhält die Frage des Servers, bereits geparst:
|
||||
|
||||
```python
|
||||
params.mode # 'form'
|
||||
params.message # 'What name should go on the card?'
|
||||
params.requested_schema # {'properties': {'name': {'title': 'Name', 'type': 'string'}},
|
||||
# 'required': ['name'], 'title': 'CardHolder', 'type': 'object'}
|
||||
```
|
||||
|
||||
Er antwortet, `ctx.elicit(...)` läuft im Tool weiter, und das Tool wird fertig:
|
||||
|
||||
```python
|
||||
result.content # [TextContent(type='text', text='Card issued to Ada Lovelace.')]
|
||||
```
|
||||
|
||||
Ein `tools/call` von dir, ein `elicitation/create` zurück vom Server, beantwortet von deiner Funktion – alles innerhalb eines einzigen Tool-Aufrufs.
|
||||
|
||||
!!! info
|
||||
`mode="legacy"` im `Client(...)`-Aufruf leistet echte Arbeit. Standardmäßig handelt `Client(...)` den modernen
|
||||
Protokollpfad aus, und dieser Pfad hat keinen Rückkanal (back-channel) für Requests vom Server an den Client: `ctx.elicit`
|
||||
schlägt fehl, bevor dein Callback überhaupt läuft. Das entscheidet nicht der Transport, sondern das ausgehandelte
|
||||
Protokoll – in-memory genauso wie über eine URL. Setze `mode="legacy"` fest, wann immer dein Client
|
||||
einen solchen Request beantworten muss; jeder Test hinter dieser Seite tut das. Alles Weitere steht in **[Protokollversionen](../protocol-versions.md)**.
|
||||
|
||||
In einer 2026-07-28-Session ist der Callback nicht tot, er wird nur anders gespeist: Gibt ein Tool ein
|
||||
`InputRequiredResult` zurück, das einen `ElicitRequest` trägt, leitet `Client` diesen Eintrag an denselben
|
||||
`elicitation_callback` weiter und wiederholt den Aufruf für dich. Dieser Ablauf heißt **[Multi-Roundtrip-Requests](../handlers/multi-round-trip.md)** (multi-round-trip requests).
|
||||
|
||||
## Ein Callback ist eine Capability {#a-callback-is-a-capability}
|
||||
|
||||
Du hast dem Server nie gesagt, dass dein Client Elicitation-Requests beantworten kann. Das SDK hat es getan.
|
||||
|
||||
Wenn sich ein Client verbindet, deklariert er seine `capabilities`, das Spiegelbild derer des Servers. Dieses Objekt schreibst du nicht. **Einen Callback zu registrieren ist die Deklaration.**
|
||||
|
||||
| du übergibst | der Client deklariert |
|
||||
| --- | --- |
|
||||
| `elicitation_callback=` | `"elicitation": {"form": {}, "url": {}}` |
|
||||
| `sampling_callback=` | `"sampling": {}` |
|
||||
| `list_roots_callback=` | `"roots": {"listChanged": true}` |
|
||||
| keinen davon | `{}` |
|
||||
|
||||
Die Sampling-Sub-Capabilities sind die eine Verfeinerung: Übergib `sampling_capabilities=SamplingCapability(tools=SamplingToolsCapability())` zusammen mit `sampling_callback`, wenn dein Sampler die Parameter `tools` / `tool_choice` verarbeitet. Server müssen `sampling.tools` deklariert sehen, bevor sie diese senden dürfen.
|
||||
|
||||
`logging_callback` und `message_handler` stehen nicht in der Tabelle. Sie verarbeiten Benachrichtigungen, und Benachrichtigungen brauchen keine Capability.
|
||||
|
||||
Der Server liest die Deklaration mit `ctx.session.check_client_capability(...)` zurück. Füge ein Tool hinzu, das genau das tut:
|
||||
|
||||
```python title="server.py" hl_lines="23-31"
|
||||
--8<-- "docs_src/client_callbacks/tutorial003.py"
|
||||
```
|
||||
|
||||
Verbinde dich nur mit `elicitation_callback` und rufe es auf:
|
||||
|
||||
```python
|
||||
result.structured_content # {'result': ['elicitation']}
|
||||
```
|
||||
|
||||
Übergibst du alle drei Callbacks, bekommst du `['elicitation', 'sampling', 'roots']`. Übergibst du keinen, bekommst du `[]`.
|
||||
|
||||
!!! check
|
||||
Jetzt mach es absichtlich falsch: Verbinde dich **ohne** `elicitation_callback` und rufe `issue_card` trotzdem auf.
|
||||
|
||||
Der `elicitation/create`-Request des Servers erreicht deinen Client trotzdem, und das SDK beantwortet ihn für
|
||||
dich – mit einem Fehler, weil du nie gesagt hast, dass du ihn verarbeiten kannst. Dieser Fehler lässt den gesamten Aufruf scheitern.
|
||||
`call_tool` gibt kein `is_error`-Ergebnis zurück; es wirft eine Exception:
|
||||
|
||||
```text
|
||||
MCPError: Elicitation not supported
|
||||
```
|
||||
|
||||
Das ist ein Protokollfehler (`-32600`, *invalid request*), kein Tool-Fehler: Es gibt nichts, was
|
||||
das Modell lesen und erneut versuchen könnte. Deshalb lohnt sich `client_features`: Ein Server,
|
||||
der sich gut benimmt, prüft, bevor er fragt.
|
||||
|
||||
## Das veraltete Paar {#the-deprecated-pair}
|
||||
|
||||
`sampling_callback` beantwortet `sampling/createMessage`: Der Server bittet *dein* Modell um eine Completion. `list_roots_callback` beantwortet `roots/list`: Der Server fragt, in welchen Verzeichnissen er arbeiten darf.
|
||||
|
||||
Beide funktionieren. Beide folgen der Regel oben. Und beide bedienen RPCs, die die **Spezifikation 2026-07-28 entfernt**: Ein moderner Server ruft nicht mitten im Request in deinen Client zurück, sondern reicht dir den Request als Teil des Tool-Ergebnisses zurück (**[Multi-Roundtrip-Requests](../handlers/multi-round-trip.md)**). Die Callbacks selbst sind nicht tot. Trägt ein `InputRequiredResult` einen `CreateMessageRequest` oder einen `ListRootsRequest`, leitet die Auto-Schleife von `Client` ihn an denselben `sampling_callback` oder `list_roots_callback` weiter, den du hier registriert hast. Die vollständige Liste steht in **[Veraltete Features](../deprecated.md)**.
|
||||
|
||||
Du brauchst die Callbacks weiterhin, um mit Servern zu sprechen, die noch nicht umgestiegen sind. Die Signaturen:
|
||||
|
||||
```python title="client.py"
|
||||
--8<-- "docs_src/client_callbacks/tutorial004.py"
|
||||
```
|
||||
|
||||
* Ein Sampling-Callback erhält die vollständigen `CreateMessageRequestParams` (`messages`, `model_preferences`, `max_tokens`) und gibt ein `CreateMessageResult` zurück. *Du* betreibst das Modell, ganz wie du willst; das SDK transportiert nur den Request.
|
||||
* Ein Roots-Callback nimmt überhaupt keine Parameter entgegen und gibt ein `ListRootsResult` zurück.
|
||||
* Beide dürfen stattdessen `ErrorData(...)` zurückgeben, um abzulehnen.
|
||||
|
||||
Übergib sie an `Client(...)` genau wie `elicitation_callback`.
|
||||
|
||||
## Die Benachrichtigungs-Callbacks {#the-notification-callbacks}
|
||||
|
||||
Zwei weitere. Keiner deklariert etwas.
|
||||
|
||||
`logging_callback` erhält die `notifications/message`, die ein Server sendet, als `LoggingMessageNotificationParams` (`level`, `logger`, `data`). Das Protokoll-Logging selbst ist mit der Spezifikation 2026-07-28 veraltet (was du stattdessen tust, steht in **[Logging](../handlers/logging.md)**), dieser Callback existiert also für die Server, die es noch ausgeben. Auf einer Verbindung der 2026er-Generation bringt dir der Callback allein nichts, denn 2026er-Server senden Log-Nachrichten nur an Requests, die sich dafür anmelden: Übergib `log_level="info"` (oder ein anderes Level) an `Client(...)`, um dieses Opt-in jedem Request aufzuprägen und dieses Level und alles darüber zu empfangen. Server vor 2026 ignorieren das und behalten ihr `logging/setLevel`-Verhalten.
|
||||
|
||||
`message_handler` ist das Sammelbecken: Jede Server-Benachrichtigung, die die Session nach oben reicht, landet dort (zusätzlich zu ihrem spezifischen Callback), und auf einem Stream-gestützten Transport auch jede `Exception` auf Transportebene. Zwei kommen nie an: `notifications/cancelled` wendet das SDK an, statt sie nach oben zu reichen, und eine Abonnement-Bestätigung für einen laufenden `listen()`-Stream verbraucht dieser Stream selbst. Annotiere den Parameter mit `IncomingMessage` (`ServerNotification | Exception`, exportiert aus `mcp.client`). Das eine Muster, das du kennen solltest, ist `if isinstance(message, Exception): raise message`, damit eine unterbrochene Verbindung laut fehlschlägt, statt still zu verschwinden.
|
||||
|
||||
## Zusammenfassung {#recap}
|
||||
|
||||
* Ein Server kann Requests an den Client senden. Du beantwortest sie mit Callbacks, die du `Client(...)` übergibst.
|
||||
* Der Elicitation-Callback ist der aktuelle: `async (context, params) -> ElicitResult`, eine Funktion für Formular- und URL-Modus.
|
||||
* **Einen Callback zu registrieren heißt, die Capability zu deklarieren.** Ohne ihn weist das SDK den Request des Servers in deinem Namen zurück, und der gesamte Aufruf schlägt mit `MCPError` fehl.
|
||||
* Ein Server findet das vor dem Fragen mit `ctx.session.check_client_capability(...)` heraus.
|
||||
* `sampling_callback` und `list_roots_callback` funktionieren genauso, bedienen aber veraltete Features; moderne Server verwenden stattdessen Multi-Roundtrip-Requests.
|
||||
* `logging_callback` und `message_handler` empfangen Benachrichtigungen. Sie deklarieren nichts.
|
||||
|
||||
Das erste Argument von `Client(...)` ist ein Transport-Objekt. **[Client-Transporte](transports.md)** behandelt jede Art davon.
|
||||
155
i18n/de/pages/client/identity-assertion.md
Normal file
155
i18n/de/pages/client/identity-assertion.md
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
---
|
||||
translation:
|
||||
sections: [a91322c46111d16d, 8e6fd6d6f59bb568, e7828fd2729b2c9d, a03ec26bfc678b65, 1034c653c0bcf1b0]
|
||||
tool: 1
|
||||
---
|
||||
# Identity Assertion {#identity-assertion}
|
||||
|
||||
Ein gewöhnlicher OAuth-Provider (**[OAuth-Clients](oauth-clients.md)**) stellt dem MCP-Server zuerst eine Frage: *Welchem Autorisierungsserver vertraust du?* Er folgt der Antwort, wohin sie auch zeigt, und dann meldet sich entweder eine Person an oder ein vorab geteiltes Secret tritt an ihre Stelle.
|
||||
|
||||
Ein Unternehmen will weder das eine noch das andere pro Server entschieden haben. Es betreibt längst einen Identity Provider (Okta, Microsoft Entra ID, einen eigenen); die Person hat sich dort heute Morgen schon angemeldet; und es ist der eine Ort, an dem das Security-Team entscheiden will, wer was erreichen darf. [SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990), die Erweiterung **Enterprise-Managed Authorization**, verlegt die Entscheidung dorthin. Der IdP signiert ein kurzlebiges JWT, einen **Identity Assertion JWT Authorization Grant**, den **ID-JAG**: die Aussage, dass *diese Person* über *diesen Client* *diesen MCP-Server* erreichen darf. Der Client tauscht ihn gegen ein gewöhnliches Access Token. Kein Browser, kein Zustimmungsdialog, keine dynamische Registrierung.
|
||||
|
||||
Diese Seite zeigt beide Seiten dieses Tauschs. Der MCP-Server selbst ändert sich nie: Er ist nach wie vor der Ressourcenserver aus **[Autorisierung](../run/authorization.md)** und prüft jedes Token, das ankommt.
|
||||
|
||||
## Zwei Token-Requests {#two-token-requests}
|
||||
|
||||
Zwei verschiedene Instanzen sind im Spiel, und sie auseinanderzuhalten ist schon fast das ganze Verständnis dieser Seite. Der **Unternehmens-IdP** ist der Identity Provider deiner Organisation: Er kennt die Identität der Beschäftigten, bei ihm liegen die Richtlinien, und er stellt den ID-JAG aus. Das SDK spricht nie mit ihm. Der **MCP-Autorisierungsserver** ist dieselbe Partei wie in **[Autorisierung](../run/authorization.md)**: der Issuer, den die Metadaten des MCP-Servers nennen, die Stelle, die die Tokens ausstellt, die dieser MCP-Server akzeptiert. In einem gewöhnlichen OAuth-Flow sind diese beiden Rollen meist ein und dasselbe System. Hier sind es zwei, und der ganze Grant besteht darin, dass der zweite zustimmt, dem ersten zu vertrauen.
|
||||
|
||||
Der Client stellt an jeden der beiden genau einen Token-Request.
|
||||
|
||||
1. **An den Unternehmens-IdP.** Der Client tauscht die Anmeldung der Person (ihr OpenID-Connect-ID-Token) gegen den ID-JAG. Das ist ein Token Exchange nach [RFC 8693](https://datatracker.ietf.org/doc/html/rfc8693), er läuft vollständig über die API deines IdP, und **das SDK führt ihn nicht aus**. Das machst du, in einem einzigen asynchronen Callback. Hier fällt auch die Richtlinienentscheidung: Ein IdP, der Nein sagt, stellt den ID-JAG gar nicht erst aus, und es gibt nichts vorzulegen.
|
||||
2. **An den MCP-Autorisierungsserver.** Der Client legt den ID-JAG im `jwt-bearer`-Grant nach [RFC 7523](https://datatracker.ietf.org/doc/html/rfc7523) vor (`grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer`, der ID-JAG als `assertion`) und erhält das Access Token. **Diesen Request stellt das SDK**, und ihn anzunehmen ist das Einzige, was diese Seite einem Autorisierungsserver hinzufügt.
|
||||
|
||||
Alles Weitere dreht sich um den zweiten Request: den Client, der ihn sendet, und den Autorisierungsserver, der ihn beantwortet.
|
||||
|
||||
## Der Client {#the-client}
|
||||
|
||||
**`IdentityAssertionOAuthProvider`** liegt in `mcp.client.auth.extensions.identity_assertion`. Wie jeder Provider in **[OAuth-Clients](oauth-clients.md)** ist er ein `httpx2.Auth`: Erzeuge einen, setze ihn auf `auth=` und übergib den `httpx2.AsyncClient` an den Transport.
|
||||
|
||||
```python title="client.py" hl_lines="49-50 53-61"
|
||||
--8<-- "docs_src/identity_assertion/tutorial001.py"
|
||||
```
|
||||
|
||||
Lies die Datei von unten nach oben.
|
||||
|
||||
* `main()` ist das übliche `main()` eines OAuth-Clients (**[OAuth-Clients](oauth-clients.md)**), Zeile für Zeile unverändert. Genau darum geht es: Sobald der Provider existiert, weiß nichts dahinter, welcher Grant das Token erzeugt hat.
|
||||
* Der Provider nimmt entgegen, was die anderen Provider nicht per Discovery herausfinden können: eine `client_id` und ein `client_secret`, die jemand beim Autorisierungsserver **vorab registriert** hat, den `issuer` dieses Autorisierungsservers und `assertion_provider`, einen asynchronen Callback, der auf Anforderung einen frischen ID-JAG liefert.
|
||||
* `storage` ist dasselbe `TokenStorage`-Protokoll. Aufgerufen werden nur die beiden Token-Methoden; dynamische Registrierung gibt es hier nicht, also auch kein `client_info`, das man sich merken müsste.
|
||||
|
||||
### Der Assertion-Provider {#the-assertion-provider}
|
||||
|
||||
`fetch_id_jag(audience, resource)` ist der einzige Code, den du schreibst. Er wird einmal pro Token-Austausch aufgerufen, nie beim Konstruieren, und erst *nachdem* die Metadaten des Autorisierungsservers abgerufen und validiert wurden – so gibt ein falsch konfigurierter Issuer nie eine Assertion preis. Seine beiden Argumente sind zwei der Claims, mit denen der ID-JAG ausgestellt werden muss: `audience` ist der Issuer des Autorisierungsservers (das `aud` des ID-JAG) und `resource` der kanonische Bezeichner des MCP-Servers (das `resource` des ID-JAG). Den dritten hast du bereits: Der `client_id`-Claim des ID-JAG muss die `client_id` nennen, die du dem Provider gegeben hast, sonst verweigert der Autorisierungsserver den Austausch.
|
||||
|
||||
`idp_issue_id_jag` darüber ist **nicht dein Code**. Die Funktion steht stellvertretend für den Identity Provider und signiert die Assertion im selben Prozess, damit die Datei vollständig ist und du jeden Claim lesen kannst, den ein ID-JAG trägt. Ein echtes `fetch_id_jag` stellt stattdessen den ersten Token-Request aus dem vorigen Abschnitt: einen Token Exchange nach [RFC 8693](https://datatracker.ietf.org/doc/html/rfc8693) gegen deinen IdP, definiert im Draft zum Identity Assertion JWT Authorization Grant, den [SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990) profiliert. Das ID-Token der angemeldeten Person geht als `subject_token` hinein, der `requested_token_type` ist der eigene URN des ID-JAG (`urn:ietf:params:oauth:token-type:id-jag`), `audience` und `resource` werden unverändert durchgereicht, und die Response enthält den ID-JAG. Nach genau diesem Austausch unter genau diesen Namen suchst du in der Dokumentation deines IdP.
|
||||
|
||||
!!! tip
|
||||
Für jeden Austausch wird ein frischer ID-JAG angefordert, und genau das ist der Sinn: Er ist ein
|
||||
Grant zur einmaligen Verwendung, der nur Minuten lebt, und der Autorisierungsserver auf dieser Seite
|
||||
nimmt denselben kein zweites Mal an. Cache ihn nicht. Wiederverwendet wird das Access Token, das du
|
||||
dafür bekommst.
|
||||
|
||||
### Der Issuer ist Konfiguration {#the-issuer-is-configuration}
|
||||
|
||||
Hier liegt die Umkehrung. `OAuthClientProvider` fragt den Ressourcenserver, welchen Autorisierungsserver er verwenden soll, und folgt der Antwort, wohin sie auch zeigt. Dieser Provider weigert sich: `issuer` ist erforderlich, die Metadaten nach [RFC 8414](https://datatracker.ietf.org/doc/html/rfc8414) werden vom eigenen Well-known-Pfad dieses Issuers abgerufen, der Token-Endpunkt muss auf dem Origin dieses Issuers liegen, und der Ressourcenserver wird nie irgendetwas gefragt.
|
||||
|
||||
Die Erweiterung verlangt das nicht; es ist eine bewusst strengere Entscheidung. Dieser Client trägt zwei Dinge mit sich, die sich zu stehlen lohnen – ein vorab registriertes Secret und eine an eine Audience gebundene Assertion –, und ein Client, der sich von einem kompromittierten MCP-Server zu einem von Angreifenden kontrollierten Autorisierungsserver lenken ließe, würde beides dorthin posten. Den Issuer beim Konstruieren festzulegen, streicht dieses Gespräch komplett.
|
||||
|
||||
!!! warning
|
||||
Der konfigurierte `issuer` wird mit dem Feld `issuer` des Metadatendokuments per einfachem
|
||||
String-Vergleich nach RFC 8414 §3.3 verglichen: Zeichen für Zeichen, abschließender Schrägstrich
|
||||
inklusive, ohne Normalisierung. Rate ihn nicht. Rufe `/.well-known/oauth-authorization-server` von
|
||||
deinem Autorisierungsserver ab und kopiere den `issuer`-Wert, den er zurückgibt. Für den
|
||||
Autorisierungsserver auf dieser Seite ist das `https://auth.example.com/`, mit dem Schrägstrich, weil
|
||||
sein Issuer aus einem Pydantic-URL-Objekt gebaut wurde. Eine Abweichung stoppt den Flow bei
|
||||
`OAuthFlowError: Authorization server metadata issuer
|
||||
mismatch`, bevor auch nur ein einziges Credential oder eine Assertion gesendet wird.
|
||||
|
||||
### Ein vertraulicher Client {#a-confidential-client}
|
||||
|
||||
`client_secret` ist erforderlich; ohne löst der Konstruktor einen `ValueError` aus. Das IETF-Profil unter [SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990) reserviert diesen Grant für vertrauliche Clients, SEP-990 verlangt, dass sich der Client authentifiziert, und dieses SDK setzt beides durch, indem es auf einem geteilten Secret besteht. `token_endpoint_auth_method` legt fest, wo es mitreist: `client_secret_post` (der Standardwert, im Formular-Body) oder `client_secret_basic` (ein HTTP-Basic-Header). Das Profil erlaubt außerdem `private_key_jwt`; dieser Provider unterstützt es nicht.
|
||||
|
||||
!!! tip
|
||||
Lies `client_secret` aus der Umgebung oder einem Secret-Manager, nie aus der Versionsverwaltung.
|
||||
|
||||
### Was der Provider für dich erledigt {#what-the-provider-does-for-you}
|
||||
|
||||
Der erste Request geht unauthentifiziert raus, und das `401` des Servers startet den Flow.
|
||||
|
||||
1. **Discovery.** Er ruft die Metadaten des Autorisierungsservers vom Well-known-Pfad nach [RFC 8414](https://datatracker.ietf.org/doc/html/rfc8414) des konfigurierten Issuers ab, prüft, dass der `issuer` des Dokuments übereinstimmt, und prüft, dass der Token-Endpunkt auf dem Origin des Issuers liegt.
|
||||
2. **Die Assertion.** Er ruft deinen `assertion_provider` auf und wartet auf das Ergebnis.
|
||||
3. **Austausch.** Er sendet den `jwt-bearer`-Grant per POST an den Token-Endpunkt, speichert das `OAuthToken` und wiederholt deinen ursprünglichen Request mit `Authorization: Bearer ...`.
|
||||
|
||||
Ein `403`, dessen `WWW-Authenticate` `insufficient_scope` nennt, führt die Schritte 2 und 3 erneut aus, mit der Vereinigung aus deinem `scope` und dem in der Challenge geforderten. (`scope` ist immer nur eine Bitte; der Autorisierungsserver dieser Seite gewährt, was der ID-JAG sagt, und nichts sonst.) Ein Refresh Token gibt es hier nirgends: Läuft das Access Token ab, lässt das nächste `401` einen frischen ID-JAG ausstellen und tauscht erneut, und *das* ist der Hebel, den der IdP in der Hand hält. Fehler sind dieselben zwei Exceptions wie überall in **[OAuth-Clients](oauth-clients.md)**: `OAuthFlowError` für Discovery und Validierung, ihre Unterklasse `OAuthTokenError`, wenn der Token-Endpunkt Nein sagt.
|
||||
|
||||
## Der Autorisierungsserver {#the-authorization-server}
|
||||
|
||||
Meistens hörst du hier auf. Der MCP-Autorisierungsserver ist das Produkt von jemand anderem, ID-JAGs anzunehmen ist eine Einstellung in dessen Konfiguration, die du einschaltest, und die SDK-Hälfte von [SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990) ist der Client oben.
|
||||
|
||||
Das SDK kann aber auch selbst der Autorisierungsserver *sein*: `create_auth_routes` gibt die Routen des Autorisierungsservers als Liste zurück, die jede Starlette-App mounten kann – so betreibt `examples/servers/simple-auth/` im Repository einen. SEP-990 fügt dieser Oberfläche ein Flag und eine Methode hinzu:
|
||||
|
||||
```python title="auth_server.py" hl_lines="48-50 105-107"
|
||||
--8<-- "docs_src/identity_assertion/tutorial002.py"
|
||||
```
|
||||
|
||||
* `identity_assertion_enabled=True` schaltet alles frei. Ausgeschaltet – das ist der Standardwert – beantwortet `/token` diesen Grant mit `unsupported_grant_type`, selbst wenn du den Hook implementiert hast, und die Metadaten erwähnen ihn nicht. Eingeschaltet erhalten die Metadaten den Grant-Typ `jwt-bearer` und listen `urn:ietf:params:oauth:grant-profile:id-jag` in `authorization_grant_profiles_supported`, dem Feld, mit dem die Erweiterung Unterstützung bekannt gibt. (Der Client dieses SDK liest es nie: Er ist für genau einen Issuer eingerichtet und fragt einfach.)
|
||||
* **`exchange_identity_assertion`** ist der Hook. Bevor er läuft, hat das SDK den Client authentifiziert, öffentliche Clients abgewiesen und Clients abgewiesen, deren Registrierung den Grant nicht aufführt. Du bekommst ein `IdentityAssertionParams` (die rohe `assertion`, die angeforderten `scopes` und `resource`) und gibst ein schlichtes `OAuthToken` zurück.
|
||||
* Die dynamische Client-Registrierung lehnt diesen Grant ausnahmslos ab, deshalb bedient `get_client` hier einen von Hand eingerichteten Client. Ein ID-JAG-Client kann sich nicht selbst ins Leben registrieren.
|
||||
* Die halbe Klasse besteht aus Ablehnungen. `OAuthAuthorizationServerProvider` ist der *ganze* Autorisierungsserver, also verlangt er auch den Authorization-Code-Flow; ein Server, der Personen zusätzlich anmeldet, implementiert diese Methoden wirklich, und dieser hier hat genau eine Tür.
|
||||
|
||||
!!! warning
|
||||
Das SDK dekodiert die Assertion nie: Nur dein Deployment weiß, welchem IdP es vertraut und welche
|
||||
Schlüssel dieser IdP veröffentlicht, deshalb ist alles innerhalb von `exchange_identity_assertion`
|
||||
tragend. Prüfe die Signatur gegen die veröffentlichten Schlüssel des IdP (sein JWKS; das geteilte
|
||||
Secret hier gehört zur Demo) sowie `iss` und `exp`, gemäß [RFC 7523](https://datatracker.ietf.org/doc/html/rfc7523) §3. Verlange, dass `typ`
|
||||
im JWT-Header `oauth-id-jag+jwt` ist – der Schutz des Profils dagegen, dass irgendein anderes JWT
|
||||
als Grant wiedereingespielt wird. Verlange, dass `aud` dein eigener Issuer ist. Verlange, dass der
|
||||
`client_id`-Claim des ID-JAG dem Client entspricht, den der Handler authentifiziert hat, und dass
|
||||
sein `resource`-Claim eine Ressource nennt, die du tatsächlich bedienst. Merke dir `jti` bis zum
|
||||
`exp` der Assertion, damit sie nur einmal akzeptiert wird. Und entnimm die gewährten Scopes und vor
|
||||
allem das `resource` des ausgestellten Tokens dem validierten ID-JAG, nie dem Request:
|
||||
`params.resource` ist, was immer der Client eingetippt hat. Die vollständigen Verarbeitungsregeln
|
||||
stehen in der [Spezifikation zu Enterprise-Managed Authorization](https://modelcontextprotocol.io/extensions/auth/enterprise-managed-authorization).
|
||||
|
||||
Eine fehlerhafte Assertion weist du mit `TokenError("invalid_grant", ...)` ab. Der andere Fehlercode in diesem Flow ist `invalid_target`: Ein ID-JAG, der eine Ressource nennt, die du nicht bedienst, wird damit abgelehnt – das verhindert, dass dieser Server Tokens für die Ressource von jemand anderem ausstellt. Und die gewährten Scopes stammen aus dem `scope`-Claim des ID-JAG (eine Assertion ohne ihn wird ebenfalls abgelehnt); deiner könnte stattdessen die Gruppen der Person abbilden.
|
||||
|
||||
Und beachte, was das zurückgegebene `OAuthToken` nicht enthält: ein Refresh Token. Der IdP entscheidet, wie lange diese Person Zugang behält, indem er entscheidet, ob er den nächsten ID-JAG ausstellt. Ein hier ausgestelltes Refresh Token gäbe diese Entscheidung stillschweigend wieder ab.
|
||||
|
||||
!!! info
|
||||
Ein Server, der seinen Autorisierungsserver noch mit `auth_server_provider=` einbettet, erreicht
|
||||
denselben Code über `AuthSettings(identity_assertion_enabled=True)`. **[Autorisierung](../run/authorization.md)** erklärt,
|
||||
warum neue Server nicht dort anfangen sollten.
|
||||
|
||||
!!! check
|
||||
Verbinde die beiden Dateien dieser Seite miteinander, und der ganze Grant ist ein einziges `POST /token`:
|
||||
|
||||
```text
|
||||
grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer
|
||||
assertion=eyJhbGciOiJIUzI1NiIsInR5cCI6Im9hdXRoLWlkLWphZytqd3QifQ...
|
||||
client_id=finance-agent
|
||||
resource=http://localhost:8001/mcp
|
||||
scope=notes:read
|
||||
client_secret=finance-agent-secret
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
{"access_token": "mcp_...", "token_type": "Bearer", "expires_in": 300, "scope": "notes:read"}
|
||||
```
|
||||
|
||||
Kein `/authorize`, kein `/register`, kein Abruf der Protected-Resource-Metadaten. Die einzigen
|
||||
Requests auf der Leitung sind der, der das `401` ausgelöst hat, der Well-known-Abruf, dieser
|
||||
Austausch und danach gewöhnlicher MCP-Verkehr mit angehängtem Bearer-Token. Und das `sub`, das dein
|
||||
Validator aus dem ID-JAG gelesen hat, ist genau das, was `get_access_token().subject` innerhalb
|
||||
eines Tools meldet.
|
||||
|
||||
### Ausprobieren {#try-it}
|
||||
|
||||
`examples/stories/identity_assertion/` im SDK-Repository ist diese Seite in echt: derselbe `exchange_identity_assertion`-Validator, ein MCP-Server, der durch dessen Tokens abgesichert ist, ein Stellvertreter-IdP und der Client, in einem einzigen Programm, das sich selbst prüft. `uv run python -m stories.identity_assertion.client --http` führt den ganzen Austausch aus und prüft, dass die Person, die der IdP benannt hat, dieselbe ist, die das Tool sieht.
|
||||
|
||||
## Zusammenfassung {#recap}
|
||||
|
||||
* [SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990) lässt den Identity Provider des Unternehmens – nicht die Person am Host – entscheiden, welche MCP-Server ein Client erreichen darf. Der IdP signiert diese Entscheidung in einen **ID-JAG**.
|
||||
* Den ID-JAG zu beschaffen ist ein Token Exchange nach [RFC 8693](https://datatracker.ietf.org/doc/html/rfc8693) gegen *deinen IdP*, und das SDK führt ihn nicht aus. Ihn dem MCP-Autorisierungsserver vorzulegen ist der `jwt-bearer`-Grant nach [RFC 7523](https://datatracker.ietf.org/doc/html/rfc7523), und davon übernimmt das SDK beide Seiten.
|
||||
* `IdentityAssertionOAuthProvider` ist ein weiteres `httpx2.Auth`: ein vorab registrierter vertraulicher Client, ein festgelegter `issuer` und ein einziger Callback `assertion_provider(audience, resource)`. Kein Browser, keine Registrierung, kein Refresh Token.
|
||||
* Der Autorisierungsserver wird nie über den Ressourcenserver entdeckt. Setze `issuer` auf genau den String, den sein Metadatendokument ausliefert; verglichen wird Zeichen für Zeichen.
|
||||
* Serverseitig: `identity_assertion_enabled=True` plus `exchange_identity_assertion`. Das SDK authentifiziert den Client und schaltet den Grant frei; den ID-JAG zu validieren ist ganz deine Sache, und das ausgestellte Token ist an das `resource` des ID-JAG gebunden, nicht an das des Requests.
|
||||
|
||||
Die eine Partei, die diese Seite nie angefasst hat, ist der MCP-Server. Was er mit dem Token macht, das du gerade ausgestellt hast, hat er schon in **[Autorisierung](../run/authorization.md)** getan.
|
||||
217
i18n/de/pages/client/index.md
Normal file
217
i18n/de/pages/client/index.md
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
---
|
||||
translation:
|
||||
sections: [ebef1e7a0df854f4, a4c687d3d627d516, 8e79141fc2985342, b345dd05b9c3c7ab, 80ce41579825a6fa, 5f0fa90494de8f65, 83d10514eaa62fa5, 9190555aa39a5d28, 84a4c9d8bf14dddb, 927d71cf40b58c30]
|
||||
tool: 1
|
||||
---
|
||||
# Der Client {#the-client}
|
||||
|
||||
Ein **`Client`** ist der Weg, auf dem ein Python-Programm mit einem MCP-Server spricht.
|
||||
|
||||
Er ist ein einziges Objekt mit einem einzigen Lebenszyklus: erzeugen, `async with` betreten, Methoden aufrufen. Jedes Verb des Protokolls (die Tools auflisten, eines aufrufen, eine Ressource lesen, einen Prompt rendern) ist eine `async`-Methode darauf, die ein typisiertes Ergebnis zurückgibt.
|
||||
|
||||
## Der erste Client {#your-first-client}
|
||||
|
||||
```python title="client.py" hl_lines="14-18"
|
||||
--8<-- "docs_src/client/tutorial001.py"
|
||||
```
|
||||
|
||||
Der Server oben ist nur da, damit du etwas hast, womit du dich verbinden kannst. Der Client sind die fünf hervorgehobenen Zeilen.
|
||||
|
||||
* `Client(mcp)` bekommt das **Server-Objekt selbst**. Das ist der In-Memory-Transport: kein Subprozess, kein Port, kein HTTP. So verbindet sich jedes Beispiel auf dieser Seite und jeder Test, den du schreibst.
|
||||
* `async with` ist der **Lebenszyklus**. Beim Betreten wird verbunden und ausgehandelt, beim Verlassen getrennt. Es gibt kein `connect()`/`close()`-Paar, und ein `Client` lässt sich nach dem Ende des Blocks nicht wiederverwenden.
|
||||
* Innerhalb des Blocks liegen die Fakten zur Verbindung bereits als einfache Properties vor.
|
||||
|
||||
### Was sich an `Client` übergeben lässt {#what-you-can-pass-to-client}
|
||||
|
||||
`Client` nimmt ein positionelles Argument und leitet den Transport aus dessen Typ ab:
|
||||
|
||||
* Eine Instanz von `MCPServer` (oder des Low-Level-`Server`): Verbindung **im selben Prozess**.
|
||||
* Ein URL-String (`Client("http://localhost:8000/mcp")`): Streamable HTTP, der Weg für die Produktion.
|
||||
* Ein **Transport**: alles, was sich mit `async with ... as (read, write)` verwenden lässt, etwa `stdio_client(...)` um einen Subprozess herum.
|
||||
|
||||
Alles Übrige auf dieser Seite ist in allen drei Fällen identisch. Header, Subprozesse, Timeouts und das `Transport`-Protokoll haben ihre eigene Seite: **[Client-Transporte](transports.md)**.
|
||||
|
||||
### Was ein verbundener Client mitbringt {#whats-on-a-connected-client}
|
||||
|
||||
Vier schreibgeschützte Properties, die gefüllt sind, sobald du den Block betrittst:
|
||||
|
||||
* `client.server_info`: die Identität des Servers oder `None` bei einem Server der 2026er-Generation, der keine meldet (python-sdk-Server tun das standardmäßig). `server_info.name` ist hier `"Bookshop"`, `server_info.version` ist das, was der Server meldet.
|
||||
* `client.server_capabilities`: was der Server kann (`tools`, `resources`, `prompts`, `completions`, ...). Eine Capability, die der Server nicht hat, ist `None`.
|
||||
* `client.protocol_version`: die Protokollversion, auf die sich beide Seiten geeinigt haben. Hier ist sie `"2026-07-28"`.
|
||||
* `client.instructions`: der `instructions=`-String des Servers oder `None`, wenn er keinen gesetzt hat.
|
||||
|
||||
Eine Protokollversion hast du nie ausgewählt. Standardmäßig sondiert der `Client` den Server und fällt bei älteren auf den klassischen Handshake zurück, sodass ein einziger Client mit Servern jeder Generation funktioniert. Wenn du das steuern musst: Alles Weitere steht in **[Protokollversionen](../protocol-versions.md)**.
|
||||
|
||||
!!! tip
|
||||
`client.session` ist die darunterliegende `ClientSession`, der Low-Level-Notausgang.
|
||||
Für nichts auf dieser Seite wirst du sie brauchen.
|
||||
|
||||
## Tools auflisten {#listing-tools}
|
||||
|
||||
```python title="client.py" hl_lines="15-20"
|
||||
--8<-- "docs_src/client/tutorial002.py"
|
||||
```
|
||||
|
||||
`list_tools()` gibt ein `ListToolsResult` zurück; die Tools stehen in `.tools`. Jedes davon ist die vollständige Definition, die ein Host einem Modell übergeben würde:
|
||||
|
||||
```python
|
||||
tool.name # 'search_books'
|
||||
tool.title # 'Search the catalog'
|
||||
tool.description # 'Search the catalog by title or author.'
|
||||
```
|
||||
|
||||
und `tool.input_schema` ist das JSON-Schema, das der Server aus den Type Hints der Funktion abgeleitet hat:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {"title": "Query", "type": "string"},
|
||||
"limit": {"default": 10, "title": "Limit", "type": "integer"}
|
||||
},
|
||||
"required": ["query"],
|
||||
"title": "search_booksArguments"
|
||||
}
|
||||
```
|
||||
|
||||
Dieses Schema ist alles, was eine UI braucht, um ein Argumentformular zu rendern, und alles, was ein Modell braucht, um gültige Argumente zu erzeugen.
|
||||
|
||||
!!! tip
|
||||
`title` ist optional, also muss sich eine UI, die einem Menschen Tools anzeigt, entscheiden: den `title`, wenn es einen gibt,
|
||||
sonst den `name`. `from mcp.shared.metadata_utils import get_display_name` macht genau das –
|
||||
für Tools, Ressourcen, Ressourcen-Templates und Prompts.
|
||||
|
||||
## Ein Tool aufrufen {#calling-a-tool}
|
||||
|
||||
`call_tool(name, arguments)` führt das Tool aus und gibt dir ein `CallToolResult` zurück.
|
||||
|
||||
```python title="client.py" hl_lines="26-33"
|
||||
--8<-- "docs_src/client/tutorial003.py"
|
||||
```
|
||||
|
||||
`lookup_book` auf dem Server gibt ein Pydantic-`Book` zurück. Das sieht der Client:
|
||||
|
||||
```python
|
||||
result.content # [TextContent(type='text', text='{\n "title": "Dune",\n "author": "Frank Herbert",\n "year": 1965\n}')]
|
||||
result.structured_content # {'title': 'Dune', 'author': 'Frank Herbert', 'year': 1965}
|
||||
result.is_error # False
|
||||
```
|
||||
|
||||
Ein Rückgabewert, drei Dinge zu lesen. Jedes hat einen anderen Abnehmer.
|
||||
|
||||
### `content`: was das Modell liest {#content-what-the-model-reads}
|
||||
|
||||
`content` ist eine `list` von **Content-Blöcken**, und ein Content-Block ist eine Union: `TextContent`, `ImageContent`, `AudioContent`, `ResourceLink` oder `EmbeddedResource`. Ein Tool kann mehrere zurückgeben, auch verschiedener Art.
|
||||
|
||||
Deshalb grenzt `main` mit `isinstance(block, TextContent)` ein, bevor es `block.text` anfasst. Beachte, dass es kein `.text` außerhalb des `isinstance` gibt: Der Typprüfer lässt das nicht zu, denn `ImageContent` hat `.data`, nicht `.text`. Die Union ist ehrlich darüber, was ein Tool dir schicken darf; dein Code sollte es auch sein.
|
||||
|
||||
### `structured_content`: was deine Anwendung liest {#structured_content-what-your-application-reads}
|
||||
|
||||
`structured_content` ist der Rückgabewert des Tools als JSON, passend zum deklarierten `output_schema` des Tools. Kein String-Parsing, kein Raten.
|
||||
|
||||
Wenn beide vorhanden sind, sagen sie absichtlich zweimal dasselbe: `content` ist für ein Modell, `structured_content` ist für Code. Woher die strukturierte Hälfte kommt und wie du sie steuerst, steht auf der Seite **[Strukturierte Ausgabe](../servers/structured-output.md)**.
|
||||
|
||||
### `is_error`: ob das Tool fehlgeschlagen ist {#is_error-whether-the-tool-failed}
|
||||
|
||||
Ein Tool, das eine Exception auslöst, löst in deinem Client **keine** aus. Es kommt als gewöhnliches Ergebnis mit `is_error=True` zurück.
|
||||
|
||||
!!! check
|
||||
Frag `lookup_book` nach `"Solaris"` (einem Titel, der nicht im Katalog steht), und die Funktion löst
|
||||
`ValueError` aus. Der Aufruf kehrt trotzdem normal zurück:
|
||||
|
||||
```python
|
||||
result.is_error # True
|
||||
result.content # [TextContent(type='text', text="Error executing tool lookup_book: No book titled 'Solaris' in the catalog.")]
|
||||
result.structured_content # None
|
||||
```
|
||||
|
||||
Die Meldung der Exception ist in `content` gelandet, wo das **Modell** sie lesen und es erneut versuchen kann. Das
|
||||
ist Absicht: Ein Tool-Fehler ist Teil des Gesprächs, kein Absturz. Sieh dir immer `is_error` an,
|
||||
bevor du `structured_content` vertraust.
|
||||
|
||||
!!! warning
|
||||
`is_error=True` deckt mehr ab als dein eigenes `raise`. Frag nach einem Tool, das der Server gar nicht hat
|
||||
(`call_tool("does_not_exist", {})`), und nichts wird ausgelöst. Du bekommst dieselbe Form zurück,
|
||||
`is_error=True` mit `Unknown tool: does_not_exist` in `content`. Eine `Client`-Methode löst
|
||||
`MCPError` nur aus, wenn der Server mit einem JSON-RPC-**Fehler** statt eines Ergebnisses antwortet, und
|
||||
**[Fehler behandeln](../servers/handling-errors.md)** erklärt, wann ein Server welches davon erzeugt.
|
||||
|
||||
## Ressourcen {#resources}
|
||||
|
||||
Die Ressourcen-Verben kommen paarweise: zwei Wege zum Auflisten, einer zum Lesen.
|
||||
|
||||
```python title="client.py" hl_lines="22-31"
|
||||
--8<-- "docs_src/client/tutorial004.py"
|
||||
```
|
||||
|
||||
* `list_resources()` gibt die **konkreten** Ressourcen zurück, die mit festem URI. Hier: `['catalog://genres']`.
|
||||
* `list_resource_templates()` gibt die **parametrisierten** zurück. Hier: `['catalog://genres/{genre}']`. Es sind zwei verschiedene Listen, weil ein Template erst lesbar ist, wenn du es ausfüllst.
|
||||
* `read_resource(uri)` nimmt einen URI als einfachen `str` und funktioniert mit beiden: Übergib `"catalog://genres/poetry"`, und der Server ordnet ihn dem Template zu.
|
||||
|
||||
`read_resource` gibt `contents` zurück, eine Liste aus `TextResourceContents` oder `BlobResourceContents`. Dieselbe Idee wie beim Tool-Content: mit `isinstance` eingrenzen, dann `.text` (oder `.blob`) lesen.
|
||||
|
||||
Ein Client kann sich auch mitteilen lassen, wann sich eine Ressource ändert. Auf Verbindungen der 2025er-Generation geschieht das über `subscribe_resource(uri)` / `unsubscribe_resource(uri)` – ein Methodenpaar, das `MCPServer` nicht implementiert, sodass der Request auf der 2026-07-28-Leitung (wo es diese Verben nicht mehr gibt) mit `-32601`, *Method not found*, beantwortet wird. Der Ersatz in 2026 ist ein `subscriptions/listen`-Stream, den `MCPServer` *sehr wohl* bedient – `server_capabilities.resources.subscribe` ist dort `True` –, und wie du ihn mit `client.listen(...)` konsumierst, steht auf der Seite **[Abonnements](subscriptions.md)** in diesem Abschnitt.
|
||||
|
||||
## Prompts {#prompts}
|
||||
|
||||
```python title="client.py" hl_lines="15-20"
|
||||
--8<-- "docs_src/client/tutorial005.py"
|
||||
```
|
||||
|
||||
`list_prompts()` sagt dir, was der Server anbietet und was jeder Prompt braucht:
|
||||
|
||||
```python
|
||||
prompt.name # 'recommend'
|
||||
prompt.title # 'Recommend a book'
|
||||
prompt.arguments # [PromptArgument(name='genre', required=True)]
|
||||
```
|
||||
|
||||
`get_prompt(name, arguments)` rendert ihn. Das Argument-Dict ist `str -> str`: Prompt-Argumente sind immer Strings. Das Ergebnis ist `messages`, eine Liste von `PromptMessage`, jeweils mit einer `role` und einem `content`-Block:
|
||||
|
||||
```python
|
||||
message.role # 'user'
|
||||
message.content # TextContent(type='text', text='Recommend one poetry book from the catalog and say why.')
|
||||
```
|
||||
|
||||
Ein Host reicht diese Nachrichten direkt an das Modell weiter. Das ist das ganze Feature.
|
||||
|
||||
## Vervollständigungen {#completions}
|
||||
|
||||
Ein Server mit einem Handler für Vervollständigungen kann Argumente von Prompts und Ressourcen-Templates automatisch vervollständigen, während die Person tippt.
|
||||
|
||||
```python title="client.py" hl_lines="27-31"
|
||||
--8<-- "docs_src/client/tutorial006.py"
|
||||
```
|
||||
|
||||
* `ref` sagt, *welchen* Prompt oder welches Template du ausfüllst: eine `PromptReference` oder eine `ResourceTemplateReference`.
|
||||
* `argument` ist `{"name": ..., "value": ...}`: das Argument und das, was die Person bisher getippt hat.
|
||||
|
||||
Die Antwort steht in `result.completion.values`. Tippe `"p"`, und der Server liefert `['poetry']`. Die Serverseite, und wie ein Handler die *anderen*, bereits ausgefüllten Argumente nutzt, um seine Vorschläge einzugrenzen, steht auf der Seite **[Vervollständigungen](../servers/completions.md)**.
|
||||
|
||||
## Paginierung {#pagination}
|
||||
|
||||
Jede `list_*`-Methode nimmt ein Keyword-Argument `cursor=`, und jedes Ergebnis trägt einen `next_cursor`. Wenn `next_cursor` `None` ist, hast du alles.
|
||||
|
||||
```python title="client.py" hl_lines="22-30"
|
||||
--8<-- "docs_src/client/tutorial007.py"
|
||||
```
|
||||
|
||||
Diese Schleife ist gegenüber jedem Server korrekt. `MCPServer` gibt alles auf einer Seite zurück, also ist `next_cursor` `None` und die Schleife läuft einmal – deshalb schreibt der meiste Code sie nie. Server, die wirklich paginieren, und die Regeln, denen Cursor gehorchen, stehen in **[Paginierung](../advanced/pagination.md)**.
|
||||
|
||||
## In Tests {#in-tests}
|
||||
|
||||
`Client(mcp)` ohne Prozess und ohne Port ist bereits ein Test-Harness für deinen Server.
|
||||
|
||||
Dafür gibt es ein eigenes Konstruktor-Flag: `Client(mcp, raise_exceptions=True)`. Es wirkt nur auf In-Memory-Verbindungen, und **[Testen](../get-started/testing.md)** ist die Seite, die es erklärt und das ganze Muster darum herum aufbaut.
|
||||
|
||||
## Zusammenfassung {#recap}
|
||||
|
||||
* `Client(x)` verbindet sich in-memory mit einem Server-Objekt, über Streamable HTTP mit einem URL-String und über alles andere per Transport.
|
||||
* `async with` ist der ganze Lebenszyklus. Darin sind `server_capabilities` und `protocol_version` bereits gefüllt; `server_info` und `instructions` ebenfalls, wenn der Server sie liefert.
|
||||
* `list_tools()` gibt dir für jedes Tool `name`, `title`, `description` und `input_schema`.
|
||||
* `call_tool()` gibt `content` für das Modell, `structured_content` für deinen Code und `is_error` zurück. Ein Tool, das eine Exception auslöst, ist ein Ergebnis, keine Exception.
|
||||
* `content` ist eine Union von Blocktypen; grenze mit `isinstance` ein, bevor du liest.
|
||||
* `list_resources` / `list_resource_templates` / `read_resource`, `list_prompts` / `get_prompt` und `complete` runden die Verben ab.
|
||||
* Jede `list_*`-Methode nimmt `cursor=`; iteriere, bis `next_cursor` `None` ist.
|
||||
|
||||
Was ein Server vom *Client* anfordern kann und wie du darauf antwortest, steht in **[Client-Callbacks](callbacks.md)**.
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue