From 5150037733ecbd4f85f127d7fb1c4135ed9483db Mon Sep 17 00:00:00 2001 From: Max Isbey <224885523+maxisbey@users.noreply.github.com> Date: Fri, 31 Jul 2026 21:11:06 +0000 Subject: [PATCH 1/2] Add machine-translated docs sites for zh-CN, ja, ko and pt-BR MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ship preview translations of the docs in Simplified Chinese, Japanese, Korean and Brazilian Portuguese, served at /zh-CN/, /ja/, /ko/ and /pt-BR/ next to the English site (three pages each to start). English stays the single source of truth; everything else is generated and never hand-edited. scripts/docs/translations.py drives it (status / translate / stage / languages). `translate` re-translates only the `##` sections whose English changed and carries the rest of the page over byte-for-byte from the previous translation, then gates the result: heading anchors and code blocks are re-imposed from the English, the structure (sections, headings, fences, links, inline code, admonitions, glossary rules) is checked with the findings fed back for another attempt, and a stronger model reviews the meaning against the English. Per-language inputs — instructions.md (register, voice, typography) and glossary.json (termbase, keep-in-English list, banned renderings) — are the human-edited part; the pages and state.json under i18n// are generated, so a correction goes into the inputs and holds across every future run. The build stages each language over the English tree (translations overlaid, notices stamped in — machine-translated, may be behind English, not translated — and links into the API reference pointed at the English one) and builds it strictly into site//. Heading ids are pinned into the translations from the ids the English site renders, so #fragment links resolve on every language site without touching the English pages. An outdated translation is served with a warning unless the English page's links or anchors moved under it, so an English edit can never break a language build. Every config carries the same language switcher. --- .gitattributes | 2 + .github/ISSUE_TEMPLATE/translation.yaml | 57 ++ .github/workflows/deploy-docs.yml | 2 + .github/workflows/docs-preview.yml | 13 +- .gitignore | 7 +- .pre-commit-config.yaml | 4 + CONTRIBUTING.md | 4 + docs/translations.md | 24 + i18n/README.md | 72 ++ i18n/general-prompt.md | 59 ++ i18n/ja/glossary.json | 295 ++++++ i18n/ja/instructions.md | 168 ++++ i18n/ja/pages/get-started/first-steps.md | 139 +++ i18n/ja/pages/handlers/elicitation.md | 186 ++++ i18n/ja/pages/index.md | 97 ++ i18n/ja/state.json | 164 ++++ i18n/ko/glossary.json | 225 +++++ i18n/ko/instructions.md | 140 +++ i18n/ko/pages/get-started/first-steps.md | 139 +++ i18n/ko/pages/handlers/elicitation.md | 185 ++++ i18n/ko/pages/index.md | 97 ++ i18n/ko/state.json | 164 ++++ i18n/languages.yml | 46 + i18n/pt-BR/glossary.json | 232 +++++ i18n/pt-BR/instructions.md | 190 ++++ i18n/pt-BR/pages/get-started/first-steps.md | 139 +++ i18n/pt-BR/pages/handlers/elicitation.md | 185 ++++ i18n/pt-BR/pages/index.md | 97 ++ i18n/pt-BR/state.json | 164 ++++ i18n/zh-CN/glossary.json | 232 +++++ i18n/zh-CN/instructions.md | 146 +++ i18n/zh-CN/pages/get-started/first-steps.md | 138 +++ i18n/zh-CN/pages/handlers/elicitation.md | 180 ++++ i18n/zh-CN/pages/index.md | 97 ++ i18n/zh-CN/state.json | 164 ++++ mkdocs.yml | 1 + pyproject.toml | 7 + scripts/docs/build.sh | 57 +- scripts/docs/build_config.py | 85 +- scripts/docs/translations.py | 995 ++++++++++++++++++++ scripts/serve-docs.sh | 8 +- tests/test_docs_i18n.py | 330 +++++++ uv.lock | 166 ++++ 43 files changed, 5870 insertions(+), 32 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/translation.yaml create mode 100644 docs/translations.md create mode 100644 i18n/README.md create mode 100644 i18n/general-prompt.md create mode 100644 i18n/ja/glossary.json create mode 100644 i18n/ja/instructions.md create mode 100644 i18n/ja/pages/get-started/first-steps.md create mode 100644 i18n/ja/pages/handlers/elicitation.md create mode 100644 i18n/ja/pages/index.md create mode 100644 i18n/ja/state.json create mode 100644 i18n/ko/glossary.json create mode 100644 i18n/ko/instructions.md create mode 100644 i18n/ko/pages/get-started/first-steps.md create mode 100644 i18n/ko/pages/handlers/elicitation.md create mode 100644 i18n/ko/pages/index.md create mode 100644 i18n/ko/state.json create mode 100644 i18n/languages.yml create mode 100644 i18n/pt-BR/glossary.json create mode 100644 i18n/pt-BR/instructions.md create mode 100644 i18n/pt-BR/pages/get-started/first-steps.md create mode 100644 i18n/pt-BR/pages/handlers/elicitation.md create mode 100644 i18n/pt-BR/pages/index.md create mode 100644 i18n/pt-BR/state.json create mode 100644 i18n/zh-CN/glossary.json create mode 100644 i18n/zh-CN/instructions.md create mode 100644 i18n/zh-CN/pages/get-started/first-steps.md create mode 100644 i18n/zh-CN/pages/handlers/elicitation.md create mode 100644 i18n/zh-CN/pages/index.md create mode 100644 i18n/zh-CN/state.json create mode 100644 scripts/docs/translations.py create mode 100644 tests/test_docs_i18n.py diff --git a/.gitattributes b/.gitattributes index 0ab3744850..8fc271c438 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,4 @@ # Generated uv.lock linguist-generated=true +i18n/*/pages/** linguist-generated=true +i18n/*/state.json linguist-generated=true diff --git a/.github/ISSUE_TEMPLATE/translation.yaml b/.github/ISSUE_TEMPLATE/translation.yaml new file mode 100644 index 0000000000..f1dbcf1036 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/translation.yaml @@ -0,0 +1,57 @@ +name: 🌐 Translation problem +description: Report a wrong, awkward, or misleading passage in a translated docs page +labels: ["translation"] + +body: + - type: markdown + attributes: + value: | + The translated docs are machine-generated from the English pages; https://py.sdk.modelcontextprotocol.io/translations/ explains how. + Fixes never go into the translated text directly. They go into that language's glossary or style guide under `i18n/`, so you can also open a PR there instead of an issue. + + - type: dropdown + id: language + attributes: + label: Language + options: + - Simplified Chinese (zh-CN) + - Japanese (ja) + - Korean (ko) + - Brazilian Portuguese (pt-BR) + validations: + required: true + + - type: input + id: page + attributes: + label: Page URL + description: The translated page where you found the problem. + placeholder: https://py.sdk.modelcontextprotocol.io/ja/servers/tools/ + validations: + required: true + + - type: textarea + id: passage + attributes: + label: The passage + description: Quote the translated text that's wrong, and the English it corresponds to if you have it. + validations: + required: true + + - type: textarea + id: problem + attributes: + label: What's wrong, or how it should read + description: A wrong term, awkward phrasing, meaning that drifted from the English, tone that's off. If you know the better rendering, give it. + validations: + required: true + + - type: dropdown + id: native-speaker + attributes: + label: Are you a native or fluent speaker of this language? + options: + - "Yes" + - "No" + validations: + required: true diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 334ba818c8..c1d9f288bc 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -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/** + # the translated pages and the language registry feed the language sites + - i18n/** - mkdocs.yml - src/mcp/** - src/mcp-types/** diff --git a/.github/workflows/docs-preview.yml b/.github/workflows/docs-preview.yml index 6f9ec2cc34..40d4c1dc5c 100644 --- a/.github/workflows/docs-preview.yml +++ b/.github/workflows/docs-preview.yml @@ -21,6 +21,7 @@ on: paths: - docs/** - docs_src/** + - i18n/** - mkdocs.yml - scripts/docs/** - pyproject.toml @@ -137,7 +138,17 @@ jobs: # /preview-docs, still build with MkDocs. Both arms must write the site # to site/. Keep the detection in sync with build_site() in # scripts/build-docs.sh. - - run: | + # + # DOCS_SITE_URL is the preview's Cloudflare branch-alias host (deploy + # publishes to `--branch=pr-`, served at pr-..pages.dev), + # so the absolute links the build bakes (language switcher, language + # sites' links into the English API reference) point at the preview + # instead of production; empty when no Pages project is configured, which + # makes build.sh fall back to the production site_url. + - env: + DOCS_SITE_URL: >- + ${{ vars.CLOUDFLARE_PAGES_PROJECT && format('https://pr-{0}.{1}.pages.dev', needs.authorize.outputs.pr_number, vars.CLOUDFLARE_PAGES_PROJECT) || '' }} + run: | if [ -f scripts/docs/build.sh ]; then bash scripts/docs/build.sh else diff --git a/.gitignore b/.gitignore index 2e788e71d8..ce390c689e 100644 --- a/.gitignore +++ b/.gitignore @@ -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 +# per-language docs trees). /docs/api/ +/.build/ /mkdocs.gen.yml +/mkdocs.*.gen.yml # mypy .mypy_cache/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 321b60bc52..f9f32ce7a4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,6 +25,10 @@ repos: "/tool/markdown/lint", ] types: [markdown] + # Machine-translated pages are generated artefacts: corrections flow + # through i18n//{instructions.md,glossary.json}, never through + # hand or linter edits to the pages themselves. + exclude: ^i18n/[^/]+/pages/ - repo: local hooks: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b0fb9fa57b..52717b0df6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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//`. Never edit the generated pages under `i18n//pages/`—the next translation run overwrites them. 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 the fix 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. diff --git a/docs/translations.md b/docs/translations.md new file mode 100644 index 0000000000..df529e043d --- /dev/null +++ b/docs/translations.md @@ -0,0 +1,24 @@ +# Translations + +This documentation is written in English. To make it useful to more people, we also publish it in a few other languages. Those editions are machine-translated, and this page explains what that means for you and how to help improve them. + +## What's available + +Translated documentation is currently a **preview**: it covers a growing subset of the pages, published in four languages — Simplified Chinese, Japanese, Korean and Brazilian Portuguese. Pick one from the language switcher at the top of any page. + +Every translated page opens with a note saying it was machine-translated and linking to its English original. The API reference is not translated: every language 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. Two situations are called out on the page itself: + +- A page that hasn't been translated yet shows the English text, with a note saying so. +- A page whose English original changed after it was translated carries a warning that it may be behind, until the translation catches up. + +## How the translations are made + +Translated pages are 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 a translation issue](https://github.com/modelcontextprotocol/python-sdk/issues/new?template=translation.yaml) with the language, the page and the passage; reports from native speakers are especially valuable, and maintainers track these with the `translation` label. If you know the fix, propose it directly as a pull request against that language's style guide or glossary under [`i18n/`](https://github.com/modelcontextprotocol/python-sdk/tree/main/i18n) — the correction then shows up on 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. diff --git a/i18n/README.md b/i18n/README.md new file mode 100644 index 0000000000..57df0b9abe --- /dev/null +++ b/i18n/README.md @@ -0,0 +1,72 @@ +# Documentation translations + +The English pages under `docs/` are the source of truth. This directory holds +what steers their machine translation (human-authored) and the generated +result; the public-facing explanation is [`docs/translations.md`](../docs/translations.md). + +- `languages.yml` — the language registry (one entry per translated site, + served at `//`), the pages that stay English, the model IDs, and the + English text of the notices staged onto every translated page. +- `general-prompt.md` — the translation rules shared by every language. +- `/instructions.md` — register, voice, humour and typography for one + language. Human-authored. +- `/glossary.json` — the termbase for one language: terms that stay in + English, required renderings, and banned ones (`"enforce": true` makes a ban + a hard check). Human-authored. +- `/pages/` and `/state.json` — generated: the translated pages and + the record of what each was made from (English content and section hashes, prompt + inputs, model, timestamp), plus the translated sidebar labels and notices. Never + edited by hand. + +## The tool + +`scripts/docs/translations.py` (run from the repository root): + +```bash +uv run --frozen --group docs python scripts/docs/translations.py status # missing/outdated/current per language +uv run --frozen --group docs python scripts/docs/translations.py translate --lang ja # translate what is missing or outdated +uv run --frozen --group docs python scripts/docs/translations.py stage --lang ja # the docs tree the ja site builds from +``` + +`translate` calls the Claude API (set `ANTHROPIC_API_KEY` or +`ANTHROPIC_AUTH_TOKEN`). It re-translates only the `##` sections whose English +changed and carries the rest of the page over byte-for-byte from the previous +translation, then gates the result: heading anchors and code blocks are +re-imposed from the English, the page's structure (headings, code fences, +links, admonitions, glossary rules) is checked with the findings fed back for +another attempt, and a stronger model reviews the meaning against the English. +`--pages a.md b.md` narrows a run (a page that is already current comes back +unchanged), `--fresh` re-translates from scratch instead of updating (every +page when no `--pages` are given — the way to redo a language after a model +change), `--dry-run` shows what it would do, and `--no-verify` skips the +meaning review. A page that failed its gates keeps its previous translation and +the run exits non-zero; translations whose English page has left the nav are +deleted. Everything else is offline; `scripts/docs/build.sh` runs `stage` for +each language and builds it at `site//`. + +## Correcting a translation + +Never edit a file under `/pages/` — the next run overwrites it. Fix the +input instead: a wrong term goes into `glossary.json`, a recurring style or +register problem into `instructions.md`, and ambiguous English into the page +under `docs/`. Editing a glossary or instructions marks that language's pages +outdated, so the next `translate` run regenerates them with the fix in place. +Readers report problems through the "Translation problem" issue form. + +## Staleness + +A page is *current* when the English content and the prompt inputs it was +translated from are unchanged, *outdated* otherwise, and *missing* when it has +no translation. The build serves an outdated translation with a warning +notice, unless the English page's links or heading anchors moved under it — +then the English page is served until the next `translate` run refreshes it. +Untranslated and excluded pages are served in English with a notice. + +## Adding a language + +Add an entry to `languages.yml` (`code`, native `name`; `theme_language` and +`hreflang` default to the code and must be values the theme knows), write +`/instructions.md` and `/glossary.json` (start from an existing +language), then run `translate --lang ` and commit the generated `pages/` +and `state.json`. Also name the language in `docs/translations.md` and the +"Translation problem" issue form's dropdown. diff --git a/i18n/general-prompt.md b/i18n/general-prompt.md new file mode 100644 index 0000000000..ddbbd71a9d --- /dev/null +++ b/i18n/general-prompt.md @@ -0,0 +1,59 @@ +# Translation rules + +You are translating a page of the MCP Python SDK documentation from English into the target language named in the language instructions that follow. The readers are software developers using the SDK. + +## Your role + +- Write natural, native-quality prose in the target language. The page should read as if a developer who is a native speaker wrote it, not as a translation. +- Keep the meaning exact. Do not add claims, drop caveats, reorder steps, or change the strength of a requirement (must / should / may). +- Follow the language instructions and the glossary strictly. Where the two disagree, the glossary wins. +- Translate the whole page. Never summarise, abridge, or leave a placeholder such as "translation continues below". + +## Never translate + +Copy the following byte-for-byte from the English source: + +- Fenced code blocks: the fence markers, the info string, and every line inside them, including code comments. +- Inline code spans (text between backticks). +- URLs and link destinations, including `#fragment` anchors, and image paths. +- HTML tags and their attribute values. +- Front matter keys (the `key:` part of each front matter line). +- Snippet-include lines containing `--8<--`. +- Code-annotation markers such as `# (1)!`. +- Heading anchor attributes: the `{#some-id}` at the end of a heading (it may also be written with spaces, `{ #some-id }`; copy it exactly as it appears). +- The syntax markers for admonitions, collapsible blocks and content tabs (`!!!`, `???`, `???+`, `///`, `===`) and the block-type keyword that follows them (`note`, `tip`, `warning`, ...). +- Footnote labels (`[^1]`), abbreviation definitions (`*[HTML]: ...`) and emoji shortcodes (`:smile:`). +- Mermaid diagram source inside `mermaid` fences. + +Do translate the human-language text around those elements: prose, headings, link text, image alt text, table cells, list items, admonition titles (the quoted text after `!!! type`) and bodies, and content-tab labels (the quoted text after `===`). + +## Preserve the structure exactly + +The translation must have the same shape as the English source, block for block: + +- The same headings, at the same levels, in the same order, each ending in the same `{#anchor}` attribute as the source. +- The same number and type of admonitions, collapsible blocks and content-tab groups, in the same order. +- The same tables, with the same number of rows and columns. +- The same lists (same nesting, same number of items) and the same code fences (same count, same info strings, identical contents). +- The same links and images, in the same order. Never add, remove or merge a link. +- The same footnotes, and the same blank lines separating blocks. + +Do not add explanatory notes, translator's remarks or extra examples. + +## Links and anchors + +- Keep every link destination exactly as written in the source, whether it is an absolute URL, a relative path such as `../servers/tools.md`, or a bare `#anchor`. Only the link text is translated. +- Do not add anchors the source does not have, and never rewrite a fragment: heading anchors are pinned in the English source, so the same `#id` is valid on every language site. +- Preserve the link syntax the source uses (Markdown `[text](target)` or HTML ``). + +## Updating an existing translation + +When the request includes a previous translation of the page and marks which sections of the English page changed: + +- Outside the changed sections, reproduce the previous translation verbatim. Do not rephrase, "improve" or re-punctuate text whose English has not changed. +- Inside the changed sections, translate the new English following all of the rules above, and keep terminology, register and tone consistent with the surrounding unchanged text. +- A section is the page's front matter, the text before the first second-level heading, or one second-level heading (`##`) together with everything under it up to the next. + +## Output + +Return only the translated Markdown document, from its first line to its last. Do not add a preamble, a summary or any commentary, and do not wrap the document in a code fence. diff --git a/i18n/ja/glossary.json b/i18n/ja/glossary.json new file mode 100644 index 0000000000..ba38f88367 --- /dev/null +++ b/i18n/ja/glossary.json @@ -0,0 +1,295 @@ +{ + "version": 1, + "keep_in_source_language": [ + "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": "ツール", + "note": "MCP protocol noun (a server exposes tools). Standard rendering. Wire identifiers such as `tools/call` and `tools/list` are code and stay Latin.", + "avoid": [], + "enforce": false + }, + { + "source": "resource", + "target": "リソース", + "note": "MCP protocol noun, and also the general noun (a pool acquired in a lifespan is still リソース). Standard rendering. Never 資源, which is the natural-resources sense; `resources/read` stays Latin.", + "avoid": ["資源"], + "enforce": true + }, + { + "source": "prompt", + "target": "プロンプト", + "note": "The MCP feature (a reusable prompt a server exposes) and the everyday word; プロンプト in both senses. Standard rendering. `prompts/get` stays Latin.", + "avoid": [], + "enforce": false + }, + { + "source": "sampling", + "target": "サンプリング", + "note": "The (deprecated) client feature that lets a server borrow the client's model. Standard rendering. Never 標本抽出, which is statistical sampling and the wrong sense; the `sampling` capability key and `sampling/createMessage` stay Latin.", + "avoid": ["標本抽出"], + "enforce": true + }, + { + "source": "roots", + "target": "ルート", + "note": "The (deprecated) client feature listing workspace folders. Provisional pending native review: ルート also spells \"route\" and \"root path\", so gloss the English on first use per page — ルート(roots). Never ルーツ (ancestry/origins). A `Root` object in code font stays Latin.", + "avoid": ["ルーツ"], + "enforce": true + }, + { + "source": "elicitation", + "target": "エリシテーション", + "note": "OPEN QUESTION for native review: there is no established Japanese term for the server asking the user a question mid-request. Provisionally pinned to the transliteration エリシテーション, glossed with the English on its first appearance per page — エリシテーション(elicitation). Do not substitute 誘導 or 引き出し unless review settles on one. `elicitation/create` and the `Elicit` class stay Latin.", + "avoid": [], + "enforce": false + }, + { + "source": "capability", + "target": "ケイパビリティ", + "note": "A negotiated protocol capability (what a client or server declared it supports). Provisional pending native review: ケイパビリティ rather than the general-purpose 機能 (feature) or 能力 (ability). The `capabilities` field and keys such as `sampling.tools` stay Latin.", + "avoid": [], + "enforce": false + }, + { + "source": "transport", + "target": "トランスポート", + "note": "The connection mechanism (\"every standard transport\" → 標準のトランスポート). Standard rendering; never 輸送 (freight transport) or 輸送手段. The transport names stdio, Streamable HTTP and SSE stay in English.", + "avoid": ["輸送"], + "enforce": true + }, + { + "source": "session", + "target": "セッション", + "note": "An MCP session (the negotiated connection state). Standard rendering; never the coinage 会期. `session` objects in code font stay Latin.", + "avoid": ["会期"], + "enforce": false + }, + { + "source": "handler", + "target": "ハンドラー", + "note": "The tool, resource or prompt function you register (nav section \"Inside your handler\" → ハンドラーの中で). Standard word; the long-vowel spelling ハンドラー (not ハンドラ) follows the katakana rule in instructions.md and is provisional pending native review.", + "avoid": [], + "enforce": false + }, + { + "source": "dependency", + "target": "依存関係", + "note": "The SDK's parameter-injection feature (the \"Dependencies\" page → 依存関係). Provisional pending native review: the pattern name \"dependency injection\" is customarily 依存性の注入, so a page may use that phrase for the pattern while individual dependencies are 依存関係. The `Resolve` marker class stays Latin.", + "avoid": [], + "enforce": false + }, + { + "source": "client", + "target": "クライアント", + "note": "An MCP client, and the client side of a connection. Standard rendering; never 顧客 (a customer). The `Client` class name stays Latin in code font.", + "avoid": ["顧客"], + "enforce": true + }, + { + "source": "server", + "target": "サーバー", + "note": "An MCP server (the program you build). Standard rendering with the long-vowel mark — サーバー, never サーバ (see instructions.md). The low-level `Server` class stays Latin in code font.", + "avoid": [], + "enforce": false + }, + { + "source": "host", + "target": "ホスト", + "note": "The MCP host: the application that embeds the client and drives the model, and also a network host. Standard rendering in both senses; never 宿主 (a biological host).", + "avoid": ["宿主"], + "enforce": true + }, + { + "source": "context", + "target": "コンテキスト", + "note": "The generic lower-case word (\"provide context to LLMs\" → LLM にコンテキストを提供する). Provisional pending native review: pin one spelling per corpus — コンテキスト, not コンテクスト. The capitalised `Context` is the SDK object injected as `ctx`; it is on the keep-in-source list and stays Latin in prose (\"The Context\" → Context).", + "avoid": ["コンテクスト"], + "enforce": false + }, + { + "source": "resolver", + "target": "リゾルバー", + "note": "The function attached to a parameter with `Resolve(...)` that computes or asks for its value. Provisional pending native review: the loanword リゾルバー, not 解決器. The `Resolve` class stays Latin.", + "avoid": ["解決器"], + "enforce": false + }, + { + "source": "lifespan", + "target": "ライフスパン", + "note": "The server's startup/shutdown scope (the \"Lifespan\" page, as in the ASGI lifespan). Provisional pending native review: the loanword ライフスパン, not 寿命 (the biological sense). Not enforced, because the neighbouring English word \"lifetime\" (\"for the lifetime of the host app\") can legitimately render as 寿命 in the same block. The `lifespan` parameter name stays Latin in code font.", + "avoid": ["寿命"], + "enforce": false + }, + { + "source": "deprecated", + "target": "非推奨", + "note": "Advisory status: still works, scheduled for removal later — 非推奨, not 廃止 (which reads as already removed); \"removed\" is 削除. \"Deprecation warning\" → 非推奨の警告; the `MCPDeprecationWarning` class stays Latin. Provisional pending native review.", + "avoid": ["廃止"], + "enforce": false + }, + { + "source": "back-channel", + "target": "バックチャネル", + "note": "The server-to-client request channel that exists only on legacy connections. Provisional coinage pending native review: gloss the English on first use per page — バックチャネル(back-channel). Not the older spelling バックチャンネル.", + "avoid": ["バックチャンネル"], + "enforce": false + }, + { + "source": "wire", + "target": "通信路", + "note": "The corpus's light metaphor for the byte stream between client and server (\"stdout is the wire\" → stdout が通信路そのものです; \"invisible on the wire\" → 通信上には現れません; \"the JSON on the wire\" → 実際に送受信される JSON). Provisional pending native review. Never a literal 電線 or ワイヤー.", + "avoid": ["電線", "ワイヤー"], + "enforce": false + }, + { + "source": "era", + "target": "世代", + "note": "\"Protocol era\" (\"a 2025-era client\", \"whatever era the client speaks\") → プロトコルの世代, 2025 年世代のクライアント. Provisional pending native review; not the literal 時代.", + "avoid": ["時代"], + "enforce": false + }, + { + "source": "legacy", + "target": "レガシー", + "note": "\"A legacy connection/client\" = one negotiated at spec version 2025-11-25 or earlier → レガシー接続, レガシークライアント (prenominal loanword). Provisional pending native review.", + "avoid": [], + "enforce": false + }, + { + "source": "multi-round-trip", + "target": "マルチラウンドトリップ", + "note": "The 2026-07-28 request pattern (\"Multi-round-trip requests\" → マルチラウンドトリップリクエスト); a single \"round trip\" → ラウンドトリップ or 往復 by context. Provisional coinage pending native review: gloss the English on first use per page — マルチラウンドトリップ(multi-round-trip). The abbreviation MRTR stays Latin.", + "avoid": [], + "enforce": false + }, + { + "source": "handshake", + "target": "ハンドシェイク", + "note": "The initialization handshake (\"the classic handshake\" → 従来のハンドシェイク). The established loanword; never the literal 握手. Provisional pending native review.", + "avoid": ["握手"], + "enforce": true + }, + { + "source": "request", + "target": "リクエスト", + "note": "A JSON-RPC or HTTP request (\"the initialize request\" → 初期化リクエスト); the verb is リクエストする or 要求する by context. `Request` types in code font stay Latin. Provisional pending native review.", + "avoid": [], + "enforce": false + }, + { + "source": "response", + "target": "レスポンス", + "note": "A JSON-RPC or HTTP response; `Response` types in code font stay Latin. Provisional pending native review.", + "avoid": [], + "enforce": false + }, + { + "source": "callback", + "target": "コールバック", + "note": "Client callbacks and OAuth redirect callbacks alike; parameter names such as `sampling_callback` stay Latin. Provisional pending native review.", + "avoid": [], + "enforce": false + }, + { + "source": "decorator", + "target": "デコレーター", + "note": "The Python decorators the SDK is built on; `@mcp.tool()` and its siblings are code and stay untouched. Long-vowel spelling per instructions.md. Provisional pending native review.", + "avoid": [], + "enforce": false + }, + { + "source": "type hint", + "target": "型ヒント", + "note": "Python type hints (\"from your type hints\" → 型ヒントから). Provisional pending native review.", + "avoid": [], + "enforce": false + }, + { + "source": "argument", + "target": "引数", + "note": "A call argument; the declared parameter is パラメーター (see the katakana rule in instructions.md). Provisional pending native review.", + "avoid": [], + "enforce": false + }, + { + "source": "return value", + "target": "戻り値", + "note": "A function's return value; the `return` keyword and return annotations are code. Provisional pending native review.", + "avoid": [], + "enforce": false + }, + { + "source": "exception", + "target": "例外", + "note": "A raised Python exception (\"raises an exception\" → 例外を送出する); exception class names stay Latin. Provisional pending native review.", + "avoid": [], + "enforce": false + }, + { + "source": "async", + "target": "非同期", + "note": "The prose adjective (\"the async runtime\" → 非同期ランタイム, \"an async callback\" → 非同期コールバック); the `async` and `await` keywords in code font stay Latin. Provisional pending native review.", + "avoid": [], + "enforce": false + } + ] +} diff --git a/i18n/ja/instructions.md b/i18n/ja/instructions.md new file mode 100644 index 0000000000..eaa8d02353 --- /dev/null +++ b/i18n/ja/instructions.md @@ -0,0 +1,168 @@ +# Japanese (ja) — translation instructions + +Target language: Japanese (日本語), directory and URL code `ja`, page language +tag `ja`. 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 + +Write body prose in the polite です・ます form (敬体), consistently, on every +page — tutorials, reference tables, admonitions and troubleshooting entries +alike. + +- Never mix in だ・である (常体) sentence endings within body text, and do + not escalate into honorifics (尊敬語・謙譲語): 使うときは, not + お使いいただく際には. +- Headings, table headers, content-tab labels and other UI-like fragments + are noun phrases (体言止め) or the plain dictionary form of a verb, never + です・ます: "Run it" → 実行する or 実行方法, "The Context" → Context, + "Handling errors" → エラーの処理. A heading phrased as a question in + English may stay a question in the plain form: "Where does this go?" → + これはどこに置くべきか. +- Instructions to the reader: 〜してください for a step to perform, + 〜します / 〜できます for describing what code does, 〜しないでください + for prohibitions. Prefer 〜です over 〜になります / 〜となります when both + are grammatical. +- The reader is never named. Do not translate "you" / "your" as あなた, + あなたの, 君, ユーザー様: drop the subject, which Japanese does + naturally, or restructure the sentence. "You can pass a schema" → + スキーマを渡せます. Where a subject is unavoidable, name the role — + サーバー, クライアント, ツール, 呼び出し側 — never a pronoun. "Your server" + is サーバー, or 自分のサーバー / 作成中のサーバー only when the ownership + is the point. +- One page, one register: a page that drifts between です・ます and である, + or that reintroduces あなた, is wrong even when each sentence is + acceptable on its own. + +## 2. Voice + +The English source is warm, direct and confident: short sentences, second +person, and the occasional one-line payoff ("That's the whole API."). Carry +that voice into natural Japanese; do not flatten it into formality, and do not +mirror the English word for word. + +- Guide, don't lecture. The reader should feel accompanied by a knowledgeable + colleague, not addressed by a notice. Directness comes from concrete verbs + and plain word order; warmth comes from the polite register itself, + considerate connectives (まず, ここでは, なお) and the occasional + 〜してみましょう / 〜してみてください for an encouraging aside. +- Keep the short payoff sentences short: "That's the whole API." → + API はこれだけです。 — not a formal summary sentence. +- Split long English sentences; follow Japanese rhythm rather than the + source's clause structure, but never merge, drop or reorder the technical + claims themselves. +- Anti-patterns — the stiff, legalistic translationese that Japanese + technical translations drift into by default: no 〜なのである / + 〜のである; no nominalisation chains (〜の実施を行うことにより → + 〜すると); no boilerplate such as 〜するものとします or 〜が求められます + where 〜してください is meant; no stacked ただし / なお clauses; no + needlessly formal kanji where kana reads more easily (できる not 出来る). + The opposite over-correction is also wrong: no よ endings, no + buddy-casual tone, and ね at most sparingly in tutorial prose, never in + reference pages. + +Example — English: "You don't construct it and you don't configure it. You +ask for it." + +- Not this (translationese): 利用者がその構築および構成を実施する必要はなく、 + 要求のみを行うものとする。 +- Not this either (pronoun + casual): あなたはそれを構築しないし、設定もしない。 + 要求するだけだよ。 +- This: 自分で組み立てる必要も、設定する必要もありません。要求するだけです。 + +## 3. Humour and idioms + +- Translate the intent of a joke, aside or idiom, never its words. Recast + it as a friendly plain sentence carrying the same information; if a + lighthearted phrase carries no information at all, keep the sentence brief + and natural rather than inventing a Japanese joke. Never drop the technical + content around it. +- Recurring English tags get fixed renderings: "X has the whole story" / + "The whole story is in X" → 詳しくは X を参照してください; + "That's it. It's just Python." → これだけです。ただの Python です。 +- Idioms take the plain meaning, not the picture: "Out of the box the app + answers **only** requests addressed to localhost." → デフォルトでは、この + アプリは localhost 宛てのリクエストに**だけ**応答します。 — not the literal + 箱から出してすぐ. +- Exclamation marks: drop them by default. Keep a single full-width ! + only where the English is a genuine exclamation of encouragement, never + after a warning or instruction, never doubled, never in a heading. +- Emoji: reproduce an emoji only where the English page has one, in the same + place (the source occasionally closes a step with ✨); never add emoji and + never put one in a heading. + +## 4. Typography + +- Punctuation is full-width 「、」 and 「。」; never 「,」「.」, and never a + half-width `,` or `.` closing Japanese prose. A colon that introduces a + code block, list or example becomes 「:」, or better a complete sentence + ending in 「。」 (次のように書きます。). +- Full-width forms inside Japanese text: 「」 for quoted terms and English + scare quotes, 『』 for nested quotes and titles, ? and ! when kept, and + () always — Japanese parentheses are full-width even when they enclose + only Latin text or code, as in the first-use gloss ルート(roots). +- Widths: kana and kanji full-width, no half-width katakana; Latin letters, + digits and code half-width. Counting uses half-width Arabic numerals + (3 つの答え, not 三つ), except in set phrases such as 一度 or 一部. +- Spacing: insert one half-width space between Japanese text and any + half-width run — an English word, a number, an inline code span, a link + whose text is Latin: Python の型ヒント, `Context` を受け取ります, + MCP サーバー. No space next to 「、」「。」 or full-width brackets + (`ctx.session` を使うと、), and none inside katakana compounds + (エラーメッセージ, ツール呼び出し). This spacing convention is provisional; + apply it uniformly. +- No italics: Japanese type has no true italic. When the English + italicises a word that gets translated, drop the emphasis or use 「」; + keep `**bold**` where the source has it, and keep the bold on negations + (**not** → **ではありません** / **しません**). Emphasis markers around + text that stays in English are copied as-is. +- Dashes and ranges: an English em-dash aside is recast with 、, () or a + second sentence, not with a ――; ranges use から (3.10 から 3.14), not 〜 + or –. +- Sentence length: one idea per sentence and at most three 「、」. In one + bulleted list, items either all end in 「。」 (complete sentences) or none + do (fragments). + +## 5. Terminology pointer + +The glossary is sent separately and takes precedence over anything here. +It holds every term-by-term rendering — the six core MCP nouns and the +everyday computing vocabulary alike — and marks each one as standard, +provisional or an open question; use its renderings and its first-use +glosses exactly as noted. The rules below are the conventions those +renderings assume. + +- Identifiers stay in Latin script exactly as written: class, function, + method, parameter, environment-variable, error and package names, + protocol method names such as `tools/call`, and everything in code font. + Product and standard names, and every term under + `keep_in_source_language`, stay in English too (MCP, Streamable HTTP, + JSON-RPC, OAuth, the SDK's class names, spec revision dates such as + 2026-07-28), always in the singular: an English plural "s" is dropped, + "the APIs" → API. Do not append a katakana reading after them. +- A term the glossary marks for a first-use gloss carries the English in + full-width parentheses on its first appearance in a page — ルート(roots), + エリシテーション(elicitation) — and appears alone after that. A glossary + word used as a wire identifier or a key in code font is code and stays + Latin. +- Katakana loanwords take the long-vowel mark for -er, -or and -ar endings: + サーバー (never サーバ), ハンドラー, リゾルバー, ユーザー, パラメーター, + ヘッダー, フォルダー, プロバイダー. Words ending in -y keep their customary + short form: プロパティ, ディレクトリ, ライブラリ, セキュリティ, メモリ. Words + ending in -ware take ウェア: ミドルウェア, ソフトウェア. +- Katakana compounds are written solid, without a space or a 中黒: + エラーメッセージ, プロトコルバージョン (use ・ only between two proper + names). +- Prefer the established loanword over an invented native coinage; the + glossary lists the settled pairs (セッション not 会期, トランスポート not + 輸送手段, ハンドシェイク not 握手). + +## 6. Provisional note + +These conventions are provisional and awaiting review by native +Japanese-speaking contributors. To propose a change — a better rendering, a +rule that produces awkward Japanese, a term that needs pinning — edit this +file, or `glossary.json` next to it, in a pull request. The generated pages +are never edited by hand; they are regenerated from these inputs. diff --git a/i18n/ja/pages/get-started/first-steps.md b/i18n/ja/pages/get-started/first-steps.md new file mode 100644 index 0000000000..c8bf8930c3 --- /dev/null +++ b/i18n/ja/pages/get-started/first-steps.md @@ -0,0 +1,139 @@ +# 最初の一歩 {#first-steps} + +**[トップページ](../index.md)**では駆け足で進みました。サーバーを書き、実行し、ツールを呼び出す、という流れです。 + +このページでは、サーバーが公開できる 3 種類すべてを、ひとつずつ名前を確認しながらゆっくり見ていきます。 + +## ホスト、クライアント、サーバー {#host-client-and-server} + +ここから先のすべてのページに登場する 3 つの言葉です。 + +* **ホスト**は LLM アプリケーションです。Claude、IDE、エージェントランタイムなど、ユーザーが対話している相手がホストです。 +* **クライアント**はホストの内部にあり、MCP を話します。ホストは接続先のサーバーごとにクライアントを 1 つ実行します。 +* **サーバー**は、この SDK で作るものです。クライアントに対して機能を公開します。モデルと直接やり取りすることはありません。 + +書くのはサーバーです。ホストは他社のプロダクトです。SDK は `Client` も提供します。これは自分のサーバーをテストするために使うもので、このページの後半にも登場します。 + +## 3 つのプリミティブ {#the-three-primitives} + +サーバーが公開できるものは、ちょうど 3 種類です。違いは**誰が使うことを決めるか**にあります。 + +| プリミティブ | 制御する主体 | 内容 | 例 | +|---------------|-----------------|-----------------------------------------------------|------------------------------------| +| **ツール** | モデル | モデルが操作を実行するために呼び出す関数 | API 呼び出し、データベースへの書き込み | +| **リソース** | アプリケーション | ホストがモデルのコンテキストに読み込むデータ | ファイルの内容、API のレスポンス | +| **プロンプト** | ユーザー | ユーザーが名前で呼び出す再利用可能なメッセージテンプレート | スラッシュコマンド、メニュー項目 | + +この「制御する主体」こそが、3 つを分ける理由そのものです。ツールが動くのは、**モデル**が呼び出すと決めたからです。リソースが添えられるのは、**アプリケーション**がモデルに必要だと判断したからです。プロンプトが実行されるのは、**ユーザー**が選んだからです。 + +!!! info + Web API を作った経験があれば、直感の大部分はすでに持っています。**リソース**は `GET` + (データを読み込むだけで何も変更しない)で、**ツール**は `POST`(処理を行い、副作用を + 持つこともある)です。**プロンプト**に対応する HTTP の概念はありません。ユーザーが名前で + 実行する、保存済みのクエリに近いものです。 + +## 1 つのサーバーに 3 つすべて {#one-server-all-three} + +```python title="server.py" hl_lines="6 12 18" +--8<-- "docs_src/first_steps/tutorial001.py" +``` + +素朴な関数が 3 つ、デコレーターが 3 つ。登録作業はこのデコレーターだけで完結します。 + +* `@mcp.tool()` は `add` を**ツール**にします。 +* `@mcp.resource("greeting://{name}")` は `greeting` を**リソーステンプレート**にします。URI の `{name}` が関数のパラメーターに対応します。 +* `@mcp.prompt()` は `summarize` を**プロンプト**にします。返した文字列がユーザーメッセージになります。 + +それ以外(名前、説明、引数のスキーマ)は、SDK が関数そのものから読み取ります。関数名、docstring、型ヒントです。別途宣言したものは何もありません。 + +!!! tip + SDK の 2 つの半分には、それぞれ別のインポートパスがあります。`from mcp import Client` と + `from mcp.server import MCPServer` です。`from mcp import MCPServer` は存在しません。 + +### 試してみる {#try-it} + +MCP Inspector で実行します。 + +```console +uv run mcp dev server.py +``` + +表示された URL を開いてください。Inspector にはプリミティブごとにタブがあります。順番に見ていきましょう。 + +**Tools。** 項目は 1 つ、`add` で、説明は *Add two numbers.* です。フォームには `a` 用の必須の整数フィールドと、`b` 用のフィールドがあります。値を入れて呼び出すと、結果は `3` です。Inspector はこのフォームを `a: int, b: int` から組み立てました。他のどのクライアントでも同じです。 + +**Resources。** *Resources* のリストは空です。`greeting` は **Resource Templates** の下にあります。`greeting://{name}` にはパラメーターがあるため、誰かが `name` を与えるまで、一覧に載せられる具体的なリソースが存在しないからです。`World` を渡して読み取ってみてください。 + +```text +Hello, World! +``` + +**Prompts。** 項目は 1 つ、`summarize` で、必須の `text` 引数が 1 つあります。適当なテキストを渡して取得すると、`role: user` のメッセージが 1 つ返り、内容はレンダリングされた文字列です。プロンプトとはこれだけのものです。メッセージを組み立てる関数にすぎません。 + +Inspector はサーバーを **stdio** 上で実行しました。MCP サーバーが話せるトランスポートの 1 つです。どれを使うかはまだ決める必要はありません。それは**[サーバーの実行](../run/index.md)**のページで扱います。 + +## ケイパビリティ {#capabilities} + +Inspector には 3 つのタブがありました。3 つあると、どうして分かったのでしょうか。 + +クライアントが接続すると、サーバーは自分の**ケイパビリティ**、つまりどの種類のリクエストに応答するかを宣言します。クライアントはその宣言をもとに、そもそも何を尋ねるかを決めます。この宣言を書いた覚えはないはずです。`MCPServer` が代わりに宣言しています。 + +実際に見てみましょう。SDK の `Client` はサーバーオブジェクトを直接受け取り、**インメモリ**で接続します(サブプロセスもポートも使いません)。 + +```python +import asyncio + +from mcp import Client + +from server import mcp + + +async def main() -> None: + async with Client(mcp) as client: + print(client.server_capabilities.model_dump(exclude_none=True)) + + +asyncio.run(main()) +``` + +```text +{'prompts': {'list_changed': True}, 'resources': {'subscribe': True, 'list_changed': True}, 'tools': {'list_changed': True}} +``` + +この辞書が、サーバーが宣言した**ケイパビリティ**です。接続してきたクライアントが最初に知る情報です。 + +| ケイパビリティ | クライアントが呼び出せるようになるもの | +|-------------|------------------------------------------------------------| +| `tools` | `tools/list`、`tools/call` | +| `resources` | `resources/list`、`resources/templates/list`、`resources/read` | +| `prompts` | `prompts/list`、`prompts/get` | + +`MCPServer` は 3 つのプリミティブすべてを提供するので、3 つとも常に宣言されます。 + +そこに無いものにも注目してください。`completions`(リソーステンプレートとプロンプトの引数の自動補完)は自分で書いたハンドラーを必要とします。このサーバーにはそれが無いので、ケイパビリティも現れず、行儀のよいクライアントは尋ねてきません。これはオプション機能すべてに共通するルールです。登録すればケイパビリティが現れます。その証拠は**[補完](../servers/completions.md)**にあります。 + +!!! info + `Client(mcp)` は、このドキュメントのすべての例をテストしているのと同じインメモリクライアントで、 + 自分のサーバーをテストするときにも使います。専用のページがあります。**[テスト](testing.md)**です。 + +## 書かなかったもの {#what-you-did-not-write} + +このページを振り返ってみてください。書いたのは小さな Python 関数が 3 つだけです。次のものは**書いていません**。 + +* JSON Schema。`a: int, b: int` が `add` のスキーマそのものです。 +* リクエストハンドラー。`tools/list`、`resources/read`、`prompts/get` はすべて代わりに処理されます。 +* ケイパビリティの宣言。`MCPServer` が組み立てました。 +* プロトコルのコード。バージョンのネゴシエーション、JSON-RPC のフレーミング、ケイパビリティの交換。すべて `mcp dev` と `Client(mcp)` の内部で行われ、目にすることはありませんでした。 + +この比率こそが、SDK の狙いそのものです。 + +## まとめ {#recap} + +* **ホスト**は LLM アプリ、**クライアント**はその MCP を話す部分、**サーバー**は自分で作るものです。 +* ツールは**モデル**が制御し、リソースは**アプリケーション**が制御し、プロンプトは**ユーザー**が制御します。 +* プリミティブごとにデコレーターが 1 つ。`@mcp.tool()`、`@mcp.resource(uri)`、`@mcp.prompt()` です。名前、説明、スキーマは関数から得られます。 +* `{param}` を含む URI はリソース**テンプレート**になり、具体的なリソースとは分けて一覧されます。 +* サーバーの**ケイパビリティ**は自動的に宣言され、クライアントはサーバーが宣言したものしか尋ねません。 +* `Client(mcp)` はサーバーオブジェクトにインメモリで接続します。初日から使えるテスト環境です。 + +次は**[実際のホストに接続する](real-host.md)**です。このサーバーを Claude Desktop や IDE の中で、本番同様に動かします。続いて**[テスト](testing.md)**。1 ページ、インメモリクライアント 1 つで、動いているかどうかを推測する必要はなくなります。その後は、プリミティブごとに専用のページがあります。まずはモデルが動かすもの、**[ツール](../servers/tools.md)**からです。 diff --git a/i18n/ja/pages/handlers/elicitation.md b/i18n/ja/pages/handlers/elicitation.md new file mode 100644 index 0000000000..8c726575a2 --- /dev/null +++ b/i18n/ja/pages/handlers/elicitation.md @@ -0,0 +1,186 @@ +# エリシテーション {#elicitation} + +処理の途中まで進んだのに答えが 1 つ足りない。そんなツールも、失敗する必要はありません。 + +**エリシテーション(elicitation)**を使えば、その場で尋ねられます。ツール呼び出しの途中でユーザーに質問が届き、その答えが同じ関数呼び出しの中に返ってきます。 + +モードは 2 つあります。 + +* **フォームモード**:値が必要な場合(確認、日付、数量など)。フィールドを記述すると、クライアントがフォームを描画します。 +* **URL モード**:ユーザーに別の場所へ移動してもらう必要がある場合(OAuth の同意画面、決済ページなど)。そこでの操作はプロトコルを一切通りません。 + +尋ね方も 2 つあります。まず選ぶべきなのは**リゾルバー**です。質問をパラメーターに結び付けておけば、SDK が代わりに尋ねます。どんな接続でも、クライアントが話すプロトコルの世代が何であっても動きます。もう一方の直接的な方法である `await ctx.elicit(...)` は、サーバーからクライアントへのリクエストであり、このチャネルはレガシー接続(仕様バージョン 2025-11-25 以前)のクライアントにしか存在しません。このページではどちらも扱いますが、まずはリゾルバーから始めます。 + +## リゾルバーで尋ねる {#ask-with-a-resolver} + +ツール全体の実行可否を決める質問(「本当に実行しますか?」「一致した 3 つのアカウントのどれですか?」)は、ツール本体から**リゾルバー**に切り出せます。あとはフレームワークが代わりに尋ねてくれます。 + +`Annotated[T, Resolve(fn)]` を付けたパラメーターは、ツール本体の前に `fn` を実行して埋められます。リゾルバーは値がすでに分かっていればそのまま返し、フレームワークに尋ねてほしいときは `Elicit(...)` を返します。 + +```python title="server.py" hl_lines="24-30 35-36" +--8<-- "docs_src/elicitation/tutorial004.py" +``` + +* `confirm_delete` はツール自身の `path` 引数を名前で受け取り、フォルダーの中身を一覧して、**必要なときだけエリシテーションを行います**。空のフォルダーなら、クライアントとのラウンドトリップなしに `Confirm(ok=True)` へ解決します。 +* `delete_folder` は `ElicitationResult[Confirm]` を注釈しているので、フレームワークは結果全体を注入し、ツールは `match` ですべてのケースを処理します。承諾して確定、承諾したが保持(`ok=False`)、辞退、キャンセルです。 +* `confirm` パラメーターはツールの入力スキーマには一切現れません。`path` はクライアントが、`confirm` はリゾルバーが供給します。 + +ツール側で分岐する必要がなければ、ラップしていないモデル(`Annotated[Confirm, Resolve(confirm_delete)]`)を注釈してください。承諾時にはモデルを受け取り、辞退やキャンセルのときは呼び出しがエラーで中断します。 + +リゾルバーは**すべての**接続で動きます。レガシー接続のクライアントには、SDK が質問を直接送ります。**2026-07-28** の接続では、SDK は呼び出しの戻り値として質問を返し、クライアントの次の試行が答えを運んできます。リゾルバー側がその違いを意識することはありません。内部で何が起きているかは**[マルチラウンドトリップ(multi-round-trip)リクエスト](multi-round-trip.md)**を参照してください。 + +尋ねることは、リゾルバーができることの 1 つにすぎません。より一般的な仕組み(尋ねずに計算する依存関係、依存関係の依存関係、モデルが供給できるものとできないもの)については、**[依存関係](dependencies.md)**のページを参照してください。 + +## ツールの中から尋ねる {#ask-from-inside-the-tool} + +ツールは、自分の本体の途中で処理を止めて尋ねることもできます。 + +!!! warning + `ctx.elicit()` と `ctx.elicit_url()` はサーバーからクライアントへのリクエストであり、 + このチャネルはレガシー接続(仕様バージョン **2025-11-25** 以前)のクライアントにしか + 存在しません。**2026-07-28** の接続にはサーバー発のリクエストがないため、これらの + 呼び出しは失敗します。リゾルバーはどちらでも動作します。詳しくは + **[プロトコルバージョン](../protocol-versions.md)**を参照してください。 + +`await ctx.elicit()` はメッセージと Pydantic モデルを受け取ります。 + +```python title="server.py" hl_lines="9-11 20-23 25" +--8<-- "docs_src/elicitation/tutorial001.py" +``` + +* **`Context`** パラメーターがあるからこそ `ctx.elicit` が使えます。どのツールでも受け取れます。このオブジェクトには専用のページがあります:**[Context](context.md)**。 +* `AlternativeDate` は、受け取りたい答えの**スキーマ**です。 +* このツールは `async def` です。途中で止まって人の入力を待つのですから、そうでなければなりません。 +* それ以外の日付なら、ツールはすぐに結果を返します。尋ねるのは必要なときだけです。 +* ユーザーが承諾した日付は `book_table` 自身に戻ってきます。答えも他と同じ入力です。代替日もまた満席であれば、確認なしに予約するのではなく、もう一度尋ねます。 + +### クライアントが受け取るもの {#what-the-client-receives} + +クライアントは、こちらのメッセージと、その隣にモデルから生成された JSON Schema を受け取ります。 + +```json +{ + "properties": { + "accept_alternative": { + "description": "Try another date?", + "title": "Accept Alternative", + "type": "boolean" + }, + "date": { + "default": "2025-12-26", + "description": "Alternative date (YYYY-MM-DD)", + "title": "Date", + "type": "string" + } + }, + "required": ["accept_alternative"], + "title": "AlternativeDate", + "type": "object" +} +``` + +このスキーマがフォームそのものです。`Field(description=...)` がラベルになり、デフォルト値は入力欄にあらかじめ埋まって、そのフィールドを省略可能にします。これは**[ツール](../servers/tools.md)**がツールの引数について説明しているのと同じ、Pydantic から JSON Schema への変換機構です。 + +!!! warning + エリシテーションのスキーマは、ツールの入力スキーマほど表現力がありません。使えるのは + フラットなプリミティブ型のフィールドだけです:`str`、`int`、`float`、`bool`、または + 文字列の `Literal`(`enum` になります)。モデルの中にモデルを入れると、クライアントへ + 何かを送る前に `ctx.elicit` が例外を送出します。 + + ```text + TypeError: Elicitation schema field 'address' rendered as {'$ref': '#/$defs/Address'}, which is not a valid PrimitiveSchemaDefinition + ``` + + 相手は作業中の人です。答えにネストが必要なら、それはツールの引数であるべきだった + ということです。 + +### 3 つの答え {#the-three-answers} + +`result.action` には、ユーザーが何をしたかが入ります。可能性はちょうど 3 つです。 + +* `"accept"`:フォームを送信しました。`result.data` は検証済みの `AlternativeDate` インスタンスです。 +* `"decline"`:断られました。 +* `"cancel"`:選ばずに質問を閉じました。 + +`result.data` が存在するのは `"accept"` のときだけです。だからこの例では、まず `result.action` を確認しています。型チェッカーもこの順序を強制します。`result.action == "accept"` の後では `result.data` は `AlternativeDate` ですが、その前には `.data` 自体がありません。 + +拒否はエラーではありません。辞退が何を意味するか(ここでは予約しないこと)はツールが決め、モデルには通常どおり応答します。 + +!!! tip + 答えは、コードが見る前に定義したモデルで検証されます。`bool` に対して `"maybe"` を + 送ってくるクライアントがいても、予約が壊れることはありません。呼び出しはスキーマ + 不一致のエラーで失敗し、`if` は実行されません。 + +## ユーザーを URL へ送る {#send-the-user-to-a-url} + +モデルやクライアントを通してはいけないものもあります。認証情報、カード番号、OAuth の同意などです。そうしたものではデータを尋ねるのではなく、ユーザーにどこかへ移動してもらいます。 + +```python title="server.py" hl_lines="10-14 23" +--8<-- "docs_src/elicitation/tutorial002.py" +``` + +* `ctx.elicit_url()` はメッセージ、訪問先の **URL**、そして自分で決める `elicitation_id` を受け取ります。`elicitation_id` は、サーバー内でこのエリシテーションを識別する任意の文字列です。 +* 結果にはアクションしか入りません。`"accept"` はユーザーが URL を開くことに同意したという意味であり、その先の手続きを完了したという意味では**ありません**。 +* 決済は帯域外で、ユーザーのブラウザーと決済プロバイダーの間で行われます。コンテンツが MCP を通って戻ってくることはありません。 + +2 つ目のツールを見てください。帯域外のフローが完了したことをサーバーが知ったとき(Webhook やポーリングなど。ここでは 2 つ目のツールとして表現しています)、`ctx.session.send_elicit_complete(...)` が同じ `elicitation_id` を付けて `notifications/elicitation/complete` を送ります。クライアントは、これによって「支払いを待っています...」の表示をやめてよいと分かります。これがなければ、クライアントは推測するしかありません。 + +## クライアント側 {#the-client-side} + +尋ねるのはサーバーです。クライアントは `Client(...)` に **`elicitation_callback`** を渡して答えます。 + +```python title="client.py" hl_lines="6-7 18" +--8<-- "docs_src/elicitation/tutorial003.py" +``` + +* 1 つのコールバックで両方のモードを処理します。`params` は `ElicitRequestFormParams` と `ElicitRequestURLParams` のユニオンで、`isinstance` で分岐します。 +* URL の場合は `params.url` をユーザーに表示し、選ばれたアクションを返します。`content` は決して返しません。 +* フォームの場合、実際のアプリケーションは `params.requested_schema` を描画し、ユーザーの入力を `content` として返します。ここでは常に決め打ちの答えで承諾していますが、テストではまさにこういうコールバックが欲しくなります。 +* コールバックを渡すことは**ケイパビリティの宣言**でもあります。これによってサーバーは、このクライアントに尋ねられると知ります。クライアントがサーバーのために答えられる他の事柄は**[クライアントコールバック](../client/callbacks.md)**にまとめてあります。 + +!!! info + エリシテーションはサーバーからクライアントへのリクエストであり、それが存在するのは + 従来のハンドシェイクで確立したセッションだけです。だからこのクライアントは + `mode="legacy"` を渡しています。**2026-07-28** の接続では、ツールは呼び出しの戻り値 + として質問を返す形で尋ねます。その流れは**[マルチラウンドトリップリクエスト](multi-round-trip.md)**です。 + +### 試してみる {#try-it} + +`ctx.elicit` を使うフォームモードの `server.py`(`book_table` のほう)を Streamable HTTP で起動し(ワンライナーは**[サーバーの実行](../run/index.md)**にあります)、クライアントの `main()` を実行して `book_table` にクリスマス当日を尋ねてみてください。 + +コールバックは、送られてきた質問を表示します。 + +```text +No tables for 2 on 2025-12-25. Would you like to try another date? +``` + +コールバックは `{"accept_alternative": True, "date": "2025-12-27"}` と答え、その間ずっと `await ctx.elicit(...)` の中で待っていたツールが予約を完了します。 + +```text +Booked a table for 2 on 2025-12-27. +``` + +次に URL モードの `server.py` に差し替えて、同じ `main()` を `pay_deposit` に向けてみてください。同じコールバックがもう一方の分岐に入り、決済リンクを表示し、ツールは「Complete the payment in your browser.」を返してきます。呼び出しの途中での、双方向の 1 往復です。 + +!!! check + 次に `Client` から `elicitation_callback=` を外して、もう一度クリスマス当日で + `book_table` を呼んでみてください。呼び出し全体がプロトコルエラーで失敗します。 + + ```text + Elicitation not supported + ``` + + コールバックを登録していないクライアントは `elicitation` ケイパビリティを宣言していないので、 + 尋ねる相手がいません。ツールが受け取ったのは `"decline"` ではなく例外です。これを前提に + 設計してください。どのエリシテーションにも「尋ねられなかったらどうするか」への妥当な答えが必要です。 + +## まとめ {#recap} + +* `Annotated[T, Resolve(fn)]` を付けたパラメーターはリゾルバーが埋め、リゾルバーは尋ねる必要があるときに `Elicit(...)` を返します。これはすべての接続で動きます。 +* スキーマはフラットな Pydantic モデルです。プリミティブなフィールドだけで、戻ってくるときに検証されます。 +* `result.action` は `"accept"`、`"decline"`、`"cancel"` のいずれかです。`result.data` は accept のときだけ存在します。 +* `await ctx.elicit(message, schema=Model)` はツール本体の中から尋ねます。`await ctx.elicit_url(message, url, elicitation_id)` は、モデルを通してはいけないすべてのもののためにあります(`ctx.session.send_elicit_complete(elicitation_id)` が帯域外の処理の完了を伝えます)。どちらもサーバーからクライアントへのリクエストなので、クライアントがレガシー接続であることが必要です。 +* クライアントは 1 つの `elicitation_callback` で答え、params の型で分岐します。これを登録することがケイパビリティの宣言になります。 +* 2026-07-28 の接続では、サーバーは質問を押し出すのではなく返します。同じコールバックには**[マルチラウンドトリップリクエスト](multi-round-trip.md)**から値が渡されます。 + +その戻り値の下で起きていること(リトライループ、`requestState` の保護、自分で駆動する方法)はすべて**[マルチラウンドトリップリクエスト](multi-round-trip.md)**にあります。 diff --git a/i18n/ja/pages/index.md b/i18n/ja/pages/index.md new file mode 100644 index 0000000000..fab3affac4 --- /dev/null +++ b/i18n/ja/pages/index.md @@ -0,0 +1,97 @@ +# MCP Python SDK {#mcp-python-sdk} + +!!! info "このドキュメントは、現在の安定版リリースラインである v2 について説明しています" + v2 が初めて、あるいは v1 から移ってきた場合は、**[v2 の新機能](whats-new.md)** が変更点を 5 分でたどれるツアーになっています。破壊的変更は **[移行ガイド](migration.md)** がすべて網羅しています。 + まだ v1.x を使っている場合、そのドキュメントは [v1.x のドキュメント](https://py.sdk.modelcontextprotocol.io/v1/) にあります。 + わかりにくい点や不備を見つけたら、[お知らせください](https://github.com/modelcontextprotocol/python-sdk/issues/new?template=v2-feedback.yaml)。 + +**Model Context Protocol(MCP)** を使うと、アプリケーションは標準化された方法で LLM にコンテキストを提供できます。コンテキストを「提供する」という関心事を、LLM とのやり取りそのものから切り離せます。 + +これはその公式 Python SDK です。この SDK では次のことができます。 + +* 任意の MCP ホストに対してツール、リソース、プロンプトを公開する **MCP サーバーを構築する**。 +* 任意の MCP サーバーに接続する **MCP クライアントを構築する**。 +* 標準のトランスポートをすべて扱う:stdio、Streamable HTTP、SSE。 + +## 要件 {#requirements} + +Python 3.10 以上。 + +## インストール {#installation} + +=== "uv" + + ```bash + uv add "mcp[cli]" + ``` + +=== "pip" + + ```bash + pip install "mcp[cli]" + ``` + +`[cli]` エクストラを入れると `mcp` コマンドが使えます。開発では必要になるでしょう。 +各依存関係が何のためのものかは [インストール](get-started/installation.md) を参照してください。 + +## 例 {#example} + +### 作成する {#create-it} + +`server.py` というファイルを作成します。 + +```python title="server.py" +--8<-- "docs_src/index/tutorial001.py" +``` + +これで完全な MCP サーバーです。 + +**ツール** `add` を 1 つと、テンプレート化された **リソース** `greeting://{name}` を 1 つ公開しています。 + +### 実行する {#run-it} + +```console +uv run mcp dev server.py +``` + +これでサーバーが起動し、[MCP Inspector](https://github.com/modelcontextprotocol/inspector) が開きます。Inspector はサーバーを対話的に試せる UI です。表示された URL を開いてください。 + +!!! note + Inspector は Node.js アプリなので、`mcp dev` を使うには `PATH` に `npx` が必要です。 + +### 試す {#try-it} + +Inspector で **Tools** を開き、`a=1`、`b=2` を指定して `add` を呼び出します。 + +`3` が返ってきます。✨ + +Inspector は、そのフォーム(`a` 用の必須の整数フィールドと、`b` 用のもう 1 つのフィールド)を型ヒントから組み立てています。Claude も、ほかのすべての MCP ホストも同じようにします。 + +次に **Resources** を開き、`greeting://World` を読み取ってみましょう。 + +```text +Hello, World! +``` + +### おさらい {#recap} + +**書かなかった** ものをもう一度見てみましょう。 + +* JSON Schema はありません。`a: int, b: int` がそのままスキーマです。 +* リクエストの解析も、シリアライズも、バリデーションのコードもありません。 +* プロトコルの処理は一切ありません。 + +書いたのは、型ヒントと docstring が付いた Python 関数 2 つだけです。残りは SDK が引き受けます。 + +## 次に読むもの {#where-to-go-next} + +* **[はじめる](get-started/index.md)** では、インストールから動作しテスト済みのサーバーまでを案内します。 +* MCP サーバーを「利用する」アプリケーションを作るなら、**[クライアント](client/index.md)** から始めてください。 +* すでに FastAPI や Starlette のアプリがあるなら、**[既存のアプリに追加する](run/asgi.md)** でその中に MCP サーバーをマウントできます。 +* 特定のエラーメッセージを探しているなら、**[トラブルシューティング](troubleshooting.md)** がそのままの文言で引けるようになっています。 +* v2 の変更点が気になるなら、**[v2 の新機能](whats-new.md)** で 5 分で把握できます。 +* v1 から移行するなら、**[移行ガイド](migration.md)** から始めてください。 +* 正確なシグネチャを探しているなら、**[API リファレンス](api/mcp/index.md)** がソースから生成されています。 +* LLM と一緒に読むなら、このドキュメントは [llms.txt](https://llmstxt.org/) 形式でも公開されています。 + [llms.txt](https://py.sdk.modelcontextprotocol.io/llms.txt) はページの索引で、 + [llms-full.txt](https://py.sdk.modelcontextprotocol.io/llms-full.txt) にはすべてのページが 1 つのファイルにまとめられています。 diff --git a/i18n/ja/state.json b/i18n/ja/state.json new file mode 100644 index 0000000000..512e84339f --- /dev/null +++ b/i18n/ja/state.json @@ -0,0 +1,164 @@ +{ + "pages": { + "get-started/first-steps.md": { + "block_hashes": [ + "f4e9527fee44ad246985003e8319e71c2b72f28e3301ee128f3d2c0e3f6d264d", + "3e1766d27f443ca4bbccdda3389f3dff2d2b6737133564ee0ca133a5dbfd958d", + "2a97f1d75b49d9a9b70e82ce3ad0ee70072b2676db3248614ae2ba29bc3fff28", + "59dc6c66de1c4ea589e4b984889814b842613605c638ae450db4fa44243aee06", + "ee42a5e18f8e868b5d29bb9db85915406e7e97e789013ee108ee2d199626d274", + "7d8344d6cf1ecfc4130812f643a148ea572ad7d319b371029197fe162d513602", + "cfcd0cd4aed4f275cb8cf632eed301192aed12aeb11c10c77fabab2fe0fc7b30" + ], + "inputs_hash": "b69ce7845c7f83104ed349d0396213789edd45eecbf8af12230df3302b124a93", + "model": "claude-opus-5", + "source_hash": "3531c5686a6ff79aabe6f0f8a3cb96213b6fc13b0bbbb0e762dc9f4a827b4be3", + "translated_at": "2026-07-31T19:11:10Z" + }, + "handlers/elicitation.md": { + "block_hashes": [ + "f5dade066115eb33121f7bbb02a3fb288e95e2662001bea5eac322e23f4cbd1d", + "c368d8a40fd8d9879d90fbb10a81870fdb3ca84675bcf04c91740febe56ed4c4", + "191a9cfc619f523acf23ef20a04a9a318abe5280bb459f4691dfc2c60bc8dc34", + "ed47c27010f781038cd2095fedb17be05b16a9c2b1b93650416dd7e1be3154b7", + "4db8b6b1183f96319677911fd22baa3e65faf1b2f107b7d0929a3ee39ebd3ed4", + "0265da2c81bc50cf3a83415aa16c1ded414fe59a3e23e4c0b1a83cd03d41ba5c" + ], + "inputs_hash": "b69ce7845c7f83104ed349d0396213789edd45eecbf8af12230df3302b124a93", + "model": "claude-opus-5", + "source_hash": "ef8afd716d136ff25d17a34059a933bb924ea238bd2faf04f80cc0d337c2e520", + "translated_at": "2026-07-31T19:11:10Z" + }, + "index.md": { + "block_hashes": [ + "3690a6eb6f57324f620f65d5242b3bb17c0ed4a5657cd4ba7ff3a28d7291245a", + "13e00a4af763653d8c663c64dd27871bdd23314d11110a143b8ee77357f76a75", + "cfcabc919e5d343b47af2641656a86f9a0abe9f250547368e64f7e4b5a3de4b0", + "c1550578c7bd96be08abb210e1c86261127c78ee09c1d8d666d08fa96271c30b", + "71115a8dc82e7ee1cd61b609da087038d8736629ebb365972c88010fd677cc00" + ], + "inputs_hash": "b69ce7845c7f83104ed349d0396213789edd45eecbf8af12230df3302b124a93", + "model": "claude-opus-5", + "source_hash": "c46eefb49de18ed47882e13cf5bf08d0c3ca16ee77b98c257e8b66150c6be666", + "translated_at": "2026-07-31T21:09:49Z" + } + }, + "ui": { + "inputs_hash": "b69ce7845c7f83104ed349d0396213789edd45eecbf8af12230df3302b124a93", + "model": "claude-opus-5", + "source": { + "@banner:disclosure": "??? info \"This page was machine-translated\"\n Translations of this documentation are generated automatically from the English pages, and the [English version of this page]({english_url}) is the authoritative one.\n\n Found a translation problem? See [how the translations work and how to report an issue]({translations_url}).\n", + "@banner:outdated": "!!! warning \"This translation may be behind the English page\"\n The English source changed after this page was last translated, so parts of it may be out of date. When in doubt, read the [English version of this page]({english_url}).\n", + "@banner:untranslated": "!!! note \"In English\"\n You're reading this page in English. [How the translations work]({translations_url}).\n", + "API Reference": "API Reference", + "Add to an existing app": "Add to an existing app", + "Advanced": "Advanced", + "Authorization": "Authorization", + "Caching": "Caching", + "Callbacks": "Callbacks", + "Clients": "Clients", + "Completions": "Completions", + "Connect to a real host": "Connect to a real host", + "Dependencies": "Dependencies", + "Deploy & scale": "Deploy & scale", + "Deprecated features": "Deprecated features", + "Elicitation": "Elicitation", + "Extensions": "Extensions", + "First steps": "First steps", + "Get started": "Get started", + "Handling errors": "Handling errors", + "Identity assertion": "Identity assertion", + "Images, audio & icons": "Images, audio & icons", + "Inside your handler": "Inside your handler", + "Installation": "Installation", + "Lifespan": "Lifespan", + "Logging": "Logging", + "MCP Apps": "MCP Apps", + "MCP Python SDK": "MCP Python SDK", + "Middleware": "Middleware", + "Migration Guide": "Migration Guide", + "Multi-round-trip requests": "Multi-round-trip requests", + "Multiple servers": "Multiple servers", + "OAuth": "OAuth", + "OpenTelemetry": "OpenTelemetry", + "Pagination": "Pagination", + "Progress": "Progress", + "Prompts": "Prompts", + "Protocol versions": "Protocol versions", + "Resources": "Resources", + "Running your server": "Running your server", + "Sampling and roots": "Sampling and roots", + "Servers": "Servers", + "Serving legacy clients": "Serving legacy clients", + "Structured Output": "Structured Output", + "Subscriptions": "Subscriptions", + "Testing": "Testing", + "The Context": "The Context", + "The low-level Server": "The low-level Server", + "Tools": "Tools", + "Translations": "Translations", + "Transports": "Transports", + "Troubleshooting": "Troubleshooting", + "URI templates": "URI templates", + "What's new in v2": "What's new in v2" + }, + "source_hash": "219aa0f5adfe57c4403a1b6fdf04d4e2cc79df194bdbcb30958d24e7bb387433", + "strings": { + "@banner:disclosure": "??? info \"このページは機械翻訳されています\"\n このドキュメントの翻訳は英語ページから自動生成されたもので、正式な内容は[このページの英語版]({english_url})です。\n\n 翻訳に問題を見つけた場合は、[翻訳の仕組みと問題の報告方法]({translations_url})を参照してください。\n", + "@banner:outdated": "!!! warning \"この翻訳は英語ページより古い可能性があります\"\n このページの翻訳後に英語の原文が変更されたため、一部の内容が古くなっている可能性があります。疑わしい場合は[このページの英語版]({english_url})を参照してください。\n", + "@banner:untranslated": "!!! note \"英語版\"\n このページは英語で表示されています。[翻訳の仕組みについて]({translations_url})。\n", + "API Reference": "API リファレンス", + "Add to an existing app": "既存のアプリに追加する", + "Advanced": "応用", + "Authorization": "認可", + "Caching": "キャッシュ", + "Callbacks": "コールバック", + "Clients": "クライアント", + "Completions": "補完", + "Connect to a real host": "実際のホストに接続する", + "Dependencies": "依存関係", + "Deploy & scale": "デプロイとスケール", + "Deprecated features": "非推奨の機能", + "Elicitation": "エリシテーション", + "Extensions": "拡張機能", + "First steps": "最初のステップ", + "Get started": "はじめに", + "Handling errors": "エラーの処理", + "Identity assertion": "アイデンティティアサーション", + "Images, audio & icons": "画像・音声・アイコン", + "Inside your handler": "ハンドラーの中で", + "Installation": "インストール", + "Lifespan": "ライフスパン", + "Logging": "ロギング", + "MCP Apps": "MCP Apps", + "MCP Python SDK": "MCP Python SDK", + "Middleware": "ミドルウェア", + "Migration Guide": "移行ガイド", + "Multi-round-trip requests": "マルチラウンドトリップリクエスト", + "Multiple servers": "複数のサーバー", + "OAuth": "OAuth", + "OpenTelemetry": "OpenTelemetry", + "Pagination": "ページネーション", + "Progress": "進捗", + "Prompts": "プロンプト", + "Protocol versions": "プロトコルバージョン", + "Resources": "リソース", + "Running your server": "サーバーの実行", + "Sampling and roots": "サンプリングとルート", + "Servers": "サーバー", + "Serving legacy clients": "レガシークライアントへの対応", + "Structured Output": "構造化出力", + "Subscriptions": "サブスクリプション", + "Testing": "テスト", + "The Context": "Context", + "The low-level Server": "低レベル Server", + "Tools": "ツール", + "Translations": "翻訳", + "Transports": "トランスポート", + "Troubleshooting": "トラブルシューティング", + "URI templates": "URI テンプレート", + "What's new in v2": "v2 の新機能" + }, + "translated_at": "2026-07-31T21:09:49Z" + } +} diff --git a/i18n/ko/glossary.json b/i18n/ko/glossary.json new file mode 100644 index 0000000000..9fbdbdc3c8 --- /dev/null +++ b/i18n/ko/glossary.json @@ -0,0 +1,225 @@ +{ + "version": 1, + "keep_in_source_language": [ + "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": "도구", + "note": "MCP primitive: an action the model chooses and calls. The wire identifiers `tools/call` and `tools/list` and any code identifiers stay in Latin script inside code font. Provisional pending native review.", + "avoid": ["툴"], + "enforce": false + }, + { + "source": "resource", + "target": "리소스", + "note": "MCP primitive: read-only data the application reads. `resources/read` and other identifiers stay Latin in code font. Provisional pending native review; 자원 is the general system-resources word and is avoided here.", + "avoid": ["자원"], + "enforce": false + }, + { + "source": "prompt", + "target": "프롬프트", + "note": "Both the MCP primitive (a message template a person invokes) and the general LLM sense; 프롬프트 in both. Provisional pending native review.", + "avoid": [], + "enforce": false + }, + { + "source": "sampling", + "target": "샘플링", + "note": "MCP feature where the server asks the client's model for a completion; not the statistics sense (표본 추출). Provisional pending native review.", + "avoid": ["표본 추출", "표집"], + "enforce": false + }, + { + "source": "roots", + "target": "루트", + "note": "Filesystem locations the client exposes. No plural marker (write 루트, not 루트들); the `roots/list` identifier stays Latin. Target provisional pending native review; 뿌리 (the botanical root) is never correct here.", + "avoid": ["뿌리"], + "enforce": true + }, + { + "source": "elicitation", + "target": "엘리시테이션", + "note": "Transliteration; write 엘리시테이션(elicitation) at the first occurrence on a page, 엘리시테이션 alone afterwards. The verb \"elicit\" in prose is 사용자에게 입력을 요청하다; `ctx.elicit(...)` stays code. Provisional pending native review — the least settled term in this file; 유도 is the candidate alternative to confirm against, so it is noted here rather than banned.", + "avoid": ["도출"], + "enforce": false + }, + { + "source": "capability", + "target": "기능", + "note": "A negotiated protocol feature (\"capability negotiation\" → 기능 협상); the `capabilities` wire field and `client.server_capabilities` stay Latin in code. Provisional pending native review — 기능 doubles as \"feature\"; native review should confirm the collision is acceptable.", + "avoid": ["역량", "능력"], + "enforce": false + }, + { + "source": "transport", + "target": "트랜스포트", + "note": "The countable, named connection mechanism (stdio 트랜스포트, 인메모리 트랜스포트) and the `Transport` protocol name in code. Prose describing the generic idea of how messages are carried may say 전송 방식 without implying a separate term. Provisional pending native review; 운송/수송 (freight transport) are never correct.", + "avoid": ["운송", "수송"], + "enforce": true + }, + { + "source": "session", + "target": "세션", + "note": "Standard loanword; `ctx.session` and other identifiers stay code. Provisional pending native review.", + "avoid": [], + "enforce": false + }, + { + "source": "handler", + "target": "핸들러", + "note": "The function registered for a tool, resource or prompt. 핸들러 rather than 처리기, to match modern Korean developer docs and the code vocabulary. Provisional pending native review.", + "avoid": ["처리기"], + "enforce": false + }, + { + "source": "dependency", + "target": "의존성", + "note": "Dependency-injection sense (\"dependency injection\" → 의존성 주입); a package dependency in installation contexts is also 의존성. Provisional pending native review; 종속성 is the competing house style.", + "avoid": ["종속성"], + "enforce": false + }, + { + "source": "client", + "target": "클라이언트", + "note": "The protocol role. The `Client` and `ClientSession` class names stay Latin in code font (see keep list). Provisional pending native review; 고객 means a customer and is never correct here.", + "avoid": ["고객"], + "enforce": true + }, + { + "source": "server", + "target": "서버", + "note": "The protocol role. The `MCPServer` and `Server` class names stay Latin in code font (see keep list). Provisional pending native review.", + "avoid": [], + "enforce": false + }, + { + "source": "host", + "target": "호스트", + "note": "\"MCP host\" is the application that embeds a client (a desktop assistant, an editor); the corpus also says \"ASGI host\" for the app that serves an ASGI app — 호스트 covers both. Provisional pending native review.", + "avoid": ["주최자"], + "enforce": false + }, + { + "source": "request", + "target": "요청", + "note": "A JSON-RPC or HTTP request; the verb \"to request\" is 요청하다. Provisional pending native review.", + "avoid": ["리퀘스트"], + "enforce": false + }, + { + "source": "response", + "target": "응답", + "note": "A JSON-RPC or HTTP response. Provisional pending native review.", + "avoid": ["리스폰스"], + "enforce": false + }, + { + "source": "context", + "target": "컨텍스트", + "note": "The \"context provided to a model\" sense (as in Model Context Protocol). The `Context` class and `ctx` are identifiers and stay Latin (see keep list). The everyday idiom \"in this context\" is not this term and may be recast (이 경우, 여기서는). Provisional pending native review; 맥락 is the competing rendering for the general word.", + "avoid": [], + "enforce": false + }, + { + "source": "notification", + "target": "알림", + "note": "Protocol notifications (`notifications/...` identifiers stay Latin); \"send a notification\" → 알림을 보내다. Provisional pending native review.", + "avoid": ["통지"], + "enforce": false + }, + { + "source": "resolver", + "target": "리졸버", + "note": "The SDK's `Resolve(...)`-annotated parameter resolvers. Provisional pending native review; 해석기 (parser/interpreter) and 해결자 are avoided.", + "avoid": ["해석기", "해결자"], + "enforce": false + }, + { + "source": "authorization", + "target": "인가", + "note": "Security sense: authorization → 인가 (인가 서버 for authorization server, 인가 코드 for authorization code), distinct from authentication → 인증. The `Authorization` HTTP header and code identifiers stay Latin. Provisional pending native review — 승인 and 권한 부여 are competing renderings to confirm against.", + "avoid": [], + "enforce": false + }, + { + "source": "deprecated", + "target": "지원 중단 예정", + "note": "Advisory status: still works but discouraged. First occurrence on a page may add the original: 지원 중단 예정(deprecated). \"Removed\" is a different word (제거됨); the corpus contrasts the two, so never render deprecated as 폐기됨 or 제거됨. Provisional pending native review.", + "avoid": ["폐기된", "폐기 예정"], + "enforce": false + }, + { + "source": "round-trip", + "target": "왕복", + "note": "A network round trip; \"multi-round-trip requests\" → 다중 왕복 요청. Provisional pending native review.", + "avoid": ["라운드 트립"], + "enforce": false + }, + { + "source": "lifespan", + "target": "lifespan", + "note": "Keep the English word in Korean prose (lifespan 함수, 호스트 앱의 lifespan), matching the `lifespan=` parameter it names. Provisional pending native review — 수명 주기 was rejected because it collides with \"lifecycle\" (생명 주기).", + "avoid": ["수명"], + "enforce": false + } + ] +} diff --git a/i18n/ko/instructions.md b/i18n/ko/instructions.md new file mode 100644 index 0000000000..784e6c66b5 --- /dev/null +++ b/i18n/ko/instructions.md @@ -0,0 +1,140 @@ +# Korean (ko) — translation instructions + +Target language: Korean (한국어), directory and URL code `ko`, page language +tag `ko`. 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 + +Write 합쇼체 (formal-polite, sentence endings in -습니다 / -ㅂ니다) as the one +register for the whole page. + +- Body prose, list items, table cells and admonition bodies end in -습니다 / + -ㅂ니다: "The SDK does the rest." → SDK가 나머지를 처리합니다. +- Short imperatives (steps, instructions, calls to action) use -세요: + "Create a file `server.py`" → `server.py` 파일을 만드세요. Never -십시오, never + the bare 해요체 (-어요 / -예요 / -해요), and never plain-style -다 endings. +- Headings are noun phrases where the English heading is a noun phrase + ("Installation" → 설치). An English heading phrased as a sentence or a + question becomes a noun phrase too: "What's new in v2" → v2에서 달라진 점. + Do not write -나요? or -습니까? headings. +- Never address the reader with 당신, 여러분 or 우리. Korean drops the + subject: "you can pass a URL" → URL을 전달할 수 있습니다. Where a subject is + unavoidable, name the role — 클라이언트, 서버, 사용자 — never a pronoun. + "Your server" is 서버 or, when the contrast matters, 작성한 서버. +- One page, one register. Mixing -습니다 with -어요, or -세요 with -십시오, is + wrong even when each sentence is correct on its own. + +## 2. Voice + +Warm, direct and considerate: the reader is a capable developer being +guided by a colleague, not lectured by a manual. + +- Keep the source's directness and its short payoff sentences. "That's a + complete MCP server." → 이것으로 완전한 MCP 서버가 완성됩니다. Do not pad the + translation with hedges the English does not have. +- A brief friendly aside is welcome in Korean too — 참고로, 다행히, a plain + 환영합니다 — as long as it stays in 합쇼체. +- Prefer verbs over noun stacks. "Configuration of the transport" is 트랜스포트를 + 설정하는 방법, not 트랜스포트의 설정. +- Avoid translationese (번역체): + - no double passives: -되어지다 → -되다; no -할 것입니다 chains where -합니다 + says the same thing; + - no pronoun crutches: drop 그것, 그들, 이것들 — repeat the noun or restructure; + - do not stack a conditional marker on top of -면: drop 만약 when -(으)면 + already carries the condition; + - mark plurals sparingly: Korean rarely needs -들 ("the tools" → 도구); + - no honorific inflation: 살펴보시면 ✗ → 살펴보면 ✓ (-세요 endings are the + only place -시- appears); + - do not overuse -에 대해 / -에 대하여 where a plain object particle works. + +Example — English: "A **host** is the LLM application: Claude, an IDE, an +agent runtime. It's the thing the user is talking to." + +- Wrong (translationese): **호스트**는 LLM 애플리케이션입니다: Claude, IDE, + 에이전트 런타임. 그것은 사용자가 그것에게 이야기하는 것입니다. +- Right: **호스트**는 LLM 애플리케이션입니다. Claude, IDE, 에이전트 런타임이 여기에 + 해당하며, 사용자가 대화하는 상대가 바로 호스트입니다. + +## 3. Humour and idioms + +Translate the information, not the joke. + +- Idioms, puns and light asides are recast into a plain friendly 합쇼체 + sentence that carries the same fact, never translated word for word: "Out + of the box the app answers **only** requests addressed to localhost." → + 기본적으로 이 앱은 localhost로 오는 요청**만** 받습니다. — not 상자에서 꺼내자마자. +- Recurring English tags get fixed renderings: "**[X](…)** has the whole + story" / "The whole story is in **[X](…)**" → 자세한 내용은 **[X](…)**에서 + 확인하세요.; "That's the whole API." / "That's the whole protocol." → 이것이 + API의 전부입니다. / 프로토콜은 이것이 전부입니다.; "That's it. It's just Python." + → 이게 전부입니다. 평범한 Python일 뿐입니다. +- Exclamation marks: keep one only where the English is genuinely + emphatic; a routine sentence ends with 온점 even if the source ends in "!". +- Emoji: the source's only emoji are two ✨ closing payoff lines, and they + are dropped in Korean; the friendliness moves into the wording. "You get + `3` back. ✨" → `3`이 돌아옵니다. Emoji shortcodes (`:smile:`) are syntax and + are left exactly as the shared rules say. +- If a light aside has no natural Korean equivalent, replace it with a + neutral sentence stating the underlying point — never leave a gap and + never add a translator's note explaining the joke. + +## 4. Typography + +- Punctuation is ASCII: `. , ? ! ( )`. Never 。 、 「」 or full-width forms. + Every sentence, including -세요 imperatives, ends with 온점 `.`. +- No sentence-final colon or dash before a code block or list: "Try this:" + → 다음을 시도해 보세요. An English em-dash aside becomes a comma, a + parenthesis, or its own sentence — no ` — ` in Korean prose. +- Straight quotes only. No italics on Hangul: where the source italicises a + word that becomes Korean, use `**굵게**` or nothing; italics may stay + around Latin-script words. +- Spacing follows 한글 맞춤법: words are separated by spaces, but a + particle (조사) attaches to the word before it — also after Latin words and + code spans, with no space in between: Python은, MCP를, `add`를 호출합니다, + `Client`가 연결을 맺습니다. Latin words otherwise sit in the sentence like + Korean words, with normal spacing on each side. +- Choose the particle after a Latin word or code span by how the term is + read aloud: Python은 (파이썬), stdio는, MCP는 (엠씨피), `list_tools`를, + Streamable HTTP를. When the reading is unclear (symbols, mixed digits), + restructure so a Korean noun carries the particle — `x` 값을, `--port` + 옵션은. Never write the double form 은(는) / 을(를) / 이(가). +- Digits are ASCII; a unit or counter follows a numeral without a space: + 3개, 30초, 8000번 포트, 5MB. Version numbers and the protocol's date-shaped + revision strings are identifiers and are copied byte-for-byte (they are in + the glossary's keep list). A calendar date written out in prose, if any, + becomes 2026년 7월 28일. +- Parenthetical originals use ASCII parentheses with no space before them: + 엘리시테이션(elicitation). + +## 5. Terminology pointer + +The glossary (`glossary.json`) is injected separately and overrides this +file on every term it covers. These conventions apply to everything the +glossary does not pin: + +- Loanword spellings follow the standard 외래어 표기법: 서버, 클라이언트, + 콜백 (not 콜빽), 프롬프트, 세션, 토큰, 스키마, 데코레이터, 미들웨어. Where an + ICT term is not in the glossary, prefer the rendering that mainstream + Korean developer documentation uses; treat 국립국어원 and TTA usage as the + tie-breaker. +- Three strategies coexist and the glossary decides which applies per term: + transliterate established loanwords (스트림, 서버), translate into the common + Sino-Korean word where that is the mainstream (요청, 응답, 알림, 도구, 인가, + 의존성), and keep in Latin script anything that is an identifier or a + proper name — class and function names, wire method names such as + `tools/call`, package names, protocol and product names. +- 한글(English) 병기: the glossary marks a few MCP-specific nouns for a + parenthetical original on first mention only — 엘리시테이션(elicitation) once, + then 엘리시테이션. Class names never get a Hangul gloss. +- One term, one rendering, throughout the page. Do not alternate between + 객체 and 오브젝트, or between 컨텍스트 and 맥락, for the same source term. +- Abbreviations stay Latin and lose the English plural "s": "the APIs" → API. + +## 6. Provisional note + +Every decision in this file is provisional pending review by native Korean +speakers. To propose a change, edit this file (or `glossary.json`) in a pull +request — never edit the generated pages under `pages/`. diff --git a/i18n/ko/pages/get-started/first-steps.md b/i18n/ko/pages/get-started/first-steps.md new file mode 100644 index 0000000000..030f0835ac --- /dev/null +++ b/i18n/ko/pages/get-started/first-steps.md @@ -0,0 +1,139 @@ +# 첫걸음 {#first-steps} + +**[랜딩 페이지](../index.md)**는 빠르게 진행합니다. 서버를 작성하고, 실행하고, 도구를 호출합니다. + +이 페이지에서는 서버가 노출할 수 있는 세 가지를 모두 다루면서, 그 과정에 등장하는 모든 것에 이름을 붙여 가며 천천히 진행합니다. + +## 호스트, 클라이언트, 서버 {#host-client-and-server} + +앞으로 모든 페이지에서 보게 될 세 단어입니다. + +* **호스트**는 LLM 애플리케이션입니다. Claude, IDE, 에이전트 런타임이 여기에 해당하며, 사용자가 대화하는 상대가 바로 호스트입니다. +* **클라이언트**는 호스트 안에 존재하며 MCP로 통신합니다. 호스트는 연결된 서버마다 클라이언트를 하나씩 실행합니다. +* **서버**는 이 SDK로 만드는 대상입니다. 클라이언트에 여러 가지를 노출하며, 모델과 직접 대화하지는 않습니다. + +작성하는 쪽은 서버입니다. 호스트는 다른 누군가의 제품입니다. SDK는 `Client`도 함께 제공합니다. 서버를 테스트할 때 사용하며, 이 페이지 뒤쪽에서 다시 등장합니다. + +## 세 가지 프리미티브 {#the-three-primitives} + +서버가 노출하는 것은 정확히 세 종류입니다. 이들을 가르는 기준은 **누가 사용을 결정하는가**입니다. + +| 프리미티브 | 제어 주체 | 설명 | 예시 | +|---------------|-----------------|-----------------------------------------------------|------------------------------------| +| **도구** | 모델 | 모델이 동작을 수행하기 위해 호출하는 함수 | API 호출, 데이터베이스 쓰기 | +| **리소스** | 애플리케이션 | 호스트가 모델의 컨텍스트로 불러오는 데이터 | 파일 내용, API 응답 | +| **프롬프트** | 사용자 | 사용자가 이름으로 호출하는 재사용 가능한 메시지 템플릿 | 슬래시 명령, 메뉴 항목 | + +이 구분의 핵심은 바로 "제어 주체"입니다. 도구는 **모델**이 호출하기로 결정했기 때문에 실행됩니다. 리소스는 **애플리케이션**이 모델에 필요하다고 판단했기 때문에 첨부됩니다. 프롬프트는 **사용자**가 선택했기 때문에 실행됩니다. + +!!! info + 웹 API를 만들어 본 적이 있다면 필요한 감은 대부분 이미 갖춘 셈입니다. **리소스**는 `GET`이고 + (데이터를 읽어 올 뿐 아무것도 바꾸지 않습니다), **도구**는 `POST`입니다(작업을 수행하며 부수 + 효과가 있을 수 있습니다). **프롬프트**에 대응하는 HTTP 개념은 없으며, 사용자가 이름으로 실행하는 + 저장된 쿼리에 더 가깝습니다. + +## 하나의 서버, 세 가지 모두 {#one-server-all-three} + +```python title="server.py" hl_lines="6 12 18" +--8<-- "docs_src/first_steps/tutorial001.py" +``` + +평범한 함수 세 개와 데코레이터 세 개입니다. 각 데코레이터가 등록의 전부입니다. + +* `@mcp.tool()` 데코레이터는 `add`를 **도구**로 만듭니다. +* `@mcp.resource("greeting://{name}")` 데코레이터는 `greeting`을 **리소스 템플릿**으로 만듭니다. URI의 `{name}`이 함수의 매개변수가 됩니다. +* `@mcp.prompt()` 데코레이터는 `summarize`를 **프롬프트**로 만듭니다. 반환하는 문자열이 사용자 메시지가 됩니다. + +나머지(이름, 설명, 인자 스키마)는 SDK가 함수 자체에서 읽어 냅니다. 함수 이름, 독스트링, 타입 힌트가 그 출처입니다. 그중 어느 것도 따로 선언하지 않았습니다. + +!!! tip + SDK의 두 축은 임포트 경로도 둘로 나뉩니다. `from mcp import Client`와 + `from mcp.server import MCPServer`입니다. `from mcp import MCPServer`는 없습니다. + +### 직접 해 보기 {#try-it} + +MCP Inspector로 실행하세요. + +```console +uv run mcp dev server.py +``` + +출력된 URL을 여세요. Inspector에는 프리미티브마다 탭이 하나씩 있으니 순서대로 살펴보면 됩니다. + +**도구.** 항목은 하나, *Add two numbers.*라는 설명이 붙은 `add`입니다. 폼에는 `a`와 `b`를 위한 필수 정수 필드가 각각 하나씩 있습니다. 값을 채우고 호출하면 결과는 `3`입니다. Inspector는 `a: int, b: int`로부터 그 폼을 만들었습니다. 다른 모든 클라이언트도 마찬가지입니다. + +**리소스.** *Resources* 목록은 비어 있습니다. `greeting`은 **Resource Templates** 아래에 있는데, `greeting://{name}`에 매개변수가 있기 때문입니다. 누군가 `name`을 제공하기 전까지는 목록에 올릴 구체적인 리소스가 없습니다. `World`를 넣고 읽어 보세요. + +```text +Hello, World! +``` + +**프롬프트.** 항목은 하나, 필수 인자 `text` 하나를 받는 `summarize`입니다. 적당한 텍스트를 넣어 가져오면 `role: user`와 렌더링된 문자열을 내용으로 담은 메시지 하나가 돌아옵니다. 프롬프트는 이게 전부입니다. 메시지를 만드는 함수일 뿐입니다. + +Inspector는 방금 그 서버를 **stdio**로 실행했습니다. stdio는 MCP 서버가 사용할 수 있는 트랜스포트 중 하나입니다. 아직 무엇을 쓸지 고를 필요는 없습니다. 그 이야기는 **[서버 실행하기](../run/index.md)**에서 다룹니다. + +## 기능 {#capabilities} + +Inspector에서 탭 세 개를 봤습니다. Inspector는 탭이 세 개라는 것을 어떻게 알았을까요? + +클라이언트가 연결하면 서버는 자신의 **기능**, 즉 어떤 계열의 요청에 응답할지를 선언합니다. 클라이언트는 그 선언을 보고 무엇을 요청할지 결정합니다. 이 선언은 직접 작성하지 않았습니다. `MCPServer`가 대신 선언해 줍니다. + +직접 확인해 보세요. SDK의 `Client`는 서버 객체를 그대로 받아 **인메모리**로 연결합니다(하위 프로세스도, 포트도 필요 없습니다). + +```python +import asyncio + +from mcp import Client + +from server import mcp + + +async def main() -> None: + async with Client(mcp) as client: + print(client.server_capabilities.model_dump(exclude_none=True)) + + +asyncio.run(main()) +``` + +```text +{'prompts': {'list_changed': True}, 'resources': {'subscribe': True, 'list_changed': True}, 'tools': {'list_changed': True}} +``` + +이 딕셔너리가 서버가 선언한 **기능**입니다. 연결하는 모든 클라이언트가 가장 먼저 알게 되는 정보입니다. + +| 기능 | 클라이언트가 호출할 수 있게 되는 것 | +|-------------|------------------------------------------------------------| +| `tools` | `tools/list`, `tools/call` | +| `resources` | `resources/list`, `resources/templates/list`, `resources/read` | +| `prompts` | `prompts/list`, `prompts/get` | + +`MCPServer`는 세 프리미티브를 모두 제공하므로 셋 다 항상 선언됩니다. + +빠져 있는 것도 눈여겨보세요. `completions`(리소스 템플릿과 프롬프트의 인자 자동 완성)는 직접 작성한 핸들러가 필요한데 이 서버에는 없으므로, 해당 기능은 선언되지 않고 규약을 잘 지키는 클라이언트는 요청하지 않습니다. 선택적인 모든 것에 같은 규칙이 적용됩니다. 해당 항목을 등록하면 기능이 나타나며, **[자동 완성](../servers/completions.md)**이 이를 보여 줍니다. + +!!! info + `Client(mcp)`는 이 문서의 모든 예제를 테스트할 때 쓰는 바로 그 인메모리 클라이언트이며, 직접 만든 + 서버도 이것으로 테스트하게 됩니다. 여기에는 페이지 하나가 통째로 할애되어 있습니다. **[테스트](testing.md)**를 확인하세요. + +## 작성하지 않은 것 {#what-you-did-not-write} + +이 페이지를 되짚어 보세요. 작성한 것은 작은 Python 함수 세 개뿐입니다. 다음은 작성하지 **않았습니다**. + +* JSON 스키마. `add`의 스키마는 `a: int, b: int` 그 자체입니다. +* 요청 핸들러. `tools/list`, `resources/read`, `prompts/get` 모두 대신 처리됩니다. +* 기능 선언. `MCPServer`가 만들어 주었습니다. +* 프로토콜 코드 한 줄. 버전 협상, JSON-RPC 프레이밍, 기능 교환까지 모두 `mcp dev`와 `Client(mcp)` 내부에서 일어났고, 겉으로 드러나지 않았습니다. + +이 비율이 바로 SDK의 존재 이유입니다. + +## 요약 {#recap} + +* **호스트**는 LLM 앱이고, **클라이언트**는 그중 MCP로 통신하는 부분이며, **서버**는 직접 만드는 것입니다. +* 도구는 **모델**이, 리소스는 **애플리케이션**이, 프롬프트는 **사용자**가 제어합니다. +* 프리미티브마다 데코레이터 하나씩입니다. `@mcp.tool()`, `@mcp.resource(uri)`, `@mcp.prompt()`. 이름, 설명, 스키마는 함수에서 가져옵니다. +* `{param}`이 들어간 URI는 리소스 **템플릿**이 되며, 구체적인 리소스와는 따로 나열됩니다. +* 서버의 **기능**은 대신 선언되며, 클라이언트는 서버가 선언한 것만 요청합니다. +* `Client(mcp)`는 서버 객체에 인메모리로 연결합니다. 첫날부터 쓸 수 있는 테스트 도구입니다. + +다음은 **[실제 호스트에 연결하기](real-host.md)**입니다. 지금 만든 서버를 Claude Desktop이나 IDE 안에서 실제로 동작시켜 봅니다. 그다음은 **[테스트](testing.md)**입니다. 페이지 하나와 인메모리 클라이언트 하나면 제대로 동작하는지 짐작할 일이 없어집니다. 이후에는 프리미티브마다 페이지가 하나씩 이어지며, 모델이 주도하는 **[도구](../servers/tools.md)**부터 시작합니다. diff --git a/i18n/ko/pages/handlers/elicitation.md b/i18n/ko/pages/handlers/elicitation.md new file mode 100644 index 0000000000..e50dd0ff24 --- /dev/null +++ b/i18n/ko/pages/handlers/elicitation.md @@ -0,0 +1,185 @@ +# 엘리시테이션 {#elicitation} + +작업을 절반쯤 진행한 도구가 답 하나를 얻지 못했다고 해서 실패할 필요는 없습니다. + +**엘리시테이션(elicitation)**을 사용하면 도구가 직접 물어볼 수 있습니다. 도구 호출 도중에 사용자에게 질문이 전달되고, 그 답은 같은 함수 호출 안으로 돌아옵니다. + +모드는 두 가지입니다. + +* **폼 모드**: 값이 필요한 경우(확인, 날짜, 수량). 필드를 설명하면 클라이언트가 폼을 렌더링합니다. +* **URL 모드**: 사용자를 다른 곳으로 보내야 하는 경우(OAuth 동의 화면, 결제 페이지). 거기서 하는 일은 프로토콜을 전혀 거치지 않습니다. + +그리고 묻는 방법도 두 가지입니다. 먼저 손이 가야 할 쪽은 **리졸버**입니다. 질문을 파라미터에 달아 두면 SDK가 대신 묻습니다. 어떤 연결에서든, 클라이언트가 어느 시대의 프로토콜을 사용하든 동작합니다. 직접적인 방법인 `await ctx.elicit(...)`은 **서버**가 **클라이언트**에게 보내는 요청이며, 이 채널은 레거시 연결의 클라이언트에만 존재합니다(사양 버전 2025-11-25 이하). 이 페이지에서는 둘 다 다루지만, 리졸버부터 시작하세요. + +## 리졸버로 묻기 {#ask-with-a-resolver} + +도구 전체를 좌우하는 질문, 이를테면 **정말 괜찮습니까? 일치하는 세 계정 중 어느 것입니까?** 같은 질문은 도구 본문 밖으로 빼내 **리졸버**에 둘 수 있고, 그러면 프레임워크가 대신 물어봅니다. + +`Annotated[T, Resolve(fn)]`로 애노테이션된 파라미터는 도구 본문보다 먼저 `fn`을 실행해 채워집니다. 리졸버는 값을 이미 알고 있으면 그대로 반환하고, 프레임워크가 묻게 하려면 `Elicit(...)`을 반환합니다. + +```python title="server.py" hl_lines="24-30 35-36" +--8<-- "docs_src/elicitation/tutorial004.py" +``` + +* `confirm_delete`는 도구 자신의 `path` 인자를 이름으로 읽어 폴더 목록을 확인하고, **꼭 필요할 때만 사용자에게 묻습니다**. 빈 폴더라면 클라이언트와 왕복 없이 `Confirm(ok=True)`로 해결됩니다. +* `delete_folder`는 `ElicitationResult[Confirm]`으로 애노테이션하므로 프레임워크가 결과 전체를 주입하고, 도구는 모든 경우를 `match`로 처리합니다. 수락 후 확인, 수락했지만 유지(`ok=False`), 거절, 취소가 그것입니다. +* `confirm` 파라미터는 도구의 입력 스키마에 전혀 나타나지 않습니다. `path`는 클라이언트가, `confirm`은 리졸버가 제공합니다. + +도구가 분기할 필요가 없다면 감싸지 않은 모델(`Annotated[Confirm, Resolve(confirm_delete)]`)로 애노테이션하세요. 수락 시에는 모델을 받고, 거절이나 취소 시에는 호출이 오류와 함께 중단됩니다. + +리졸버는 **모든** 연결에서 동작합니다. 레거시 연결의 클라이언트에는 SDK가 질문을 직접 보내고, **2026-07-28** 연결에서는 SDK가 호출의 결과로 질문을 **반환**하며 클라이언트의 다음 시도가 답을 실어 옵니다. 리졸버는 그 차이를 전혀 알지 못합니다. 그 아래에서 벌어지는 일은 **[다중 왕복 요청](multi-round-trip.md)**에서 다룹니다. + +묻는 것은 리졸버가 할 수 있는 일 중 하나일 뿐입니다. 묻지 않고 계산하는 의존성, 의존성의 의존성, 모델이 제공할 수 있는 것과 없는 것 같은 일반적인 메커니즘은 **[의존성](dependencies.md)** 페이지에서 다룹니다. + +## 도구 내부에서 묻기 {#ask-from-inside-the-tool} + +도구는 자신의 본문 도중에 멈춰서 직접 물어볼 수도 있습니다. + +!!! warning + `ctx.elicit()`과 `ctx.elicit_url()`은 **서버**가 **클라이언트**에게 보내는 요청이며, + 이 채널은 레거시 연결의 클라이언트에만 존재합니다(사양 버전 **2025-11-25** 이하). + **2026-07-28** 연결에는 서버가 시작하는 요청이 없으므로 이 호출은 실패합니다. + 리졸버는 양쪽 모두에서 동작합니다. 자세한 내용은 **[프로토콜 버전](../protocol-versions.md)**에서 + 확인하세요. + +`await ctx.elicit()`은 메시지와 Pydantic 모델을 받습니다. + +```python title="server.py" hl_lines="9-11 20-23 25" +--8<-- "docs_src/elicitation/tutorial001.py" +``` + +* `ctx.elicit`을 쓸 수 있게 해 주는 것은 **`Context`** 파라미터이며, 어떤 도구든 이 파라미터를 받을 수 있습니다. 이 객체는 별도 페이지인 **[Context](context.md)**에서 다룹니다. +* `AlternativeDate`는 받고 싶은 답의 **스키마**입니다. +* 이 도구는 `async def`입니다. 그럴 수밖에 없습니다. 중간에 멈춰서 사람을 기다리기 때문입니다. +* 다른 날짜라면 도구는 곧바로 반환합니다. 꼭 필요할 때만 묻습니다. +* 사용자가 수락한 날짜는 `book_table` 자체를 다시 거칩니다. 답도 다른 입력과 똑같이 취급되므로, 대안 날짜마저 예약이 꽉 찼다면 무턱대고 확정하지 않고 다시 물어봅니다. + +### 클라이언트가 받는 것 {#what-the-client-receives} + +클라이언트는 메시지와 함께 모델에서 생성된 JSON Schema를 받습니다. + +```json +{ + "properties": { + "accept_alternative": { + "description": "Try another date?", + "title": "Accept Alternative", + "type": "boolean" + }, + "date": { + "default": "2025-12-26", + "description": "Alternative date (YYYY-MM-DD)", + "title": "Date", + "type": "string" + } + }, + "required": ["accept_alternative"], + "title": "AlternativeDate", + "type": "object" +} +``` + +이 스키마가 곧 폼입니다. `Field(description=...)`은 레이블이 되고, 기본값은 입력란을 미리 채우면서 해당 필드를 선택 사항으로 만듭니다. **[도구](../servers/tools.md)**에서 도구 인자를 두고 설명한 것과 같은 Pydantic-to-JSON-Schema 장치입니다. + +!!! warning + 엘리시테이션 스키마는 도구의 입력 스키마만큼 표현력이 높지 않습니다. 평평한 기본 타입 필드만 + 허용됩니다. `str`, `int`, `float`, `bool`, 또는 문자열 `Literal`(이는 `enum`이 됩니다)입니다. + 모델 안에 모델을 넣으면 클라이언트로 아무것도 전송되기 전에 `ctx.elicit`이 예외를 발생시킵니다. + + ```text + TypeError: Elicitation schema field 'address' rendered as {'$ref': '#/$defs/Address'}, which is not a valid PrimitiveSchemaDefinition + ``` + + 이 기능은 작업 중인 사람을 가로막는 일입니다. 답에 중첩 구조가 필요하다면, 그것은 애초에 + 도구의 인자여야 했습니다. + +### 세 가지 답 {#the-three-answers} + +`result.action`은 사용자가 무엇을 했는지 알려주며, 가능한 값은 정확히 세 가지입니다. + +* `"accept"`: 사용자가 폼을 제출했습니다. `result.data`는 이미 검증을 마친 `AlternativeDate` 인스턴스입니다. +* `"decline"`: 사용자가 거절했습니다. +* `"cancel"`: 사용자가 선택하지 않고 질문을 닫았습니다. + +`result.data`는 `"accept"`일 때만 존재하므로, 예제는 `result.action`을 먼저 확인합니다. 타입 검사기가 이 순서를 강제합니다. `result.action == "accept"` 이후에 `result.data`는 `AlternativeDate`이고, 그 전에는 `.data` 자체가 없습니다. + +거절은 오류가 아닙니다. 거절이 무엇을 뜻하는지(여기서는 예약하지 않음)는 도구가 정하고, 모델에는 평소처럼 답합니다. + +!!! tip + 답은 코드가 보기 전에 모델을 기준으로 검증됩니다. `bool` 자리에 `"maybe"`를 보내는 + 클라이언트가 있어도 예약은 망가지지 않습니다. 호출이 스키마 불일치 오류로 실패하므로 + 작성한 `if` 문은 실행되지 않습니다. + +## 사용자를 URL로 보내기 {#send-the-user-to-a-url} + +자격 증명, 카드 번호, OAuth 동의처럼 모델이나 클라이언트를 거쳐서는 안 되는 것들이 있습니다. 이런 경우에는 데이터를 요청하는 대신 사용자에게 어딘가로 이동해 달라고 요청합니다. + +```python title="server.py" hl_lines="10-14 23" +--8<-- "docs_src/elicitation/tutorial002.py" +``` + +* `ctx.elicit_url()`은 메시지, 방문할 **URL**, 그리고 직접 정하는 `elicitation_id`를 받습니다. `elicitation_id`는 서버 안에서 이 엘리시테이션을 식별하는 임의의 문자열입니다. +* 결과에는 액션만 있고 그 밖에는 아무것도 없습니다. `"accept"`는 사용자가 URL 열기에 동의했다는 뜻이지, 그 너머의 일을 끝냈다는 뜻은 **아닙니다**. +* 결제는 대역 외에서, 사용자의 브라우저와 결제 제공자 사이에서 이루어집니다. 어떤 내용도 MCP를 통해 돌아오지 않습니다. + +두 번째 도구를 보세요. 대역 외 흐름이 끝났다는 사실을 서버가 알게 되면(웹훅이나 폴링으로, 여기서는 두 번째 도구로 모델링했습니다) `ctx.session.send_elicit_complete(...)`가 같은 `elicitation_id`로 `notifications/elicitation/complete`를 보냅니다. 클라이언트는 이를 통해 **"결제 대기 중..."** 표시를 멈춰도 된다는 것을 알게 됩니다. 이 알림이 없으면 클라이언트는 추측할 수밖에 없습니다. + +## 클라이언트 쪽 {#the-client-side} + +서버가 묻고 클라이언트가 답합니다. 클라이언트는 `Client(...)`에 **`elicitation_callback`**을 전달해 답합니다. + +```python title="client.py" hl_lines="6-7 18" +--8<-- "docs_src/elicitation/tutorial003.py" +``` + +* 콜백 하나가 두 모드를 모두 처리합니다. `params`는 `ElicitRequestFormParams`와 `ElicitRequestURLParams`의 유니온이며, `isinstance`로 분기합니다. +* URL 모드에서는 `params.url`을 사용자에게 보여주고 사용자가 선택한 액션을 반환합니다. `content`는 절대 반환하지 않습니다. +* 폼 모드에서는 실제 애플리케이션이라면 `params.requested_schema`를 렌더링하고 사용자의 입력을 `content`로 반환합니다. 여기 있는 콜백은 미리 정해 둔 답으로 항상 수락하는데, 테스트에는 바로 이런 콜백이 적합합니다. +* 콜백을 전달하는 것은 곧 **기능 선언**이기도 합니다. 서버는 이를 통해 이 클라이언트에 질문할 수 있다는 사실을 알게 됩니다. 클라이언트가 서버를 대신해 답할 수 있는 나머지 항목은 **[클라이언트 콜백](../client/callbacks.md)**에서 다룹니다. + +!!! info + 엘리시테이션은 **서버**가 **클라이언트**에게 보내는 요청이고, 이런 요청은 클래식 핸드셰이크 + 세션에서만 존재합니다. 그래서 이 클라이언트는 `mode="legacy"`를 전달합니다. + **2026-07-28** 연결에서는 도구가 호출의 결과로 질문을 **반환**해 묻습니다. + 그 흐름은 **[다중 왕복 요청](multi-round-trip.md)**에서 다룹니다. + +### 직접 해 보기 {#try-it} + +`ctx.elicit` 폼 모드 `server.py`(`book_table`이 있는 쪽)를 Streamable HTTP로 실행하고(한 줄 명령은 **[서버 실행하기](../run/index.md)**에 있습니다), 클라이언트의 `main()`을 실행해 `book_table`에 크리스마스 당일을 요청하세요. + +콜백은 전달받은 질문을 출력합니다. + +```text +No tables for 2 on 2025-12-25. Would you like to try another date? +``` + +콜백이 `{"accept_alternative": True, "date": "2025-12-27"}`으로 답하면, 그동안 계속 `await ctx.elicit(...)` 안에서 기다리던 도구가 예약을 마칩니다. + +```text +Booked a table for 2 on 2025-12-27. +``` + +이제 URL 모드 `server.py`로 바꾸고 같은 `main()`을 `pay_deposit`으로 향하게 해 보세요. 같은 콜백이 다른 분기를 타서 결제 링크를 출력하고, 도구는 **"Complete the payment in your browser."**를 돌려줍니다. 호출 도중에 양방향으로 한 번 왕복한 것입니다. + +!!! check + 이제 `Client`에서 `elicitation_callback=`을 제거하고 크리스마스 당일로 `book_table`을 + 다시 호출해 보세요. 호출 전체가 프로토콜 오류로 실패합니다. + + ```text + Elicitation not supported + ``` + + 콜백을 등록하지 않은 클라이언트는 `elicitation` 기능을 선언하지 않았으므로 물어볼 상대가 + 없습니다. 도구가 받은 것은 `"decline"`이 아니라 예외입니다. 이를 염두에 두고 설계하세요. + 모든 엘리시테이션에는 "물어볼 수 없으면 어떻게 할 것인가"에 대한 합리적인 답이 필요합니다. + +## 정리 {#recap} + +* `Annotated[T, Resolve(fn)]`로 애노테이션된 파라미터는 리졸버가 채우며, 리졸버는 물어봐야 할 때 `Elicit(...)`을 반환합니다. 모든 연결에서 동작합니다. +* 스키마는 평평한 Pydantic 모델입니다. 기본 타입 필드만 쓸 수 있고, 돌아오는 길에 검증됩니다. +* `result.action`은 `"accept"`, `"decline"`, `"cancel"` 중 하나이며, `result.data`는 수락일 때만 존재합니다. +* `await ctx.elicit(message, schema=Model)`은 도구 본문 안에서 묻고, `await ctx.elicit_url(message, url, elicitation_id)`은 모델을 거쳐서는 안 되는 모든 것에 사용합니다(`ctx.session.send_elicit_complete(elicitation_id)`는 대역 외 작업이 끝났음을 알립니다). 둘 다 서버에서 클라이언트로 보내는 요청이므로 클라이언트가 레거시 연결이어야 합니다. +* 클라이언트는 `elicitation_callback` 하나로 답하며, params 타입에 따라 분기합니다. 이 콜백을 등록하는 것이 곧 기능 선언입니다. +* 2026-07-28 연결에서는 서버가 질문을 밀어 보내지 않고 반환하며, 같은 콜백에 질문을 전달하는 것은 **[다중 왕복 요청](multi-round-trip.md)**입니다. + +그 반환 아래에서 벌어지는 모든 것(재시도 루프, `requestState` 보호, 직접 구동하기)은 **[다중 왕복 요청](multi-round-trip.md)**에서 다룹니다. diff --git a/i18n/ko/pages/index.md b/i18n/ko/pages/index.md new file mode 100644 index 0000000000..377517609a --- /dev/null +++ b/i18n/ko/pages/index.md @@ -0,0 +1,97 @@ +# MCP Python SDK {#mcp-python-sdk} + +!!! info "이 문서는 현재 안정 버전 라인인 v2를 다룹니다" + v2를 처음 접하거나 v1에서 넘어왔다면, **[v2에서 달라진 점](whats-new.md)**에서 변경 사항을 5분 만에 훑어볼 수 있고, **[마이그레이션 가이드](migration.md)**에서 모든 호환성 파괴 변경을 확인할 수 있습니다. + 아직 v1.x를 사용 중이라면 해당 문서는 [v1.x 문서](https://py.sdk.modelcontextprotocol.io/v1/)에 있습니다. + 불편하거나 헷갈리는 부분이 있다면 [알려주세요](https://github.com/modelcontextprotocol/python-sdk/issues/new?template=v2-feedback.yaml). + +**Model Context Protocol(MCP)**은 애플리케이션이 표준화된 방식으로 LLM에 컨텍스트를 제공하게 해주며, 컨텍스트를 *제공하는* 일과 LLM과 상호작용하는 일을 분리합니다. + +이 문서는 그 공식 Python SDK를 다룹니다. 이 SDK로 다음을 할 수 있습니다. + +* 모든 MCP 호스트에 도구, 리소스, 프롬프트를 노출하는 **MCP 서버 만들기**. +* 모든 MCP 서버에 연결하는 **MCP 클라이언트 만들기**. +* 모든 표준 트랜스포트 사용하기: stdio, Streamable HTTP, SSE. + +## 요구 사항 {#requirements} + +Python 3.10 이상. + +## 설치 {#installation} + +=== "uv" + + ```bash + uv add "mcp[cli]" + ``` + +=== "pip" + + ```bash + pip install "mcp[cli]" + ``` + +`[cli]` 엑스트라를 설치하면 `mcp` 명령을 사용할 수 있으며, 개발할 때 필요합니다. +각 의존성이 어떤 역할을 하는지는 [설치](get-started/installation.md)에서 확인하세요. + +## 예제 {#example} + +### 만들기 {#create-it} + +`server.py` 파일을 만드세요. + +```python title="server.py" +--8<-- "docs_src/index/tutorial001.py" +``` + +이것으로 완전한 MCP 서버가 완성됩니다. + +이 서버는 **도구** `add` 하나와 템플릿 **리소스** `greeting://{name}` 하나를 노출합니다. + +### 실행하기 {#run-it} + +```console +uv run mcp dev server.py +``` + +이 명령은 서버를 실행하고, 서버를 직접 조작해 볼 수 있는 대화형 UI인 [MCP Inspector](https://github.com/modelcontextprotocol/inspector)를 엽니다. 출력된 URL을 여세요. + +!!! note + Inspector는 Node.js 앱이므로 `mcp dev`를 실행하려면 `PATH`에 `npx`가 있어야 합니다. + +### 사용해 보기 {#try-it} + +Inspector에서 **Tools**로 이동해 `a=1`, `b=2`로 `add`를 호출하세요. + +`3`이 돌아옵니다. + +Inspector는 타입 힌트를 보고 그 폼(`a`에 필요한 정수 필드 하나, `b`에 또 하나)을 만들었습니다. Claude를 비롯한 다른 모든 MCP 호스트도 마찬가지로 동작합니다. + +이제 **Resources**로 이동해 `greeting://World`를 읽어 보세요. + +```text +Hello, World! +``` + +### 정리 {#recap} + +작성하지 **않은** 것을 다시 살펴보세요. + +* JSON Schema가 없습니다. `a: int, b: int`가 *곧* 스키마입니다. +* 요청 파싱도, 직렬화도, 검증 코드도 없습니다. +* 프로토콜 처리 코드는 전혀 없습니다. + +타입 힌트와 독스트링이 있는 Python 함수 두 개를 작성했을 뿐입니다. 나머지는 SDK가 처리합니다. + +## 다음으로 볼 내용 {#where-to-go-next} + +* **[시작하기](get-started/index.md)**는 설치부터 동작하고 테스트까지 마친 서버까지 안내합니다. +* MCP 서버를 *사용하는* 애플리케이션을 만든다면 **[클라이언트](client/index.md)**부터 시작하세요. +* 이미 FastAPI나 Starlette 앱이 있다면, **[기존 앱에 추가하기](run/asgi.md)**에서 그 안에 MCP 서버를 마운트할 수 있습니다. +* 특정 오류 메시지를 찾고 있다면, **[문제 해결](troubleshooting.md)**이 메시지 원문을 기준으로 정리되어 있습니다. +* v2에서 무엇이 바뀌었는지 궁금하다면, **[v2에서 달라진 점](whats-new.md)**에서 5분 만에 훑어볼 수 있습니다. +* v1에서 마이그레이션한다면 **[마이그레이션 가이드](migration.md)**부터 시작하세요. +* 정확한 시그니처를 찾고 있다면, **[API 레퍼런스](api/mcp/index.md)**가 소스에서 생성되어 있습니다. +* LLM과 함께 읽고 있다면, 이 문서는 [llms.txt](https://llmstxt.org/) 형식으로도 제공됩니다. + [llms.txt](https://py.sdk.modelcontextprotocol.io/llms.txt)는 페이지 목록이고, + [llms-full.txt](https://py.sdk.modelcontextprotocol.io/llms-full.txt)에는 모든 페이지가 하나의 파일에 담겨 있습니다. diff --git a/i18n/ko/state.json b/i18n/ko/state.json new file mode 100644 index 0000000000..27b839bef9 --- /dev/null +++ b/i18n/ko/state.json @@ -0,0 +1,164 @@ +{ + "pages": { + "get-started/first-steps.md": { + "block_hashes": [ + "f4e9527fee44ad246985003e8319e71c2b72f28e3301ee128f3d2c0e3f6d264d", + "3e1766d27f443ca4bbccdda3389f3dff2d2b6737133564ee0ca133a5dbfd958d", + "2a97f1d75b49d9a9b70e82ce3ad0ee70072b2676db3248614ae2ba29bc3fff28", + "59dc6c66de1c4ea589e4b984889814b842613605c638ae450db4fa44243aee06", + "ee42a5e18f8e868b5d29bb9db85915406e7e97e789013ee108ee2d199626d274", + "7d8344d6cf1ecfc4130812f643a148ea572ad7d319b371029197fe162d513602", + "cfcd0cd4aed4f275cb8cf632eed301192aed12aeb11c10c77fabab2fe0fc7b30" + ], + "inputs_hash": "7b1e7bbf5eab17443d0f3afe129549118e19d3dc6d408ac1704c19e59c3a09ca", + "model": "claude-opus-5", + "source_hash": "3531c5686a6ff79aabe6f0f8a3cb96213b6fc13b0bbbb0e762dc9f4a827b4be3", + "translated_at": "2026-07-31T19:11:09Z" + }, + "handlers/elicitation.md": { + "block_hashes": [ + "f5dade066115eb33121f7bbb02a3fb288e95e2662001bea5eac322e23f4cbd1d", + "c368d8a40fd8d9879d90fbb10a81870fdb3ca84675bcf04c91740febe56ed4c4", + "191a9cfc619f523acf23ef20a04a9a318abe5280bb459f4691dfc2c60bc8dc34", + "ed47c27010f781038cd2095fedb17be05b16a9c2b1b93650416dd7e1be3154b7", + "4db8b6b1183f96319677911fd22baa3e65faf1b2f107b7d0929a3ee39ebd3ed4", + "0265da2c81bc50cf3a83415aa16c1ded414fe59a3e23e4c0b1a83cd03d41ba5c" + ], + "inputs_hash": "7b1e7bbf5eab17443d0f3afe129549118e19d3dc6d408ac1704c19e59c3a09ca", + "model": "claude-opus-5", + "source_hash": "ef8afd716d136ff25d17a34059a933bb924ea238bd2faf04f80cc0d337c2e520", + "translated_at": "2026-07-31T19:11:09Z" + }, + "index.md": { + "block_hashes": [ + "3690a6eb6f57324f620f65d5242b3bb17c0ed4a5657cd4ba7ff3a28d7291245a", + "13e00a4af763653d8c663c64dd27871bdd23314d11110a143b8ee77357f76a75", + "cfcabc919e5d343b47af2641656a86f9a0abe9f250547368e64f7e4b5a3de4b0", + "c1550578c7bd96be08abb210e1c86261127c78ee09c1d8d666d08fa96271c30b", + "71115a8dc82e7ee1cd61b609da087038d8736629ebb365972c88010fd677cc00" + ], + "inputs_hash": "7b1e7bbf5eab17443d0f3afe129549118e19d3dc6d408ac1704c19e59c3a09ca", + "model": "claude-opus-5", + "source_hash": "c46eefb49de18ed47882e13cf5bf08d0c3ca16ee77b98c257e8b66150c6be666", + "translated_at": "2026-07-31T21:09:57Z" + } + }, + "ui": { + "inputs_hash": "7b1e7bbf5eab17443d0f3afe129549118e19d3dc6d408ac1704c19e59c3a09ca", + "model": "claude-opus-5", + "source": { + "@banner:disclosure": "??? info \"This page was machine-translated\"\n Translations of this documentation are generated automatically from the English pages, and the [English version of this page]({english_url}) is the authoritative one.\n\n Found a translation problem? See [how the translations work and how to report an issue]({translations_url}).\n", + "@banner:outdated": "!!! warning \"This translation may be behind the English page\"\n The English source changed after this page was last translated, so parts of it may be out of date. When in doubt, read the [English version of this page]({english_url}).\n", + "@banner:untranslated": "!!! note \"In English\"\n You're reading this page in English. [How the translations work]({translations_url}).\n", + "API Reference": "API Reference", + "Add to an existing app": "Add to an existing app", + "Advanced": "Advanced", + "Authorization": "Authorization", + "Caching": "Caching", + "Callbacks": "Callbacks", + "Clients": "Clients", + "Completions": "Completions", + "Connect to a real host": "Connect to a real host", + "Dependencies": "Dependencies", + "Deploy & scale": "Deploy & scale", + "Deprecated features": "Deprecated features", + "Elicitation": "Elicitation", + "Extensions": "Extensions", + "First steps": "First steps", + "Get started": "Get started", + "Handling errors": "Handling errors", + "Identity assertion": "Identity assertion", + "Images, audio & icons": "Images, audio & icons", + "Inside your handler": "Inside your handler", + "Installation": "Installation", + "Lifespan": "Lifespan", + "Logging": "Logging", + "MCP Apps": "MCP Apps", + "MCP Python SDK": "MCP Python SDK", + "Middleware": "Middleware", + "Migration Guide": "Migration Guide", + "Multi-round-trip requests": "Multi-round-trip requests", + "Multiple servers": "Multiple servers", + "OAuth": "OAuth", + "OpenTelemetry": "OpenTelemetry", + "Pagination": "Pagination", + "Progress": "Progress", + "Prompts": "Prompts", + "Protocol versions": "Protocol versions", + "Resources": "Resources", + "Running your server": "Running your server", + "Sampling and roots": "Sampling and roots", + "Servers": "Servers", + "Serving legacy clients": "Serving legacy clients", + "Structured Output": "Structured Output", + "Subscriptions": "Subscriptions", + "Testing": "Testing", + "The Context": "The Context", + "The low-level Server": "The low-level Server", + "Tools": "Tools", + "Translations": "Translations", + "Transports": "Transports", + "Troubleshooting": "Troubleshooting", + "URI templates": "URI templates", + "What's new in v2": "What's new in v2" + }, + "source_hash": "219aa0f5adfe57c4403a1b6fdf04d4e2cc79df194bdbcb30958d24e7bb387433", + "strings": { + "@banner:disclosure": "??? info \"이 페이지는 기계 번역되었습니다\"\n 이 문서의 번역은 영어 페이지에서 자동으로 생성되며, [이 페이지의 영어 버전]({english_url})이 기준입니다.\n\n 번역에서 문제를 발견했다면 [번역이 동작하는 방식과 문제를 보고하는 방법]({translations_url})을 확인하세요.\n", + "@banner:outdated": "!!! warning \"이 번역은 영어 페이지보다 오래되었을 수 있습니다\"\n 이 페이지를 마지막으로 번역한 이후 영어 원문이 변경되어 일부 내용이 오래되었을 수 있습니다. 확실하지 않다면 [이 페이지의 영어 버전]({english_url})을 읽어 보세요.\n", + "@banner:untranslated": "!!! note \"영어 페이지\"\n 이 페이지는 영어로 표시되고 있습니다. [번역이 동작하는 방식]({translations_url}).\n", + "API Reference": "API 레퍼런스", + "Add to an existing app": "기존 앱에 추가하기", + "Advanced": "고급", + "Authorization": "인가", + "Caching": "캐싱", + "Callbacks": "콜백", + "Clients": "클라이언트", + "Completions": "자동 완성", + "Connect to a real host": "실제 호스트에 연결하기", + "Dependencies": "의존성", + "Deploy & scale": "배포와 확장", + "Deprecated features": "지원 중단 예정 기능", + "Elicitation": "엘리시테이션", + "Extensions": "확장", + "First steps": "첫걸음", + "Get started": "시작하기", + "Handling errors": "오류 처리", + "Identity assertion": "신원 어설션", + "Images, audio & icons": "이미지, 오디오, 아이콘", + "Inside your handler": "핸들러 내부", + "Installation": "설치", + "Lifespan": "Lifespan", + "Logging": "로깅", + "MCP Apps": "MCP Apps", + "MCP Python SDK": "MCP Python SDK", + "Middleware": "미들웨어", + "Migration Guide": "마이그레이션 가이드", + "Multi-round-trip requests": "다중 왕복 요청", + "Multiple servers": "다중 서버", + "OAuth": "OAuth", + "OpenTelemetry": "OpenTelemetry", + "Pagination": "페이지네이션", + "Progress": "진행 상황", + "Prompts": "프롬프트", + "Protocol versions": "프로토콜 버전", + "Resources": "리소스", + "Running your server": "서버 실행", + "Sampling and roots": "샘플링과 루트", + "Servers": "서버", + "Serving legacy clients": "레거시 클라이언트 지원", + "Structured Output": "구조화된 출력", + "Subscriptions": "구독", + "Testing": "테스트", + "The Context": "Context", + "The low-level Server": "저수준 Server", + "Tools": "도구", + "Translations": "번역", + "Transports": "트랜스포트", + "Troubleshooting": "문제 해결", + "URI templates": "URI 템플릿", + "What's new in v2": "v2에서 달라진 점" + }, + "translated_at": "2026-07-31T21:09:57Z" + } +} diff --git a/i18n/languages.yml b/i18n/languages.yml new file mode 100644 index 0000000000..dcd4bdb247 --- /dev/null +++ b/i18n/languages.yml @@ -0,0 +1,46 @@ +# The translated documentation sites: English at docs/ is the source and each +# entry below is one machine-translated site published next to it at //. +# Read by the docs build (scripts/docs/build_config.py) and by the translation +# tool (scripts/docs/translations.py). docs/translations.md and the translation +# issue form name the languages too — keep them in step when adding one. + +languages: + - code: zh-CN # directory under i18n/ and the site's URL prefix + name: 简体中文 # native name shown in the language switcher + theme_language: zh # the theme's `theme.language` (UI strings, search) + hreflang: zh-Hans # announced in + - code: ja + name: 日本語 + - code: ko + name: 한국어 + - code: pt-BR + name: Português (Brasil) + +# Pages the language sites serve in English (as written in mkdocs.yml's nav; +# a trailing /** covers a subtree). The API reference is never translated: the +# language sites link the single English one. +exclude_pages: + - migration.md + +# Publicly documented Claude model IDs for the translation and the meaning +# review; the reviewer is deliberately the stronger of the two. +models: + translate: claude-opus-5 + verify: claude-fable-5 + +# The notices staged onto every page of a language site, in English; the +# tool translates them with each language's UI strings. {english_url} is the +# page's English original and {translations_url} the page explaining how the +# translated sites work. +banners: + disclosure: | + ??? info "This page was machine-translated" + Translations of this documentation are generated automatically from the English pages, and the [English version of this page]({english_url}) is the authoritative one. + + Found a translation problem? See [how the translations work and how to report an issue]({translations_url}). + outdated: | + !!! warning "This translation may be behind the English page" + The English source changed after this page was last translated, so parts of it may be out of date. When in doubt, read the [English version of this page]({english_url}). + untranslated: | + !!! note "In English" + You're reading this page in English. [How the translations work]({translations_url}). diff --git a/i18n/pt-BR/glossary.json b/i18n/pt-BR/glossary.json new file mode 100644 index 0000000000..f18caee835 --- /dev/null +++ b/i18n/pt-BR/glossary.json @@ -0,0 +1,232 @@ +{ + "version": 1, + "keep_in_source_language": [ + "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": "ferramenta", + "note": "MCP protocol noun (a server exposes tools), feminine: a ferramenta / as ferramentas. Wire identifiers such as `tools/call` and the `@mcp.tool()` decorator are code and stay untouched. First mention on a page may read \"ferramenta (tool)\". Provisional pending native review.", + "avoid": [], + "enforce": false + }, + { + "source": "resource", + "target": "recurso", + "note": "MCP protocol noun (data a server exposes for reading), masculine: o recurso / os recursos. `resources/read` and `@mcp.resource()` are code. Provisional pending native review.", + "avoid": [], + "enforce": false + }, + { + "source": "prompt", + "target": "prompt", + "note": "MCP protocol noun for the reusable message templates a server exposes, and the general AI sense; kept in English as Brazilian AI writing does. Masculine: o prompt / os prompts. `prompts/get` and `@mcp.prompt()` are code. Provisional pending native review.", + "avoid": [], + "enforce": false + }, + { + "source": "sampling", + "target": "amostragem", + "note": "MCP feature where the server asks the client for an LLM completion. First mention on a page reads \"amostragem (sampling)\" so the reader can map it to `sampling/createMessage`, which is code. Keeping the English word instead is the open alternative — provisional pending native review.", + "avoid": [], + "enforce": false + }, + { + "source": "roots", + "target": "roots", + "note": "MCP client capability (the directories a client exposes to the server); kept in English because readers meet it as the identifier `roots/list`. Masculine plural: os roots. May take the gloss \"roots (diretórios raiz)\" on first mention. Translating it as raízes is the open alternative — provisional pending native review.", + "avoid": ["raízes"], + "enforce": false + }, + { + "source": "elicitation", + "target": "elicitação", + "note": "MCP feature where the server asks the user a question through the client (`elicitation/create`, `ctx.elicit()` are code). The Portuguese noun is rare but exact; first mention on a page reads \"elicitação (elicitation)\". Feminine: a elicitação. Provisional pending native review.", + "avoid": [], + "enforce": false + }, + { + "source": "capability", + "target": "capacidade", + "note": "What client and server declare during initialization (\"negociação de capacidades\"). Note that funcionalidade is the word for a feature, which is a different thing. Provisional pending native review.", + "avoid": ["habilidade"], + "enforce": false + }, + { + "source": "transport", + "target": "transporte", + "note": "The connection layer. The individual transports — stdio, Streamable HTTP, SSE — are names on the keep list and stay in English (\"o transporte stdio\"). Provisional pending native review.", + "avoid": [], + "enforce": false + }, + { + "source": "session", + "target": "sessão", + "note": "Feminine: a sessão / as sessões (\"ID de sessão\"). The `session` object, `ClientSession` and `ServerSession` are code and stay Latin. Provisional pending native review.", + "avoid": [], + "enforce": false + }, + { + "source": "handler", + "target": "handler", + "note": "The functions you register on a server; kept in English as everyday Brazilian developer usage does. Masculine: o handler / os handlers. \"manipulador\" reads dated in this audience — provisional pending native review.", + "avoid": ["manipulador"], + "enforce": false + }, + { + "source": "dependency", + "target": "dependência", + "note": "Both package dependencies and the SDK's dependency injection — parameters declared with `Resolve(...)` (\"injeção de dependência\"). Feminine: a dependência. Provisional pending native review.", + "avoid": [], + "enforce": false + }, + { + "source": "resolver", + "target": "resolvedor", + "note": "The plain function that fills a `Resolve(...)`-annotated parameter before the tool runs. Rendered as the noun \"resolvedor\" (masculine: o resolvedor) because in Portuguese \"resolver\" is a verb; keeping the English noun is the open alternative — provisional pending native review.", + "avoid": [], + "enforce": false + }, + { + "source": "client", + "target": "cliente", + "note": "Masculine: o cliente. The `Client` class and the `mcp.client` module are code and stay Latin. Provisional pending native review.", + "avoid": [], + "enforce": false + }, + { + "source": "server", + "target": "servidor", + "note": "Masculine: o servidor. The `MCPServer`, `Server` and `ServerSession` classes are code and stay Latin. Provisional pending native review.", + "avoid": [], + "enforce": false + }, + { + "source": "host", + "target": "host", + "note": "The MCP host — the application the user talks to (Claude Desktop, an IDE) — kept in English as Brazilian developers say it. Masculine: o host / os hosts. Provisional pending native review.", + "avoid": ["anfitrião", "hospedeiro"], + "enforce": true + }, + { + "source": "request", + "target": "requisição", + "note": "HTTP and JSON-RPC requests (\"requisição HTTP\", \"a requisição de inicialização\"). Feminine: a requisição. solicitação is understood too, but use requisição throughout rather than alternating. Provisional pending native review.", + "avoid": ["pedido"], + "enforce": false + }, + { + "source": "token", + "target": "token", + "note": "Kept in English for both OAuth tokens (\"token de acesso\", \"refresh token\") and LLM tokens. Masculine: o token / os tokens. Provisional pending native review.", + "avoid": ["ficha"], + "enforce": false + }, + { + "source": "lifespan", + "target": "lifespan", + "note": "The SDK feature and its `lifespan=` parameter; kept in English so the prose matches the parameter name. Masculine: o lifespan. May take the gloss \"lifespan (ciclo de vida do servidor)\" on first mention. Provisional pending native review.", + "avoid": ["expectativa de vida", "vida útil"], + "enforce": true + }, + { + "source": "callback", + "target": "callback", + "note": "Kept in English, covering both OAuth redirect callbacks and function callbacks. Masculine: o callback / os callbacks. Provisional pending native review.", + "avoid": ["retorno de chamada"], + "enforce": false + }, + { + "source": "deploy", + "target": "deploy", + "note": "Borrow the noun, not a verb: \"o deploy\", \"fazer o deploy\", \"depois do deploy\". Masculine. \"implantação\" is acceptable in formal contexts but pin \"deploy\" for consistency. Provisional pending native review.", + "avoid": ["deployar", "deployado"], + "enforce": true + }, + { + "source": "library", + "target": "biblioteca", + "note": "Feminine: a biblioteca. The false friend \"livraria\" means a bookshop and is never right here.", + "avoid": ["livraria"], + "enforce": true + }, + { + "source": "back-channel", + "target": "canal de retorno", + "note": "This documentation's term for the server calling back into the client during a request. First mention on a page reads \"canal de retorno (back-channel)\" so the reader can connect it to the `NoBackChannelError` exception, which is code. Provisional pending native review.", + "avoid": [], + "enforce": false + }, + { + "source": "file", + "target": "arquivo", + "note": "Masculine: o arquivo. \"ficheiro\" is European Portuguese and is always an error in this Brazilian Portuguese target.", + "avoid": ["ficheiro"], + "enforce": true + }, + { + "source": "user", + "target": "usuário", + "note": "Masculine as the generic form: o usuário / os usuários. \"utilizador\" is European Portuguese and is always an error in this Brazilian Portuguese target.", + "avoid": ["utilizador"], + "enforce": true + } + ] +} diff --git a/i18n/pt-BR/instructions.md b/i18n/pt-BR/instructions.md new file mode 100644 index 0000000000..4fafc0b070 --- /dev/null +++ b/i18n/pt-BR/instructions.md @@ -0,0 +1,190 @@ +# Brazilian Portuguese (pt-BR) — translation instructions + +Target language: Brazilian Portuguese (Português do Brasil), directory and +URL code `pt-BR`, page language tag `pt-BR`. This file is sent verbatim with +every translation request for this language, on top of the shared translation +rules in `../general-prompt.md`. The termbase in `glossary.json` is sent +alongside it and wins any terminology conflict with this file. + +## 1. Register + +Write the casual-neutral register Brazilian developer documentation uses: +professional, relaxed, and direct. + +- Address the reader as **você**, with third-person-singular verb forms to + match. Never o senhor / a senhora, never tu, never vós, and never a mix. + The rule holds in body prose, headings, admonition titles, table cells and + link text. +- Instructions and steps are direct imperatives in the você form: "Install + the SDK, then run the server" → Instale o SDK e depois execute o servidor — + not Instala o SDK (tu form), and not Você deve instalar o SDK (needless + modal). A bare imperative per step is fine; a por favor in front of every + step is not. +- Portuguese drops the subject pronoun freely. Write você where a sentence + needs an explicit subject or a contrast, and let the verb carry the person + otherwise; three or four você in one paragraph is a signal to rephrase. + Object pronouns follow the same person: para você / a você, never the + tu-form te / ti. +- The authorial "we" is nós (Recomendamos, chamamos), never the spoken + a gente. +- The register is uniform across a page. A page that drifts between você and + o senhor, or between direct imperatives and an impersonal officialese voice, + is wrong even when each sentence is acceptable on its own. +- This is Brazilian Portuguese only. Every European Portuguese form is an + error here: + - vocabulary: arquivo (never ficheiro), tela (never ecrã), usuário (never + utilizador), salvar (never guardar), excluir / apagar (never eliminar for + "delete"), baixar (never transferir / descarregar for "download"), mouse + (never rato), site (never sítio); + - grammar: the progressive is estar + gerund — o servidor está rodando — + never estar a + infinitive (está a rodar, está a correr); + - spelling: the post-1990 orthography — ação, ótimo, ideia — never acção, + óptimo, idéia. + +## 2. Voice + +Aim for the voice of an experienced Brazilian engineer explaining a library +to a colleague: warm, direct, plain-spoken. The English is built on short +declarative payoff sentences ("That's the whole API."); keep them short — Essa +é a API inteira. + +Do: + +- Follow Portuguese rhythm. Split a long English sentence into two Portuguese + ones instead of mirroring its clause chain, and use everyday connectives + (então, ou seja, por isso) where they help the reader along. +- Use concrete verbs (executar, passar, retornar, declarar, bloquear) rather + than nominal chains: fazer a execução de → executar. +- Keep the source's directness. Where the English says "don't", the + Portuguese says não faça isso / não use, not a hedge like talvez seja + interessante evitar. + +Avoid — these are the marks of a machine or bureaucratic translation: + +- Officialese and legalistic filler: o presente documento, supracitado, + outrossim, faz-se necessário, deve-se ressaltar que, and o mesmo used as a + pronoun. +- Gerundismo: vamos estar mostrando → vamos mostrar; irá estar retornando → + vai retornar. +- Verbified anglicisms from spoken developer slang: deployar, commitar, + buildar, startar, mergear. Write fazer o deploy, fazer commit, gerar o build, + iniciar, fazer o merge. +- English-shaped Portuguese: calqued idioms (sob o capô for "under the hood" — + the Brazilian phrase is por baixo dos panos; no fim do dia for "at the end + of the day" — say no fim das contas), possessive chains, and passives where + an active sentence is natural ("The tool is called by the model" → o modelo + chama a ferramenta, not a ferramenta é chamada pelo modelo). +- Marketing hype and stacked exclamation marks. Keep an exclamation mark only + where the English one carries genuine emphasis. + +## 3. Humour and idioms + +The English is friendly and dry rather than jokey — short payoff sentences, a +few stock phrases, the rare emoji — and Brazilian technical writing is warm by +default, so most of that carries over unchanged. The idioms still need +recasting. + +- Never translate a pun, idiom or aside literally. Say what it means as a + short, natural Brazilian sentence in the same register. Where a common + Brazilian idiom happens to carry the same meaning, use it; where nothing + fits, use the plain statement. If an aside carries no information you may + drop it — but never drop a technical caveat that happens to be phrased + lightly. +- Recurring English tags get fixed renderings: "**[X](…)** has the whole + story" / "The whole story is in **[X](…)**" → **[X](…)** tem a história + completa; "That's the whole API." / "That's the whole protocol." → A API + inteira é essa. / O protocolo inteiro é esse.; "That's it. It's just + Python." → É só isso. É apenas Python. +- Idioms take the plain meaning, not the picture: "Out of the box the app + answers **only** requests addressed to localhost." → Por padrão, o app + responde **apenas** a requisições endereçadas ao localhost — not a calqued + fora da caixa. +- Culture-bound references (US sports, TV shows, holidays) → the plain + meaning. +- Emoji: keep the source's rare, deliberately placed emoji exactly where they + are — two payoff lines end in ✨ ("You get `3` back. ✨"). Never add new + ones. + +Worked examples (source → good / bad): + +- "You get `3` back. ✨" → good: Você recebe `3` de volta. ✨ / bad: Você + recebe `3` de volta! ✨ (added exclamation mark). +- "Give a parameter a default value and it stops being required. That's it. + It's just Python." → good: Dê um valor padrão a um parâmetro e ele deixa de + ser obrigatório. É só isso. É apenas Python. / bad: Dê um valor default + para um parâmetro e ele para de ser requerido. É isso aí, é só Python! ✨ + (untranslated default and requerido, slangy tag, added exclamation and + emoji). + +## 4. Typography + +- Prose punctuation is standard Brazilian usage written with the same + characters the source uses: keep straight double quotes ("…") and + apostrophes as they are; do not switch to «guillemets» or “curly quotes”; no + inverted ¿ ¡; no space before ! ? : ; (that is a French convention). +- Sentence case for headings, admonition titles and content-tab labels: + capitalise the first word and proper nouns only (Configurando o transporte, + not Configurando O Transporte). Language names, months and weekdays are + lower-case in Portuguese (a versão em inglês, em julho); proper nouns stay + capitalised (Python, GitHub, Claude Desktop). +- Digits stay ASCII. Protocol revision strings such as `2026-07-28` and + `2025-11-25` are identifiers, copied byte-for-byte — never 28/07/2026, + never 28 de julho de 2026. Version numbers, HTTP status codes, ports, error + codes, and RFC and SEP numbers are copied exactly. +- Ordinary prose quantities take the decimal comma only when nothing but the + separator changes (a timeout of 2.5 seconds → um timeout de 2,5 segundos); + when in doubt, keep the number as the source writes it. A space separates a + number from a Latin unit (100 MB, 30 s); % attaches with no space (100%). +- Latin abbreviations: e.g. → por exemplo, i.e. → ou seja / isto é; etc. + stays etc.; vs → versus, or ou / contra when a plain word reads better. + Where the English uses & in prose, write e. +- Loanwords kept in English are set in normal type — no italics, no scare + quotes — and take a Portuguese article: o handler, os tokens, a string. + Bold and italics land on the same words the source emphasises; a bolded + negation ("**not**" → **não**) stays bold. +- Ordinals use the indicators º / ª (1º, 2ª). Keep the source's dashes, + colons and parentheses as they are; do not turn a colon into a travessão + or the reverse. + +## 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 listed under `keep_in_source_language` are copied exactly as they + appear in the English source — same spelling, casing and plural "s" (SDKs + stays SDKs). They are not translated, italicised, re-cased or wrapped in + quotes. +- Everything in code font — class, function, method, parameter and module + names, protocol method strings (`tools/call`, `notifications/...`), header + names, error text, config keys — stays byte-identical. You may put a + Portuguese article or the word for the kind of thing in front of it: a + classe `Context`, o parâmetro `lifespan=`, o método `session.list_tools()`. + A glossary term used as a code-font identifier stays Latin even though its + prose noun is translated: "the `sampling` capability" → a capacidade + `sampling`. +- English technical nouns that stay in English keep their English spelling, + take a fixed grammatical gender, and pluralise the Brazilian way (add "s"). + Masculine by default — o token / os tokens, o handler, o callback, o host, + o schema, o payload, o endpoint, o log, o loop, o build, o commit, o deploy, + o prompt, o middleware — feminine where usage is settled: a string, a + thread, a query, a flag, a tag, a URL, a API, a issue. Where a glossary + entry's note gives a gender, it wins. +- Nouns are borrowed, verbs are not: fazer o deploy, fazer commit, fazer o + merge — never deployar, commitar, mergear (see §2). +- First-use gloss: a translated MCP concept the reader may need to map back to + the English specification carries the English in parentheses on its first + occurrence on a page — elicitação (elicitation) — and appears alone after + that. Each glossary entry's note says whether the term takes the gloss. +- 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 rather than picking per sentence. + +## 6. Provisional note + +The register, voice and terminology decisions above are provisional, +pending review by native Brazilian Portuguese-speaking readers. To propose a +change, edit this file or `glossary.json` in a pull request — ideally with a +short good/bad example when the change is about phrasing; never edit the +generated pages under `pages/`, which the next translation run overwrites. diff --git a/i18n/pt-BR/pages/get-started/first-steps.md b/i18n/pt-BR/pages/get-started/first-steps.md new file mode 100644 index 0000000000..71a9d25546 --- /dev/null +++ b/i18n/pt-BR/pages/get-started/first-steps.md @@ -0,0 +1,139 @@ +# Primeiros passos {#first-steps} + +A **[página inicial](../index.md)** vai rápido: escreva um servidor, execute-o, chame uma ferramenta. + +Esta página vai devagar, com as três coisas que um servidor pode expor e um nome para cada uma delas pelo caminho. + +## Host, cliente e servidor {#host-client-and-server} + +Três palavras que você vai ver em todas as páginas daqui em diante: + +* Um **host** é a aplicação de LLM: Claude, uma IDE, um runtime de agente. É com ele que o usuário conversa. +* Um **cliente** vive dentro do host e fala MCP. O host roda um cliente para cada servidor ao qual está conectado. +* Um **servidor** é o que você constrói com este SDK. Ele expõe coisas para os clientes. Nunca fala diretamente com o modelo. + +Você escreve o servidor. Os hosts são produto de outra pessoa. O SDK também te dá um `Client`. Você vai usá-lo para testar seus servidores, e ele aparece mais adiante nesta página. + +## As três primitivas {#the-three-primitives} + +Um servidor expõe exatamente três tipos de coisa. O que as separa é **quem decide usá-las**: + +| Primitiva | Controlada por | O que é | Exemplo | +|----------------|-----------------|-------------------------------------------------------|----------------------------------------| +| **Ferramentas** | O modelo | Uma função que o modelo chama para executar uma ação | Uma chamada de API, uma escrita no banco | +| **Recursos** | A aplicação | Dados que o host carrega no contexto do modelo | O conteúdo de um arquivo, uma resposta de API | +| **Prompts** | O usuário | Um template de mensagem reutilizável que o usuário invoca pelo nome | Um comando de barra, um item de menu | + +"Controlada por" é justamente o ponto da divisão. Uma ferramenta roda porque o **modelo** decidiu chamá-la. Um recurso é anexado porque a **aplicação** decidiu que o modelo precisava dele. Um prompt roda porque o **usuário** o escolheu. + +!!! info + Se você já construiu uma API web, já tem quase toda a intuição: um **recurso** é um `GET` + (carrega dados e não muda nada) e uma **ferramenta** é um `POST` (faz trabalho e pode ter + efeitos colaterais). Um **prompt** não tem análogo em HTTP; está mais perto de uma query salva + que o usuário executa pelo nome. + +## Um servidor, os três {#one-server-all-three} + +```python title="server.py" hl_lines="6 12 18" +--8<-- "docs_src/first_steps/tutorial001.py" +``` + +Três funções simples, três decoradores. Cada decorador é o registro inteiro: + +* `@mcp.tool()` transforma `add` em uma **ferramenta**. +* `@mcp.resource("greeting://{name}")` transforma `greeting` em um **template de recurso**: o `{name}` na URI é o parâmetro da função. +* `@mcp.prompt()` transforma `summarize` em um **prompt**. A string que ele retorna vira uma mensagem do usuário. + +Todo o resto (o nome, a descrição, o schema dos argumentos) o SDK lê da própria função: o nome dela, a docstring, as type hints. Você nunca declarou nada disso separadamente. + +!!! tip + As duas metades do SDK têm dois caminhos de import: `from mcp import Client` e + `from mcp.server import MCPServer`. Não existe `from mcp import MCPServer`. + +### Experimente {#try-it} + +Execute com o MCP Inspector: + +```console +uv run mcp dev server.py +``` + +Abra a URL que ele imprime. O Inspector tem uma aba por primitiva; percorra-as na ordem. + +**Ferramentas.** Uma entrada: `add`, descrita como *Add two numbers.* O formulário tem um campo inteiro obrigatório para `a` e outro para `b`. Preencha, chame e o resultado é `3`. O Inspector montou esse formulário a partir de `a: int, b: int`. Todo outro cliente faz o mesmo. + +**Recursos.** A lista *Resources* está vazia. `greeting` está em **Resource Templates**, porque `greeting://{name}` tem um parâmetro: não há um recurso único para listar até que alguém forneça um `name`. Dê `World` a ele e leia: + +```text +Hello, World! +``` + +**Prompts.** Uma entrada: `summarize`, com um único argumento obrigatório `text`. Obtenha-o com algum texto e você recebe uma mensagem com `role: user` e a sua string renderizada como conteúdo. Um prompt é só isso: uma função que monta mensagens. + +O Inspector executou seu servidor sobre **stdio**, um dos transportes que um servidor MCP pode falar. Você ainda não escolhe um; **[Executando seu servidor](../run/index.md)** é a página para isso. + +## Capacidades {#capabilities} + +Você viu três abas no Inspector. Como ele soube que eram três? + +Quando um cliente se conecta, o servidor declara suas **capacidades**: quais famílias de requisições ele vai atender. O cliente usa essa declaração para decidir o que sequer pedir. Você nunca escreveu isso; o `MCPServer` declara por você. + +Veja você mesmo. O `Client` do SDK aceita o objeto do servidor diretamente e se conecta a ele **em memória** (sem subprocesso, sem porta): + +```python +import asyncio + +from mcp import Client + +from server import mcp + + +async def main() -> None: + async with Client(mcp) as client: + print(client.server_capabilities.model_dump(exclude_none=True)) + + +asyncio.run(main()) +``` + +```text +{'prompts': {'list_changed': True}, 'resources': {'subscribe': True, 'list_changed': True}, 'tools': {'list_changed': True}} +``` + +Esse dicionário são as **capacidades** declaradas do seu servidor. É a primeira coisa que todo cliente que se conecta descobre: + +| Capacidade | O cliente agora pode chamar | +|-------------|--------------------------------------------------------------| +| `tools` | `tools/list`, `tools/call` | +| `resources` | `resources/list`, `resources/templates/list`, `resources/read` | +| `prompts` | `prompts/list`, `prompts/get` | + +O `MCPServer` serve as três primitivas, então as três são sempre declaradas. + +Repare no que não está lá. `completions` (autocompletar de argumentos para templates de recursos e prompts) precisa de um handler que você escreve, este servidor não tem um, então a capacidade está ausente e um cliente bem-comportado não vai pedir. Essa é a regra para tudo que é opcional: registre a coisa e a capacidade aparece; **[Completions](../servers/completions.md)** prova isso. + +!!! info + `Client(mcp)` é o mesmo cliente em memória com o qual todo exemplo destes docs é testado, e + é assim que você vai testar os seus. Ele tem uma página inteira: **[Testes](testing.md)**. + +## O que você não escreveu {#what-you-did-not-write} + +Revise esta página. Você escreveu três pequenas funções Python. Você **não** escreveu: + +* Um JSON Schema. `a: int, b: int` *é* o schema de `add`. +* Um handler de requisições. `tools/list`, `resources/read`, `prompts/get`: todos servidos para você. +* Uma declaração de capacidades. O `MCPServer` fez isso por você. +* Uma linha de protocolo. A negociação de versão, o framing JSON-RPC, a troca de capacidades: tudo aconteceu dentro de `mcp dev` e `Client(mcp)`, e você nunca viu nada disso. + +Essa proporção é exatamente o propósito do SDK. + +## Resumo {#recap} + +* Um **host** é o app de LLM, um **cliente** é a metade dele que fala MCP, um **servidor** é o que você constrói. +* Ferramentas são controladas pelo **modelo**, recursos pela **aplicação**, prompts pelo **usuário**. +* Um decorador por primitiva: `@mcp.tool()`, `@mcp.resource(uri)`, `@mcp.prompt()`. Nome, descrição e schema vêm da função. +* Uma URI com um `{param}` cria um **template** de recurso, listado separadamente dos recursos concretos. +* As **capacidades** do servidor são declaradas por você automaticamente, e um cliente só pede o que um servidor declara. +* `Client(mcp)` se conecta ao objeto do servidor em memória: seu ambiente de testes desde o primeiro dia. + +O próximo passo é **[Conectar a um host real](real-host.md)**: este servidor dentro do Claude Desktop ou de uma IDE, de verdade. Depois **[Testes](testing.md)**: uma página, um cliente em memória, e você nunca mais fica adivinhando se funciona. Em seguida, cada primitiva ganha sua própria página, começando pela que o modelo dirige: **[Ferramentas](../servers/tools.md)**. diff --git a/i18n/pt-BR/pages/handlers/elicitation.md b/i18n/pt-BR/pages/handlers/elicitation.md new file mode 100644 index 0000000000..3ca6e397b5 --- /dev/null +++ b/i18n/pt-BR/pages/handlers/elicitation.md @@ -0,0 +1,185 @@ +# Elicitação {#elicitation} + +Uma ferramenta que está na metade do trabalho e sem uma resposta não precisa falhar. + +**Elicitação (elicitation)** deixa que ela pergunte. No meio de uma chamada de ferramenta o usuário recebe uma pergunta, e a resposta dele volta para dentro da mesma chamada de função. + +Existem dois modos: + +* **Modo formulário**: você precisa de um valor (uma confirmação, uma data, uma quantidade). Você descreve os campos, o cliente renderiza o formulário. +* **Modo URL**: você precisa que o usuário vá para outro lugar (uma tela de consentimento OAuth, uma página de pagamento). Nada do que ele fizer lá passa pelo protocolo. + +E há duas formas de perguntar. A que você deve usar é um **resolvedor**: você pendura a pergunta em um parâmetro e o SDK pergunta - em qualquer conexão, seja qual for a era de protocolo que o cliente fala. A forma direta, `await ctx.elicit(...)`, é uma requisição do *servidor* para o *cliente*, um canal que só existe para um cliente em uma conexão legada (versão da especificação 2025-11-25 ou anterior). As duas estão nesta página; comece pelo resolvedor. + +## Perguntar com um resolvedor {#ask-with-a-resolver} + +Uma pergunta que condiciona a ferramenta inteira - *tem certeza? qual das três contas correspondentes?* - pode sair do corpo da ferramenta e virar um **resolvedor**, e o framework pergunta por você. + +Um parâmetro anotado com `Annotated[T, Resolve(fn)]` é preenchido executando `fn` antes do corpo da ferramenta. O resolvedor retorna o valor diretamente quando já o conhece, ou retorna `Elicit(...)` para que o framework pergunte: + +```python title="server.py" hl_lines="24-30 35-36" +--8<-- "docs_src/elicitation/tutorial004.py" +``` + +* `confirm_delete` lê o próprio argumento `path` da ferramenta pelo nome, lista a pasta e **só faz a elicitação quando é necessário** - uma pasta vazia resolve para `Confirm(ok=True)` sem nenhuma ida e volta até o cliente. +* `delete_folder` anota `ElicitationResult[Confirm]`, então o framework injeta o resultado inteiro e a ferramenta faz `match` de todos os casos: aceitar-e-confirmar, aceitar-mas-manter (`ok=False`), recusar, cancelar. +* O parâmetro `confirm` nunca aparece no schema de entrada da ferramenta - o cliente fornece `path`, o resolvedor fornece `confirm`. + +Anote o modelo sem o wrapper (`Annotated[Confirm, Resolve(confirm_delete)]`) quando a ferramenta não precisar se ramificar: ela recebe o modelo no accept e a chamada aborta com um erro no decline ou no cancel. + +Um resolvedor funciona em **todas** as conexões. Para um cliente em uma conexão legada, o SDK envia a pergunta diretamente para ele; em uma conexão **2026-07-28**, o SDK *retorna* a pergunta a partir da chamada, e a próxima tentativa do cliente carrega a resposta. O seu resolvedor nunca percebe a diferença; o que acontece por baixo dos panos é **[Requisições com múltiplas idas e voltas](multi-round-trip.md)**. + +Perguntar é só uma das coisas que um resolvedor pode fazer. O mecanismo geral - dependências que calculam sem perguntar, dependências de dependências, o que o modelo pode e não pode fornecer - está na página **[Dependências](dependencies.md)**. + +## Perguntar de dentro da ferramenta {#ask-from-inside-the-tool} + +Uma ferramenta também pode parar no meio do próprio corpo e perguntar. + +!!! warning + `ctx.elicit()` e `ctx.elicit_url()` são requisições do *servidor* para o *cliente* - um + canal que só existe para um cliente em uma conexão legada (versão da especificação **2025-11-25** + ou anterior). Em uma conexão **2026-07-28** não existem requisições iniciadas pelo servidor, então + essas chamadas falham. Um resolvedor funciona nos dois casos. **[Versões do protocolo](../protocol-versions.md)** + tem a história completa. + +`await ctx.elicit()` recebe uma mensagem e um modelo Pydantic: + +```python title="server.py" hl_lines="9-11 20-23 25" +--8<-- "docs_src/elicitation/tutorial001.py" +``` + +* O parâmetro **`Context`** é o que dá acesso ao `ctx.elicit`; qualquer ferramenta pode receber um. Esse objeto tem a própria página: **[O Context](context.md)**. +* `AlternativeDate` é o **schema** da resposta que você quer. +* A ferramenta é `async def`. Tem que ser: ela para no meio e espera por uma pessoa. +* Em qualquer outra data, a ferramenta retorna na hora. Ela só pergunta quando precisa. +* A data que o usuário aceita passa de novo pelo próprio `book_table`. Uma resposta é entrada como qualquer outra: uma alternativa que também está lotada gera uma nova pergunta, em vez de ser confirmada às cegas. + +### O que o cliente recebe {#what-the-client-receives} + +O cliente recebe a sua mensagem e, junto dela, um JSON Schema gerado a partir do modelo: + +```json +{ + "properties": { + "accept_alternative": { + "description": "Try another date?", + "title": "Accept Alternative", + "type": "boolean" + }, + "date": { + "default": "2025-12-26", + "description": "Alternative date (YYYY-MM-DD)", + "title": "Date", + "type": "string" + } + }, + "required": ["accept_alternative"], + "title": "AlternativeDate", + "type": "object" +} +``` + +Esse schema é o formulário. `Field(description=...)` é o rótulo; um valor padrão já preenche o campo e o torna opcional. É a mesma maquinaria de Pydantic para JSON Schema que **[Ferramentas](../servers/tools.md)** descreve para os argumentos de uma ferramenta. + +!!! warning + Um schema de elicitação não é tão expressivo quanto o schema de entrada de uma ferramenta. Só campos + primitivos e planos: `str`, `int`, `float`, `bool` ou um `Literal` de strings (que vira um `enum`). + Coloque um modelo dentro do modelo e o `ctx.elicit` levanta um erro antes de qualquer coisa ser enviada ao cliente: + + ```text + TypeError: Elicitation schema field 'address' rendered as {'$ref': '#/$defs/Address'}, which is not a valid PrimitiveSchemaDefinition + ``` + + Você está interrompendo uma pessoa no meio de uma tarefa. Se a resposta precisa de aninhamento, ela deveria ter sido um + argumento da ferramenta. + +### As três respostas {#the-three-answers} + +`result.action` diz o que o usuário fez, e existem exatamente três possibilidades: + +* `"accept"`: ele enviou o formulário. `result.data` é uma instância de `AlternativeDate`, já validada. +* `"decline"`: ele disse não. +* `"cancel"`: ele descartou a pergunta sem escolher. + +`result.data` só existe no `"accept"`, e é por isso que o exemplo verifica `result.action` primeiro. O seu verificador de tipos garante a ordem: depois de `result.action == "accept"`, `result.data` é um `AlternativeDate`; antes disso, não existe `.data` nenhum. + +Uma recusa não é um erro. A ferramenta decide o que recusar significa (aqui, nenhuma reserva) e responde ao modelo normalmente. + +!!! tip + A resposta é validada contra o seu modelo antes de o seu código vê-la. Um cliente que envia + `"maybe"` para um `bool` não corrompe a sua reserva: a chamada falha com um + erro de incompatibilidade de schema, e o seu `if` nunca roda. + +## Mandar o usuário para uma URL {#send-the-user-to-a-url} + +Algumas coisas não podem passar pelo modelo nem pelo cliente: credenciais, números de cartão, consentimento OAuth. Para essas, você não pede dados; você pede que o usuário vá a algum lugar: + +```python title="server.py" hl_lines="10-14 23" +--8<-- "docs_src/elicitation/tutorial002.py" +``` + +* `ctx.elicit_url()` recebe a mensagem, a **URL** a visitar e um `elicitation_id` escolhido por você: qualquer string que identifique essa elicitação dentro do seu servidor. +* O resultado tem uma ação e nada mais. `"accept"` significa que o usuário concordou em abrir a URL, **não** que ele terminou o que está do outro lado. +* O pagamento acontece fora de banda, entre o navegador do usuário e o seu provedor de pagamento. Nenhum conteúdo volta pelo MCP. + +Olhe a segunda ferramenta. Quando o seu servidor descobre que o fluxo fora de banda terminou (um webhook, um polling; aqui isso é modelado como uma segunda ferramenta), `ctx.session.send_elicit_complete(...)` envia `notifications/elicitation/complete` com o mesmo `elicitation_id`. É assim que o cliente sabe que pode parar de mostrar *"aguardando pagamento..."*. Sem isso, o cliente só pode adivinhar. + +## O lado do cliente {#the-client-side} + +Servidores perguntam. Clientes respondem passando um **`elicitation_callback`** para `Client(...)`: + +```python title="client.py" hl_lines="6-7 18" +--8<-- "docs_src/elicitation/tutorial003.py" +``` + +* Um único callback dá conta dos dois modos. `params` é uma união de `ElicitRequestFormParams` e `ElicitRequestURLParams`; o `isinstance` faz a ramificação. +* Para uma URL, você mostra `params.url` ao usuário e retorna a ação que ele escolheu. Nunca nenhum `content`. +* Para um formulário, uma aplicação real renderiza `params.requested_schema` e retorna a entrada do usuário como `content`. Este aqui sempre diz sim com uma resposta pronta, que é exatamente o callback que você quer em um teste. +* Passar o callback também é a **declaração de capacidade**: é assim que o servidor descobre que dá para perguntar a este cliente. As outras coisas que um cliente pode responder para um servidor estão em **[Callbacks do cliente](../client/callbacks.md)**. + +!!! info + Elicitação é uma requisição do *servidor* para o *cliente*, e essas só existem em uma + sessão com handshake clássico, e é por isso que este cliente passa `mode="legacy"`. + Em uma conexão **2026-07-28**, uma ferramenta pergunta *retornando* a pergunta a partir da chamada; + esse fluxo é **[Requisições com múltiplas idas e voltas](multi-round-trip.md)**. + +### Experimente {#try-it} + +Inicie o `server.py` de modo formulário com `ctx.elicit` (o do `book_table`) em Streamable HTTP (**[Executando o seu servidor](../run/index.md)** tem o comando de uma linha), depois execute o `main()` do cliente e peça ao `book_table` uma mesa no dia de Natal. + +O callback imprime a pergunta que recebeu: + +```text +No tables for 2 on 2025-12-25. Would you like to try another date? +``` + +Ele responde com `{"accept_alternative": True, "date": "2025-12-27"}`, e a ferramenta, que esteve esperando dentro de `await ctx.elicit(...)` esse tempo todo, conclui a reserva: + +```text +Booked a table for 2 on 2025-12-27. +``` + +Agora troque para o `server.py` de modo URL e aponte o mesmo `main()` para `pay_deposit`: o mesmo callback pega o outro ramo, imprime o link de pagamento, e a ferramenta volta com *"Complete the payment in your browser."* Uma ida e volta, no meio da chamada, nos dois sentidos. + +!!! check + Agora remova `elicitation_callback=` do `Client` e chame `book_table` para o dia de Natal + de novo. A chamada inteira falha com um erro de protocolo: + + ```text + Elicitation not supported + ``` + + Um cliente que não registrou nenhum callback nunca declarou a capacidade `elicitation`, então não há + ninguém a quem perguntar. A sua ferramenta não recebeu um `"decline"`; ela recebeu uma exceção. Projete pensando nisso: toda + elicitação precisa de uma resposta sensata para "e se eu não puder perguntar?". + +## Recapitulando {#recap} + +* Um parâmetro anotado com `Annotated[T, Resolve(fn)]` é preenchido por um resolvedor, que retorna `Elicit(...)` quando precisa perguntar. Funciona em todas as conexões. +* O schema é um modelo Pydantic plano: só campos primitivos, validados na volta. +* `result.action` é `"accept"`, `"decline"` ou `"cancel"`; `result.data` só existe no accept. +* `await ctx.elicit(message, schema=Model)` pergunta de dentro do corpo da ferramenta, e `await ctx.elicit_url(message, url, elicitation_id)` serve para tudo que não pode passar pelo modelo (`ctx.session.send_elicit_complete(elicitation_id)` avisa que a parte fora de banda terminou). As duas são requisições do servidor para o cliente: precisam do cliente em uma conexão legada. +* O cliente responde com um único `elicitation_callback`, ramificando pelo tipo dos params; registrá-lo é o que declara a capacidade. +* Em uma conexão 2026-07-28, o servidor retorna a pergunta em vez de empurrá-la; o mesmo callback é alimentado por **[Requisições com múltiplas idas e voltas](multi-round-trip.md)**. + +Tudo que está por baixo desse retorno (o loop de retry, a proteção do `requestState`, conduzir isso você mesmo) está em **[Requisições com múltiplas idas e voltas](multi-round-trip.md)**. diff --git a/i18n/pt-BR/pages/index.md b/i18n/pt-BR/pages/index.md new file mode 100644 index 0000000000..18d5207dd5 --- /dev/null +++ b/i18n/pt-BR/pages/index.md @@ -0,0 +1,97 @@ +# MCP Python SDK {#mcp-python-sdk} + +!!! info "Esta é a documentação da v2, a linha de lançamentos estável atual" + Novo na v2 ou vindo da v1? **[O que há de novo na v2](whats-new.md)** é o tour de cinco minutos pelo que mudou, e o **[Guia de migração](migration.md)** cobre cada mudança incompatível. + Ainda na v1.x? A documentação dela fica na [documentação da v1.x](https://py.sdk.modelcontextprotocol.io/v1/). + Achou algo confuso ou mal acabado? [Nos avise](https://github.com/modelcontextprotocol/python-sdk/issues/new?template=v2-feedback.yaml). + +O **Model Context Protocol (MCP)** permite que aplicações forneçam contexto a LLMs de forma padronizada, separando a preocupação de *fornecer* contexto da interação com o LLM em si. + +Este é o SDK Python oficial dele. Com ele, você pode: + +* **Construir servidores MCP** que expõem ferramentas (tools), recursos (resources) e prompts para qualquer host MCP. +* **Construir clientes MCP** que se conectam a qualquer servidor MCP. +* Falar todos os transportes padrão: stdio, Streamable HTTP e SSE. + +## Requisitos {#requirements} + +Python 3.10+. + +## Instalação {#installation} + +=== "uv" + + ```bash + uv add "mcp[cli]" + ``` + +=== "pip" + + ```bash + pip install "mcp[cli]" + ``` + +O extra `[cli]` dá a você o comando `mcp`; você vai querer usá-lo durante o desenvolvimento. +Veja [Instalação](get-started/installation.md) para saber para que serve cada dependência. + +## Exemplo {#example} + +### Crie {#create-it} + +Crie um arquivo `server.py`: + +```python title="server.py" +--8<-- "docs_src/index/tutorial001.py" +``` + +Esse é um servidor MCP completo. + +Ele expõe uma **ferramenta**, `add`, e um **recurso** com template, `greeting://{name}`. + +### Execute {#run-it} + +```console +uv run mcp dev server.py +``` + +Isso inicia o seu servidor e abre o [MCP Inspector](https://github.com/modelcontextprotocol/inspector), uma UI interativa para explorá-lo. Abra a URL que ele imprime. + +!!! note + O Inspector é um app Node.js, então `mcp dev` precisa do `npx` no seu `PATH`. + +### Teste {#try-it} + +No Inspector, vá em **Tools** e chame `add` com `a=1`, `b=2`. + +Você recebe `3` de volta. ✨ + +O Inspector montou esse formulário (um campo inteiro obrigatório para `a`, outro para `b`) a partir das suas type hints. O Claude vai fazer o mesmo, e todo outro host MCP também. + +Agora vá em **Resources** e leia `greeting://World`: + +```text +Hello, World! +``` + +### Recapitulando {#recap} + +Olhe de novo para o que você **não** escreveu: + +* Nenhum JSON Schema. `a: int, b: int` *é* o schema. +* Nenhum parsing de requisição, nenhuma serialização, nenhum código de validação. +* Nenhum tratamento de protocolo. + +Você escreveu duas funções Python com type hints e uma docstring. O SDK faz o resto. + +## Onde ir agora {#where-to-go-next} + +* **[Primeiros passos](get-started/index.md)** leva você da instalação até um servidor funcionando e testado. +* Está construindo uma aplicação que *usa* servidores MCP? Comece por **[Clientes](client/index.md)**. +* Já tem um app FastAPI ou Starlette? **[Adicionar a um app existente](run/asgi.md)** monta um servidor MCP dentro dele. +* Procurando uma mensagem de erro exata? **[Solução de problemas](troubleshooting.md)** é indexado pelo texto literal. +* Quer saber o que mudou na v2? **[O que há de novo na v2](whats-new.md)** é o tour de cinco minutos. +* Migrando da v1? Comece pelo **[Guia de migração](migration.md)**. +* Procurando uma assinatura exata? A **[Referência da API](api/mcp/index.md)** é gerada a partir do código-fonte. +* Lendo com um LLM? Esta documentação também é publicada no formato [llms.txt](https://llmstxt.org/): + [llms.txt](https://py.sdk.modelcontextprotocol.io/llms.txt) é um índice das páginas, e + [llms-full.txt](https://py.sdk.modelcontextprotocol.io/llms-full.txt) contém todas as páginas em um único arquivo. diff --git a/i18n/pt-BR/state.json b/i18n/pt-BR/state.json new file mode 100644 index 0000000000..ce49578b9d --- /dev/null +++ b/i18n/pt-BR/state.json @@ -0,0 +1,164 @@ +{ + "pages": { + "get-started/first-steps.md": { + "block_hashes": [ + "f4e9527fee44ad246985003e8319e71c2b72f28e3301ee128f3d2c0e3f6d264d", + "3e1766d27f443ca4bbccdda3389f3dff2d2b6737133564ee0ca133a5dbfd958d", + "2a97f1d75b49d9a9b70e82ce3ad0ee70072b2676db3248614ae2ba29bc3fff28", + "59dc6c66de1c4ea589e4b984889814b842613605c638ae450db4fa44243aee06", + "ee42a5e18f8e868b5d29bb9db85915406e7e97e789013ee108ee2d199626d274", + "7d8344d6cf1ecfc4130812f643a148ea572ad7d319b371029197fe162d513602", + "cfcd0cd4aed4f275cb8cf632eed301192aed12aeb11c10c77fabab2fe0fc7b30" + ], + "inputs_hash": "ad06b80fddc3dab40092161924d753c6b1b3425cea82786911d84f88940de636", + "model": "claude-opus-5", + "source_hash": "3531c5686a6ff79aabe6f0f8a3cb96213b6fc13b0bbbb0e762dc9f4a827b4be3", + "translated_at": "2026-07-31T19:11:10Z" + }, + "handlers/elicitation.md": { + "block_hashes": [ + "f5dade066115eb33121f7bbb02a3fb288e95e2662001bea5eac322e23f4cbd1d", + "c368d8a40fd8d9879d90fbb10a81870fdb3ca84675bcf04c91740febe56ed4c4", + "191a9cfc619f523acf23ef20a04a9a318abe5280bb459f4691dfc2c60bc8dc34", + "ed47c27010f781038cd2095fedb17be05b16a9c2b1b93650416dd7e1be3154b7", + "4db8b6b1183f96319677911fd22baa3e65faf1b2f107b7d0929a3ee39ebd3ed4", + "0265da2c81bc50cf3a83415aa16c1ded414fe59a3e23e4c0b1a83cd03d41ba5c" + ], + "inputs_hash": "ad06b80fddc3dab40092161924d753c6b1b3425cea82786911d84f88940de636", + "model": "claude-opus-5", + "source_hash": "ef8afd716d136ff25d17a34059a933bb924ea238bd2faf04f80cc0d337c2e520", + "translated_at": "2026-07-31T19:11:10Z" + }, + "index.md": { + "block_hashes": [ + "3690a6eb6f57324f620f65d5242b3bb17c0ed4a5657cd4ba7ff3a28d7291245a", + "13e00a4af763653d8c663c64dd27871bdd23314d11110a143b8ee77357f76a75", + "cfcabc919e5d343b47af2641656a86f9a0abe9f250547368e64f7e4b5a3de4b0", + "c1550578c7bd96be08abb210e1c86261127c78ee09c1d8d666d08fa96271c30b", + "71115a8dc82e7ee1cd61b609da087038d8736629ebb365972c88010fd677cc00" + ], + "inputs_hash": "ad06b80fddc3dab40092161924d753c6b1b3425cea82786911d84f88940de636", + "model": "claude-opus-5", + "source_hash": "c46eefb49de18ed47882e13cf5bf08d0c3ca16ee77b98c257e8b66150c6be666", + "translated_at": "2026-07-31T21:10:06Z" + } + }, + "ui": { + "inputs_hash": "ad06b80fddc3dab40092161924d753c6b1b3425cea82786911d84f88940de636", + "model": "claude-opus-5", + "source": { + "@banner:disclosure": "??? info \"This page was machine-translated\"\n Translations of this documentation are generated automatically from the English pages, and the [English version of this page]({english_url}) is the authoritative one.\n\n Found a translation problem? See [how the translations work and how to report an issue]({translations_url}).\n", + "@banner:outdated": "!!! warning \"This translation may be behind the English page\"\n The English source changed after this page was last translated, so parts of it may be out of date. When in doubt, read the [English version of this page]({english_url}).\n", + "@banner:untranslated": "!!! note \"In English\"\n You're reading this page in English. [How the translations work]({translations_url}).\n", + "API Reference": "API Reference", + "Add to an existing app": "Add to an existing app", + "Advanced": "Advanced", + "Authorization": "Authorization", + "Caching": "Caching", + "Callbacks": "Callbacks", + "Clients": "Clients", + "Completions": "Completions", + "Connect to a real host": "Connect to a real host", + "Dependencies": "Dependencies", + "Deploy & scale": "Deploy & scale", + "Deprecated features": "Deprecated features", + "Elicitation": "Elicitation", + "Extensions": "Extensions", + "First steps": "First steps", + "Get started": "Get started", + "Handling errors": "Handling errors", + "Identity assertion": "Identity assertion", + "Images, audio & icons": "Images, audio & icons", + "Inside your handler": "Inside your handler", + "Installation": "Installation", + "Lifespan": "Lifespan", + "Logging": "Logging", + "MCP Apps": "MCP Apps", + "MCP Python SDK": "MCP Python SDK", + "Middleware": "Middleware", + "Migration Guide": "Migration Guide", + "Multi-round-trip requests": "Multi-round-trip requests", + "Multiple servers": "Multiple servers", + "OAuth": "OAuth", + "OpenTelemetry": "OpenTelemetry", + "Pagination": "Pagination", + "Progress": "Progress", + "Prompts": "Prompts", + "Protocol versions": "Protocol versions", + "Resources": "Resources", + "Running your server": "Running your server", + "Sampling and roots": "Sampling and roots", + "Servers": "Servers", + "Serving legacy clients": "Serving legacy clients", + "Structured Output": "Structured Output", + "Subscriptions": "Subscriptions", + "Testing": "Testing", + "The Context": "The Context", + "The low-level Server": "The low-level Server", + "Tools": "Tools", + "Translations": "Translations", + "Transports": "Transports", + "Troubleshooting": "Troubleshooting", + "URI templates": "URI templates", + "What's new in v2": "What's new in v2" + }, + "source_hash": "219aa0f5adfe57c4403a1b6fdf04d4e2cc79df194bdbcb30958d24e7bb387433", + "strings": { + "@banner:disclosure": "??? info \"Esta página foi traduzida automaticamente\"\n As traduções desta documentação são geradas automaticamente a partir das páginas em inglês, e a [versão em inglês desta página]({english_url}) é a oficial.\n\n Encontrou algum problema na tradução? Veja [como as traduções funcionam e como relatar um problema]({translations_url}).\n", + "@banner:outdated": "!!! warning \"Esta tradução pode estar atrasada em relação à página em inglês\"\n O texto original em inglês mudou depois da última tradução desta página, então partes dela podem estar desatualizadas. Na dúvida, leia a [versão em inglês desta página]({english_url}).\n", + "@banner:untranslated": "!!! note \"Em inglês\"\n Você está lendo esta página em inglês. [Como as traduções funcionam]({translations_url}).\n", + "API Reference": "Referência da API", + "Add to an existing app": "Adicionar a um app existente", + "Advanced": "Avançado", + "Authorization": "Autorização", + "Caching": "Cache", + "Callbacks": "Callbacks", + "Clients": "Clientes", + "Completions": "Completions", + "Connect to a real host": "Conectar a um host real", + "Dependencies": "Dependências", + "Deploy & scale": "Deploy e escala", + "Deprecated features": "Funcionalidades obsoletas", + "Elicitation": "Elicitação", + "Extensions": "Extensões", + "First steps": "Primeiros passos", + "Get started": "Comece aqui", + "Handling errors": "Tratamento de erros", + "Identity assertion": "Asserção de identidade", + "Images, audio & icons": "Imagens, áudio e ícones", + "Inside your handler": "Dentro do seu handler", + "Installation": "Instalação", + "Lifespan": "Lifespan", + "Logging": "Logging", + "MCP Apps": "MCP Apps", + "MCP Python SDK": "MCP Python SDK", + "Middleware": "Middleware", + "Migration Guide": "Guia de migração", + "Multi-round-trip requests": "Requisições com múltiplas idas e voltas", + "Multiple servers": "Múltiplos servidores", + "OAuth": "OAuth", + "OpenTelemetry": "OpenTelemetry", + "Pagination": "Paginação", + "Progress": "Progresso", + "Prompts": "Prompts", + "Protocol versions": "Versões do protocolo", + "Resources": "Recursos", + "Running your server": "Executando seu servidor", + "Sampling and roots": "Amostragem e roots", + "Servers": "Servidores", + "Serving legacy clients": "Atendendo clientes legados", + "Structured Output": "Saída estruturada", + "Subscriptions": "Assinaturas", + "Testing": "Testes", + "The Context": "O Context", + "The low-level Server": "O Server de baixo nível", + "Tools": "Ferramentas", + "Translations": "Traduções", + "Transports": "Transportes", + "Troubleshooting": "Solução de problemas", + "URI templates": "Templates de URI", + "What's new in v2": "Novidades da v2" + }, + "translated_at": "2026-07-31T21:10:06Z" + } +} diff --git a/i18n/zh-CN/glossary.json b/i18n/zh-CN/glossary.json new file mode 100644 index 0000000000..95cd2dc157 --- /dev/null +++ b/i18n/zh-CN/glossary.json @@ -0,0 +1,232 @@ +{ + "version": 1, + "keep_in_source_language": [ + "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": "工具", + "note": "MCP protocol noun (a server exposes tools). Wire identifiers such as `tools/call` and `tools/list` are code and stay Latin. Provisional pending native review.", + "avoid": [], + "enforce": false + }, + { + "source": "resource", + "target": "资源", + "note": "MCP protocol noun; \"resource template\" → 资源模板. `resources/read` stays Latin. Provisional pending native review.", + "avoid": [], + "enforce": false + }, + { + "source": "prompt", + "target": "提示词", + "note": "The MCP feature: a reusable prompt a server exposes (`prompts/get` stays Latin). Not the bare 提示, which reads as \"hint\" and is the standard rendering of the `tip` admonition title. Provisional pending native review.", + "avoid": [], + "enforce": false + }, + { + "source": "sampling", + "target": "采样", + "note": "The (deprecated) client feature that lets a server borrow the client's model. Gloss the English on first use per page: 采样(sampling). The `sampling` capability key and `sampling/createMessage` stay Latin. 抽样/取样 mean statistical sampling and are the wrong sense here.", + "avoid": ["抽样", "取样"], + "enforce": true + }, + { + "source": "roots", + "target": "根目录", + "note": "The (deprecated) client feature listing workspace folders; a `Root` object in code font stays Latin. Gloss the English on first use per page: 根目录(roots). Never the bare 根 and never 根节点 (a tree node). Provisional pending native review.", + "avoid": ["根节点"], + "enforce": false + }, + { + "source": "elicitation", + "target": "征询", + "note": "OPEN QUESTION for native review: existing Chinese material renders this concept as 引导, 引导获取, 询问 or 征询, and even our own drafts disagree between 征询 and 引导. Provisionally pinned to 征询, glossed with the English on first use per page: 征询(elicitation). Use it consistently within a page whichever way the review settles. `elicitation/create` and the `Elicit` class stay Latin.", + "avoid": [], + "enforce": false + }, + { + "source": "capability", + "target": "能力", + "note": "A negotiated protocol capability (声明了 `sampling` 能力). The `capabilities` field and keys such as `sampling.tools` stay Latin. Not 功能, which means \"feature\" — the corpus uses \"feature\" as a separate word. Provisional pending native review.", + "avoid": ["功能"], + "enforce": false + }, + { + "source": "transport", + "target": "传输", + "note": "As a countable noun use 传输方式 (\"three transports\" → 三种传输方式). The transport names stdio, Streamable HTTP and SSE stay in English. Provisional pending native review.", + "avoid": [], + "enforce": false + }, + { + "source": "session", + "target": "会话", + "note": "An MCP session (the negotiated connection state); `session` objects in code font stay Latin. Not 会议 (a meeting). Provisional pending native review.", + "avoid": ["会议"], + "enforce": false + }, + { + "source": "handler", + "target": "处理函数", + "note": "The tool, resource or prompt function you register (nav section \"Inside your handler\" → 在处理函数内部). Open question for native review: 处理器 is the competing rendering; pick one and never mix within a page. Provisional.", + "avoid": [], + "enforce": false + }, + { + "source": "dependency", + "target": "依赖", + "note": "The SDK's dependency-injection feature (the \"Dependencies\" page); use 依赖项 where a countable noun is needed. The `Resolve` marker class stays Latin. Provisional pending native review.", + "avoid": [], + "enforce": false + }, + { + "source": "client", + "target": "客户端", + "note": "An MCP client, and the client side of a connection. The `Client` class name stays Latin in code font. Not 客户 (a customer). Provisional pending native review.", + "avoid": [], + "enforce": false + }, + { + "source": "server", + "target": "服务器", + "note": "An MCP server (the program you build). \"server-side\" → 服务器端. The low-level `Server` class stays Latin in code font. Not 伺服器 (the zh-TW term); do not mix in 服务端 for the same noun. Provisional pending native review.", + "avoid": ["伺服器", "服务端"], + "enforce": false + }, + { + "source": "host", + "target": "宿主", + "note": "The MCP host: the application that embeds the client and drives the model. Not 主机 (a machine or hostname). Provisional pending native review.", + "avoid": ["主机"], + "enforce": false + }, + { + "source": "resolver", + "target": "解析器", + "note": "The SDK's dependency-resolver mechanism (\"an elicitation resolver\" → elicitation 解析器 in glossary terms: 征询解析器). The `Resolve` class stays Latin. Provisional pending native review, together with the handler entry.", + "avoid": [], + "enforce": false + }, + { + "source": "lifespan", + "target": "生命周期", + "note": "The server's startup/shutdown scope (the \"Lifespan\" page). The `lifespan` parameter name stays Latin in code font. 寿命 is the biological sense and is wrong here. Provisional pending native review.", + "avoid": ["寿命"], + "enforce": false + }, + { + "source": "callback", + "target": "回调", + "note": "Client callbacks; parameter names such as `sampling_callback` stay Latin. Provisional pending native review.", + "avoid": [], + "enforce": false + }, + { + "source": "notification", + "target": "通知", + "note": "A JSON-RPC notification (a message that expects no response); method strings such as `notifications/tools/list_changed` stay Latin. Provisional pending native review.", + "avoid": [], + "enforce": false + }, + { + "source": "back-channel", + "target": "反向通道", + "note": "The server-to-client request channel that exists only on 2025-era, non-stateless connections. Provisional coinage: gloss the English on first use per page — 反向通道(back-channel). Pending native review.", + "avoid": [], + "enforce": false + }, + { + "source": "wire", + "target": "线路", + "note": "The corpus's light metaphor for the transport stream (\"on the wire\" → 在线路上; \"nothing changes on the wire\" → 线路上没有任何变化). Never 电线 (a physical cable). Provisional pending native review.", + "avoid": ["电线"], + "enforce": false + }, + { + "source": "deprecated", + "target": "已弃用", + "note": "Also \"deprecation warning\" → 弃用警告 and \"X is deprecated\" → X 已弃用. Pin the 弃用 family throughout; do not switch to 废弃 or 淘汰 for the same concept. Provisional pending native review.", + "avoid": ["废弃", "淘汰"], + "enforce": false + }, + { + "source": "context", + "target": "上下文", + "note": "The generic lower-case word (\"provide context to LLMs\" → 为 LLM 提供上下文). The capitalised `Context` is the SDK object injected as `ctx`; both are on the keep-in-source list and stay in English in prose (\"The Context\" → Context). Provisional pending native review.", + "avoid": [], + "enforce": false + }, + { + "source": "multi-round-trip", + "target": "多轮往返", + "note": "The 2026-07-28 request pattern (\"Multi-round-trip requests\" → 多轮往返请求); \"round-trip\" alone → 往返. Provisional coinage: gloss the English on first use per page — 多轮往返(multi-round-trip). Pending native review.", + "avoid": [], + "enforce": false + }, + { + "source": "you", + "target": "你", + "note": "The register rule from instructions.md made machine-checkable: 您 must never appear on a page. Prefer dropping the pronoun; when one is needed it is 你.", + "avoid": ["您"], + "enforce": true + } + ] +} diff --git a/i18n/zh-CN/instructions.md b/i18n/zh-CN/instructions.md new file mode 100644 index 0000000000..8b1991bb40 --- /dev/null +++ b/i18n/zh-CN/instructions.md @@ -0,0 +1,146 @@ +# Simplified Chinese (zh-CN) — translation instructions + +Target language: Simplified Chinese (简体中文), directory and URL code +`zh-CN`, page language tag `zh-Hans`. This file is sent verbatim with every +translation request for this language, on top of the shared translation rules +in `../general-prompt.md`. The termbase in `glossary.json` is sent +alongside it and wins any terminology conflict with this file. + +## 1. Register + +Write the casual-neutral written register that Chinese developer +documentation uses: plain, matter-of-fact, and even. + +- Address the reader as 你. Never use the honorific 您, and never mix the + two. The rule holds in body prose, headings, admonition titles, table + cells and link text. +- Prefer no pronoun at all when the sentence stays clear — Chinese + instructions read naturally without a subject: "You can pass a schema" → + 可以传入一个模式. Reach for 你 only where the sentence would otherwise be + ambiguous about who acts. +- Steps and instructions are bare imperatives without a subject: + "Run the server" → 运行服务器, not 请您运行服务器. A single 请 is fine where + it reads natural; a 请 in front of every step is not. +- The register is uniform across a page. A page that drifts between 你 and + 您, or between plain and formal sentence endings, is wrong even when each + sentence is acceptable on its own. + +## 2. Voice + +Aim for the voice of an experienced Chinese-speaking engineer explaining a +library to a colleague: warm, direct, professional, compact. The English is +built on short declarative payoff sentences ("That's a complete MCP +server."); keep them short — 这就是一个完整的 MCP 服务器。 + +Do: + +- Follow Chinese word order and rhythm. Break one long English sentence + into two Chinese ones instead of mirroring its clause structure. +- Use concrete verbs (运行, 传入, 返回, 声明, 阻塞) rather than nominal chains. +- Keep the source's directness. Where the English says "don't", the + Chinese says 不要, not a hedge like 也许可以考虑避免. + +Avoid — these are the marks of a machine or customer-service translation: + +- 您, and its whole register: 温馨提示, 亲, 敬请, 感谢您的耐心等待. +- Formal padding: 进行……操作, 对……进行处理, and 可能像下面这样, where a plain + 是这样的 does the job. +- English-shaped Chinese: possessive chains (你的服务器的工具的模式), 被 passives + where a topic-comment sentence is natural, and a translated connective + (然而, 因此, 此外) at the start of every sentence. +- Marketing hype and internet slang: 神器, 保姆级, 给力, 强大到没朋友. + +## 3. Humour and idioms + +The English is friendly and dry rather than jokey: short payoff sentences, +a few stock phrases, the rare emoji. Carry the friendliness; recast the +idioms. + +- Never translate a pun, idiom or aside literally. Say what it means as a + short, natural Chinese sentence in the same register. If an aside carries + no information you may drop it — but never drop a technical caveat that + happens to be phrased lightly. +- Recurring English tags get fixed renderings: "**[X](…)** has the whole + story" / "The whole story is in **[X](…)**" → 详见 **[X](…)**; + "That's the whole API." / "That's the whole protocol." → 整个 API 就这些。 + / 整个协议就是这样。; "That's it. It's just Python." → 就这样,只是普通的 + Python。 +- Idioms take the plain meaning, not the picture: "Out of the box the app + answers **only** requests addressed to localhost." → 默认情况下,这个应用 + **只**响应发往 localhost 的请求。— not the literal 开箱即用地. +- Emoji: keep the source's rare, deliberately placed emoji exactly where + they are — two payoff lines end in ✨ ("You get `3` back. ✨"). Never + add new ones. +- Exclamation marks are rare in Chinese technical prose and the English + hardly uses them; do not add one to a plain payoff sentence. + +Worked examples (source → good / bad): + +- "You get `3` back. ✨" → good: 返回值是 `3`。✨ / bad: 你会得到3!✨ + (missing Han–Latin spacing, added exclamation). +- "Give a parameter a default value and it stops being required. That's + it. It's just Python." → good: 给参数设一个默认值,它就不再是必填参数。 + 就这样,只是普通的 Python。/ bad: 给一个参数一个默认值,然后它就停止是必需的了。 + 就是它。它只是Python而已!(English-shaped 它 chain, missing Han–Latin + spacing, added exclamation). + +## 4. Typography + +- Chinese prose takes full-width punctuation: ,。:;!?、()“” with ‘’ + nested inside “”, the dash —— and the ellipsis ……. Punctuation inside + code spans, code blocks, commands, URLs and quoted English text stays + half-width and untouched. +- Enumerations in prose use the enumeration comma 、: "a, b, and c" → + a、b 和 c, not a,b,和 c. +- Put one half-width space between Han characters and any run of Latin + letters or digits (使用 Streamable HTTP 传输; 需要 Python 3.10+); put no + space between a full-width punctuation mark and adjacent Latin text + (配置好 stdio。). Keep the spaces around Markdown markers (`**…**`, + links) exactly as the source has them. +- No italics in Chinese text. Where the source italicises a word for + emphasis, use **bold**; where the source italicises an example utterance + or a hypothetical question the user might see, wrap it in “” instead. + Keep bold on the same words the source bolds — a bolded negation + ("**not**" → "**不是**" / "**不会**") stays bold. +- Digits stay half-width Arabic numerals. Protocol revision strings such + as `2026-07-28` and `2025-11-25` are identifiers, copied byte-for-byte — + never 2026年7月28日, never 2026/07/28. Other dates keep the source's format. +- Numbers and units: half-width digits with a space before a Latin unit + (10 MB); % and ° attach with no space; a Chinese unit needs no space + (5 秒). +- Do not hard-wrap a line between two Han characters (the renderer turns + the line break into a stray space). Keep the source's block layout and + wrap only where the source wraps or after full-width punctuation. + +## 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 listed under `keep_in_source_language`, and any other English word + left in Latin script, are copied exactly as spelled and always in the + singular, with no article and no English plural "s": "the URIs" → URI, + "children" → child. They are never transliterated or re-cased; the spacing + rule in §4 sets them off from the surrounding Han text. +- Everything in code font, plus API names, class, function and parameter + names, protocol method and message strings (`tools/call`, + `notifications/...`), header names, error codes, SEP numbers and product + names, stays in Latin script inline. A glossary term used as a code-font + identifier stays Latin even though its prose noun is translated: + "the `sampling` capability" → `sampling` 能力. +- First-use gloss: a translated MCP concept the reader may need to map back + to the English specification carries the English in full-width parentheses + on its first occurrence on a page — 采样(sampling) — and appears alone + after that. Each glossary entry's note says whether the term takes the + gloss. +- 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 rather than picking per sentence. + +## 6. Provisional note + +The register, voice and terminology decisions above are provisional, +pending review by native Chinese-speaking readers. To propose a change, edit +this file or `glossary.json` in a pull request; never edit the generated +pages under `pages/`, which the next translation run overwrites. diff --git a/i18n/zh-CN/pages/get-started/first-steps.md b/i18n/zh-CN/pages/get-started/first-steps.md new file mode 100644 index 0000000000..22dadd806a --- /dev/null +++ b/i18n/zh-CN/pages/get-started/first-steps.md @@ -0,0 +1,138 @@ +# 第一步 {#first-steps} + +**[首页](../index.md)** 节奏很快:写一个服务器、运行它、调用一个工具。 + +这一页走得慢一些,把服务器能暴露的三样东西都过一遍,并且沿途给每样东西一个名字。 + +## 宿主、客户端和服务器 {#host-client-and-server} + +从这里开始,每一页都会出现的三个词: + +* **宿主**是 LLM 应用:Claude、IDE、某个 agent 运行时。它就是用户在对话的那个东西。 +* **客户端**住在宿主里面,负责讲 MCP。宿主每连接一个服务器,就运行一个客户端。 +* **服务器**是你用这个 SDK 构建的东西。它向客户端暴露能力,从不直接和模型对话。 + +你写的是服务器。宿主是别人的产品。SDK 也给了你一个 `Client`,用它来测试你的服务器,本页后面就会出现。 + +## 三种原语 {#the-three-primitives} + +一个服务器恰好暴露三类东西。区分它们的是**由谁决定使用它们**: + +| 原语 | 由谁控制 | 它是什么 | 示例 | +|--------------|-----------|-----------------------------------------|-------------------------| +| **工具** | 模型 | 模型调用来执行动作的函数 | 一次 API 调用、一次数据库写入 | +| **资源** | 应用 | 宿主加载进模型上下文的数据 | 文件内容、API 响应 | +| **提示词** | 用户 | 用户按名称调用的可复用消息模板 | 一个斜杠命令、一个菜单项 | + +“由谁控制”正是这种划分的全部要点。工具运行,是因为**模型**决定调用它。资源被附加进来,是因为**应用**判断模型需要它。提示词运行,是因为**用户**选了它。 + +!!! info + 如果你写过 Web API,大部分直觉你已经有了:**资源**相当于 `GET` + (只加载数据,不改变任何东西),**工具**相当于 `POST`(它会做事,并且可能有 + 副作用)。**提示词**没有对应的 HTTP 类比,它更接近用户按名称运行的一条保存好的查询。 + +## 一个服务器,三者齐全 {#one-server-all-three} + +```python title="server.py" hl_lines="6 12 18" +--8<-- "docs_src/first_steps/tutorial001.py" +``` + +三个普通函数,三个装饰器。每个装饰器就是全部的注册工作: + +* `@mcp.tool()` 让 `add` 成为一个**工具**。 +* `@mcp.resource("greeting://{name}")` 让 `greeting` 成为一个**资源模板**:URI 里的 `{name}` 就是函数的参数。 +* `@mcp.prompt()` 让 `summarize` 成为一个**提示词**。它返回的字符串会变成一条用户消息。 + +其余的一切(名称、描述、参数模式)SDK 都从函数本身读取:它的名字、它的文档字符串、它的类型标注。你从来不用单独声明这些。 + +!!! tip + SDK 的两半有两条导入路径:`from mcp import Client` 和 + `from mcp.server import MCPServer`。不存在 `from mcp import MCPServer`。 + +### 试一试 {#try-it} + +用 MCP Inspector 运行它: + +```console +uv run mcp dev server.py +``` + +打开它打印出的 URL。Inspector 每种原语各有一个标签页,按顺序逐个看。 + +**工具。** 只有一项:`add`,描述是“Add two numbers.”。表单里有一个必填的整数字段 `a`,还有一个 `b`。填好、调用,结果是 `3`。这个表单是 Inspector 根据 `a: int, b: int` 生成的。其他客户端也一样。 + +**资源。** **Resources** 列表是空的。`greeting` 在 **Resource Templates** 下面,因为 `greeting://{name}` 带了参数:在有人提供 `name` 之前,没有一个具体的资源可以列出。给它 `World` 然后读取: + +```text +Hello, World! +``` + +**提示词。** 只有一项:`summarize`,带一个必填参数 `text`。传入一些文本获取它,你会收到一条消息,`role: user`,内容是渲染后的字符串。提示词就是这么回事:一个构建消息的函数。 + +Inspector 通过 **stdio** 运行了你的服务器,这是 MCP 服务器可以讲的传输方式之一。现在还不用选,**[运行你的服务器](../run/index.md)** 那一页专门讲这个。 + +## 能力 {#capabilities} + +你在 Inspector 里看到了三个标签页。它怎么知道有三个? + +客户端连接上来时,服务器会声明它的**能力**:它会响应哪几类请求。客户端根据这份声明来决定该问什么。这段声明你从来没写过,`MCPServer` 替你声明了。 + +自己看看。SDK 的 `Client` 直接接受服务器对象,并**在内存中**连接它(没有子进程,也没有端口): + +```python +import asyncio + +from mcp import Client + +from server import mcp + + +async def main() -> None: + async with Client(mcp) as client: + print(client.server_capabilities.model_dump(exclude_none=True)) + + +asyncio.run(main()) +``` + +```text +{'prompts': {'list_changed': True}, 'resources': {'subscribe': True, 'list_changed': True}, 'tools': {'list_changed': True}} +``` + +那个字典就是你的服务器声明的**能力**。它是每个连上来的客户端最先了解到的东西: + +| 能力 | 客户端现在可以调用 | +|-------------|------------------------------------------------------------| +| `tools` | `tools/list`、`tools/call` | +| `resources` | `resources/list`、`resources/templates/list`、`resources/read` | +| `prompts` | `prompts/list`、`prompts/get` | + +`MCPServer` 提供全部三种原语,所以三者总是都会被声明。 + +注意没出现的东西。`completions`(资源模板和提示词的参数自动补全)需要你自己写一个处理函数,这个服务器没有,所以这项能力不存在,行为良好的客户端也不会去问。所有可选项都是这个规则:注册了东西,能力就出现;**[补全](../servers/completions.md)** 会证明这一点。 + +!!! info + `Client(mcp)` 就是这份文档中每个示例用来测试的那个内存内客户端,你也会用它来测试自己的 + 服务器。它有专门的一页:**[测试](testing.md)**。 + +## 你没有写的东西 {#what-you-did-not-write} + +回头看看这一页。你写了三个小小的 Python 函数。你**没有**写: + +* JSON Schema。`a: int, b: int` **就是** `add` 的模式。 +* 请求处理函数。`tools/list`、`resources/read`、`prompts/get`:全都替你处理好了。 +* 能力声明。`MCPServer` 替你生成了。 +* 一行协议代码。版本协商、JSON-RPC 帧封装、能力交换:全都发生在 `mcp dev` 和 `Client(mcp)` 内部,你从头到尾都没看见。 + +这个比例就是 SDK 的全部意义。 + +## 回顾 {#recap} + +* **宿主**是 LLM 应用,**客户端**是它讲 MCP 的那一半,**服务器**是你构建的东西。 +* 工具由**模型**控制,资源由**应用**控制,提示词由**用户**控制。 +* 每种原语一个装饰器:`@mcp.tool()`、`@mcp.resource(uri)`、`@mcp.prompt()`。名称、描述和模式都来自函数本身。 +* 带 `{param}` 的 URI 构成资源**模板**,它与具体资源分开列出。 +* 服务器的**能力**是替你声明的,客户端只会去问服务器声明过的东西。 +* `Client(mcp)` 在内存中连接服务器对象:从第一天起就是你的测试工具。 + +下一步是 **[连接到真实宿主](real-host.md)**:把这个服务器真正放进 Claude Desktop 或者 IDE 里。然后是 **[测试](testing.md)**:一页内容、一个内存内客户端,从此不用靠猜它能不能跑。再往后,每种原语各有一页,从模型驱动的那个开始:**[工具](../servers/tools.md)**。 diff --git a/i18n/zh-CN/pages/handlers/elicitation.md b/i18n/zh-CN/pages/handlers/elicitation.md new file mode 100644 index 0000000000..1edabf1fa6 --- /dev/null +++ b/i18n/zh-CN/pages/handlers/elicitation.md @@ -0,0 +1,180 @@ +# 征询 {#elicitation} + +工具干到一半、缺一个答案,并不一定就得失败。 + +**征询(elicitation)**让它可以发问。在一次工具调用的中途,用户收到一个问题,而他们的答案会回到同一次函数调用里。 + +有两种模式: + +* **表单模式**:你需要一个值(一次确认、一个日期、一个数量)。你描述字段,客户端渲染表单。 +* **URL 模式**:你需要用户去别的地方(OAuth 同意页面、支付页面)。他们在那里做的任何事都不经过协议。 + +发问也有两种方式。首选是**解析器**:把问题挂在一个参数上,由 SDK 去问——在任何连接上都可以,不管客户端说的是哪个协议年代。直接的方式 `await ctx.elicit(...)` 是一个从**服务器**发往**客户端**的请求,而这个通道只有在客户端处于旧式连接(规范版本 2025-11-25 或更早)时才存在。本页两种都会讲;先从解析器开始。 + +## 用解析器发问 {#ask-with-a-resolver} + +决定整个工具能否继续的问题——“确定吗?三个匹配的账户里选哪个?”——可以从工具体里提出来,放进一个**解析器**,由框架替你发问。 + +标注为 `Annotated[T, Resolve(fn)]` 的参数,会在执行工具体之前先运行 `fn` 来填充。解析器已经知道值时就直接返回值,否则返回 `Elicit(...)` 让框架去问: + +```python title="server.py" hl_lines="24-30 35-36" +--8<-- "docs_src/elicitation/tutorial004.py" +``` + +* `confirm_delete` 按名字读取工具自己的 `path` 参数,列出文件夹内容,**只在必须时才征询**——空文件夹会解析为 `Confirm(ok=True)`,不需要与客户端往返。 +* `delete_folder` 标注的是 `ElicitationResult[Confirm]`,于是框架注入完整的结果,工具用 `match` 处理每一种情况:接受并确认、接受但保留(`ok=False`)、拒绝、取消。 +* `confirm` 参数永远不会出现在工具的输入模式里——`path` 由客户端提供,`confirm` 由解析器提供。 + +如果工具不需要分支处理,就改为标注未包装的模型(`Annotated[Confirm, Resolve(confirm_delete)]`):接受时它拿到模型,拒绝或取消时调用以错误中止。 + +解析器在**每一种**连接上都能用。对于旧式连接上的客户端,SDK 直接把问题发给它;在 **2026-07-28** 连接上,SDK 把问题从调用中**返回**出来,客户端的下一次尝试带上答案。你的解析器完全察觉不到区别;底层发生的事情详见 **[多轮往返请求(multi-round-trip)](multi-round-trip.md)**。 + +发问只是解析器能做的事情之一。更通用的机制——不发问就能算出结果的依赖、依赖的依赖、模型能提供和不能提供什么——在 **[依赖](dependencies.md)** 页面。 + +## 在工具内部发问 {#ask-from-inside-the-tool} + +工具也可以在自己的函数体中途停下来发问。 + +!!! warning + `ctx.elicit()` 和 `ctx.elicit_url()` 是从**服务器**发往**客户端**的请求——这个通道只有在客户端处于旧式连接(规范版本 **2025-11-25** + 或更早)时才存在。在 **2026-07-28** 连接上不存在服务器发起的请求,所以 + 这些调用会失败。解析器在两种情况下都能用。详见 **[协议版本](../protocol-versions.md)**。 + +`await ctx.elicit()` 接收一条消息和一个 Pydantic 模型: + +```python title="server.py" hl_lines="9-11 20-23 25" +--8<-- "docs_src/elicitation/tutorial001.py" +``` + +* **`Context`** 参数就是 `ctx.elicit` 的来源;任何工具都可以接收一个。这个对象有自己的页面:**[Context](context.md)**。 +* `AlternativeDate` 是你想要的答案的**模式**。 +* 这个工具是 `async def`。它必须是:因为它会在中途停下来等一个人。 +* 其他任何日期,工具都会立刻返回。只有必须问的时候它才问。 +* 用户接受的日期会通过 `book_table` 自身回到流程里。答案和其他输入没有区别:如果备选日期也订满了,会再问一次,而不是盲目确认。 + +### 客户端收到什么 {#what-the-client-receives} + +客户端收到你的消息,以及旁边一份由模型生成的 JSON Schema: + +```json +{ + "properties": { + "accept_alternative": { + "description": "Try another date?", + "title": "Accept Alternative", + "type": "boolean" + }, + "date": { + "default": "2025-12-26", + "description": "Alternative date (YYYY-MM-DD)", + "title": "Date", + "type": "string" + } + }, + "required": ["accept_alternative"], + "title": "AlternativeDate", + "type": "object" +} +``` + +这份模式就是表单。`Field(description=...)` 是标签;默认值会预填输入框,并让该字段变为可选。这和 **[工具](../servers/tools.md)** 里描述的工具参数用的是同一套 Pydantic 转 JSON Schema 的机制。 + +!!! warning + 征询的模式没有工具输入模式那么有表现力。只支持扁平的原始类型字段: + `str`、`int`、`float`、`bool`,或者字符串的 `Literal`(它会变成 `enum`)。 + 在模型里再嵌一个模型,`ctx.elicit` 会在任何内容发送给客户端之前就抛出异常: + + ```text + TypeError: Elicitation schema field 'address' rendered as {'$ref': '#/$defs/Address'}, which is not a valid PrimitiveSchemaDefinition + ``` + + 你是在打断一个正在做事的人。如果答案需要嵌套,那它本来就应该是工具的参数。 + +### 三种答案 {#the-three-answers} + +`result.action` 告诉你用户做了什么,而可能性正好只有三种: + +* `"accept"`:用户提交了表单。`result.data` 是一个 `AlternativeDate` 实例,已经通过校验。 +* `"decline"`:用户说了不。 +* `"cancel"`:用户没有选择就关掉了这个问题。 + +`result.data` 只在 `"accept"` 时存在,所以示例里先检查 `result.action`。类型检查器会强制这个顺序:在 `result.action == "accept"` 之后,`result.data` 是 `AlternativeDate`;在这之前,根本没有 `.data`。 + +拒绝不是错误。工具自己决定拒绝意味着什么(这里是不预订),然后正常回复模型。 + +!!! tip + 答案在你的代码看到它之前,就已经按你的模型校验过了。客户端给一个 `bool` 字段发 + `"maybe"`,不会弄坏你的预订:调用会以模式不匹配的错误失败,你的 `if` 根本不会执行。 + +## 把用户引到一个 URL {#send-the-user-to-a-url} + +有些东西绝不能经过模型或客户端:凭据、卡号、OAuth 授权同意。对这些,你要的不是数据,而是让用户去某个地方: + +```python title="server.py" hl_lines="10-14 23" +--8<-- "docs_src/elicitation/tutorial002.py" +``` + +* `ctx.elicit_url()` 接收消息、要访问的 **URL**,以及一个由你指定的 `elicitation_id`:任意能在你的服务器内标识这次征询的字符串。 +* 结果里只有一个 action,没有别的。`"accept"` 表示用户同意打开这个 URL,**不是**表示他们完成了另一端的事情。 +* 支付发生在带外,在用户的浏览器和你的支付服务商之间。没有任何内容会通过 MCP 回来。 + +看看第二个工具。当你的服务器得知带外流程已经结束(通过 webhook、轮询;这里用第二个工具来模拟),`ctx.session.send_elicit_complete(...)` 会带上同一个 `elicitation_id` 发送 `notifications/elicitation/complete`。客户端就是靠这个知道可以不用再显示“正在等待支付……”了。没有它,客户端只能靠猜。 + +## 客户端这一侧 {#the-client-side} + +服务器发问。客户端通过向 `Client(...)` 传入 **`elicitation_callback`** 来回答: + +```python title="client.py" hl_lines="6-7 18" +--8<-- "docs_src/elicitation/tutorial003.py" +``` + +* 一个回调同时处理两种模式。`params` 是 `ElicitRequestFormParams` 和 `ElicitRequestURLParams` 的联合类型;用 `isinstance` 来分支。 +* 对于 URL,把 `params.url` 展示给用户,然后返回他们选择的 action。绝不返回任何 `content`。 +* 对于表单,真实的应用会渲染 `params.requested_schema`,并把用户的输入作为 `content` 返回。这里的实现总是用一个写死的答案说“是”,而这正是测试里想要的回调。 +* 传入回调同时也是**能力声明**:服务器就是这样知道可以向这个客户端发问的。客户端还能为服务器回答哪些事情,见 **[客户端回调](../client/callbacks.md)**。 + +!!! info + 征询是从**服务器**发往**客户端**的请求,而这类请求只存在于经典握手的会话中, + 所以这个客户端传了 `mode="legacy"`。 + 在 **2026-07-28** 连接上,工具改为通过调用的**返回值**来发问; + 那个流程见 **[多轮往返请求](multi-round-trip.md)**。 + +### 试一试 {#try-it} + +用 Streamable HTTP 启动表单模式的 `ctx.elicit` 版 `server.py`(就是带 `book_table` 的那个,一行命令见 **[运行服务器](../run/index.md)**),然后运行客户端的 `main()`,向 `book_table` 预订圣诞节当天。 + +回调会打印它收到的问题: + +```text +No tables for 2 on 2025-12-25. Would you like to try another date? +``` + +它回答 `{"accept_alternative": True, "date": "2025-12-27"}`,而一直卡在 `await ctx.elicit(...)` 里等待的工具完成了预订: + +```text +Booked a table for 2 on 2025-12-27. +``` + +现在换成 URL 模式的 `server.py`,把同一个 `main()` 指向 `pay_deposit`:同一个回调走另一个分支,打印支付链接,工具返回“Complete the payment in your browser.”。一次往返,发生在调用中途,双向都走通了。 + +!!! check + 现在把 `Client` 里的 `elicitation_callback=` 去掉,再为圣诞节当天调用一次 `book_table`。 + 整个调用会以协议错误失败: + + ```text + Elicitation not supported + ``` + + 没有注册回调的客户端从来没有声明 `elicitation` 能力,所以没有人可以问。你的工具拿到的 + 不是 `"decline"`,而是一个异常。设计时要考虑这一点:每一次征询都需要对“如果问不了怎么办?”有一个合理的答案。 + +## 回顾 {#recap} + +* 标注为 `Annotated[T, Resolve(fn)]` 的参数由解析器填充,解析器在必须发问时返回 `Elicit(...)`。它在每一种连接上都能用。 +* 模式是一个扁平的 Pydantic 模型:只允许原始类型字段,返回时会校验。 +* `result.action` 是 `"accept"`、`"decline"` 或 `"cancel"`;`result.data` 只在 accept 时存在。 +* `await ctx.elicit(message, schema=Model)` 在工具体内部发问,`await ctx.elicit_url(message, url, elicitation_id)` 用于所有不能经过模型的东西(`ctx.session.send_elicit_complete(elicitation_id)` 表示带外的那部分已经完成)。两者都是服务器发往客户端的请求:需要客户端处于旧式连接上。 +* 客户端用一个 `elicitation_callback` 来回答,按 params 类型分支;注册它就等于声明了这个能力。 +* 在 2026-07-28 连接上,服务器不是推送问题,而是返回问题;同一个回调由 **[多轮往返请求](multi-round-trip.md)** 来驱动。 + +这个返回背后的一切(重试循环、保护 `requestState`、自己驱动它)都在 **[多轮往返请求](multi-round-trip.md)**。 diff --git a/i18n/zh-CN/pages/index.md b/i18n/zh-CN/pages/index.md new file mode 100644 index 0000000000..9f59f0d360 --- /dev/null +++ b/i18n/zh-CN/pages/index.md @@ -0,0 +1,97 @@ +# MCP Python SDK {#mcp-python-sdk} + +!!! info "本文档对应 v2,即当前的稳定发布线" + 第一次接触 v2,或者从 v1 过来?**[v2 有什么新变化](whats-new.md)** 用五分钟带你了解改动,**[迁移指南](migration.md)** 覆盖了每一处破坏性变更。 + 仍在使用 v1.x?它的文档在 [v1.x 文档](https://py.sdk.modelcontextprotocol.io/v1/)。 + 哪里不顺手或者看不明白?[告诉我们](https://github.com/modelcontextprotocol/python-sdk/issues/new?template=v2-feedback.yaml)。 + +**Model Context Protocol (MCP)** 让应用以标准化的方式为 LLM 提供上下文,把**提供**上下文这件事和与 LLM 的交互本身分开。 + +这是它的官方 Python SDK。用它可以: + +* **构建 MCP 服务器**,向任意 MCP 宿主暴露工具、资源和提示词。 +* **构建 MCP 客户端**,连接任意 MCP 服务器。 +* 使用所有标准传输方式:stdio、Streamable HTTP 和 SSE。 + +## 环境要求 {#requirements} + +Python 3.10+。 + +## 安装 {#installation} + +=== "uv" + + ```bash + uv add "mcp[cli]" + ``` + +=== "pip" + + ```bash + pip install "mcp[cli]" + ``` + +`[cli]` 附加组件会提供 `mcp` 命令,开发时会用到它。 +每个依赖项的用途见 [安装](get-started/installation.md)。 + +## 示例 {#example} + +### 创建 {#create-it} + +创建文件 `server.py`: + +```python title="server.py" +--8<-- "docs_src/index/tutorial001.py" +``` + +这就是一个完整的 MCP 服务器。 + +它暴露了一个**工具** `add`,以及一个模板化的**资源** `greeting://{name}`。 + +### 运行 {#run-it} + +```console +uv run mcp dev server.py +``` + +这会启动服务器,并打开 [MCP Inspector](https://github.com/modelcontextprotocol/inspector)——一个用来动手试验的交互式界面。打开它打印出来的 URL。 + +!!! note + Inspector 是一个 Node.js 应用,所以 `mcp dev` 需要 `PATH` 中有 `npx`。 + +### 试一试 {#try-it} + +在 Inspector 里进入 **Tools**,用 `a=1`、`b=2` 调用 `add`。 + +返回值是 `3`。✨ + +Inspector 根据你的类型提示生成了那个表单(`a` 是一个必填的整数字段,`b` 也是)。Claude 以及其他所有 MCP 宿主同样如此。 + +现在进入 **Resources**,读取 `greeting://World`: + +```text +Hello, World! +``` + +### 回顾 {#recap} + +再看一眼你**没有**写的东西: + +* 没有 JSON Schema。`a: int, b: int` **就是**模式。 +* 没有请求解析,没有序列化,没有校验代码。 +* 完全没有协议处理。 + +你写的是两个带类型提示和文档字符串的 Python 函数。剩下的交给 SDK。 + +## 接下来看什么 {#where-to-go-next} + +* **[快速开始](get-started/index.md)** 带你从安装走到一个可运行、已测试的服务器。 +* 要构建一个**使用** MCP 服务器的应用?从 **[客户端](client/index.md)** 开始。 +* 已经有 FastAPI 或 Starlette 应用了?**[接入已有应用](run/asgi.md)** 讲如何在其中挂载 MCP 服务器。 +* 在查某条具体的错误信息?**[疑难排查](troubleshooting.md)** 按原文逐字编排。 +* 想知道 v2 有哪些改动?**[v2 有什么新变化](whats-new.md)** 用五分钟讲完。 +* 从 v1 迁移?先看 **[迁移指南](migration.md)**。 +* 在找某个确切的签名?**[API 参考](api/mcp/index.md)** 由源码生成。 +* 用 LLM 阅读?本文档同时以 [llms.txt](https://llmstxt.org/) 格式发布: + [llms.txt](https://py.sdk.modelcontextprotocol.io/llms.txt) 是页面索引, + [llms-full.txt](https://py.sdk.modelcontextprotocol.io/llms-full.txt) 把所有页面放在一个文件里。 diff --git a/i18n/zh-CN/state.json b/i18n/zh-CN/state.json new file mode 100644 index 0000000000..1a13488049 --- /dev/null +++ b/i18n/zh-CN/state.json @@ -0,0 +1,164 @@ +{ + "pages": { + "get-started/first-steps.md": { + "block_hashes": [ + "f4e9527fee44ad246985003e8319e71c2b72f28e3301ee128f3d2c0e3f6d264d", + "3e1766d27f443ca4bbccdda3389f3dff2d2b6737133564ee0ca133a5dbfd958d", + "2a97f1d75b49d9a9b70e82ce3ad0ee70072b2676db3248614ae2ba29bc3fff28", + "59dc6c66de1c4ea589e4b984889814b842613605c638ae450db4fa44243aee06", + "ee42a5e18f8e868b5d29bb9db85915406e7e97e789013ee108ee2d199626d274", + "7d8344d6cf1ecfc4130812f643a148ea572ad7d319b371029197fe162d513602", + "cfcd0cd4aed4f275cb8cf632eed301192aed12aeb11c10c77fabab2fe0fc7b30" + ], + "inputs_hash": "af25ee0f3cb8086ea78e60ae50a8ded6f0ae6a01884d398e6153fb66d7e4e8a8", + "model": "claude-opus-5", + "source_hash": "3531c5686a6ff79aabe6f0f8a3cb96213b6fc13b0bbbb0e762dc9f4a827b4be3", + "translated_at": "2026-07-31T20:08:09Z" + }, + "handlers/elicitation.md": { + "block_hashes": [ + "f5dade066115eb33121f7bbb02a3fb288e95e2662001bea5eac322e23f4cbd1d", + "c368d8a40fd8d9879d90fbb10a81870fdb3ca84675bcf04c91740febe56ed4c4", + "191a9cfc619f523acf23ef20a04a9a318abe5280bb459f4691dfc2c60bc8dc34", + "ed47c27010f781038cd2095fedb17be05b16a9c2b1b93650416dd7e1be3154b7", + "4db8b6b1183f96319677911fd22baa3e65faf1b2f107b7d0929a3ee39ebd3ed4", + "0265da2c81bc50cf3a83415aa16c1ded414fe59a3e23e4c0b1a83cd03d41ba5c" + ], + "inputs_hash": "af25ee0f3cb8086ea78e60ae50a8ded6f0ae6a01884d398e6153fb66d7e4e8a8", + "model": "claude-opus-5", + "source_hash": "ef8afd716d136ff25d17a34059a933bb924ea238bd2faf04f80cc0d337c2e520", + "translated_at": "2026-07-31T19:11:10Z" + }, + "index.md": { + "block_hashes": [ + "3690a6eb6f57324f620f65d5242b3bb17c0ed4a5657cd4ba7ff3a28d7291245a", + "13e00a4af763653d8c663c64dd27871bdd23314d11110a143b8ee77357f76a75", + "cfcabc919e5d343b47af2641656a86f9a0abe9f250547368e64f7e4b5a3de4b0", + "c1550578c7bd96be08abb210e1c86261127c78ee09c1d8d666d08fa96271c30b", + "71115a8dc82e7ee1cd61b609da087038d8736629ebb365972c88010fd677cc00" + ], + "inputs_hash": "af25ee0f3cb8086ea78e60ae50a8ded6f0ae6a01884d398e6153fb66d7e4e8a8", + "model": "claude-opus-5", + "source_hash": "c46eefb49de18ed47882e13cf5bf08d0c3ca16ee77b98c257e8b66150c6be666", + "translated_at": "2026-07-31T21:09:40Z" + } + }, + "ui": { + "inputs_hash": "af25ee0f3cb8086ea78e60ae50a8ded6f0ae6a01884d398e6153fb66d7e4e8a8", + "model": "claude-opus-5", + "source": { + "@banner:disclosure": "??? info \"This page was machine-translated\"\n Translations of this documentation are generated automatically from the English pages, and the [English version of this page]({english_url}) is the authoritative one.\n\n Found a translation problem? See [how the translations work and how to report an issue]({translations_url}).\n", + "@banner:outdated": "!!! warning \"This translation may be behind the English page\"\n The English source changed after this page was last translated, so parts of it may be out of date. When in doubt, read the [English version of this page]({english_url}).\n", + "@banner:untranslated": "!!! note \"In English\"\n You're reading this page in English. [How the translations work]({translations_url}).\n", + "API Reference": "API Reference", + "Add to an existing app": "Add to an existing app", + "Advanced": "Advanced", + "Authorization": "Authorization", + "Caching": "Caching", + "Callbacks": "Callbacks", + "Clients": "Clients", + "Completions": "Completions", + "Connect to a real host": "Connect to a real host", + "Dependencies": "Dependencies", + "Deploy & scale": "Deploy & scale", + "Deprecated features": "Deprecated features", + "Elicitation": "Elicitation", + "Extensions": "Extensions", + "First steps": "First steps", + "Get started": "Get started", + "Handling errors": "Handling errors", + "Identity assertion": "Identity assertion", + "Images, audio & icons": "Images, audio & icons", + "Inside your handler": "Inside your handler", + "Installation": "Installation", + "Lifespan": "Lifespan", + "Logging": "Logging", + "MCP Apps": "MCP Apps", + "MCP Python SDK": "MCP Python SDK", + "Middleware": "Middleware", + "Migration Guide": "Migration Guide", + "Multi-round-trip requests": "Multi-round-trip requests", + "Multiple servers": "Multiple servers", + "OAuth": "OAuth", + "OpenTelemetry": "OpenTelemetry", + "Pagination": "Pagination", + "Progress": "Progress", + "Prompts": "Prompts", + "Protocol versions": "Protocol versions", + "Resources": "Resources", + "Running your server": "Running your server", + "Sampling and roots": "Sampling and roots", + "Servers": "Servers", + "Serving legacy clients": "Serving legacy clients", + "Structured Output": "Structured Output", + "Subscriptions": "Subscriptions", + "Testing": "Testing", + "The Context": "The Context", + "The low-level Server": "The low-level Server", + "Tools": "Tools", + "Translations": "Translations", + "Transports": "Transports", + "Troubleshooting": "Troubleshooting", + "URI templates": "URI templates", + "What's new in v2": "What's new in v2" + }, + "source_hash": "219aa0f5adfe57c4403a1b6fdf04d4e2cc79df194bdbcb30958d24e7bb387433", + "strings": { + "@banner:disclosure": "??? info \"本页由机器翻译生成\"\n 本文档的译文由英文页面自动生成,以[本页的英文版]({english_url})为准。\n\n 发现翻译问题?请参阅[翻译的工作方式以及如何反馈问题]({translations_url})。\n", + "@banner:outdated": "!!! warning \"本译文可能落后于英文页面\"\n 本页最后一次翻译之后,英文原文已有改动,因此部分内容可能已过时。如有疑问,请阅读[本页的英文版]({english_url})。\n", + "@banner:untranslated": "!!! note \"英文页面\"\n 你正在阅读本页的英文版。[翻译的工作方式]({translations_url})。\n", + "API Reference": "API 参考", + "Add to an existing app": "集成到现有应用", + "Advanced": "进阶", + "Authorization": "授权", + "Caching": "缓存", + "Callbacks": "回调", + "Clients": "客户端", + "Completions": "补全", + "Connect to a real host": "连接真实宿主", + "Dependencies": "依赖", + "Deploy & scale": "部署与扩展", + "Deprecated features": "已弃用的特性", + "Elicitation": "征询", + "Extensions": "扩展", + "First steps": "第一步", + "Get started": "快速上手", + "Handling errors": "错误处理", + "Identity assertion": "身份断言", + "Images, audio & icons": "图像、音频与图标", + "Inside your handler": "在处理函数内部", + "Installation": "安装", + "Lifespan": "生命周期", + "Logging": "日志", + "MCP Apps": "MCP Apps", + "MCP Python SDK": "MCP Python SDK", + "Middleware": "中间件", + "Migration Guide": "迁移指南", + "Multi-round-trip requests": "多轮往返请求", + "Multiple servers": "多服务器", + "OAuth": "OAuth", + "OpenTelemetry": "OpenTelemetry", + "Pagination": "分页", + "Progress": "进度", + "Prompts": "提示词", + "Protocol versions": "协议版本", + "Resources": "资源", + "Running your server": "运行服务器", + "Sampling and roots": "采样与根目录", + "Servers": "服务器", + "Serving legacy clients": "服务旧版客户端", + "Structured Output": "结构化输出", + "Subscriptions": "订阅", + "Testing": "测试", + "The Context": "Context", + "The low-level Server": "低层 Server", + "Tools": "工具", + "Translations": "翻译", + "Transports": "传输方式", + "Troubleshooting": "问题排查", + "URI templates": "URI 模板", + "What's new in v2": "v2 新特性" + }, + "translated_at": "2026-07-31T21:09:40Z" + } +} diff --git a/mkdocs.yml b/mkdocs.yml index 06b293f876..f215e692ed 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -68,6 +68,7 @@ nav: - Extensions: advanced/extensions.md - MCP Apps: advanced/apps.md - Troubleshooting: troubleshooting.md + - Translations: translations.md - Migration Guide: migration.md - API Reference: api/ diff --git a/pyproject.toml b/pyproject.toml index 3c814106d1..c3cf30819e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -102,6 +102,9 @@ docs = [ # griffe's successor distribution (same author) and still imports as # `griffe`; the old `griffe` distribution is the incompatible 1.x line. "griffelib==2.1.0", + # The docs translation tool (scripts/docs/translations.py) calls the Claude API in + # its `translate` command; the rest of the pipeline is offline. + "anthropic>=0.120.2", ] codegen = ["datamodel-code-generator==0.57.0"] @@ -175,6 +178,7 @@ executionEnvironments = [ { root = "tests", extraPaths = [ ".", "examples", + "scripts/docs", ], reportUnusedFunction = false, reportPrivateUsage = false }, { root = "examples/stories", extraPaths = [ "examples", @@ -242,6 +246,9 @@ strict-no-cover = { git = "https://github.com/pydantic/strict-no-cover" } [tool.pytest.ini_options] log_cli = true +# tests/test_docs_i18n.py imports the docs translation tool, a top-level +# module under scripts/docs/. +pythonpath = ["scripts/docs"] xfail_strict = true markers = [ "requirement(id): links a test to the entry in tests/interaction/_requirements.py it exercises", diff --git a/scripts/docs/build.sh b/scripts/docs/build.sh index 8f545761bc..63faf083f2 100755 --- a/scripts/docs/build.sh +++ b/scripts/docs/build.sh @@ -1,15 +1,31 @@ #!/usr/bin/env bash # -# Build the v2 documentation site for this checkout into `site/`. +# Build the v2 documentation site for this checkout into `site/`: the English +# site at the root plus one machine-translated site per language listed in +# i18n/languages.yml, under `site//`. # -# Zensical runs no MkDocs plugins or hooks, so the build is three steps: -# materialise the API reference pages and the concrete config, build the -# site strictly (plus the order-independence and cross-reference checks -# Zensical doesn't do itself), then generate llms.txt and the per-page -# markdown renditions. This script is the single owner of that recipe, dependency -# sync included — CI (shared.yml, docs-preview.yml) and scripts/build-docs.sh -# all call it. The toolchain detection in docs-preview.yml and build-docs.sh -# keys on this file's path and expects the site under site/. +# Zensical runs no MkDocs plugins or hooks, so the English build is three +# steps: materialise the API reference pages and the concrete config, build +# the site strictly (plus the order-independence and cross-reference checks +# Zensical doesn't do itself), then generate llms.txt and the per-page markdown +# renditions. A language site is the lighter recipe against the docs tree the +# translation tool stages (English pages with translations overlaid and status +# notices stamped in); it carries no API reference of its own — its nav entry and +# prose links point at the English one. This script is the single owner of the +# recipe, dependency sync included — CI (shared.yml, docs-preview.yml) and +# scripts/build-docs.sh all call it. The toolchain detection in +# docs-preview.yml and build-docs.sh keys on this file's path and expects the +# site under site/. +# +# Environment: +# DOCS_LANGUAGES=en-only build only the English site (fast local loop) +# DOCS_LANGUAGES=ja,ko build only these language sites after English +# DOCS_SITE_URL= the URL the site is served from; defaults to +# mkdocs.yml's site_url. A PR preview passes its +# own host so every absolute link the build bakes +# (language switcher, banner links, links into the +# English API reference) resolves on the host the +# reader is browsing. # # Usage: # scripts/docs/build.sh @@ -27,9 +43,15 @@ uv sync --frozen --group docs # pages re-render silently drops cross-references to cache-hit pages, and # HTML for since-deleted pages lingers in site/. Build cold so the output # (and the checks below) are deterministic. -rm -rf .cache site +rm -rf .cache site .build/i18n + +# The language sites this build produces (comma-separated), decided once so +# every site's switcher lists exactly the sites that exist. +languages="${DOCS_LANGUAGES-$(uv run --frozen --no-sync python scripts/docs/translations.py languages | paste -sd, -)}" +[[ "$languages" == "en-only" ]] && languages="" +site_url="${DOCS_SITE_URL:-}" -uv run --frozen --no-sync python scripts/docs/build_config.py +uv run --frozen --no-sync python scripts/docs/build_config.py --languages "$languages" ${site_url:+--site-url "$site_url"} uv run --frozen --no-sync zensical build -f mkdocs.gen.yml --strict # The build above renders pages in one arbitrary (filesystem-dependent) @@ -44,3 +66,16 @@ uv run --frozen --no-sync python scripts/docs/check_render_order.py uv run --frozen --no-sync python scripts/docs/check_crossrefs.py --site-dir site uv run --frozen --no-sync python scripts/docs/llms_txt.py --site-dir site + +# The English site must already be in site/ before this loop: `zensical build` +# deletes anything foreign in its site_dir, so the English build (site_dir +# site/) would wipe every site//, while a language build (site_dir +# site//) leaves its siblings and parent alone. +for lang in ${languages//,/ }; do + echo "=== Building language site: ${lang} ===" + uv run --frozen --no-sync python scripts/docs/translations.py stage --lang "$lang" ${site_url:+--site-url "$site_url"} + uv run --frozen --no-sync python scripts/docs/build_config.py --lang "$lang" --languages "$languages" \ + ${site_url:+--site-url "$site_url"} + rm -rf .cache + uv run --frozen --no-sync zensical build -f "mkdocs.${lang}.gen.yml" --strict +done diff --git a/scripts/docs/build_config.py b/scripts/docs/build_config.py index daba648344..24c792f70f 100644 --- a/scripts/docs/build_config.py +++ b/scripts/docs/build_config.py @@ -6,12 +6,21 @@ gen_ref_pages) and writes `mkdocs.gen.yml` with the real API nav spliced in — that generated file is what `zensical build`/`serve` consumes. +With `--lang ` it writes `mkdocs..gen.yml` for a translated +language site instead: built from the staged tree the translation tool made +(`.build/i18n//docs`, see scripts/docs/translations.py) into `site//`, +with the sidebar labels translated and no API reference of its own — its API +entry links the English reference. Every config carries the same language +switcher (`extra.alternate`) listing the English site and each language site +this build produces (`--languages`, default: all enabled). + Usage: - python scripts/docs/build_config.py + python scripts/docs/build_config.py [--site-url URL] [--languages a,b] [--lang CODE] """ from __future__ import annotations +import argparse import posixpath import re from pathlib import Path @@ -19,6 +28,7 @@ # Both scripts live in this directory, which Python puts on sys.path[0] when # `build_config.py` is run directly (its documented invocation). import gen_ref_pages +import translations import yaml ROOT = Path(__file__).parent.parent.parent @@ -67,24 +77,69 @@ def _validate_nav(nav: list, docs_dir: Path) -> None: raise SystemExit(f"build_config: pages under docs/ that no nav entry reaches: {orphaned}") -def build_config() -> None: - config = yaml.safe_load((ROOT / "mkdocs.yml").read_text(encoding="utf-8")) +def _relabel(nav: list, labels: dict[str, str]) -> list: + """The nav with each translated sidebar label swapped in (page paths and unmapped labels untouched).""" + out = [] + for entry in nav: + if isinstance(entry, str): + out.append(entry) + continue + ((label, value),) = entry.items() + out.append({labels.get(label, label): _relabel(value, labels) if isinstance(value, list) else value}) + return out - api_nav = gen_ref_pages.generate() - if not api_nav: - raise SystemExit("build_config: gen_ref_pages produced no API pages — did the src/ layout move?") - for entry in config["nav"]: - if isinstance(entry, dict) and "API Reference" in entry: - entry["API Reference"] = api_nav - break - else: - raise SystemExit("build_config: no 'API Reference' entry found in mkdocs.yml nav") - _validate_nav(config["nav"], ROOT / "docs") +def build_config(lang: str | None = None, site_url: str | None = None, codes: list[str] | None = None) -> Path: + """Write the concrete config for the English site, or with `lang` for that language site. - output = ROOT / "mkdocs.gen.yml" + `site_url` is the URL the site is served from (mkdocs.yml's by default; a + PR preview passes its own host) and `codes` the language sites this build + produces, which is exactly what the language switcher offers. + """ + config = yaml.safe_load((ROOT / "mkdocs.yml").read_text(encoding="utf-8")) + registry = translations.load_registry() + base = (site_url or config["site_url"]).rstrip("/") + switched = registry.languages if codes is None else [registry.language(code) for code in codes] + if switched: # the same switcher on every site, listing exactly the language sites this build produces + config.setdefault("extra", {})["alternate"] = [ + {"name": "English", "link": f"{base}/", "lang": "en"}, + *({"name": la.name, "link": f"{base}/{la.code}/", "lang": la.hreflang} for la in switched), + ] + api = next((entry for entry in config["nav"] if isinstance(entry, dict) and "API Reference" in entry), None) + if api is None: + raise SystemExit("build_config: no 'API Reference' entry found in mkdocs.yml nav") + + if lang is None: + api_nav = gen_ref_pages.generate() + if not api_nav: + raise SystemExit("build_config: gen_ref_pages produced no API pages — did the src/ layout move?") + api["API Reference"] = api_nav + docs = ROOT / "docs" + config["site_url"] = f"{base}/" + output = ROOT / "mkdocs.gen.yml" + else: + # A language site carries no API reference of its own: its nav entry + # (and its prose links, rewritten at staging) point at the English one. + api["API Reference"] = f"{base}/api/mcp/" + config["plugins"] = [p for p in config["plugins"] if not (isinstance(p, dict) and "mkdocstrings" in p)] + config["nav"] = _relabel(config["nav"], translations.load_state(lang)["ui"].get("strings", {})) + config["theme"]["language"] = registry.language(lang).theme_language + docs = translations.staged_docs(lang) + config["docs_dir"] = f".build/i18n/{lang}/docs" + config["site_dir"] = f"site/{lang}" + config["site_url"] = f"{base}/{lang}/" + output = ROOT / f"mkdocs.{lang}.gen.yml" + + _validate_nav(config["nav"], docs) output.write_text(yaml.safe_dump(config, sort_keys=False, allow_unicode=True), encoding="utf-8") + return output if __name__ == "__main__": - build_config() + parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument("--lang", metavar="CODE", help="build the config for this language site") + parser.add_argument("--site-url", metavar="URL", help="URL the site is served from (default: mkdocs.yml)") + parser.add_argument("--languages", metavar="a,b", help="language sites this build produces (default: all)") + args = parser.parse_args() + codes = None if args.languages is None else [c.strip() for c in args.languages.split(",") if c.strip()] + print(f"wrote {build_config(args.lang, args.site_url, codes).relative_to(ROOT)}") diff --git a/scripts/docs/translations.py b/scripts/docs/translations.py new file mode 100644 index 0000000000..c3cddeb710 --- /dev/null +++ b/scripts/docs/translations.py @@ -0,0 +1,995 @@ +"""Machine-translate the documentation and stage the translated sites for the build. + +English under `docs/` is the source of truth. Each language listed in +`i18n/languages.yml` gets generated pages under `i18n//pages/`, driven +by that language's human-written `instructions.md` and `glossary.json` (see +`i18n/README.md`). Corrections go into those inputs, never into the generated +pages. + +Usage: + python scripts/docs/translations.py languages # enabled codes, one per line + python scripts/docs/translations.py status [--lang ja] # what is missing/outdated + python scripts/docs/translations.py translate --lang ja [--pages a.md ...] [--fresh] [--dry-run] [--no-verify] + python scripts/docs/translations.py stage --lang ja # assemble the docs tree the language site builds from + +`translate` calls the model (set `ANTHROPIC_API_KEY` or `ANTHROPIC_AUTH_TOKEN`); +every other command is offline. +""" + +from __future__ import annotations + +import argparse +import hashlib +import html +import json +import os +import posixpath +import re +import shutil +import sys +import unicodedata +from collections import Counter +from collections.abc import Callable, Iterable +from dataclasses import dataclass +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + +import anthropic +import yaml +from llms_txt import page_url + +ROOT = Path(__file__).resolve().parent.parent.parent +DOCS = ROOT / "docs" +I18N = ROOT / "i18n" +ATTEMPTS = 3 + + +def sha256(text: str) -> str: + return hashlib.sha256(text.encode("utf-8")).hexdigest() + + +# --------------------------------------------------------------------------- +# Registry (i18n/languages.yml) and a language's prompt inputs +# --------------------------------------------------------------------------- + + +@dataclass(frozen=True) +class Language: + code: str + name: str + theme_language: str + hreflang: str + + +@dataclass(frozen=True) +class Registry: + languages: list[Language] + exclude_pages: list[str] + models: dict[str, str] # {"translate": ..., "verify": ...} + banners: dict[str, str] # kind -> English notice with {english_url}/{translations_url} placeholders + + def language(self, code: str) -> Language: + for language in self.languages: + if language.code == code: + return language + raise SystemExit(f"i18n: unknown language code {code!r} (see i18n/languages.yml)") + + +def load_registry() -> Registry: + """The language registry, English banner sources and model ids from `i18n/languages.yml`.""" + data = yaml.safe_load((I18N / "languages.yml").read_text(encoding="utf-8")) + languages = [ + Language( + item["code"], item["name"], item.get("theme_language", item["code"]), item.get("hreflang", item["code"]) + ) + for item in data["languages"] + ] + return Registry(languages, data.get("exclude_pages", []), data["models"], data["banners"]) + + +@dataclass(frozen=True) +class Inputs: + """The human-written prompt inputs of one language; `hash` fingerprints all of them.""" + + language: Language + general_prompt: str + instructions: str + glossary: dict[str, Any] + hash: str + + +def load_inputs(language: Language) -> Inputs: + general = (I18N / "general-prompt.md").read_text(encoding="utf-8") + instructions = (I18N / language.code / "instructions.md").read_text(encoding="utf-8") + glossary = (I18N / language.code / "glossary.json").read_text(encoding="utf-8") + return Inputs(language, general, instructions, json.loads(glossary), sha256(general + instructions + glossary)) + + +# --------------------------------------------------------------------------- +# Pages and sidebar labels, both read from mkdocs.yml +# --------------------------------------------------------------------------- + + +def mkdocs_config() -> dict[str, Any]: + return yaml.safe_load((ROOT / "mkdocs.yml").read_text(encoding="utf-8")) + + +def _nav_entries(nav: list[Any]) -> Iterable[tuple[str | None, str]]: + """Every `(label, page)` of the nav in order; a bare page has no label, a section title has no page.""" + for entry in nav: + if isinstance(entry, str): + yield None, entry + continue + label, value = next(iter(entry.items())) + if isinstance(value, list): + yield label, "" + yield from _nav_entries(value) + else: + yield label, value + + +def translatable_pages(nav: list[Any], exclude: list[str]) -> list[str]: + """The prose pages each language should carry, in nav order (the API reference stays English).""" + excluded = tuple(pattern.removesuffix("**") for pattern in ["api/", *exclude]) + return [page for _, page in _nav_entries(nav) if page.endswith(".md") and not page.startswith(excluded)] + + +def nav_labels(nav: list[Any]) -> list[str]: + """The sidebar labels (section titles and `Label: page.md` entries) in nav order, deduplicated.""" + return list(dict.fromkeys(label for label, _ in _nav_entries(nav) if label)) + + +def read_page(root: Path, page: str) -> str: + return (root / page).read_text(encoding="utf-8") + + +# --------------------------------------------------------------------------- +# Markdown structure: fences, headings and their anchors, links, ##-sections +# --------------------------------------------------------------------------- + +_FENCE = re.compile(r"^[ \t]*(?P`{3,}|~{3,})(?P[^\n]*)$") +_HEADING = re.compile(r"^(?P#{1,6})[ \t]+(?P.*?)[ \t]*$") +_ANCHOR = re.compile(r"[ \t]+\{[ \t]*#(?P[^}\s]+)[^}]*\}[ \t]*$") +_CODE_SPAN = re.compile(r"(?.+?)(?!?)\[(?P