docs: publish translated docs in twelve languages and the tool that maintains them (#3280)
This commit is contained in:
parent
14aa889992
commit
52ad0a8876
53 changed files with 89737 additions and 54 deletions
280
i18n/uk/glossary.json
Normal file
280
i18n/uk/glossary.json
Normal file
|
|
@ -0,0 +1,280 @@
|
|||
{
|
||||
"keep": [
|
||||
"MCP",
|
||||
"Model Context Protocol",
|
||||
"MCPServer",
|
||||
"FastMCP",
|
||||
"ClientSession",
|
||||
"Context",
|
||||
"ctx",
|
||||
"stdio",
|
||||
"Streamable HTTP",
|
||||
"SSE",
|
||||
"JSON-RPC",
|
||||
"JSON",
|
||||
"OAuth",
|
||||
"PKCE",
|
||||
"JWT",
|
||||
"CIMD",
|
||||
"HTTP",
|
||||
"HTTPS",
|
||||
"TLS",
|
||||
"CORS",
|
||||
"URI",
|
||||
"URL",
|
||||
"ASGI",
|
||||
"WebSocket",
|
||||
"API",
|
||||
"SDK",
|
||||
"CLI",
|
||||
"IDE",
|
||||
"LLM",
|
||||
"SEP",
|
||||
"RFC",
|
||||
"Python",
|
||||
"TypeScript",
|
||||
"Node.js",
|
||||
"PyPI",
|
||||
"Pydantic",
|
||||
"Starlette",
|
||||
"FastAPI",
|
||||
"uvicorn",
|
||||
"httpx",
|
||||
"anyio",
|
||||
"asyncio",
|
||||
"trio",
|
||||
"pytest",
|
||||
"OpenTelemetry",
|
||||
"Inspector",
|
||||
"Claude",
|
||||
"GitHub",
|
||||
"VS Code",
|
||||
"Windows",
|
||||
"macOS",
|
||||
"Linux",
|
||||
"llms.txt",
|
||||
"2026-07-28",
|
||||
"2025-11-25",
|
||||
"2025-06-18",
|
||||
"2025-03-26"
|
||||
],
|
||||
"terms": [
|
||||
{
|
||||
"source": "tool",
|
||||
"target": "інструмент",
|
||||
"note": "MCP protocol noun (a server exposes tools): інструмент / інструменти. Standard rendering. Wire identifiers such as `tools/call` and the `@mcp.tool()` decorator are code and stay Latin; the Inspector's **Tools** tab is a UI label and stays English."
|
||||
},
|
||||
{
|
||||
"source": "resource",
|
||||
"target": "ресурс",
|
||||
"note": "MCP protocol noun (data a server exposes for reading), and also the general noun (a pool acquired in a lifespan is still ресурс). Standard rendering. `resources/read` and `@mcp.resource()` are code."
|
||||
},
|
||||
{
|
||||
"source": "prompt",
|
||||
"target": "промпт",
|
||||
"note": "The MCP feature (a reusable message template a server exposes) and the everyday LLM sense; the loanword промпт (masculine, declined: промпту, промпти) is what Ukrainian AI writing uses. Not підказка (a hint or tooltip) and not запрошення (a command-line prompt), which are other senses. `prompts/get` and `@mcp.prompt()` are code. Provisional pending native review."
|
||||
},
|
||||
{
|
||||
"source": "sampling",
|
||||
"target": "семплювання",
|
||||
"note": "The (deprecated) client feature that lets a server borrow the client's model for a completion. Gloss the English on first use per page — семплювання (sampling) — so the reader maps it to `sampling/createMessage`, which is code. Not вибірка (a statistical sample, the wrong sense). Provisional pending native review; семплінг is the competing form."
|
||||
},
|
||||
{
|
||||
"source": "roots",
|
||||
"target": "кореневі каталоги",
|
||||
"note": "The (deprecated) client feature listing the workspace directories a client exposes. Descriptive rendering with the English glossed on first use per page — кореневі каталоги (roots); a single root is кореневий каталог. `roots/list` and the `Root` type are code and stay Latin. Provisional pending native review; keeping roots in Latin script is the open alternative."
|
||||
},
|
||||
{
|
||||
"source": "elicitation",
|
||||
"target": "еліцитація",
|
||||
"note": "The mechanism by which a server asks the user a question through the client mid-request. There is no settled Ukrainian term; pinned to еліцитація (feminine, declinable), glossed on first use per page — еліцитація (elicitation). Do not alternate with запит на введення or уточнення on the same page. `elicitation/create`, `ctx.elicit()` and the `Elicit` class stay Latin. Provisional pending native review."
|
||||
},
|
||||
{
|
||||
"source": "capability",
|
||||
"target": "можливість",
|
||||
"note": "A negotiated protocol capability (what a client or server declared it supports): можливості сервера, узгодження можливостей. Not здатність or спроможність. The `capabilities` field and keys such as `sampling.tools` stay Latin. Provisional pending native review."
|
||||
},
|
||||
{
|
||||
"source": "transport",
|
||||
"target": "транспорт",
|
||||
"note": "The connection mechanism (\"every standard transport\" → усі стандартні транспорти; транспортний рівень for \"transport layer\"). Standard networking usage. The transport names stdio, Streamable HTTP and SSE stay in English: транспорт stdio, stdio-транспорт."
|
||||
},
|
||||
{
|
||||
"source": "session",
|
||||
"target": "сесія",
|
||||
"note": "An MCP session (the negotiated connection state): сесія, ідентифікатор сесії. Pinned over сеанс for consistency with prevailing developer usage; do not alternate the two. `session` objects, `ClientSession` and `ServerSession` are code. Provisional pending native review."
|
||||
},
|
||||
{
|
||||
"source": "handler",
|
||||
"target": "обробник",
|
||||
"note": "The tool, resource or prompt function you register, and request handlers generally (nav section \"Inside your handler\" → Усередині обробника). Standard term; never the slang хендлер, and not the Russian обработчик."
|
||||
},
|
||||
{
|
||||
"source": "dependency",
|
||||
"target": "залежність",
|
||||
"note": "Both package dependencies and the SDK's parameter-injection feature (the \"Dependencies\" page → Залежності; \"dependency injection\" → впровадження залежностей). Standard rendering. The `Resolve` marker class stays Latin."
|
||||
},
|
||||
{
|
||||
"source": "resolver",
|
||||
"target": "резолвер",
|
||||
"note": "The plain function attached to a parameter with `Resolve(...)` that computes or asks for its value: резолвер (masculine, declinable), or функція-резолвер where the kind needs naming. Provisional pending native review; функція розв'язання is the descriptive alternative. The `Resolve` class stays Latin."
|
||||
},
|
||||
{
|
||||
"source": "client",
|
||||
"target": "клієнт",
|
||||
"note": "An MCP client, and the client side of a connection. Standard rendering. The `Client` class and the `mcp.client` module are code and stay Latin."
|
||||
},
|
||||
{
|
||||
"source": "server",
|
||||
"target": "сервер",
|
||||
"note": "An MCP server (the program you build): сервер, MCP-сервер. Standard rendering. The `MCPServer`, `Server` and `ServerSession` classes are code and stay Latin."
|
||||
},
|
||||
{
|
||||
"source": "host",
|
||||
"target": "хост",
|
||||
"note": "The MCP host — the application the user talks to (Claude Desktop, an IDE, an agent runtime) — and also a network host; хост in both senses. Standard loanword; never господар or хазяїн."
|
||||
},
|
||||
{
|
||||
"source": "context",
|
||||
"target": "контекст",
|
||||
"note": "The generic lower-case word (\"provide context to LLMs\" → надавати контекст LLM). The capitalised `Context` is the SDK object injected as `ctx`; it is on the keep list and stays Latin in prose (\"The Context\" page title → Об'єкт Context). Standard rendering."
|
||||
},
|
||||
{
|
||||
"source": "application",
|
||||
"target": "застосунок",
|
||||
"note": "A software application, including the MCP host app and an ASGI app: застосунок (masculine: застосунку, застосунки; ASGI-застосунок). Not додаток, which is an add-on or an appendix and reads as a calque of the Russian word; not програма unless the English says program. Provisional pending native review."
|
||||
},
|
||||
{
|
||||
"source": "by default",
|
||||
"target": "за замовчуванням",
|
||||
"note": "\"By default\" / \"defaults to\" → за замовчуванням; \"the default value\" → значення за замовчуванням or типове значення. по замовчуванню is a calque and never correct. Provisional pending native review; типово / усталено are the purist alternatives.",
|
||||
"avoid": ["по замовчуванню"]
|
||||
},
|
||||
{
|
||||
"source": "next (following)",
|
||||
"target": "наступний",
|
||||
"note": "\"The next step / the following example\" → наступний крок, такий приклад / приклад нижче. слідуючий is a calque and never correct in modern Ukrainian (not on the checked avoid list only because наслідуючи contains the same letters)."
|
||||
},
|
||||
{
|
||||
"source": "is (linking verb)",
|
||||
"target": "є",
|
||||
"note": "The copula: \"A host is the LLM application\" → Хост — це LLM-застосунок, or Хостом є …. являється / являються as a linking verb is a calque of the Russian and never correct; in Ukrainian являтися means to appear in a dream. (Not on the checked avoid list only because з'являється contains the same letters.)"
|
||||
},
|
||||
{
|
||||
"source": "settings",
|
||||
"target": "налаштування",
|
||||
"note": "Settings / configuration → налаштування (also the verb: налаштувати); параметри where the English says options or parameters. настройка / настройки is Russian and never correct in Ukrainian text.",
|
||||
"avoid": ["настройк", "настройок"]
|
||||
},
|
||||
{
|
||||
"source": "cancel",
|
||||
"target": "скасувати",
|
||||
"note": "Request cancellation and cancelling a task: скасувати, скасування (\"the client cancelled the request\" → клієнт скасував запит). Not відмінити / відміна, which modern usage treats as a calque for this sense. Provisional pending native review."
|
||||
},
|
||||
{
|
||||
"source": "request",
|
||||
"target": "запит",
|
||||
"note": "A JSON-RPC or HTTP request (\"the initialize request\" → запит initialize / запит ініціалізації; HTTP-запит). Standard term, never реквест. `Request` types in code font stay Latin."
|
||||
},
|
||||
{
|
||||
"source": "response",
|
||||
"target": "відповідь",
|
||||
"note": "A JSON-RPC or HTTP response (HTTP-відповідь, тіло відповіді). Standard term, never респонс. `Response` types in code font stay Latin."
|
||||
},
|
||||
{
|
||||
"source": "notification",
|
||||
"target": "сповіщення",
|
||||
"note": "A JSON-RPC notification (a message that expects no response): надіслати сповіщення, сповіщення про перебіг виконання. Not нотифікація, not повідомлення (which is a message generally) and not the calque увідомлення. Method strings such as `notifications/tools/list_changed` stay Latin. Provisional pending native review."
|
||||
},
|
||||
{
|
||||
"source": "callback",
|
||||
"target": "колбек",
|
||||
"note": "Client callbacks and OAuth redirect callbacks alike (the \"Client callbacks\" page → Колбеки клієнта): колбек, masculine, declinable, spelled this way throughout. Provisional pending native review; функція зворотного виклику is the formal alternative and may serve as a one-time gloss. Parameter names such as `sampling_callback` stay Latin."
|
||||
},
|
||||
{
|
||||
"source": "decorator",
|
||||
"target": "декоратор",
|
||||
"note": "The Python decorators the SDK is built on; `@mcp.tool()` and its siblings are code and stay untouched. Standard rendering."
|
||||
},
|
||||
{
|
||||
"source": "type hint",
|
||||
"target": "анотація типів",
|
||||
"note": "Python type hints (\"from your type hints\" → з анотацій типів). Pinned over підказки типів; use one rendering throughout. Provisional pending native review."
|
||||
},
|
||||
{
|
||||
"source": "exception",
|
||||
"target": "виняток",
|
||||
"note": "A raised Python exception: виняток / винятки; \"raises an exception\" → викидає виняток (or генерує виняток). Not виключення, which means exclusion. Exception class names stay Latin. Provisional pending native review."
|
||||
},
|
||||
{
|
||||
"source": "async",
|
||||
"target": "асинхронний",
|
||||
"note": "The prose adjective (\"the async runtime\" → асинхронне середовище виконання, \"an async callback\" → асинхронний колбек); the `async` and `await` keywords in code font stay Latin. Standard rendering."
|
||||
},
|
||||
{
|
||||
"source": "lifespan",
|
||||
"target": "життєвий цикл",
|
||||
"note": "The server's startup/shutdown scope (the \"Lifespan\" page, as in the ASGI lifespan) → життєвий цикл, glossed on first use per page — життєвий цикл (lifespan) — so the reader maps it to the `lifespan=` parameter, which is code and stays Latin, as does the function passed to it when named in code font. Not тривалість життя. Provisional pending native review; keeping lifespan in Latin script throughout is the open alternative."
|
||||
},
|
||||
{
|
||||
"source": "back-channel",
|
||||
"target": "зворотний канал",
|
||||
"note": "This documentation's term for the server calling back into the client during a request, which exists only on legacy connections. Gloss the English on first use per page — зворотний канал (back-channel) — so the reader can connect it to `NoBackChannelError`, which is code. Provisional pending native review."
|
||||
},
|
||||
{
|
||||
"source": "multi-round-trip",
|
||||
"target": "багатораундовий",
|
||||
"note": "The 2026-07-28 request pattern: \"Multi-round-trip requests\" → Багатораундові запити, glossed on first use per page — багатораундові запити (multi-round-trip). A single \"round trip\" is раунд обміну or один цикл «запит — відповідь» by context, never a literal поїздка туди й назад. The abbreviation MRTR stays Latin. Provisional coinage pending native review; багатоходові запити is the alternative to weigh."
|
||||
},
|
||||
{
|
||||
"source": "deprecated",
|
||||
"target": "застарілий",
|
||||
"note": "Advisory status: still works, scheduled for removal later — застарілий / оголошений застарілим (\"Deprecated features\" → Застарілі можливості; \"deprecation warning\" → попередження про застарілість). \"Removed\" is a different word (вилучений / видалений); the corpus contrasts the two. The `MCPDeprecationWarning` class stays Latin. Provisional pending native review."
|
||||
},
|
||||
{
|
||||
"source": "legacy",
|
||||
"target": "старого покоління",
|
||||
"note": "\"A legacy connection / client\" = one negotiated at spec version 2025-11-25 or earlier → з'єднання старого покоління, клієнт старого покоління (the page \"Serving legacy clients\" → Обслуговування клієнтів старого покоління). Pairs with \"era\" → покоління and keeps застарілий free for \"deprecated\". Never the slang легасі. Provisional pending native review; попередніх версій is the alternative."
|
||||
},
|
||||
{
|
||||
"source": "era",
|
||||
"target": "покоління",
|
||||
"note": "\"Protocol era\" (\"a 2025-era client\", \"whatever era the client speaks\") → покоління протоколу, клієнт покоління 2025. Not the literal ера or епоха. Provisional pending native review."
|
||||
},
|
||||
{
|
||||
"source": "handshake",
|
||||
"target": "рукостискання",
|
||||
"note": "The initialization handshake (\"the classic handshake\" → класичне рукостискання). рукостискання is the standard Ukrainian networking term (as in TLS-рукостискання), so the literal word is correct here. Standard rendering."
|
||||
},
|
||||
{
|
||||
"source": "middleware",
|
||||
"target": "middleware",
|
||||
"note": "Kept in Latin script, indeclinable, lower-case in running text (the \"Middleware\" page title stays Middleware); name the kind where a case is needed: шар middleware, функція middleware. May take the one-time gloss middleware (проміжний шар). Provisional pending native review; проміжне ПЗ is the formal alternative."
|
||||
},
|
||||
{
|
||||
"source": "authorization",
|
||||
"target": "авторизація",
|
||||
"note": "Security sense: авторизація (сервер авторизації, код авторизації), distinct from authentication → автентифікація (not аутентифікація). The `Authorization` header and code identifiers stay Latin. Provisional pending native review."
|
||||
},
|
||||
{
|
||||
"source": "Get started",
|
||||
"target": "Початок роботи",
|
||||
"note": "The nav section that opens the guide, and the title of its index page. \"First steps\" is a separate page inside that section (Перші кроки), so the two need distinct renderings or the sidebar shows the same title twice. Provisional pending native review."
|
||||
},
|
||||
{
|
||||
"source": "First steps",
|
||||
"target": "Перші кроки",
|
||||
"note": "The tutorial page inside the \"Get started\" section; never reuse this rendering for the section itself (see that entry). Provisional pending native review."
|
||||
},
|
||||
{
|
||||
"source": "Recap",
|
||||
"target": "Підсумки",
|
||||
"note": "Recurring section heading that closes most pages; one rendering everywhere, not Підсумки on some pages and Резюме or Підіб'ємо підсумки on others. Provisional pending native review."
|
||||
},
|
||||
{
|
||||
"source": "Try it",
|
||||
"target": "Спробуйте самі",
|
||||
"note": "Recurring section heading above a runnable example; one rendering everywhere, not Спробуйте on some pages and Перевірка on others. Provisional pending native review."
|
||||
}
|
||||
]
|
||||
}
|
||||
170
i18n/uk/instructions.md
Normal file
170
i18n/uk/instructions.md
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
# Ukrainian (uk) — translation instructions
|
||||
|
||||
Target language: Ukrainian (українська мова), directory and URL code `uk`,
|
||||
page language tag `uk`. 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 modern, natural Ukrainian as today's Ukrainian developer community
|
||||
writes it: literate and plain, neither officialese nor chat.
|
||||
|
||||
- The reader is «ви», always lowercase mid-sentence: ви, вас, вам, ваш.
|
||||
Capitalised Ви / Ваш belongs in a personal letter to one person and is wrong
|
||||
here. Never ти, never a mix.
|
||||
- Reach for the pronoun rarely; prefer constructions that need no subject:
|
||||
"You can pass a schema" → Можна передати схему; "If you need the raw
|
||||
result" → Якщо потрібен сам результат; "You get a `CallToolResult`" →
|
||||
Повертається `CallToolResult`. Three ви in one paragraph is a signal to
|
||||
rephrase. Never replace "you" with користувач — the user is the person
|
||||
talking to the host, not the reader.
|
||||
- Steps and instructions are plain imperatives in the ви form: "Install the
|
||||
SDK, then run the server" → Встановіть SDK і запустіть сервер. A purpose
|
||||
clause is the other natural shape: "To run it: …" → Щоб запустити: …. Not
|
||||
Вам необхідно встановити, not Слід здійснити встановлення.
|
||||
- Headings, table headers and content-tab labels are noun phrases in sentence
|
||||
case with no final punctuation: "Running your server" → Запуск сервера,
|
||||
"Handling errors" → Обробка помилок, "Inside your handler" → Усередині
|
||||
обробника. "How to …" becomes Як + infinitive; a heading the English phrases
|
||||
as a question may stay a question.
|
||||
- The authorial "we" is fine where the English has it (Радимо …), but no
|
||||
давайте. One page, one register: a page that drifts between imperatives and
|
||||
officialese, or between ви and Ви, is wrong even if each sentence is fine.
|
||||
|
||||
## 2. Voice
|
||||
|
||||
The English is warm, direct and confident: short sentences, second person, the
|
||||
occasional one-line payoff ("That's the whole API."). Carry that into Ukrainian.
|
||||
|
||||
- Use concrete verbs: запустити, передати, повернути, оголосити, заблокувати.
|
||||
Prefer the active voice: "The tool is called by the model" → Модель
|
||||
викликає інструмент, not Інструмент викликається моделлю. Keep the payoff
|
||||
lines short: "That's a complete MCP server." → Це вже готовий MCP-сервер.
|
||||
- Split long English sentences and follow Ukrainian word order, but never
|
||||
merge, drop or reorder the technical claims themselves.
|
||||
- Avoid канцелярит: даний → цей; здійснювати / виконувати + noun → the verb
|
||||
itself (здійснює надсилання → надсилає); з метою → щоб; у випадку якщо →
|
||||
якщо; no chains of verbal nouns (для забезпечення можливості запуску → щоб
|
||||
запустити). Avoid active participles in -учий / -ючий, which read as
|
||||
calques: існуючий → наявний or що існує; працюючий сервер → сервер, що
|
||||
працює.
|
||||
- Avoid russianisms and суржик of every kind — vocabulary, calqued phrases,
|
||||
and Russian letters (ы, э, ъ, ё never appear in Ukrainian text; ґ, є, і, ї
|
||||
do where the orthography requires). §5 pins the common traps.
|
||||
- No hedging the English does not have ("don't" is не використовуйте, not
|
||||
можливо, варто утриматися) — and no over-correction either: no ти, no slang
|
||||
(юзати, тулза, дефолтний, задеплоїти), no smileys.
|
||||
|
||||
Example — English: "You don't construct it and you don't configure it. You ask
|
||||
for it."
|
||||
|
||||
- Not this (officialese): Користувачу не потрібно здійснювати його створення
|
||||
та конфігурування. Необхідно лише виконати відповідний запит.
|
||||
- Not this either (familiar): Ти його не створюєш і не налаштовуєш. Просто
|
||||
просиш.
|
||||
- This: Його не потрібно ні створювати, ні налаштовувати. Достатньо попросити.
|
||||
|
||||
## 3. Humour and idioms
|
||||
|
||||
- Translate the intent of a joke, aside or idiom, never its words: recast it
|
||||
as a short, natural Ukrainian sentence in the same register, or keep it
|
||||
brief where it carries nothing. 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 the whole API." / "That's the whole protocol." → Оце й
|
||||
увесь API. / Оце й увесь протокол.; "That's it. It's just Python." → От і
|
||||
все. Це звичайний Python.; "You get `3` back. ✨" → У відповідь приходить
|
||||
`3`. ✨
|
||||
- Idioms take the plain meaning, not the picture: "Out of the box the app
|
||||
answers **only** requests addressed to localhost." → За замовчуванням
|
||||
застосунок відповідає **лише** на запити, адресовані localhost. — not з
|
||||
коробки; "under the hood" → усередині, not під капотом; "on the wire" → у
|
||||
переданих даних / мережею, never по дроту.
|
||||
- Keep an exclamation mark only where the English is a genuine exclamation of
|
||||
encouragement — never after a warning or a step, never doubled, never in a
|
||||
heading. Reproduce an emoji only where the English has one, in the same
|
||||
place (two payoff lines end in ✨); never add one.
|
||||
|
||||
## 4. Typography
|
||||
|
||||
- Quotation marks in Ukrainian prose are «лапки-ялинки»; a quote nested inside
|
||||
them takes „…“. Straight quotes inside code spans, code blocks, commands and
|
||||
URLs stay untouched. When the English quotes a word the example code prints
|
||||
or a UI label, the text inside stays exactly as emitted and only the marks
|
||||
change: вкладка «Tools», кнопка «Connect».
|
||||
- The apostrophe is part of Ukrainian spelling and is never dropped or spaced:
|
||||
об'єкт, з'єднання, під'єднати, пам'ять, ім'я, комп'ютер, зв'язок,
|
||||
обов'язковий, п'ять — never обєкт. Write it as the plain character `'`
|
||||
(U+0027) on every page, rather than ’ (U+2019) or ʼ (U+02BC); this choice
|
||||
of character is provisional, apply it uniformly. It never glues an ending
|
||||
onto a Latin word (see §5).
|
||||
- Dashes: the grammatical dash is an em dash with a space on each side (Хост —
|
||||
це застосунок, з яким говорить користувач); a hyphen only joins compounds
|
||||
(MCP-сервер, HTTP-запит); numeric ranges use an en dash (3.10–3.14) or від
|
||||
3.10 до 3.14. An English em-dash aside may also become a comma pair,
|
||||
parentheses or its own sentence.
|
||||
- Sentence case everywhere: headings, admonition titles, tab labels and table
|
||||
headers capitalise the first word and proper nouns only. No capital after a
|
||||
colon. Language names, weekdays and months are lowercase (у липні).
|
||||
- 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 липня 2026 р. Version numbers, ports, HTTP status codes, error codes, RFC
|
||||
and SEP numbers are copied exactly.
|
||||
- Prose quantities take the decimal comma only when nothing but the separator
|
||||
changes (2.5 seconds → 2,5 секунди); when in doubt keep the number as
|
||||
written. A space separates a number from its unit (100 МБ, 30 секунд, 5 с);
|
||||
% attaches with no space (100%). Numerals govern the noun the Ukrainian way:
|
||||
1 інструмент, 3 інструменти, 5 інструментів.
|
||||
- e.g. → наприклад; i.e. → тобто; etc. → тощо; "&" → і / та. Emphasis lands on
|
||||
the same words the source emphasises, and a bolded negation ("**not**" →
|
||||
**не**) stays bold. Loanwords and Latin-script names are set in plain type —
|
||||
no italics, no quotes around them. Keep the source's colons and parentheses;
|
||||
a colon before a list or code block is natural Ukrainian too.
|
||||
|
||||
## 5. Terminology pointer
|
||||
|
||||
The glossary (`glossary.json`) is injected separately and overrides this file
|
||||
on every term it covers; each entry marks its choice as standard or provisional
|
||||
and says whether it takes a first-use gloss. Its renderings assume:
|
||||
|
||||
- Identifiers stay in Latin script exactly as written: class, function,
|
||||
method, parameter, module, environment-variable and header names, protocol
|
||||
method strings such as `tools/call`, and everything in code font. So do the
|
||||
keep-list terms, acronyms and product and protocol names, always without
|
||||
the English plural "s": "the SDKs" → SDK or пакети SDK.
|
||||
- Never decline a Latin-script word with an apostrophe or a glued ending
|
||||
(API'шка, SDK-а). Let a Ukrainian word carry the case instead: a hyphenated
|
||||
head noun (MCP-сервер, HTTP-запит, JSON-об'єкт, ASGI-застосунок) or the kind
|
||||
of thing in front of code (клас `Context`, параметр `lifespan=`, метод
|
||||
`client.call_tool()`). Adjectives and verbs agree with that Ukrainian word.
|
||||
- Russianism traps, pinned: application / app → застосунок (додаток is an
|
||||
add-on or an appendix); "by default" → за замовчуванням (never по
|
||||
замовчуванню; "default value" → типове значення); next → наступний (never
|
||||
слідуючий); the linking verb "is" → є or a dash (never являється); settings
|
||||
→ налаштування (never настройки); cancel → скасувати (not відмінити);
|
||||
exception → виняток (виключення means exclusion); authentication →
|
||||
автентифікація (not аутентифікація); environment → середовище (not
|
||||
оточення); get → отримати; delete → видалити.
|
||||
- Where an established Ukrainian term exists, use it, not the anglicism:
|
||||
обробник (not хендлер), сповіщення (not нотифікація), екземпляр (not
|
||||
інстанс), розгортання (not деплой), середовище виконання (not рантайм).
|
||||
Settled loanwords stay: сервер, клієнт, хост, токен, сесія, схема,
|
||||
декоратор, промпт, репозиторій, фреймворк, плагін, лог.
|
||||
- Text quoted from what the example code prints or displays — an output line,
|
||||
a log message, an Inspector tab or button label — stays exactly as the code
|
||||
emits it (usually English), in or out of code font; never translate it.
|
||||
- First-use gloss: a term the glossary marks for it carries the English in
|
||||
parentheses on its first appearance in a page — еліцитація (elicitation) —
|
||||
and appears alone after that. A glossary word used as a wire identifier or
|
||||
a key in code font stays Latin: "the `sampling` capability" → можливість
|
||||
`sampling`.
|
||||
- One rendering per term per page: the glossary target, every time.
|
||||
|
||||
## 6. Provisional note
|
||||
|
||||
Every decision in this file, and every entry in `glossary.json`, is
|
||||
provisional pending review by native Ukrainian-speaking developers. To propose
|
||||
a change, edit this file or `glossary.json` in a pull request, ideally with a
|
||||
short good/bad example; never edit the generated `pages/` or `notices.md`.
|
||||
20
i18n/uk/notices.md
Normal file
20
i18n/uk/notices.md
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
---
|
||||
translation:
|
||||
sections: [aff1b3e872b7876a, 4d80558ad052d586, 0bb81f1e62062d26, d5c35dcec50156bc]
|
||||
tool: 1
|
||||
---
|
||||
# Примітки про переклад {#translation-notices}
|
||||
|
||||
Одна з цих приміток з'являється вгорі кожної сторінки перекладеного сайту документації.
|
||||
|
||||
## Машинний переклад {#translated}
|
||||
|
||||
Цю сторінку перекладено автоматично з англомовної документації, і основною версією є [англомовна сторінка](ENGLISH_PAGE). Якщо щось читається неправильно, на сторінці [Переклади](TRANSLATIONS_PAGE) пояснено, як про це повідомити.
|
||||
|
||||
## Переклад відстає від англомовної сторінки {#outdated}
|
||||
|
||||
Англомовна сторінка змінилася після того, як було зроблено цей переклад, тож окремі його частини можуть бути застарілими. Якщо є сумніви, читайте [англомовну сторінку](ENGLISH_PAGE); на сторінці [Переклади](TRANSLATIONS_PAGE) пояснено, як влаштована перекладена документація.
|
||||
|
||||
## Показано англійською {#english}
|
||||
|
||||
Актуального перекладу цієї сторінки немає, тому ви читаєте її англійською. На сторінці [Переклади](TRANSLATIONS_PAGE) пояснено, як влаштована перекладена документація.
|
||||
123
i18n/uk/pages/advanced/apps.md
Normal file
123
i18n/uk/pages/advanced/apps.md
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
---
|
||||
translation:
|
||||
sections: [0355618e5f4d5fe4, 1821eaf50f2d0b64, 82e0b28ebd3abf5a, 8ac39614c094f2d0, dab6ff945501ab2a, bd5565c3b2d4f959, 96819ce3d63a0487]
|
||||
tool: 1
|
||||
---
|
||||
# MCP Apps {#mcp-apps}
|
||||
|
||||
**MCP App** — це інструмент із власним обличчям: поряд із даними інструмент указує на HTML-документ, який хост відображає як інтерактивну поверхню.
|
||||
|
||||
Дві частини, завжди дві частини:
|
||||
|
||||
1. **Інструмент**, який виконує роботу й повертає дані, як будь-який інший інструмент.
|
||||
2. **Ресурс `ui://`** з HTML, який хост показує для нього.
|
||||
|
||||
Інструмент несе посилання на ресурс у `_meta.ui.resourceUri`. Хост отримує його через `resources/read`, відображає в **ізольованому iframe (sandbox)** і передає результат інструмента в цей iframe через `postMessage`. Ваш сервер ніколи не надсилає й не отримує жодних повідомлень `ui/*`: цей обмін відбувається між хостом та iframe. Ви віддаєте інструмент і HTML-документ, а всю виставу ставить хост.
|
||||
|
||||
SDK постачає це як вбудоване розширення `Apps` (`io.modelcontextprotocol/ui`). Якщо [розширення](extensions.md) для вас новинка, спершу прогляньте ту сторінку. Одна хвилина — і повертайтеся.
|
||||
|
||||
## Годинник із циферблатом {#a-clock-with-a-face}
|
||||
|
||||
```python title="server.py" hl_lines="19 22 30 32"
|
||||
--8<-- "docs_src/apps/tutorial001.py"
|
||||
```
|
||||
|
||||
Чотири кроки:
|
||||
|
||||
* `Apps()`: один екземпляр тримає ваші інструменти з прив'язаним UI та їхні ресурси.
|
||||
* `@apps.tool(resource_uri="ui://clock/app.html")`: звичайний інструмент плюс позначка `_meta.ui.resourceUri`. Усе, що приймає `@mcp.tool()` (name, title, description, ...), передається далі.
|
||||
* `apps.add_html_resource("ui://clock/app.html", CLOCK_HTML)`: відповідний ресурс, який віддається як `text/html;profile=mcp-app`. Саме цей MIME-тип каже хосту: «це застосунок, відобрази його».
|
||||
* `MCPServer("clock", extensions=[apps])`: увімкнення. Тепер сервер оголошує `io.modelcontextprotocol/ui` у `capabilities.extensions`.
|
||||
|
||||
Сам HTML слухає `postMessage` від хоста й показує результат. Для справжніх застосунків використовуйте всередині HTML офіційний браузерний SDK [`@modelcontextprotocol/ext-apps`](https://github.com/modelcontextprotocol/ext-apps). Він дає `ontoolresult`, `callServerTool`, `getHostContext` і `onhostcontextchanged` замість сирих подій повідомлень.
|
||||
|
||||
## Плавна деградація {#graceful-degradation}
|
||||
|
||||
Не кожен клієнт відображає застосунки. Специфікація прямо каже, що це означає для вас:
|
||||
|
||||
> Інструменти **МУСЯТЬ** повертати змістовний масив `content`, навіть коли UI доступний.
|
||||
|
||||
Модель читає `content`; iframe — для людей. Хост із підтримкою UI все одно передає текстовий результат моделі, а суто текстовий клієнт отримує *лише* його. Тож канонічний шаблон — один інструмент, дві відповіді. Погляньте на `get_time` ще раз:
|
||||
|
||||
```python title="server.py" hl_lines="23-27"
|
||||
--8<-- "docs_src/apps/tutorial001.py"
|
||||
```
|
||||
|
||||
`client_supports_apps(ctx)` дорівнює `True` лише тоді, коли клієнт оголосив розширення `io.modelcontextprotocol/ui` **і** вказав `text/html;profile=mcp-app` у своїх налаштуваннях `mimeTypes`. Поле обов'язкове, тож клієнт, який його пропустив, не зараховується. Саме це й оголошує `main()` у тому самому файлі: клієнтську половину узгодження — і у відповідь приходить розширений варіант.
|
||||
|
||||
!!! warning
|
||||
Ніколи не повертайте заглушку на кшталт `"[Rendered UI]"` як єдиний вміст. Якщо резервний текст марний, інструмент марний для кожного суто текстового клієнта й для самої моделі. Напишіть нормальне речення.
|
||||
|
||||
## Обмеження iframe {#locking-the-iframe-down}
|
||||
|
||||
Метадані безпеки несе ресурс: що iframe може завантажувати, які дозволи браузера йому потрібні, як його бажано вбудовувати:
|
||||
|
||||
```python title="server.py" hl_lines="9 19-22"
|
||||
--8<-- "docs_src/apps/tutorial002.py"
|
||||
```
|
||||
|
||||
`csp` і `permissions` — це **запити до хоста**, а не поведінка сервера. Хост будує з них Content-Security-Policy і Permissions-Policy для iframe й може відмовити. Перевіряйте наявність можливостей у своєму JS, а не припускайте, що дозвіл надано.
|
||||
|
||||
`ResourceCsp`, поле за полем (ім'я в Python, ключ у переданих даних, що з ним робить хост):
|
||||
|
||||
| Python | У переданих даних (`_meta.ui.csp`) | Керує |
|
||||
|---|---|---|
|
||||
| `connect_domains` | `connectDomains` | `connect-src`: куди можуть звертатися `fetch`/XHR |
|
||||
| `resource_domains` | `resourceDomains` | `img-src`, `style-src`, ...: статичні ресурси |
|
||||
| `frame_domains` | `frameDomains` | `frame-src`: вкладені iframe |
|
||||
| `base_uri_domains` | `baseUriDomains` | `base-uri`: на що може вказувати `<base>` |
|
||||
|
||||
`ResourcePermissions`: кожне поле запитує для iframe дозвіл браузера.
|
||||
|
||||
| Python | У переданих даних (`_meta.ui.permissions`) |
|
||||
|---|---|
|
||||
| `camera` | `camera` |
|
||||
| `microphone` | `microphone` |
|
||||
| `geolocation` | `geolocation` |
|
||||
| `clipboard_write` | `clipboardWrite` |
|
||||
|
||||
!!! note
|
||||
CSP і дозволи живуть на **ресурсі**, ніколи на інструменті. У метаданих інструмента за специфікацією для них немає місця, і хости їх там ігнорують. SDK робить цю помилку неможливою: `@apps.tool()` просто не має параметра `csp`.
|
||||
|
||||
### Видимість {#visibility}
|
||||
|
||||
`visibility=["app"]` на інструменті означає «це існує для iframe, а не для моделі»:
|
||||
|
||||
* `"model"`: інструмент може викликати модель.
|
||||
* `"app"`: інструмент може викликати iframe (через `callServerTool`).
|
||||
* Не вказано: обидва, це значення за замовчуванням.
|
||||
|
||||
Фільтрація — справа **хоста**. Ваш сервер перелічує інструменти лише для застосунку в `tools/list`, як і будь-які інші; хост приховує їх від моделі. Не фільтруйте на боці сервера.
|
||||
|
||||
## Правила, які контролює SDK {#the-rules-the-sdk-enforces}
|
||||
|
||||
Усе це падає під час запуску, а не в продакшені:
|
||||
|
||||
* `resource_uri` або URI ресурсу, що не має вигляду `ui://...`, дає `ValueError` під час декорування чи реєстрації.
|
||||
* Інструмент, прив'язаний до URI **без відповідного зареєстрованого ресурсу**, дає `ValueError`, коли `MCPServer(extensions=[apps])` приймає розширення. Інструмент, що оголошує HTML, який повертає 404 на `resources/read`, — це помилка конфігурації, тож сервер відмовляється створюватися.
|
||||
* `meta={"ui": ...}` у `@apps.tool()` дає `ValueError`. Декоратор володіє `_meta["ui"]`; висловлюйте це через `resource_uri=` і `visibility=`. Інші ключі `meta=` спокійно зливаються поруч.
|
||||
|
||||
Ані TypeScript SDK ext-apps, ані FastMCP сьогодні нічого з цього не ловлять; краще дізнатися про це раніше, ніж дізнається хост.
|
||||
|
||||
## Не тільки вбудований HTML {#beyond-inline-html}
|
||||
|
||||
`add_html_resource` покриває типовий випадок: рядок з HTML. Для всього іншого — HTML на диску чи згенерованого вмісту — побудуйте ресурс самі й передайте його:
|
||||
|
||||
```python title="server.py" hl_lines="12 18"
|
||||
--8<-- "docs_src/apps/tutorial003.py"
|
||||
```
|
||||
|
||||
`add_resource` підставляє MIME-тип `text/html;profile=mcp-app`, коли ресурс не задає його явно, і відхиляє явну невідповідність: ресурс `ui://` з будь-яким іншим MIME-типом не відобразить жоден хост.
|
||||
|
||||
!!! tip
|
||||
Орієнтуєтеся на хост до GA-версії, який досі читає застарілий плоский ключ `_meta["ui/resourceUri"]`? Додайте його самі:
|
||||
`@apps.tool(resource_uri="ui://x", meta={"ui/resourceUri": "ui://x"})`.
|
||||
Вкладений об'єкт `ui` — це форма за специфікацією; плоский ключ доживає своє.
|
||||
|
||||
## Приклад у дії {#see-it-run}
|
||||
|
||||
Історія `apps` в `examples/stories/` — це ця сторінка у вигляді пари, яку можна запустити: сервер з інструментом-годинником із прив'язаним UI та клієнт, який узгоджує Apps, читає `_meta.ui.resourceUri` інструмента, отримує HTML і викликає інструмент.
|
||||
|
||||
```bash
|
||||
uv run python -m stories.apps.client
|
||||
```
|
||||
172
i18n/uk/pages/advanced/extensions.md
Normal file
172
i18n/uk/pages/advanced/extensions.md
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
---
|
||||
translation:
|
||||
sections: [05891e7cc1938a13, b3c01a6af28c51ee, 7ffc91f5e38bdfe0, 717d3f235a8333a7, f471a13b2fe5d737, ed6af2df4b656dff]
|
||||
tool: 1
|
||||
---
|
||||
# Розширення {#extensions}
|
||||
|
||||
**Розширення** — це набір поведінки MCP, який вмикається лише на явний запит і стоїть за одним ідентифікатором.
|
||||
|
||||
На сервері воно може додавати інструменти, ресурси й нові методи запитів, а також обгортати `tools/call`. На клієнті — заявляти додаткові форми результату `tools/call` і спостерігати за вендорськими сповіщеннями. Кожна сторона оголошує розширення у власному `capabilities.extensions`, і для тих, хто про це не просив, нічого не змінюється. Такий контракт ([SEP-2133](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2133)), і в нього одне золоте правило: **розширення за замовчуванням вимкнені**.
|
||||
|
||||
## Використання розширення {#using-an-extension}
|
||||
|
||||
Передайте екземпляри під час створення:
|
||||
|
||||
```python title="server.py"
|
||||
--8<-- "docs_src/extensions/tutorial001.py"
|
||||
```
|
||||
|
||||
Готово. Тепер сервер оголошує `io.modelcontextprotocol/ui` у `capabilities.extensions` і обслуговує все, що додає розширення.
|
||||
|
||||
`Apps` — вбудоване еталонне розширення, і йому присвячено окрему сторінку: **[MCP Apps](apps.md)**.
|
||||
|
||||
!!! note
|
||||
Розширення фіксуються під час створення. Методу `add_extension`, який можна було б викликати пізніше, немає: карта можливостей сервера не повинна змінюватися, поки до нього під'єднані клієнти.
|
||||
|
||||
Карта можливостей передається через `server/discover`, а це шлях версії **2026-07-28**. Рукостисканню `initialize` старого покоління нікуди її покласти, тож клієнт старого покоління просто не бачить розширення. Проєктуйте з урахуванням цього: розширення *доповнює* сервер і не повинно бути єдиним способом ним користуватися.
|
||||
|
||||
## Написання власного розширення {#writing-your-own}
|
||||
|
||||
Успадкуйте `Extension` і перевизначте лише те, що потрібно. Кожен метод має типову реалізацію.
|
||||
|
||||
### Ідентифікатор {#the-identifier}
|
||||
|
||||
```python
|
||||
--8<-- "docs_src/extensions/tutorial002.py"
|
||||
```
|
||||
|
||||
Ідентифікатор — це рядок вигляду `vendor-prefix/name`, що відповідає граматиці ключів `_meta` зі специфікації: розділені крапками мітки (кожна починається з літери й закінчується літерою або цифрою), скісна риска, потім ім'я. Він перевіряється **під час визначення класу**, тож друкарська помилка не чекає, поки сервер запуститься:
|
||||
|
||||
```text
|
||||
TypeError: Stamps.identifier must be a `vendor-prefix/name` string
|
||||
(reverse-DNS prefix required), got 'stamps'
|
||||
```
|
||||
|
||||
Як префікс використовуйте домен, яким ви керуєте. `io.modelcontextprotocol/*` призначено для розширень, які специфікує сам проєкт MCP.
|
||||
|
||||
### Додавання інструментів {#contributing-tools}
|
||||
|
||||
Найменше корисне розширення — один інструмент і карта налаштувань:
|
||||
|
||||
```python title="server.py" hl_lines="17 19-20 22-23 26"
|
||||
--8<-- "docs_src/extensions/tutorial003.py"
|
||||
```
|
||||
|
||||
* `tools()` повертає об'єкти `ToolBinding`. Сервер реєструє кожен із них точно так, ніби ви самі викликали `mcp.add_tool(...)`: те саме генерування схеми, те саме впровадження `Context`, усе те саме.
|
||||
* `settings()` — це значення, що оголошується в `capabilities.extensions["com.example/stamps"]`. Поверніть `{}` (типове значення), щоб оголосити розширення без налаштувань.
|
||||
* Розширення ніколи не отримує сервер. Воно оголошує свій внесок як дані; `MCPServer` їх споживає. Ніякого `self.server`, який можна було б змінювати, немає.
|
||||
|
||||
А `main()` — доказ: клієнт у пам'яті, під'єднаний безпосередньо до `mcp`:
|
||||
|
||||
```python title="server.py" hl_lines="29-34"
|
||||
--8<-- "docs_src/extensions/tutorial003.py"
|
||||
```
|
||||
|
||||
### Обслуговування власних методів {#serving-your-own-methods}
|
||||
|
||||
Розширення може реєструвати **нові методи запитів**: власні дієслова, які обслуговуються поруч із методами специфікації:
|
||||
|
||||
```python title="server.py" hl_lines="16-22 31 40-48"
|
||||
--8<-- "docs_src/extensions/tutorial004.py"
|
||||
```
|
||||
|
||||
* `SearchParams` успадковує `RequestParams`, тож конверт `_meta` версії 2026 розбирається однаково, а обробник отримує перевірені параметри, а не сирий словник. Обмежуйте те, чим керує клієнт: `Field(ge=1, le=100)` відхиляє безглуздий `limit`, перш ніж ваш код щось під нього виділить.
|
||||
* `require_client_extension(ctx, EXTENSION_ID)` — це шлагбаум: клієнт, який не оголосив розширення, отримує помилку `-32021` (відсутня обов'язкова можливість клієнта) з машиночитаним корисним навантаженням `requiredCapabilities`, якого вимагає специфікація.
|
||||
* `protocol_versions=frozenset({"2026-07-28"})` прив'язує метод до однієї версії протоколу. На будь-якій іншій версії клієнт отримує `METHOD_NOT_FOUND` — точно так, ніби методу там не існує. Для цього клієнта він і не існує.
|
||||
|
||||
Методи **лише додаються**. SDK забезпечує це під час створення, а не під час виконання:
|
||||
|
||||
* `MethodBinding` для методу, визначеного специфікацією (`tools/list`, `completion/complete`, ...), викидає `ValueError` під час створення прив'язки. Базові дієслова належать серверу.
|
||||
* Два розширення, що прив'язують той самий метод, викидають виняток, коли реєструється друге. «Перемагає останній запис» — саме так плагіни псують одне одного; ми цього не робимо.
|
||||
* Порожня множина `protocol_versions` теж викидає виняток: метод, який ніколи не може бути обслужений, — це помилка, а не конфігурація.
|
||||
|
||||
### Клієнтська сторона {#the-client-side}
|
||||
|
||||
`main()` у тому самому файлі — це вся клієнтська частина, обидві її половини:
|
||||
|
||||
```python title="server.py" hl_lines="54-58"
|
||||
--8<-- "docs_src/extensions/tutorial004.py"
|
||||
```
|
||||
|
||||
* `Client(..., extensions=[advertise(EXTENSION_ID)])` оголошує розширення. Оголошення стають `ClientCapabilities.extensions`: на з'єднанні версії 2026-07-28 карта подорожує в конверті `_meta` кожного запиту, тож сервер бачить її в **кожному** запиті; на з'єднанні старого покоління вона передається з рукостисканням `initialize`. Серверному коду байдуже, який саме шлях: `require_client_extension(ctx, ...)` і `ctx.session.check_client_capability(...)` читають правильне джерело в обох випадках.
|
||||
* Вендорські методи опускаються на один шар нижче, до `client.session.send_request(...)`; повноцінні методи в `Client` з'являються лише для дієслів специфікації. `send_request` приймає будь-який підклас `Request`, тож вендорський запит проходить як є.
|
||||
|
||||
### Перехоплення `tools/call` {#intercepting-toolscall}
|
||||
|
||||
Єдиний хук-перехоплювач. Перевизначте `intercept_tool_call`, щоб спостерігати за викликом інструмента, завершувати його достроково або забороняти:
|
||||
|
||||
```python title="server.py" hl_lines="17-24"
|
||||
--8<-- "docs_src/extensions/tutorial005.py"
|
||||
```
|
||||
|
||||
* `params` — це перевірений `CallToolRequestParams`: `params.name` і `params.arguments` доступні без роботи із сирим JSON. Саме він визначає, який виклик інструмента виконується: якщо передати через `call_next` переписаний контекст, зміниться те, що обробник бачить у `ctx`, а не сам виклик інструмента. Переписування запитів на рівні переданих даних — справа [Middleware](middleware.md).
|
||||
* `call_next(ctx)` виконує решту ланцюжка й повертає результат обробника. Поверніть його без змін (спостереження), поверніть щось інше (заміна) або викиньте `MCPError` (відмова). Усе, що ви повернете, серіалізується як будь-який результат обробника, разом зі штампом ідентичності `serverInfo` покоління 2026, тож перехоплювач, який завершує виклик достроково, ніколи не видасть анонімної відповіді чи відповіді не за схемою.
|
||||
* Коли розширень кілька, перехоплювачі вкладаються в порядку реєстрації: перше розширення в `extensions=[...]` — зовнішнє.
|
||||
* Типова реалізація просто пропускає виклик далі, а сервер, чиї розширення не перевизначають цей хук, зберігає голий обробник `tools/call` недоторканим. За те, чим не користуєтеся, не платите.
|
||||
|
||||
Хук обгортає `tools/call` і нічого більше. Для того, що стосується кожного повідомлення, використовуйте [Middleware](middleware.md). Саме для цього воно й існує.
|
||||
|
||||
## Використання клієнтського розширення {#using-a-client-extension}
|
||||
|
||||
**Клієнтське розширення** — той самий контракт з боку споживача: набір клієнтської поведінки за одним ідентифікатором. Передайте екземпляри в `Client(extensions=[...])` і викликайте інструменти як зазвичай:
|
||||
|
||||
```python title="client.py" hl_lines="66-68"
|
||||
--8<-- "docs_src/extensions/tutorial006.py"
|
||||
```
|
||||
|
||||
`call_tool("buy", ...)` повертає звичайний `CallToolResult`, як і будь-який інший виклик. Що змінило розширення: тепер сервер може відповісти на `buy` **формою результату** `receipt` замість остаточного результату, а `Receipts` доводить її до кінця (тут — погашаючи квитанцію наступним викликом), перш ніж `call_tool` поверне значення. У місці виклику нічого не змінюється.
|
||||
|
||||
Приберіть розширення — і нічого з цього не існує: шлагбаум сервера відмовляє клієнту, який його не оголосив (помилка -32021), а заявлена форма від сервера, що обходить шлагбаум, не проходить перевірку — точно так, як специфікація вимагає для нерозпізнаного `resultType`. Вимкнено за замовчуванням, з обох кінців з'єднання.
|
||||
|
||||
Щоб оголосити ідентифікатор **без** жодної клієнтської поведінки (сервер перевіряє можливість, клієнт нічого не робить, як у клієнті пошуку вище), використовуйте `advertise()`:
|
||||
|
||||
```python
|
||||
from mcp.client import advertise
|
||||
|
||||
client = Client(mcp, extensions=[advertise("com.example/search")])
|
||||
```
|
||||
|
||||
## Написання клієнтського розширення {#writing-a-client-extension}
|
||||
|
||||
Успадкуйте `ClientExtension` і перевизначте лише те, що потрібно. Три види внеску, кожен із типовою реалізацією: `settings()`, `claims()` і `notifications()`.
|
||||
|
||||
```python title="client.py" hl_lines="17-18 43-44 46-47"
|
||||
--8<-- "docs_src/extensions/tutorial006.py"
|
||||
```
|
||||
|
||||
* Ідентифікатор підпорядковується тій самій граматиці, що й на сервері, і перевіряється під час визначення класу.
|
||||
* `claims()` повертає об'єкти `ResultClaim`: тег у переданих даних, модель, яка його розбирає, і резолвер, який доводить його до кінця. Модель мусить зафіксувати тег через `result_type: Literal["receipt"]` і не повинна успадковувати базові типи результатів дієслова; обидві умови перевіряються під час створення заявки. Вендорські поля на кшталт `receipt_token` передаються мережею як є: підставлена форма доходить до клієнта дослівно.
|
||||
* Резолвер отримує розібрану модель і `ClaimContext`; `ctx.session` — той самий публічний дескриптор, що й `client.session`, тож подальші виклики — це звичайні виклики сесії. Він повертає звичайний для дієслова `CallToolResult`.
|
||||
* `settings()` — значення, що оголошується в `ClientCapabilities.extensions[identifier]`; воно читається один раз під час створення `Client`.
|
||||
|
||||
`notifications()` оголошує вендорські сповіщення сервера, за якими слід спостерігати:
|
||||
|
||||
```python
|
||||
def notifications(self) -> Sequence[NotificationBinding[Any]]:
|
||||
return [NotificationBinding(method="notifications/receipts", params_type=ReceiptEvent, handler=self.on_receipt)]
|
||||
```
|
||||
|
||||
Обробник отримує перевірені параметри по одному, у порядку диспетчеризації. Він спостерігає; накласти вето чи відповісти він не може.
|
||||
|
||||
Два негучні правила. Заявки діють лише на з'єднаннях версії 2026-07-28, і оголошення можливостей іде за ними: на з'єднанні старого покоління заявки розчиняються, а разом із ними з оголошення випадає й ідентифікатор, тож клієнт ніколи не оголошує розширення, чиї форми він би відхилив. А коли заявлена форма потрібна вам самим, а не резолверу, викликайте `client.session.call_tool(..., allow_claimed=True)`; без цього прапорця заявлена форма, що доходить до виклику на рівні сесії, викидає `UnexpectedClaimedResult`.
|
||||
|
||||
### Дієслова розширень {#extension-verbs}
|
||||
|
||||
Власні методи запитів розширення не потребують реєстрації на клієнті. Тип вендорського запиту успадковує `mcp.types.Request` і проходить через `client.session.send_request`, як у розділі [Обслуговування власних методів](#serving-your-own-methods). Одне доповнення: коли ключ параметрів мусить передаватися в заголовку `Mcp-Name` (специфікації розширень, як-от tasks, вимагають цього для своїх дієслів), тип запиту оголошує `name_param`:
|
||||
|
||||
```python title="client.py" hl_lines="22-25 46-47"
|
||||
--8<-- "docs_src/extensions/tutorial007.py"
|
||||
```
|
||||
|
||||
Сесія дзеркалить `params["jobId"]` у `Mcp-Name` на кожному шляху надсилання, а відсутнє значення дає гучну помилку замість того, щоб мовчки пропустити обов'язковий заголовок.
|
||||
|
||||
## Чого розширення не може {#what-an-extension-cannot-do}
|
||||
|
||||
Поверхня внеску **закрита** навмисно. На сервері: налаштування, інструменти, ресурси, методи, один перехоплювач `tools/call`. На клієнті: налаштування, заявки на результати, прив'язки сповіщень. Розширення не може:
|
||||
|
||||
* **Лізти в хост.** Воно оголошує дані; посилання на сервер чи клієнт у нього немає.
|
||||
* **Замінювати базову поведінку.** Методи специфікації та базові теги результатів відхиляються під час створення (`initialize` цілком зарезервовано за виконавцем); прив'язка сповіщення, перекрита базовим словником, натомість замовкає з попередженням.
|
||||
* **Реєструватися із запізненням.** Після того як `MCPServer(...)` чи `Client(...)` повернув керування, набір розширень уже такий, який є.
|
||||
|
||||
Якщо ви воюєте з цими стінами, ви пишете не розширення. Ви пишете форк. Стіни — це й є головна перевага: користувач, що читає `extensions=[Apps(), Stamps()]`, знає *все*, чого ці двоє могли торкнутися.
|
||||
34
i18n/uk/pages/advanced/index.md
Normal file
34
i18n/uk/pages/advanced/index.md
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
---
|
||||
translation:
|
||||
sections: [ca6988b7503cd2d3]
|
||||
tool: 1
|
||||
---
|
||||
# Розширені можливості {#advanced}
|
||||
|
||||
Усе, що потрібно звичайному серверу чи клієнту, має своє тематичне місце в розділах вище.
|
||||
Цей розділ — це запасні виходи, до яких звертаються, коли зручний шар `MCPServer`
|
||||
починає заважати:
|
||||
|
||||
* **[Низькорівневий Server](low-level-server.md)**: клас, на якому побудовано `MCPServer`.
|
||||
Схеми, написані вручну, обробники `on_*`, жодних перевірок за вас і власні
|
||||
JSON-RPC-методи.
|
||||
* **[Пагінація](pagination.md)** і **[Middleware](middleware.md)**: дві речі, які
|
||||
можна зробити *лише* на низькорівневому `Server`.
|
||||
* **[Розширення](extensions.md)** і **[Застосунки MCP](apps.md)**: поверхня розширень
|
||||
протоколу. Скомпонуйте пакети розширень у сервер або напишіть власні.
|
||||
|
||||
Кілька речей, які цілком логічно шукати тут, натомість розміщено там, де ними
|
||||
справді користуються:
|
||||
|
||||
* **Авторизація** — у розділі **[Запуск сервера](../run/index.md)**, бо сервер
|
||||
захищають там, де його розгортають.
|
||||
* **OAuth**, **підтвердження ідентичності**, під'єднання до **кількох серверів** і
|
||||
**кеш** відповідей — усе це в розділі **[Клієнти](../client/index.md)**.
|
||||
* **Багатораундові запити** і **Підписки** — у розділі
|
||||
**[Усередині обробника](../handlers/index.md)**, бо і те, й інше обробник
|
||||
саме *робить*.
|
||||
* **Шаблони URI** — у розділі **[Сервери](../servers/index.md)**, поруч із ресурсами.
|
||||
* **[Версії протоколу](../protocol-versions.md)** і
|
||||
**[Застарілі можливості](../deprecated.md)** мають власні сторінки верхнього рівня.
|
||||
|
||||
Якщо ви не впевнені, чи потрібен вам цей розділ, — він вам не потрібен.
|
||||
207
i18n/uk/pages/advanced/low-level-server.md
Normal file
207
i18n/uk/pages/advanced/low-level-server.md
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
---
|
||||
translation:
|
||||
sections: [2c79b6338e09b7ac, 7edc43b3fae11314, 1086e77ce561cd7f, a3f71823df5efc31, 9fc7109f72201cae, 7bf25983df655b66, 6330e1f4c6029683, 2f1749c8c133fa1c, b3530fcf4d11fd56, ebc33704fbd74262, cd0e9c933350390e]
|
||||
tool: 1
|
||||
---
|
||||
# Низькорівневий Server {#the-low-level-server}
|
||||
|
||||
`@mcp.tool()` — це шар. Під ним лежить другий серверний клас, `Server`, який говорить «сирим» MCP: ви передаєте йому об'єкти протоколу, а він надсилає їх мережею без змін.
|
||||
|
||||
`MCPServer` побудований поверх нього. Спускатися на рівень нижче варто тоді, коли зручний шар заважає:
|
||||
|
||||
* Потрібно віддати **точну** схему (завантажену з файлу, згенеровану з бази даних), а не виведену з сигнатури Python.
|
||||
* Потрібен повний контроль над результатом: `_meta`, `is_error`, кожен ключ `structured_content`.
|
||||
* Потрібно обробити метод, якого MCP не визначає.
|
||||
|
||||
Для всього іншого залишайтеся на `MCPServer`.
|
||||
|
||||
## Той самий інструмент, вручну {#the-same-tool-by-hand}
|
||||
|
||||
Це інструмент `search_books`, який сторінка **[Інструменти](../servers/tools.md)** пише дев'ятьма рядками `@mcp.tool()`, — тільки без синтаксичного цукру:
|
||||
|
||||
```python title="server.py" hl_lines="22 26 32"
|
||||
--8<-- "docs_src/lowlevel/tutorial001.py"
|
||||
```
|
||||
|
||||
Змінилися три речі, і вони й складають увесь низькорівневий API:
|
||||
|
||||
* **Обробники — це параметри конструктора.** `on_list_tools=` і `on_call_tool=` передаються в `Server(...)`. Декораторів тут немає, і кожен обробник має однакову форму: `async (ctx, params) -> result`.
|
||||
* **Вхідну схему пишете ви.** `Tool.input_schema` — це звичайний `dict` із JSON Schema. Ніхто не виводить її з анотацій типів, бо анотацій типів, з яких можна було б її вивести, немає.
|
||||
* **Результат будуєте ви.** `CallToolResult(content=[TextContent(...)])`, вручну. Нічого не загортається, не перетворюється й не виводиться з анотації значення, що повертається.
|
||||
|
||||
`params` — це розібраний запит: `CallToolRequestParams` дає `.name` і `.arguments`. `ctx` — це `ServerRequestContext`: `ctx.session`, щоб звертатися назад до клієнта, `ctx.lifespan_context`, `ctx.request_id` і `ctx.meta` — вхідні `_meta` запиту.
|
||||
|
||||
!!! info
|
||||
Якщо ви працювали з FastAPI, це співвідношення вам уже знайоме. `MCPServer` — це шар декораторів і анотацій типів; `Server` — це Starlette під ним. Вони не суперники: `MCPServer` створює `Server` і реєструє на ньому саме такі обробники.
|
||||
|
||||
### Спробуйте самі {#try-it}
|
||||
|
||||
Inspector тут не допоможе: `mcp dev` і `mcp run` приймають лише `MCPServer`. `Client`, що працює в пам'яті, до цього байдужий: низькорівневий `Server` він приймає так само, як і `MCPServer`:
|
||||
|
||||
```python title="main.py"
|
||||
import asyncio
|
||||
|
||||
from mcp import Client
|
||||
|
||||
from server import server
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
async with Client(server) as client:
|
||||
result = await client.call_tool("search_books", {"query": "dune", "limit": 5})
|
||||
print(result.content)
|
||||
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
```text
|
||||
[TextContent(type='text', text="Found 3 books matching 'dune' (showing up to 5).", annotations=None, meta=None)]
|
||||
```
|
||||
|
||||
Той самий текст, що його видавала версія з `@mcp.tool()`. Дві чесні відмінності:
|
||||
|
||||
* `result.structured_content` дорівнює `None`. Високорівневий сервер загортає `-> str` у `{"result": ...}` за вас; тут ніхто не будує того, чого не побудували ви.
|
||||
* `list_tools` повертає схему, яку набрали **ви**, символ у символ. У високорівневій версії на кожній властивості було `"title": "Query"`, а в корені — `"title": "search_booksArguments"`: артефакти Pydantic. Тут, якщо щось є в переданих даних, — це ви його туди поклали.
|
||||
|
||||
## Ніхто нічого не перевіряє за вас {#nothing-is-checked-for-you}
|
||||
|
||||
`MCPServer` відхиляє хибний аргумент ще до того, як ваша функція запуститься, перевіряючи виклик за схемою, яку сам згенерував (**[Інструменти](../servers/tools.md)**).
|
||||
|
||||
`Server` цього не робить. Ваша `input_schema` *оголошується* клієнтові; вона ніколи не *застосовується* до `params.arguments`.
|
||||
|
||||
!!! check
|
||||
Викличте `search_books` без `limit` — і ваш `args["limit"]` викине `KeyError`. Клієнт побачить:
|
||||
|
||||
```text
|
||||
MCPError: Internal server error
|
||||
```
|
||||
|
||||
Помилка JSON-RPC з кодом `-32603` і навмисно загальним повідомленням: SDK не видасть ваш traceback віддаленій стороні, що викликає. Модель так і не дізнається, що зробила не так, тож не зможе повторити спробу. (У тесті `raise_exceptions=True` натомість показує справжній виняток; див. **[Тестування](../get-started/testing.md)**.)
|
||||
|
||||
Це узагальнюється. Виняток, викинутий із низькорівневого обробника, — це **завжди** помилка протоколу й ніколи — результат інструмента з `is_error=True`. Якщо потрібно, щоб модель прочитала про невдачу й відновилася, перевіряйте `params.arguments` самі й повертайте `CallToolResult(content=[TextContent(...)], is_error=True)`. Обом видам невдач присвячена сторінка **[Обробка помилок](../servers/handling-errors.md)**.
|
||||
|
||||
## Два інструменти, один обробник {#two-tools-one-handler}
|
||||
|
||||
`on_call_tool` — єдина точка входу для всіх інструментів сервера. Маршрутизуєте за `params.name`:
|
||||
|
||||
```python title="server.py" hl_lines="38-43"
|
||||
--8<-- "docs_src/lowlevel/tutorial002.py"
|
||||
```
|
||||
|
||||
* `list_tools` оголошує обидва. `call_tool` диспетчеризує за іменем.
|
||||
* Гілка `else` важлива: `Server` спокійно передасть `tools/call` з іменем, якого ви ніколи не оголошували, просто у ваш обробник. Виняток там перетворює виклик на ту саму `-32603`, що й вище.
|
||||
|
||||
## Структурований вивід, вручну {#structured-output-by-hand}
|
||||
|
||||
Оголосіть `output_schema` на `Tool` і покладіть `structured_content` у результат. І те, й інше — ваше:
|
||||
|
||||
```python title="server.py" hl_lines="19-23 36"
|
||||
--8<-- "docs_src/lowlevel/tutorial003.py"
|
||||
```
|
||||
|
||||
Викличте його — і результат міститиме обидва подання:
|
||||
|
||||
```json
|
||||
{
|
||||
"content": [{"type": "text", "text": "Found 3 books matching 'dune'."}],
|
||||
"structuredContent": {"matches": 3, "query": "dune"},
|
||||
"isError": false,
|
||||
"resultType": "complete",
|
||||
"_meta": {"io.modelcontextprotocol/serverInfo": {"name": "Bookshop", "version": "2.0.0"}}
|
||||
}
|
||||
```
|
||||
|
||||
Блок `_meta` — це ідентифікаційна позначка сервера: SDK додає його до кожного результату покоління 2026 разом із `version` з конструктора (сервер, який її не задав, повідомляє порожній рядок). Сервер, який не повинен себе ідентифікувати, може прибрати цей ключ за допомогою middleware, який володіє результатами, що повертає.
|
||||
|
||||
Сервер ніколи не порівнює ці два поля. `Client` цього SDK — порівнює: поверніть `structured_content`, що не відповідає оголошеній вами `output_schema`, і `call_tool` викине `RuntimeError`, який починається з `Invalid structured content returned by tool search_books` і далі цитує помилку `jsonschema`. Пообіцяти схему легко; дотримати її — ваша справа. Уся драбина типів повернення та схем — на сторінці **[Структурований вивід](../servers/structured-output.md)**.
|
||||
|
||||
## `_meta`: для застосунку, а не для моделі {#\_meta-for-the-application-not-the-model}
|
||||
|
||||
`content` — це частина відповіді, яку читає модель. `structured_content` — та сама відповідь у вигляді типізованих даних. `_meta` — третій канал: дані, що їдуть разом із результатом для **клієнтського застосунку** і взагалі не є частиною відповіді.
|
||||
|
||||
Використовуйте його для ідентифікаторів записів, ідентифікаторів трасування — усього, що потрібно вашому UI, але не потрібно промпту:
|
||||
|
||||
```python title="server.py" hl_lines="37"
|
||||
--8<-- "docs_src/lowlevel/tutorial004.py"
|
||||
```
|
||||
|
||||
* Конструюєте його як `_meta=` — це ім'я в переданих даних. Клієнт зчитує його як `result.meta`.
|
||||
* Додавайте до ключів простір імен (`bookshop/record_ids`). Ключі `io.modelcontextprotocol/*` зарезервовано протоколом.
|
||||
|
||||
!!! warning
|
||||
`_meta` — це домовленість між вами й клієнтським застосунком, а не гарантія того, що дійде
|
||||
до моделі. Що показувати, вирішує хост. Ніколи не кладіть секрет у жодну частину результату інструмента.
|
||||
|
||||
## Можливості випливають з обробників {#capabilities-follow-your-handlers}
|
||||
|
||||
`Server` оголошує рівно ті сімейства методів, для яких ви дали йому обробники. `Bookshop` вище передає `on_list_tools` і `on_call_tool` і більше нічого, тож клієнт, що до нього під'єднується, бачить:
|
||||
|
||||
```json
|
||||
{"tools": {"listChanged": false}}
|
||||
```
|
||||
|
||||
Жодних `resources`, жодних `prompts`: за ними нічого не стоїть. Передайте `on_list_prompts` — і з'явиться `prompts`; передайте `on_completion` — і з'явиться `completions`.
|
||||
|
||||
`MCPServer` завжди оголошує інструменти, ресурси й промпти, зареєстрували ви щось чи ні, бо його менеджери існують завжди. Тут же оголошення — це *і є* виклик конструктора.
|
||||
|
||||
## Життєвий цикл як параметр типу {#the-lifespan-generic}
|
||||
|
||||
`Server` узагальнений за типом, який видає його життєвий цикл (lifespan). Анотуйте його один раз — і об'єкт буде типізованим усюди, де з'являється:
|
||||
|
||||
```python title="server.py" hl_lines="24-26 44-45 50"
|
||||
--8<-- "docs_src/lowlevel/tutorial005.py"
|
||||
```
|
||||
|
||||
* Життєвий цикл — це `Callable[[Server[Catalog]], AbstractAsyncContextManager[Catalog]]`; `@asynccontextmanager` над `async`-генератором дає саме це.
|
||||
* Усе, що він видає через `yield`, стає `ctx.lifespan_context`, а оскільки обробники анотовано як `ServerRequestContext[Catalog]`, `.search(...)` автодоповнюється й проходить перевірку типів.
|
||||
* У нього входять один раз під час старту сервера й виходять один раз під час зупинки. Запуск, завершення та версія тієї самої ідеї в `MCPServer` — на сторінці **[Життєвий цикл](../handlers/lifespan.md)**.
|
||||
|
||||
Без `lifespan=` `ctx.lifespan_context` — порожній `dict`.
|
||||
|
||||
## Власний метод {#a-method-of-your-own}
|
||||
|
||||
Конструктор покриває методи, які визначає MCP. `add_request_handler` покриває все інше:
|
||||
|
||||
```python title="server.py" hl_lines="35-36 39-40 43-44 48"
|
||||
--8<-- "docs_src/lowlevel/tutorial006.py"
|
||||
```
|
||||
|
||||
* Перший аргумент — рядок методу. Для сповіщень є близнюк — `add_notification_handler`.
|
||||
* `params_type` — це модель, за якою вхідні `params` перевіряються **до** запуску вашого обробника, тож власні методи *отримують* перевірку, якої інструменти не мають. Успадковуйтеся від `RequestParams`, щоб поле `_meta` розбиралося так само, як у кожного іншого методу.
|
||||
* Обробник повертає `BaseModel`, `dict` або `None`. SDK серіалізує це в результат JSON-RPC.
|
||||
|
||||
Одне чесне застереження: високорівневий `Client` має дієслова лише для методів, які визначає MCP, тож `client.reindex()` немає. Вендорний метод — для сторони, яка вже знає про його існування: клієнта, який ви теж постачаєте, або іншого вашого сервісу, що говорить JSON-RPC.
|
||||
|
||||
Один метод забрати собі не можна:
|
||||
|
||||
```text
|
||||
ValueError: 'initialize' is handled by the server runner and cannot be overridden;
|
||||
use Server.middleware to observe or wrap initialization
|
||||
```
|
||||
|
||||
Рукостискання належить засобу запуску сервера. `server/discover`, `ping` та всі інші вбудовані методи можна замінювати.
|
||||
|
||||
!!! tip
|
||||
`Server.middleware`, згаданий у цій помилці, обгортає **кожне** вхідне повідомлення, включно з `initialize`. Якщо мета — спостерігати за трафіком чи переписувати його, а не відповідати на новий метод, почніть із **[Middleware](middleware.md)**.
|
||||
|
||||
## Інші обробники {#the-other-handlers}
|
||||
|
||||
Кожен із них — одна ідея, для якої у вас тепер є словник; кожна має власну сторінку.
|
||||
|
||||
* `on_call_tool`, `on_get_prompt` і `on_read_resource` можуть повернути `InputRequiredResult` замість звичайного результату, щоб призупинити виклик і попросити клієнта про введення; див. **[Багатораундові запити](../handlers/multi-round-trip.md)** (multi-round-trip). Як і годиться цьому рівню, нічого не встановлюється за вас: якщо `MCPServer` за замовчуванням запечатує `requestState`, то тут заданий вами `request_state` передається мережею точно так, як написано, доки ви не ввімкнете захист через `server.middleware.append(RequestStateBoundary(RequestStateSecurity(keys=[...]), default_audience=server.name))`: один рядок (обидва імені імпортуються з `mcp.server.request_state`) — і отримуєте те саме запечатування й перевірку, що їх виконує `MCPServer` (**[Захист `requestState`](../handlers/multi-round-trip.md#protecting-requeststate)**).
|
||||
* `on_list_resources`, `on_read_resource`, `on_list_prompts`, `on_get_prompt`, `on_completion` — та сама форма `(ctx, params) -> result` для інших примітивів.
|
||||
* `on_subscriptions_listen` обслуговує потік `subscriptions/listen` версії 2026-07-28. Передайте `ListenHandler`, побудований поверх `SubscriptionBus`, і публікуйте події в шину з інших обробників; повну композицію див. на сторінці **[Підписки](../handlers/subscriptions.md)**.
|
||||
* `server.streamable_http_app()` повертає той самий Starlette-застосунок, що й у `MCPServer`; розгортайте його так, як **[Запуск сервера](../run/index.md)** розгортає будь-який інший ASGI-застосунок. `server.run(transport=...)` тут немає: `server.run(read_stream, write_stream, server.create_initialization_options())` веде одне з'єднання через пару потоків, і цей один рядок — оце й усе.
|
||||
|
||||
## Підсумки {#recap}
|
||||
|
||||
* Низькорівневий `Server` приймає обробники як **параметри конструктора** `on_*`; кожен обробник — це `async (ctx, params) -> result`.
|
||||
* Ви пишете словник `input_schema` і будуєте `CallToolResult`. Нічого не виводиться, не загортається й не перевіряється за вас.
|
||||
* Виняток в обробнику — це помилка протоколу `-32603`. Помилка інструмента, яку може прочитати модель, — це `CallToolResult` з `is_error=True`, який повертаєте **ви**.
|
||||
* `_meta` в результаті адресовано клієнтському застосунку, а не моделі.
|
||||
* `Server[T]` узагальнений за тим, що видає його життєвий цикл; `ctx.lifespan_context` — це типізований `T`.
|
||||
* `add_request_handler(method, params_type, handler)` обслуговує будь-який метод. `initialize` зарезервовано.
|
||||
* Можливості, які оголошує `Server`, виводяться з того, які обробники ви зареєстрували.
|
||||
|
||||
`Client(server)` поводився з обома серверами однаково, бо вони *і є* тим самим протоколом — у цьому й увесь сенс. Наступний шар нижче — взагалі не клас: це **[Middleware](middleware.md)**.
|
||||
128
i18n/uk/pages/advanced/middleware.md
Normal file
128
i18n/uk/pages/advanced/middleware.md
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
---
|
||||
translation:
|
||||
sections: [6048b4f308edbb8c, 068bda0f21ee9c1b, c3e565b61acd75c5, c62422b159c6ed09, 47204fab253cc45c]
|
||||
tool: 1
|
||||
---
|
||||
# Middleware {#middleware}
|
||||
|
||||
**Middleware** — це одна асинхронна функція, що огортає кожне повідомлення, яке отримує сервер.
|
||||
|
||||
Її пишуть у формі `async (ctx, call_next)` і додають до `server.middleware`. Оце й увесь API.
|
||||
|
||||
!!! warning
|
||||
Список middleware у вихідному коді позначено як **попередній** (provisional): його сигнатура
|
||||
й семантика можуть змінитися в мінорному випуску 2.x. Використовуйте його, щоб
|
||||
*спостерігати* (час виконання, логування, трасування) і щоб *відхиляти* повідомлення; не
|
||||
робіть його фундаментом, на якому тримається сервер.
|
||||
|
||||
`MCPServer` приймає список у конструкторі (`MCPServer(name, middleware=[...])`) і надає доступ
|
||||
до нього як `mcp.middleware`; низькорівневий `Server` надає той самий список як
|
||||
`server.middleware`. Приклад нижче використовує низькорівневий `Server`; якщо
|
||||
`Server(name, on_call_tool=...)` вам незнайомий, спершу прочитайте сторінку
|
||||
**[Низькорівневий Server](low-level-server.md)**.
|
||||
|
||||
## Middleware для вимірювання часу {#a-timing-middleware}
|
||||
|
||||
Один сервер, один інструмент, один шар middleware, який записує в лог, скільки часу забрало кожне повідомлення:
|
||||
|
||||
```python title="server.py" hl_lines="39-45 49"
|
||||
--8<-- "docs_src/middleware/tutorial001.py"
|
||||
```
|
||||
|
||||
* `ctx` — той самий `ServerRequestContext`, що його отримують обробники. `ctx.method` — сирий
|
||||
рядок методу; `ctx.params` — сирі параметри, **до** будь-якої валідації.
|
||||
* `call_next(ctx)` запускає решту ланцюжка: валідацію, пошук обробника, сам обробник.
|
||||
Поверніть те, що він повернув, — і відповідь залишиться недоторканою.
|
||||
* `try`/`finally` тут навмисно: обробник, що викидає виняток, однаково буде заміряно, бо збій
|
||||
доходить до middleware як виняток із `call_next`.
|
||||
* `server.middleware.append(...)` реєструє її. Список виконується від зовнішнього до
|
||||
внутрішнього, тож `middleware[0]` — найближча до мережі.
|
||||
|
||||
### Спробуйте самі {#try-it}
|
||||
|
||||
Під'єднайте клієнт, отримайте список інструментів, викличте один. У лозі буде **три** рядки:
|
||||
|
||||
```text
|
||||
server/discover took 18.3 ms
|
||||
tools/list took 0.1 ms
|
||||
tools/call took 0.1 ms
|
||||
```
|
||||
|
||||
Ви зробили два виклики, а рядків три. Перший — `server/discover`: запит, який клієнт надіслав,
|
||||
щоб установити з'єднання, ще до того, як ви щось попросили.
|
||||
|
||||
У цьому й суть. Middleware огортає **кожне** вхідне повідомлення:
|
||||
|
||||
* Установлення з'єднання: `server/discover`, або `initialize` і `notifications/initialized`
|
||||
у сесії старого покоління.
|
||||
* Кожен запит і кожне сповіщення. Для сповіщення `ctx.request_id is None`,
|
||||
`call_next(ctx)` повертає `None`, а все, що повернете ви, відкидається.
|
||||
* Навіть метод, для якого сервер не має обробника: `call_next` викидає
|
||||
`MCPError(-32601, "Method not found")` *крізь* ваш middleware на шляху до клієнта.
|
||||
|
||||
## Що можна робити всередині {#what-you-can-do-inside-one}
|
||||
|
||||
У порядку зростання того, наскільки варто вагатися:
|
||||
|
||||
* **Спостерігати.** Заміряти час, рахувати, логувати. Приклад вище.
|
||||
* **Відхиляти.** Викиньте `MCPError` *замість* виклику `call_next(ctx)` — і саме на це
|
||||
повідомлення клієнт отримає помилку JSON-RPC. З'єднання лишається живим; наступне
|
||||
повідомлення проходить. Саме так сервер обмежує `subscriptions/listen` для окремих
|
||||
викликачів: розділ **[Хто має право стежити](../handlers/subscriptions.md#deciding-who-may-watch)**
|
||||
на сторінці про підписки показує це крок за кроком.
|
||||
* **Переписувати.** `ctx` — це dataclass: `await call_next(dataclasses.replace(ctx, params=...))`
|
||||
передає решті ланцюжка інші параметри, ніж надіслав клієнт. Ніколи не робіть цього з
|
||||
`initialize`: результат, який отримає клієнт, будується з переписаних параметрів, але сервер
|
||||
фіксує стан з'єднання за первинними параметрами з мережі. Обидві сторони можуть завершити
|
||||
рукостискання, не погоджуючись щодо того, про що вони домовилися.
|
||||
* **Відповідати.** Поверніть результат, не викликаючи `call_next(ctx)`, — і він піде клієнтові
|
||||
як ваша відповідь. `call_next` віддає вам готову форму для передавання мережею, а конвеєр
|
||||
ніколи не латає те, що ви повертаєте, тож уся обгортка — ваша відповідальність: на з'єднанні
|
||||
покоління 2026 сюди входить і позначка `_meta` з `serverInfo`, яку SDK додає до результатів
|
||||
обробників, але не до ваших.
|
||||
|
||||
!!! check
|
||||
`initialize` — одна з речей, які огортає middleware, і це *єдиний* гачок, який ви для нього
|
||||
маєте. Спробуйте перехопити його через `add_request_handler` — і SDK відмовить:
|
||||
|
||||
```text
|
||||
ValueError: 'initialize' is handled by the server runner and cannot be overridden;
|
||||
use Server.middleware to observe or wrap initialization
|
||||
```
|
||||
|
||||
!!! warning
|
||||
`initialize` обробляється на місці: сервер не читає наступних вхідних повідомлень, доки
|
||||
ланцюжок middleware не поверне керування. Тому очікування запиту від сервера до клієнта
|
||||
(`ctx.session.send_request(...)`, еліцитація (elicitation)) під час обробки `initialize`
|
||||
**заблокує з'єднання намертво**: відповідь, на яку ви чекаєте, ніколи не буде прочитано.
|
||||
Сповіщення за принципом «надіслав і забув» — без проблем.
|
||||
|
||||
## Єдиний middleware, увімкнений за замовчуванням {#the-one-middleware-that-ships-on-by-default}
|
||||
|
||||
SDK постачає рівно один шар middleware, і він уже є в списку вашого сервера: той, що створює
|
||||
спан OpenTelemetry для кожного повідомлення. Його не потрібно додавати, і здебільшого про нього
|
||||
не доводиться думати. Він нічого не робить, доки ви не встановите експортер, і має власну
|
||||
сторінку: **[OpenTelemetry](../run/opentelemetry.md)**.
|
||||
|
||||
!!! info
|
||||
Якщо ви писали ASGI-middleware, ця форма вам уже знайома. `(scope, receive, send)` зі
|
||||
Starlette перетворилося на `(ctx, call_next)` і виконується *після* транспорту — над
|
||||
декодованим повідомленням, а не над сирим HTTP-запитом. Обидва поєднуються: middleware
|
||||
Starlette на `streamable_http_app()` бачить HTTP, а цей — MCP.
|
||||
|
||||
## Підсумки {#recap}
|
||||
|
||||
* Middleware — це `async (ctx, call_next) -> result`; його передають як
|
||||
`MCPServer(middleware=[...])` (або додають до `mcp.middleware`), а в низькорівневому `Server`
|
||||
додають до `server.middleware`.
|
||||
* Він огортає **кожне** вхідне повідомлення (`server/discover`, `initialize`, запити,
|
||||
сповіщення, невідомі методи) і виконується від зовнішнього до внутрішнього.
|
||||
* `ctx.request_id is None` — так відрізняють сповіщення від запиту.
|
||||
* Викиньте виняток замість виклику `call_next`, щоб відхилити одне повідомлення; з'єднання
|
||||
вціліє.
|
||||
* Власне трасування OpenTelemetry у SDK — теж middleware, і воно вже в списку. Див.
|
||||
**[OpenTelemetry](../run/opentelemetry.md)**.
|
||||
* Уся ця поверхня попередня. Спостерігайте через неї; не будуйте на ній.
|
||||
|
||||
Це все, що огортає запит. **[Авторизація](../run/authorization.md)** — те, що вирішує, чи
|
||||
запит узагалі буде виконано.
|
||||
85
i18n/uk/pages/advanced/pagination.md
Normal file
85
i18n/uk/pages/advanced/pagination.md
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
---
|
||||
translation:
|
||||
sections: [a9aba7a026c7bd85, ed32bda7ba9ae33a, 7e64cc5646abb91f, 22a0129ee78b3c63, d875373c06d8d2f9]
|
||||
tool: 1
|
||||
---
|
||||
# Пагінація {#pagination}
|
||||
|
||||
Більшості серверів вона не знадобиться ніколи.
|
||||
|
||||
`MCPServer` відповідає на кожен запит `list_*` усім, що має, однією сторінкою з `next_cursor=None`. Для кількох десятків інструментів, ресурсів чи промптів це правильна відповідь, і налаштовувати нічого не потрібно.
|
||||
|
||||
Пагінація — для сервера, у якого список ресурсів насправді є базою даних: тисячі рядків, які він відмовляється серіалізувати в одну відповідь. Відповідь протоколу — **курсор**: сервер повертає сторінку плюс непрозорий токен, а клієнт надсилає цей токен назад, щоб отримати наступну сторінку.
|
||||
|
||||
У `@mcp.resource()` немає жодного гачка для цього. Щоб розбивати на сторінки, обробник списку пишуть власноруч, на **[низькорівневому Server](low-level-server.md)**.
|
||||
|
||||
## Сервер зі сторінками {#a-server-that-pages}
|
||||
|
||||
```python title="server.py" hl_lines="12 15-16"
|
||||
--8<-- "docs_src/pagination/tutorial001.py"
|
||||
```
|
||||
|
||||
* На низькорівневому `Server` обробники — це аргументи конструктора, а не декоратори. `on_list_resources` відповідає на кожен запит `resources/list`; оце й усе під'єднання.
|
||||
* Кожен обробник зі сторінками має тип `params: PaginatedRequestParams | None`, і приклад приймає обидва варіанти. Утім, через з'єднання SDK ніколи не передає `None` (запит без члена `params` доходить до обробника як модель із типовими значеннями), тож важливий сигнал — `params.cursor is None`: **починайте з початку**.
|
||||
* Ви вирішуєте, чим курсор *є*. Тут це зсув, записаний як рядок. Мітка часу, первинний ключ, base64-блоб — будь-що, що можна видати на виході й упізнати на вході.
|
||||
* `next_cursor=None` — спосіб сказати «це була остання сторінка». Немає ні лічильника, ні загальної кількості, ні `has_more`. `None` — це весь сигнал.
|
||||
|
||||
!!! tip
|
||||
`PAGE_SIZE` у 10 робить приклад читабельним. Свій розмір обирайте для кожної кінцевої точки окремо: список
|
||||
однорядкових ресурсів може дозволити собі сторінку на 500; список важких шаблонів промптів — ні.
|
||||
Клієнт на це не впливає, і так задумано.
|
||||
|
||||
### Спробуйте самі {#try-it}
|
||||
|
||||
`Client(server)` під'єднується до низькорівневого `Server` у пам'яті так само, як і до `MCPServer`.
|
||||
|
||||
Викличте `list_resources()` без аргументів. Повертається десять ресурсів, від `book-1` до `book-10`, а `next_cursor` — рядок `"10"`.
|
||||
|
||||
Передайте його назад через `list_resources(cursor="10")` — і перший ресурс уже `book-11`, а новий `next_cursor` — `"20"`.
|
||||
|
||||
Десята сторінка приходить із `next_cursor`, що дорівнює `None`. Готово.
|
||||
|
||||
## Цикл на клієнті {#the-client-loop}
|
||||
|
||||
Кожен метод `list_*` класу `Client` (`list_tools`, `list_resources`, `list_resource_templates`, `list_prompts`) приймає іменований параметр `cursor=`. Вичерпати список зі сторінками — це один `while True`:
|
||||
|
||||
```python title="client.py" hl_lines="26-32"
|
||||
--8<-- "docs_src/pagination/tutorial002.py"
|
||||
```
|
||||
|
||||
* `cursor` починається з `None`, тому перший запит курсора не несе.
|
||||
* Розширюйте список **до** того, як дивитися на `next_cursor`: на останній сторінці теж є ресурси.
|
||||
* `next_cursor is None` — це вихід. Усе інше йде прямо назад у `cursor=`, без змін.
|
||||
|
||||
Запустіть його `main()` — і він надрукує `100 resources`: десять сторінок по десять, зшитих циклом, який так і не дізнався, що сторінок було десять.
|
||||
|
||||
Це той самий цикл, який **[Клієнт](../client/index.md)** показує для кожного дієслова `list_*`, і проти сервера без сторінок він нічого не коштує: `next_cursor` дорівнює `None` вже в першій відповіді, і цикл виконується один раз.
|
||||
|
||||
## Три правила {#the-three-rules}
|
||||
|
||||
**Курсори непрозорі.** Клієнт ніколи не повинен розбирати, будувати чи вгадувати курсор. Єдине законне джерело курсора — `next_cursor` попередньої сторінки, дослівно.
|
||||
|
||||
**Розмір сторінки обирає сервер.** У протоколі немає `limit=`. Якщо потрібен інший розмір сторінки, змінюють сервер.
|
||||
|
||||
**Клієнт, що ігнорує пагінацію, усе одно працює.** Він викликає `list_resources()` один раз, отримує перші десять і навіть не помічає `next_cursor`, який викинув. Нічого не ламається; він просто бачить менше.
|
||||
|
||||
!!! check
|
||||
Непрозорий означає непрозорий. Вигадайте курсор (`list_resources(cursor="page-2")`) — і
|
||||
протокол нічим не зможе допомогти. Цей сервер пробує `int("page-2")`, обробник викидає виняток,
|
||||
а до клієнта повертається:
|
||||
|
||||
```text
|
||||
MCPError(-32603, 'Internal server error', None)
|
||||
```
|
||||
|
||||
Курсор, отриманий не від сервера, — це помилка, а не запит на нову можливість.
|
||||
|
||||
## Підсумки {#recap}
|
||||
|
||||
* `MCPServer` повертає все однією сторінкою. Пагінацію вмикають свідомо, і роблять це на низькорівневому `Server`.
|
||||
* `on_list_resources` (а також `on_list_tools`, `on_list_prompts`, `on_list_resource_templates`) отримує `PaginatedRequestParams | None`; для першої сторінки `params.cursor` дорівнює `None`.
|
||||
* Повертається сторінка плюс `next_cursor`: будь-який рядок, який ви згодом упізнаєте, або `None`, коли більше нічого не лишилося.
|
||||
* Цикл на клієнті: передати `cursor=`, накопичити, повторювати, доки `next_cursor is None`.
|
||||
* Курсори непрозорі, розмір сторінки належить серверу, а клієнт без пагінації все одно отримує першу сторінку.
|
||||
|
||||
Решта написаного власноруч API `Server` (`on_call_tool`, словники `input_schema`, `_meta`) — на сторінці **[Низькорівневий Server](low-level-server.md)**.
|
||||
122
i18n/uk/pages/client/caching.md
Normal file
122
i18n/uk/pages/client/caching.md
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
---
|
||||
translation:
|
||||
sections: [9e7b9a1710e5aeba, b74ca4c1d2ddddee, fa8714e61bf90c5a, 04db67a886b7271c, 857690fb8f876800]
|
||||
tool: 1
|
||||
---
|
||||
# Підказки щодо кешування {#caching-hints}
|
||||
|
||||
Кожен результат, який сервер повертає для `tools/list`, `prompts/list`, `resources/list`, `resources/templates/list`, `resources/read` і `server/discover`, у протоколі 2026-07-28 містить два поля: `ttlMs` — скільки мілісекунд клієнт може вважати результат свіжим, і `cacheScope` — чи можна ділитися закешованим результатом між користувачами (`"public"`), чи він належить одному контексту авторизації (`"private"`).
|
||||
|
||||
Сервер нічого не кешує. Ці поля — *декларація*: «цей список інструментів однаковий для всіх і не зміниться протягом хвилини». Тоді клієнт (або шлюз перед вами) може пропустити звернення до сервера. Дотримуватися підказок чи ні — вибір клієнта; видавати їх — робота сервера, і SDK робить це за вас.
|
||||
|
||||
За замовчуванням кожен результат каже `ttlMs: 0, cacheScope: "private"`: одразу застарілий, ніколи не спільний. Це завжди безпечно і завжди відповідає специфікації. Якщо ваші списки справді стабільні й однакові для всіх викликачів, скажіть про це під час створення:
|
||||
|
||||
```python title="server.py" hl_lines="5-8"
|
||||
--8<-- "docs_src/caching/tutorial001.py"
|
||||
```
|
||||
|
||||
* Ключами мапи є **імена методів**, і шість кешованих методів — єдині допустимі ключі. Параметр має тип `Mapping[CacheableMethod, CacheHint]`, тож редактор доповнює ключі автоматично й позначає описку ще до запуску; усе, що проскочить повз перевірку типів, викине виняток під час створення.
|
||||
* Метод, який ви не згадали, зберігає типові значення. Мапа — це набір перевизначень, а не маніфест.
|
||||
* `CacheHint(ttl_ms=5_000)` залишив `scope` незаданим, тож він лишається `"private"`: п'ять секунд свіжості, окремо для кожного викликача. Область і TTL — незалежні рішення.
|
||||
* `"server/discover"` — теж допустимий ключ, бо результат discovery кешується так само, як будь-який список.
|
||||
|
||||
!!! warning
|
||||
`cacheScope: "public"` означає, що вашу закешовану відповідь можуть віддати *будь-кому*.
|
||||
Спільний шлюз охоче передасть результат одного користувача іншому, навіть якщо запит був
|
||||
автентифікований. Позначайте результат як `"public"` лише тоді, коли він однаковий для кожного
|
||||
викликача, і ніколи не використовуйте `cacheScope` як контроль доступу: це мітка, а не замок.
|
||||
|
||||
## Перевизначення в обробнику {#per-handler-override}
|
||||
|
||||
На низькорівневому `Server` обробники будують результати вручну, а `ttl_ms` / `cache_scope` — звичайні поля моделей результатів. Обробник, який задає їх явно, завжди перемагає мапу конструктора, поле за полем:
|
||||
|
||||
```python title="server.py" hl_lines="10 16"
|
||||
--8<-- "docs_src/caching/tutorial002.py"
|
||||
```
|
||||
|
||||
Обробник сказав `ttl_ms=1_000` і нічого про область. У переданих даних: `ttlMs: 1000` (значення обробника, а не `60_000` з мапи) і `cacheScope: "public"` (з мапи, бо обробник залишив поле незаданим). Явне перемагає налаштоване, а налаштоване перемагає типове. Це діє для кожного поля окремо, тож обробник може зафіксувати одне поле, а інше залишити загальносерверній політиці.
|
||||
|
||||
Це також запасний вихід для динаміки, про яку конструктор знати не може: обробник, що фільтрує `resources/read` для кожного користувача, може повернути `cache_scope="private"` для одного URI на сервері, який загалом публічний.
|
||||
|
||||
Одне застереження щодо списків із пагінацією: протокол вимагає **однакового `cacheScope` на кожній сторінці** одного списку. Мапа конструктора забезпечує це за побудовою, бо її ключі — методи, а не сторінки. Але обробник, який перевизначає область сам, відповідає за цю узгодженість: перевизначайте її на *кожній* сторінці, а не лише коли є курсор, інакше перша й друга сторінки розійдуться.
|
||||
|
||||
## Що бачить клієнт {#what-the-client-sees}
|
||||
|
||||
У сесії 2026-07-28 `Client` дотримується підказок за вас: він має вбудований кеш відповідей, увімкнений за замовчуванням. Результат, що прийшов із `ttlMs`, зберігається, і ідентичний виклик у межах цього TTL обслуговується з кешу без звернення до сервера. Результат *без* підказки не кешується: результати без підказок отримують `CacheConfig.default_ttl_ms`, типове значення якого `0` (одразу застарілий), тож сервер, який нічого не оголошує, бачить рівно той самий трафік виклик-за-викликом, що й завжди.
|
||||
|
||||
```python title="client.py" hl_lines="33 35 38"
|
||||
--8<-- "docs_src/caching/tutorial003.py"
|
||||
```
|
||||
|
||||
Чотири виклики, три отримання. Другий виклик знайшов свіжий запис і не дійшов до сервера; переведення (впровадженого) годинника за межу TTL змусило третій знову звернутися до сервера; четвертий сказав `cache_mode="refresh"`. Цей іменований аргумент є в п'яти методів із кешуванням (`list_tools`, `list_prompts`, `list_resources`, `list_resource_templates`, `read_resource`):
|
||||
|
||||
* `"use"` (за замовчуванням) віддає свіжий запис, якщо він є, а якщо ні — звертається до сервера і зберігає результат.
|
||||
* `"refresh"` ніколи не віддає з кешу: звертається до сервера і зберігає результат, замінюючи те, що було закешовано.
|
||||
* `"bypass"` звертається до сервера, взагалі не торкаючись кешу: ні читання, ні запису.
|
||||
|
||||
Одне правило стоїть над `"use"`: **виклики з `meta` завжди доходять до сервера.** Запит із заданим `meta` (токен перебігу виконання, поля трасування) очікує запиту мережею, тому за `cache_mode="use"` його обробляють як `"refresh"`: читання з кешу пропускається, а отриманий результат усе одно замінює закешований запис. `"bypass"` і явний `"refresh"` поводяться як завжди.
|
||||
|
||||
Щоб повністю вимкнути кешування, створіть клієнт як `Client(server, cache=None)`: кожен виклик знову стає зверненням до сервера, а `cache_mode`, хоч і приймається далі, нічого не робить.
|
||||
|
||||
Область також дотримується автоматично: записи `"private"` прив'язані до *розділу* (partition) кешу (нижче), тоді як записи `"public"` можуть за бажанням ділитися ширше. А **сповіщення перемагають TTL** для конкретних записів, які вони називають: сповіщення `list_changed` витісняє відповідний закешований список, а `resources/updated` витісняє закешоване читання, збережене рівно під його URI, хоч би якими свіжими вони були. На з'єднанні 2026-07-28 ці сповіщення надходять потоком `subscriptions/listen`, який відкривають через `client.listen(...)`, і витіснення завершується до того, як ваш спостерігач побачить подію; докладніше — на сторінці **[Підписки](subscriptions.md)**.
|
||||
|
||||
Одне застереження щодо `resources/updated`: витіснення працює лише за точним URI. Контракт сховища не має операції перелічення чи сканування (так само, як еталонна реалізація на TypeScript), тож сповіщення з URI *під*ресурсу не витісняє закешоване читання його батьківського ресурсу. Якщо ваш сервер сигналізує про підресурси таким чином, перечитайте батьківський ресурс із `cache_mode="refresh"`.
|
||||
|
||||
### Налаштування: `CacheConfig` {#configuring-it-cacheconfig}
|
||||
|
||||
```python
|
||||
from mcp.client import CacheConfig
|
||||
|
||||
client = Client("https://api.example.com/mcp", cache=CacheConfig(default_ttl_ms=5_000))
|
||||
```
|
||||
|
||||
* `store`: де живуть записи. За замовчуванням — свіже сховище в пам'яті для кожного клієнта; передайте власну реалізацію `ResponseCacheStore` (скажімо, на Redis), щоб ділити кеш між клієнтами або процесами. Типи контракту (`ResponseCacheStore`, `CacheKey`, `CacheEntry` і типовий `InMemoryResponseCacheStore`) імпортуються з `mcp.client`. Один пошук може виконати до двох послідовних `get` у сховищі (приватна гілка, потім публічна), тож розраховуйте очікувану затримку віддаленого сховища відповідно. Власне сховище **вимагає** явного `partition`.
|
||||
* `partition`: мітка контексту авторизації, яка не дає віддати записи `"private"` одного принципала іншому в межах спільного сховища.
|
||||
* `target_id`: явна ідентичність сервера, для власних транспортів і серверів у тому самому процесі (нижче).
|
||||
* `default_ttl_ms`: TTL, що застосовується до результатів без підказки `ttlMs`. Типове `0` залишає результати без підказок незакешованими.
|
||||
* `share_public`: віддавати записи, які сервер оголосив `"public"`, між розділами (нижче). За замовчуванням вимкнено.
|
||||
* `clock`: джерело реального часу, у секундах епохи. Впровадьте його, як у прикладі вище, і тестам на закінчення терміну не доведеться засинати.
|
||||
|
||||
!!! warning "Розділ = перевірений принципал"
|
||||
Виводьте `partition` з **перевіреного облікового посвідчення**, наприклад із суб'єкта провалідованого токена. Ніколи не виводьте його з даних, наданих у запиті, і ніколи з URL сервера (ідентичність сервера — окрема вісь ключа). SDK — це бібліотека без власної автентифікації: якорем довіри є той, хто створює `CacheConfig`, тобто розгортання, а не орендар. Багатоорендний шлюз створює по одному `CacheConfig` на кожного автентифікованого принципала.
|
||||
|
||||
Розділ також фіксований на весь час життя `Client`. Якщо контекст авторизації з'єднання змінюється посеред сесії (скажімо, повторна автентифікація як інший принципал), кеш за цим не стежить; створіть новий `Client` для нового принципала.
|
||||
|
||||
Ключі кешу також містять **ідентичність сервера**: рядок URL, до якого ви під'єдналися, з вилученим userinfo `user:pass@` і в усьому іншому побайтово точний. Жодного зведення регістру, жодного переставляння параметрів запиту, жодного прибирання кінцевої косої риски. Недостатня нормалізація коштує лише втраченого спільного використання, тоді як надмірна могла б злити двох орендарів (`?tenant=a` проти `?tenant=b`), тому поверхово різні URL просто не ділять записи. Коли URL немає (сервер у тому самому процесі або екземпляр `Transport`), клієнт натомість отримує випадкову ідентичність для кожного екземпляра; задайте `CacheConfig.target_id`, щоб назвати сервер (із власним сховищем це обов'язково, і створення про це скаже). Ідентичність хешується sha256 перед тим, як потрапити до матеріалу ключа, тож URL із секретами в рядку запиту ніколи не з'являється в ключах сховища. І самі не логуйте дохешовану форму.
|
||||
|
||||
!!! warning "`share_public` довіряє серверу для всього парку"
|
||||
За замовчуванням навіть записи `"public"` лишаються в межах свого розділу. `share_public=True` віддає записи, які сервер позначив `cacheScope: "public"`, **кожному** розділу, що користується сховищем, довіряючи класифікації сервера від імені їх усіх. Сервер, який ставить `"public"` на дані окремого орендаря (через помилку чи зі злого наміру), тоді витікає відповіддю одного орендаря до інших. Прапорець навмисно існує лише на рівні конструктора: `cache_mode` для окремого виклику може звузити кешування, але ніщо на рівні виклику не може розширити спільний доступ.
|
||||
|
||||
### Чого кеш ніколи не робить {#what-the-cache-never-does}
|
||||
|
||||
* **Виклики рівня сесії його оминають.** `client.session.list_tools()` і подібні завжди звертаються до сервера; кеш живе на методах `Client`.
|
||||
* **`server/discover` тримається осторонь.** Результат discover доставляється один раз, під час під'єднання, і ніколи не потрапляє до кешу відповідей, навіть якщо містить `ttlMs`. Якщо ви зберігаєте його самі, щоб пропустити пробу під час повторного під'єднання ([`prior_discover`](../protocol-versions.md#reconnecting-with-prior_discover)), його свіжість — ваш облік: `DiscoverResult` містить `ttl_ms` і `cache_scope`, уже розібрані, саме для цього.
|
||||
* **Сторінки продовження ніколи не кешуються.** Участь беруть лише виклики без курсора. Сторінка продовження, відхилена через протермінований курсор, таки *витісняє* закешований список, бо список під нею змінився.
|
||||
* **Багатораундові читання (multi-round-trip) ніколи не кешуються.** `read_resource`, засіяний `input_responses`/`request_state`, або такий, що розв'язується через раунди введення, ніколи не потрапляє до кешу (MUST специфікації).
|
||||
* **Витіснення за сповіщеннями потребує сповіщень.** Витіснення працює рівно настільки добре, наскільки транспорт їх доставляє, а сучасний шлях у тому самому процесі (`Client(server)` з типовим `mode="auto"`) сьогодні не доставляє окремих сповіщень.
|
||||
* **Витіснення відбувається зрештою, а не миттєво.** Сповіщення мережевого шляху розсилаються з породжених задач, тож виклик, що змагається з надходженням сповіщення, може ще раз отримати запис до витіснення; вікно обмежене затримкою розсилання, і витіснення все одно відбувається.
|
||||
* **Жодного stale-if-error.** Протермінований запис ніколи не віддається через те, що повторне отримання не вдалося; помилка поширюється далі.
|
||||
* **Жодного раннього повторного отримання.** Збережений запис віддається, доки не спливе його TTL, а наступний після цього виклик платить зверненням до сервера; нічого не оновлюється у фоновому режимі.
|
||||
* **Жодного об'єднання.** Два одночасні ідентичні виклики — це два отримання.
|
||||
* **Жодного TTL понад 24 години.** Більший `ttlMs`, надісланий сервером чи налаштований, обрізається під час збереження (`mcp.client.caching.MAX_TTL_MS`), обмежуючи, як довго можна віддавати будь-який запис, хоч би якою щедрою була підказка.
|
||||
* У **спільному сховищі** клієнти змагаються між собою. Кожен клієнт відкидає власний запис, коли витіснення випередило отримання в польоті, але клієнт-*співорендар* усе ще може записати назад запис, який прибрало витіснення, якого він не бачив; і сам облік цих перегонів обмежений: понад 4096 відстежуваних ключів захист найстарішого ключа відкидається першим. Обидва вікна прийнятні й закриваються обмеженням TTL вище.
|
||||
* **Жодного обслуговування між поколіннями протоколу.** Записи прив'язані до узгодженої версії протоколу: у спільному постійному сховищі сесія ніколи не віддає запис, записаний за іншої узгодженої версії (той самий список справді відрізняється залежно від покоління, бо SDK прибирає поля 2026 для старіших сесій). Витіснення так само торкається лише записів поточного покоління; записи іншого покоління просто старіють за TTL.
|
||||
|
||||
### Читання підказок самостійно {#reading-the-hints-yourself}
|
||||
|
||||
Підказки — це також звичайні поля на кожному кешованому результаті (`result.ttl_ms` і `result.cache_scope`, уже розібрані), на випадок якщо ви хочете накласти власний облік поверх вбудованого кешу (або замість нього).
|
||||
|
||||
Зі **старішим сервером** (протокол до 2026) полів у переданих даних просто немає, і моделі показують свої обережні типові значення: `ttl_ms == 0` і `cache_scope == "private"`, застарілий і неспільний — правильне припущення для сервера, який нічого не оголосив. Кеш ставиться до сесії старого покоління так само: підказки там ніколи не враховуються (хоч би які ключі з'явилися в переданих даних), застосовується лише `default_ttl_ms`, а його типове `0` нічого не кешує, тож з'єднання до 2026 поводиться рівно так, як до появи кешу. Якщо потрібно відрізнити «сервер сказав 0» від «сервер нічого не сказав», перевірте `"ttl_ms" in result.model_fields_set`: воно задане лише тоді, коли поле справді прийшло.
|
||||
|
||||
## Старіші клієнти {#older-clients}
|
||||
|
||||
Клієнти на версіях протоколу до 2026 ніколи не бачать жодного з двох полів; SDK прибирає їх під час серіалізації для таких з'єднань. Налаштуйте підказки один раз; нічого специфічного для версії писати не потрібно.
|
||||
|
||||
## Підсумки {#recap}
|
||||
|
||||
* Шість методів містять `ttlMs`/`cacheScope`; SDK за замовчуванням ставить `0`/`"private"` — застарілий і неспільний, завжди безпечно.
|
||||
* `cache_hints={method: CacheHint(...)}` під час створення (і `MCPServer`, і `Server`) задає загальносерверні значення для кожного методу.
|
||||
* Обробник, що задає поля на своєму результаті, перевизначає мапу, поле за полем.
|
||||
* `"public"` — це обіцянка, що результат однаковий для кожного викликача. Це не контроль доступу.
|
||||
* `Client` дотримується підказок автоматично: його кеш відповідей увімкнений за замовчуванням, віддає свіжі записи замість повторного отримання і нічого не кешує для серверів (або сесій), які не надають підказок.
|
||||
* Для окремого виклику `cache_mode="refresh"` отримує заново, а `"bypass"` оминає кеш; `cache=None` під час створення вимикає його повністю.
|
||||
154
i18n/uk/pages/client/callbacks.md
Normal file
154
i18n/uk/pages/client/callbacks.md
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
---
|
||||
translation:
|
||||
sections: [adf3c545b5be46b6, 916cd3ab1c03f461, e9be7a8d0eb0a456, 565890a636288ecf, 6af7e49db9129ec3, 06b0238c174186af, 90c6043be435fcb0]
|
||||
tool: 1
|
||||
---
|
||||
# Колбеки клієнта {#client-callbacks}
|
||||
|
||||
Майже кожен запит у MCP іде в один бік: від клієнта до сервера.
|
||||
|
||||
Сервер теж може дещо попросити в **клієнта**: поставити запитання користувачеві, скористатися моделлю користувача для семплювання (sampling), отримати список папок його робочого простору. На ці запити відповідають **колбеки**, які передаються в `Client(...)`.
|
||||
|
||||
## Сервер, який запитує {#a-server-that-asks}
|
||||
|
||||
Ось сервер, інструмент якого не може завершитися самотужки:
|
||||
|
||||
```python title="server.py" hl_lines="16"
|
||||
--8<-- "docs_src/client_callbacks/tutorial001.py"
|
||||
```
|
||||
|
||||
* `ctx.elicit(...)` надсилає запит `elicitation/create` **клієнтові** й чекає.
|
||||
* Інструмент не повертає результат, доки хтось (людина у формі або ваш код) не надасть `name`.
|
||||
|
||||
Це серверна половина, і вона належить сторінці **[Еліцитація](../handlers/elicitation.md)**. Ця сторінка — про інший кінець з'єднання.
|
||||
|
||||
## Колбек еліцитації {#the-elicitation-callback}
|
||||
|
||||
```python title="client.py" hl_lines="6-10 16-17"
|
||||
--8<-- "docs_src/client_callbacks/tutorial002.py"
|
||||
```
|
||||
|
||||
* Колбек еліцитації (elicitation) — це `async (context, params) -> ElicitResult`.
|
||||
* `params.message` — це запитання. `params.requested_schema` — JSON Schema відповіді, яку хоче отримати сервер. Справжній клієнт будує з неї форму; цей заповнює її автоматично.
|
||||
* Повертається `ElicitResult(action="accept", content={...})`, або `action="decline"`, або `action="cancel"`. Єдиний інший варіант — `ErrorData(...)`: він відхиляє запит, і весь виклик завершується помилкою.
|
||||
* `context` — це `ClientRequestContext`: активна `session`, `request_id` сервера та будь-які `meta`, які він додав.
|
||||
|
||||
!!! tip
|
||||
`params` — об'єднання двох режимів еліцитації. Тут `params.mode` дорівнює `"form"`; запит `"url"`
|
||||
замість схеми несе `params.url`. Обидва обробляє один колбек; розгалужуйтеся за `params.mode`.
|
||||
Повний шаблон показано на сторінці **[Еліцитація](../handlers/elicitation.md)**.
|
||||
|
||||
### Спробуйте самі {#try-it}
|
||||
|
||||
Викличте `issue_card` і простежте за обома кінцями.
|
||||
|
||||
Колбек отримує запитання сервера, уже розібране:
|
||||
|
||||
```python
|
||||
params.mode # 'form'
|
||||
params.message # 'What name should go on the card?'
|
||||
params.requested_schema # {'properties': {'name': {'title': 'Name', 'type': 'string'}},
|
||||
# 'required': ['name'], 'title': 'CardHolder', 'type': 'object'}
|
||||
```
|
||||
|
||||
Він відповідає, `ctx.elicit(...)` усередині інструмента відновлює роботу, й інструмент завершується:
|
||||
|
||||
```python
|
||||
result.content # [TextContent(type='text', text='Card issued to Ada Lovelace.')]
|
||||
```
|
||||
|
||||
Один `tools/call` від вас, один `elicitation/create` у відповідь від сервера, на який відповіла ваша функція, — і все це всередині одного виклику інструмента.
|
||||
|
||||
!!! info
|
||||
`mode="legacy"` у виклику `Client(...)` стоїть не просто так. За замовчуванням `Client(...)` узгоджує сучасний
|
||||
шлях протоколу, а на ньому немає зворотного каналу (back-channel) для запитів від сервера до клієнта: `ctx.elicit`
|
||||
завершується помилкою ще до того, як запуститься колбек. Вирішує це не транспорт, а узгоджений
|
||||
протокол — однаково і в пам'яті, і за URL. Фіксуйте `mode="legacy"` щоразу, коли клієнт має
|
||||
відповідати на такий запит; так робить кожен тест за цією сторінкою. Докладніше — на сторінці **[Версії протоколу](../protocol-versions.md)**.
|
||||
|
||||
У сесії 2026-07-28 колбек не зникає, він просто отримує дані інакше: коли інструмент повертає
|
||||
`InputRequiredResult` з `ElicitRequest` усередині, `Client` передає цей запис тому самому
|
||||
`elicitation_callback` і повторює виклик за вас. Цей сценарій описано на сторінці **[Багатораундові запити](../handlers/multi-round-trip.md)** (multi-round-trip).
|
||||
|
||||
## Колбек — це можливість {#a-callback-is-a-capability}
|
||||
|
||||
Ви ніде не повідомляли серверу, що ваш клієнт уміє відповідати на запити еліцитації. Це зробив SDK.
|
||||
|
||||
Під'єднуючись, клієнт оголошує свої `capabilities` — дзеркальне відображення серверних. Цей об'єкт ви не пишете. **Реєстрація колбека і є оголошенням.**
|
||||
|
||||
| що передається | що оголошує клієнт |
|
||||
| --- | --- |
|
||||
| `elicitation_callback=` | `"elicitation": {"form": {}, "url": {}}` |
|
||||
| `sampling_callback=` | `"sampling": {}` |
|
||||
| `list_roots_callback=` | `"roots": {"listChanged": true}` |
|
||||
| жодного з них | `{}` |
|
||||
|
||||
Єдине уточнення — підможливості семплювання: передавайте `sampling_capabilities=SamplingCapability(tools=SamplingToolsCapability())` разом із `sampling_callback`, якщо ваш семплер обробляє параметри `tools` / `tool_choice`. Сервери мають побачити оголошену `sampling.tools`, перш ніж надсилати їх.
|
||||
|
||||
`logging_callback` і `message_handler` у таблиці немає. Вони обробляють сповіщення, а сповіщенням можливість не потрібна.
|
||||
|
||||
Сервер зчитує оголошення методом `ctx.session.check_client_capability(...)`. Додайте інструмент, який це робить:
|
||||
|
||||
```python title="server.py" hl_lines="23-31"
|
||||
--8<-- "docs_src/client_callbacks/tutorial003.py"
|
||||
```
|
||||
|
||||
Під'єднайтеся лише з `elicitation_callback` і викличте його:
|
||||
|
||||
```python
|
||||
result.structured_content # {'result': ['elicitation']}
|
||||
```
|
||||
|
||||
Передайте всі три колбеки — отримаєте `['elicitation', 'sampling', 'roots']`. Не передайте жодного — отримаєте `[]`.
|
||||
|
||||
!!! check
|
||||
Тепер зробіть неправильно: під'єднайтеся **без** `elicitation_callback` і все одно викличте `issue_card`.
|
||||
|
||||
Запит сервера `elicitation/create` все одно доходить до клієнта, і SDK відповідає на нього за
|
||||
вас — помилкою, бо ви ніде не сказали, що можете його обробити. Ця помилка топить весь виклик.
|
||||
`call_tool` не повертає результат із `is_error`; він викидає виняток:
|
||||
|
||||
```text
|
||||
MCPError: Elicitation not supported
|
||||
```
|
||||
|
||||
Це помилка протоколу (`-32600`, *invalid request*), а не помилка інструмента: моделі тут нічого
|
||||
прочитати й повторити. Саме тому `client_features` варто мати: чемний сервер
|
||||
перевіряє, перш ніж питати.
|
||||
|
||||
## Застаріла пара {#the-deprecated-pair}
|
||||
|
||||
`sampling_callback` відповідає на `sampling/createMessage`: сервер просить *вашу* модель щось доповнити. `list_roots_callback` відповідає на `roots/list`: сервер питає, у яких каталогах йому можна працювати.
|
||||
|
||||
Обидва працюють. Обидва дотримуються правила вище. І обидва обслуговують RPC, які **специфікація 2026-07-28 вилучає**: сучасний сервер не звертається до клієнта посеред запиту, а повертає запит вам як частину результату інструмента (**[Багатораундові запити](../handlers/multi-round-trip.md)**). Самі колбеки нікуди не зникають. Коли `InputRequiredResult` несе `CreateMessageRequest` або `ListRootsRequest`, автоматичний цикл `Client` передає його тому самому `sampling_callback` чи `list_roots_callback`, який ви зареєстрували тут. Повний список — на сторінці **[Застарілі можливості](../deprecated.md)**.
|
||||
|
||||
Колбеки досі потрібні, щоб спілкуватися із серверами, які ще не перейшли. Сигнатури:
|
||||
|
||||
```python title="client.py"
|
||||
--8<-- "docs_src/client_callbacks/tutorial004.py"
|
||||
```
|
||||
|
||||
* Колбек семплювання отримує повний `CreateMessageRequestParams` (`messages`, `model_preferences`, `max_tokens`) і повертає `CreateMessageResult`. Модель запускаєте *ви*, як завгодно; SDK лише переносить запит.
|
||||
* Колбек кореневих каталогів (roots) не приймає жодних параметрів і повертає `ListRootsResult`.
|
||||
* Кожен із них натомість може повернути `ErrorData(...)`, щоб відмовити.
|
||||
|
||||
Передавайте їх у `Client(...)` так само, як `elicitation_callback`.
|
||||
|
||||
## Колбеки сповіщень {#the-notification-callbacks}
|
||||
|
||||
Ще два. Жоден нічого не оголошує.
|
||||
|
||||
`logging_callback` отримує `notifications/message`, які надсилає сервер, у вигляді `LoggingMessageNotificationParams` (`level`, `logger`, `data`). Протокольне логування саме оголошене застарілим у специфікації 2026-07-28 (що робити натомість — на сторінці **[Логування](../handlers/logging.md)**), тож цей колбек існує для серверів, які досі його надсилають. На з'єднанні покоління 2026 самого колбека недостатньо, бо сервери 2026 надсилають лог-повідомлення лише у відповідь на запити, які на це погодилися: передайте `log_level="info"` (або інший рівень) у `Client(...)`, щоб проставляти цю згоду в кожному запиті й отримувати повідомлення цього рівня та вище. Сервери до 2026 ігнорують її й зберігають свою поведінку `logging/setLevel`.
|
||||
|
||||
`message_handler` — універсальний приймач: до нього доходить кожне сповіщення сервера, яке сесія передає назовні (на додачу до свого спеціального колбека), а на транспорті на основі потоку — ще й кожен `Exception` транспортного рівня. Два ніколи не доходять: `notifications/cancelled` SDK застосовує сам, а не передає назовні, а підтвердження підписки для активного потоку `listen()` споживає сам цей потік. Анотуйте параметр типом `IncomingMessage` (`ServerNotification | Exception`, експортується з `mcp.client`). Єдиний шаблон, який варто знати, — `if isinstance(message, Exception): raise message`, щоб розірване з'єднання падало гучно, а не зникало безслідно.
|
||||
|
||||
## Підсумки {#recap}
|
||||
|
||||
* Сервер може надсилати запити клієнтові. Відповідають на них колбеки, передані в `Client(...)`.
|
||||
* Колбек еліцитації — актуальний: `async (context, params) -> ElicitResult`, одна функція і для режиму форми, і для режиму URL.
|
||||
* **Реєстрація колбека — це оголошення можливості.** Без нього SDK відхиляє запит сервера від вашого імені, і весь виклик завершується з `MCPError`.
|
||||
* Сервер дізнається про це ще до запиту за допомогою `ctx.session.check_client_capability(...)`.
|
||||
* `sampling_callback` і `list_roots_callback` працюють так само, але обслуговують застарілі можливості; сучасні сервери натомість використовують багатораундові запити.
|
||||
* `logging_callback` і `message_handler` отримують сповіщення. Вони нічого не оголошують.
|
||||
|
||||
Перший аргумент `Client(...)` — об'єкт транспорту. Усі його різновиди описано на сторінці **[Транспорти клієнта](transports.md)**.
|
||||
152
i18n/uk/pages/client/identity-assertion.md
Normal file
152
i18n/uk/pages/client/identity-assertion.md
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
---
|
||||
translation:
|
||||
sections: [a91322c46111d16d, 8e6fd6d6f59bb568, e7828fd2729b2c9d, a03ec26bfc678b65, 1034c653c0bcf1b0]
|
||||
tool: 1
|
||||
---
|
||||
# Твердження про ідентичність {#identity-assertion}
|
||||
|
||||
Звичайний OAuth-провайдер (**[OAuth-клієнти](oauth-clients.md)**) починає з запитання до MCP-сервера: *якому серверу авторизації ти довіряєш?* Він іде за відповіддю, куди б вона не вказувала, а далі або людина входить у систему, або замість неї це робить заздалегідь узгоджений секрет.
|
||||
|
||||
Підприємство не хоче, щоб ані те, ані інше вирішувалося для кожного сервера окремо. У нього вже працює провайдер ідентичності (Okta, Microsoft Entra ID, власний); користувач уже ввійшов у нього сьогодні вранці; і саме там команда безпеки хоче вирішувати, хто й до чого має доступ. [SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990), розширення **Enterprise-Managed Authorization**, переносить це рішення туди. IdP підписує короткоживучий JWT — **Identity Assertion JWT Authorization Grant**, або **ID-JAG**: твердження, що *цей користувач* через *цей клієнт* може звертатися до *цього MCP-сервера*. Клієнт обмінює його на звичайний токен доступу. Жодного браузера, жодного екрана згоди, жодної динамічної реєстрації.
|
||||
|
||||
Ця сторінка описує обидва боки цього обміну. Сам MCP-сервер не змінюється взагалі: це й далі сервер ресурсів зі сторінки **[Авторизація](../run/authorization.md)**, який перевіряє будь-який токен, що надходить.
|
||||
|
||||
## Два запити токена {#two-token-requests}
|
||||
|
||||
Тут діють дві різні інстанції, і навчитися розрізняти їх за назвою — це вже більша частина розуміння цієї сторінки. **Корпоративний IdP** — це провайдер ідентичності вашої організації: він знає, хто цей працівник, у ньому живе політика, і він видає ID-JAG. SDK ніколи з ним не спілкується. **Сервер авторизації MCP** — та сама сторона, що й на сторінці **[Авторизація](../run/authorization.md)**: емітент, названий у метаданих MCP-сервера, те, що випускає токени, які цей MCP-сервер приймає. У звичайному OAuth-потоці ці дві ролі зазвичай поєднані в одному вузлі. Тут їх два, і весь грант зводиться до того, що другий погоджується довіряти першому.
|
||||
|
||||
Клієнт робить по одному запиту токена до кожного.
|
||||
|
||||
1. **До корпоративного IdP.** Клієнт обмінює вхід користувача (його ID-токен OpenID Connect) на ID-JAG. Це обмін токенів за [RFC 8693](https://datatracker.ietf.org/doc/html/rfc8693), це цілком API вашого IdP, і **SDK його не виконує**. Це робите ви — всередині одного асинхронного колбека. Саме тут також ухвалюється рішення політики: IdP, який каже «ні», ніколи не видає ID-JAG, і пред'являти просто нічого.
|
||||
2. **До сервера авторизації MCP.** Клієнт пред'являє ID-JAG за грантом `jwt-bearer` з [RFC 7523](https://datatracker.ietf.org/doc/html/rfc7523) (`grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer`, ID-JAG як `assertion`) й отримує токен доступу. **Саме цей запит робить SDK**, і приймати його — єдине, що ця сторінка додає до сервера авторизації.
|
||||
|
||||
Усе нижче стосується другого запиту: клієнта, який його надсилає, і сервера авторизації, який на нього відповідає.
|
||||
|
||||
## Клієнт {#the-client}
|
||||
|
||||
**`IdentityAssertionOAuthProvider`** живе в `mcp.client.auth.extensions.identity_assertion`. Як і кожен провайдер зі сторінки **[OAuth-клієнти](oauth-clients.md)**, це `httpx2.Auth`: створіть його, передайте в `auth=`, віддайте `httpx2.AsyncClient` транспорту.
|
||||
|
||||
```python title="client.py" hl_lines="49-50 53-61"
|
||||
--8<-- "docs_src/identity_assertion/tutorial001.py"
|
||||
```
|
||||
|
||||
Читайте знизу вгору.
|
||||
|
||||
* `main()` — стандартний `main()` OAuth-клієнта (**[OAuth-клієнти](oauth-clients.md)**), без жодних змін, рядок у рядок. У цьому й суть: щойно провайдер існує, нічого далі за течією не знає, який грант породив токен.
|
||||
* Провайдер приймає те, чого інші провайдери не можуть виявити самі: `client_id` і `client_secret`, які хтось **заздалегідь зареєстрував** на сервері авторизації, `issuer` цього сервера авторизації та `assertion_provider` — асинхронний колбек, що повертає свіжий ID-JAG на вимогу.
|
||||
* `storage` — той самий протокол `TokenStorage`. Викликаються лише два методи для токенів; динамічної реєстрації тут немає, тож немає й `client_info`, який треба пам'ятати.
|
||||
|
||||
### Провайдер твердження {#the-assertion-provider}
|
||||
|
||||
`fetch_id_jag(audience, resource)` — єдиний код, який ви пишете. Його очікують (await) один раз на кожен обмін токенів, ніколи під час створення провайдера, і лише *після* того, як метадані сервера авторизації отримано й перевірено, тож неправильно налаштований емітент ніколи не призведе до витоку твердження. Два його аргументи — це два з полів (claims), з якими має бути випущено ID-JAG: `audience` — це емітент сервера авторизації (поле `aud` в ID-JAG), а `resource` — канонічний ідентифікатор MCP-сервера (поле `resource` в ID-JAG). Третє ви вже маєте: поле `client_id` в ID-JAG має називати той `client_id`, який передано провайдеру, інакше сервер авторизації відмовить в обміні.
|
||||
|
||||
`idp_issue_id_jag` над нею — **не ваш код**. Вона заміняє провайдера ідентичності, підписуючи твердження в тому самому процесі, щоб файл був повним і можна було прочитати кожне поле, яке несе ID-JAG. Справжня `fetch_id_jag` натомість робить перший запит токена з попереднього розділу: обмін токенів за [RFC 8693](https://datatracker.ietf.org/doc/html/rfc8693) з вашим IdP, визначений чернеткою Identity Assertion JWT Authorization Grant, яку профілює [SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990). ID-токен користувача, що ввійшов, передається як `subject_token`, `requested_token_type` — власний URN ID-JAG (`urn:ietf:params:oauth:token-type:id-jag`), `audience` і `resource` проходять без змін, а відповідь містить ID-JAG. Саме цей обмін, під цими назвами, і варто шукати в документації свого IdP.
|
||||
|
||||
!!! tip
|
||||
Свіжий ID-JAG запитується для кожного обміну, і в цьому суть: це одноразовий грант, що живе
|
||||
лічені хвилини, і сервер авторизації на цій сторінці відмовляється приймати той самий двічі.
|
||||
Не кешуйте його. Повторно використовується токен доступу, який за нього отримано.
|
||||
|
||||
### Емітент як налаштування {#the-issuer-is-configuration}
|
||||
|
||||
Ось де інверсія. `OAuthClientProvider` питає сервер ресурсів, який сервер авторизації використовувати, і йде за відповіддю, куди б вона не вказувала. Цей провайдер відмовляється так робити: `issuer` обов'язковий, метадані за [RFC 8414](https://datatracker.ietf.org/doc/html/rfc8414) отримуються з власного well-known-шляху цього емітента, кінцева точка токена має належати до origin цього емітента, а сервер ресурсів ніколи ні про що не питають.
|
||||
|
||||
Розширення цього не вимагає; це свідомо суворіший вибір. Цей клієнт несе дві речі, які варто вкрасти: заздалегідь зареєстрований секрет і твердження, прив'язане до аудиторії, — і клієнт, який дозволив би скомпрометованому MCP-серверу скерувати себе на сервер авторизації зловмисника, надіслав би туди обидві. Фіксація емітента під час створення прибирає цю розмову взагалі.
|
||||
|
||||
!!! warning
|
||||
Налаштований `issuer` порівнюється з полем `issuer` документа метаданих простим порівнянням
|
||||
рядків за RFC 8414 §3.3: символ за символом, разом із кінцевою скісною рискою, без
|
||||
нормалізації. Не вгадуйте його. Отримайте `/.well-known/oauth-authorization-server` зі свого
|
||||
сервера авторизації й скопіюйте значення `issuer`, яке він повертає. Для сервера авторизації
|
||||
на цій сторінці це `https://auth.example.com/`, зі скісною рискою, бо його емітент побудовано
|
||||
з URL-об'єкта pydantic. Розбіжність зупиняє потік на `OAuthFlowError: Authorization server metadata issuer
|
||||
mismatch` ще до того, як буде надіслано бодай одні облікові дані чи твердження.
|
||||
|
||||
### Конфіденційний клієнт {#a-confidential-client}
|
||||
|
||||
`client_secret` обов'язковий; без нього конструктор викидає `ValueError`. Профіль IETF, на якому ґрунтується [SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990), відводить цей грант лише конфіденційним клієнтам, SEP-990 вимагає, щоб клієнт автентифікувався, а цей SDK забезпечує і те, і те, наполягаючи на спільному секреті. `token_endpoint_auth_method` визначає, де він передається: `client_secret_post` (за замовчуванням, у тілі форми) або `client_secret_basic` (заголовок HTTP Basic). Профіль також дозволяє `private_key_jwt`; цей провайдер його не підтримує.
|
||||
|
||||
!!! tip
|
||||
Читайте `client_secret` із середовища або менеджера секретів, ніколи — із системи керування версіями.
|
||||
|
||||
### Що провайдер робить за вас {#what-the-provider-does-for-you}
|
||||
|
||||
Перший запит іде без автентифікації, і відповідь сервера `401` запускає потік.
|
||||
|
||||
1. **Виявлення.** Він отримує метадані сервера авторизації з well-known-шляху за [RFC 8414](https://datatracker.ietf.org/doc/html/rfc8414) налаштованого емітента, перевіряє, що `issuer` документа збігається, і перевіряє, що кінцева точка токена належить до origin емітента.
|
||||
2. **Твердження.** Він очікує (await) ваш `assertion_provider`.
|
||||
3. **Обмін.** Він надсилає грант `jwt-bearer` методом POST на кінцеву точку токена, зберігає `OAuthToken` і повторює ваш початковий запит із `Authorization: Bearer ...`.
|
||||
|
||||
`403`, чий `WWW-Authenticate` називає `insufficient_scope`, знову виконує кроки 2 і 3 з об'єднанням вашого `scope` і того, що вимагається у виклику. (`scope` — це завжди лише прохання; сервер авторизації цієї сторінки надає те, що сказано в ID-JAG, і нічого більше.) Токена оновлення тут немає ніде: коли токен доступу спливає, наступний `401` випускає свіжий ID-JAG і обмінює знову, і *саме це* — важіль, який тримає IdP. Збої — ті самі два винятки, що й на решті сторінки **[OAuth-клієнти](oauth-clients.md)**: `OAuthFlowError` для виявлення й перевірки, його підклас `OAuthTokenError`, коли кінцева точка токена каже «ні».
|
||||
|
||||
## Сервер авторизації {#the-authorization-server}
|
||||
|
||||
Здебільшого на цьому можна зупинитися. Сервер авторизації MCP — це чийсь чужий продукт, приймання ID-JAG — налаштування, яке вмикається в ньому, а частина [SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990), що належить SDK, — це клієнт вище.
|
||||
|
||||
SDK також може *бути* сервером авторизації: `create_auth_routes` повертає маршрути сервера авторизації списком, який може змонтувати будь-який Starlette-застосунок, — саме так `examples/servers/simple-auth/` у репозиторії його запускає. SEP-990 додає до цієї поверхні один прапорець і один метод:
|
||||
|
||||
```python title="auth_server.py" hl_lines="48-50 105-107"
|
||||
--8<-- "docs_src/identity_assertion/tutorial002.py"
|
||||
```
|
||||
|
||||
* `identity_assertion_enabled=True` керує всім. Якщо вимкнено (а це за замовчуванням), `/token` відповідає на цей грант `unsupported_grant_type`, навіть якщо хук реалізовано, а метадані про нього не згадують. Якщо ввімкнено, метадані отримують тип гранту `jwt-bearer` і вказують `urn:ietf:params:oauth:grant-profile:id-jag` у `authorization_grant_profiles_supported` — полі, яким розширення оголошує підтримку. (Клієнт цього SDK його ніколи не читає: він налаштований на одного емітента й просто запитує.)
|
||||
* **`exchange_identity_assertion`** — це хук. До його запуску SDK вже автентифікував клієнта, відмовив публічним клієнтам і відмовив клієнтам, чия реєстрація не містить цього гранту. Повертається `IdentityAssertionParams` (сире `assertion`, запитані `scopes` і `resource`), а ви повертаєте звичайний `OAuthToken`.
|
||||
* Динамічна реєстрація клієнтів відхиляє цей грант безумовно, тож `get_client` тут обслуговує клієнта, заведеного вручну. ID-JAG-клієнт не може зареєструвати сам себе з нічого.
|
||||
* Половина класу — відмови. `OAuthAuthorizationServerProvider` — це *весь* сервер авторизації, тож він вимагає й потоку з кодом авторизації; сервер, який також виконує вхід користувачів, реалізує його по-справжньому, а в цього рівно одні двері.
|
||||
|
||||
!!! warning
|
||||
SDK ніколи не декодує твердження: лише ваше розгортання знає, якому IdP воно довіряє і які
|
||||
ключі той IdP публікує, тож усе всередині `exchange_identity_assertion` критично важливе.
|
||||
Перевірте підпис за опублікованими ключами IdP (його JWKS; спільний секрет тут — лише для
|
||||
демонстрації), а також `iss` і `exp` згідно з [RFC 7523](https://datatracker.ietf.org/doc/html/rfc7523) §3. Вимагайте, щоб `typ` у заголовку
|
||||
JWT дорівнював `oauth-id-jag+jwt` — це запобіжник профілю проти повторного пред'явлення
|
||||
якогось іншого JWT як гранту. Вимагайте, щоб `aud` був вашим власним емітентом. Вимагайте,
|
||||
щоб поле `client_id` в ID-JAG дорівнювало клієнту, якого автентифікував обробник, а його поле
|
||||
`resource` називало ресурс, який ви справді обслуговуєте. Відстежуйте `jti` до `exp`
|
||||
твердження, щоб воно приймалося лише раз. І беріть надані області доступу та, насамперед,
|
||||
`resource` випущеного токена з перевіреного ID-JAG, а не із запиту: `params.resource` — це те,
|
||||
що набрав клієнт. Повні правила обробки — у
|
||||
[специфікації Enterprise-Managed Authorization](https://modelcontextprotocol.io/extensions/auth/enterprise-managed-authorization).
|
||||
|
||||
Відхиляйте погане твердження через `TokenError("invalid_grant", ...)`. Інший код помилки в цьому потоці — `invalid_target`: ним відхиляється ID-JAG, що називає ресурс, якого ви не обслуговуєте, — саме це не дає цьому серверу випускати токени для чужого. А надані області доступу беруться з поля `scope` в ID-JAG (твердження без нього теж відхиляється); у вашому випадку вони можуть натомість відображати групи користувача.
|
||||
|
||||
І зверніть увагу, чого повернений `OAuthToken` не містить: токена оновлення. IdP вирішує, як довго цей користувач зберігає доступ, вирішуючи, чи видавати наступний ID-JAG. Токен оновлення, випущений тут, тихо повернув би це рішення назад.
|
||||
|
||||
!!! info
|
||||
Сервер, який досі вбудовує свій сервер авторизації через `auth_server_provider=`, потрапляє до
|
||||
того самого коду через `AuthSettings(identity_assertion_enabled=True)`. На сторінці
|
||||
**[Авторизація](../run/authorization.md)** пояснено, чому новим серверам не варто з цього починати.
|
||||
|
||||
!!! check
|
||||
З'єднайте два файли цієї сторінки — і весь грант зведеться до одного `POST /token`:
|
||||
|
||||
```text
|
||||
grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer
|
||||
assertion=eyJhbGciOiJIUzI1NiIsInR5cCI6Im9hdXRoLWlkLWphZytqd3QifQ...
|
||||
client_id=finance-agent
|
||||
resource=http://localhost:8001/mcp
|
||||
scope=notes:read
|
||||
client_secret=finance-agent-secret
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
{"access_token": "mcp_...", "token_type": "Bearer", "expires_in": 300, "scope": "notes:read"}
|
||||
```
|
||||
|
||||
Жодного `/authorize`, жодного `/register`, жодного отримання protected-resource-metadata.
|
||||
Єдині запити в мережі — той, що отримав `401`, запит well-known, цей обмін, а далі звичайний
|
||||
MCP-трафік із прикріпленим bearer-токеном. А `sub`, який ваш валідатор зчитав з ID-JAG, —
|
||||
саме те, що `get_access_token().subject` показує всередині інструмента.
|
||||
|
||||
### Спробуйте самі {#try-it}
|
||||
|
||||
`examples/stories/identity_assertion/` у репозиторії SDK — це ця сторінка в дії: той самий валідатор `exchange_identity_assertion`, MCP-сервер, захищений його токенами, IdP-замінник і клієнт — в одній програмі, що перевіряє сама себе. `uv run python -m stories.identity_assertion.client --http` виконує весь обмін і перевіряє (assert), що користувач, якого назвав IdP, — це користувач, якого бачить інструмент.
|
||||
|
||||
## Підсумки {#recap}
|
||||
|
||||
* [SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990) дає змогу корпоративному провайдеру ідентичності, а не кінцевому користувачу, вирішувати, до яких MCP-серверів клієнт може звертатися. IdP підписує це рішення у вигляді **ID-JAG**.
|
||||
* Отримання ID-JAG — це обмін токенів за [RFC 8693](https://datatracker.ietf.org/doc/html/rfc8693) з *вашим IdP*, і SDK його не виконує. Пред'явлення його серверу авторизації MCP — це грант `jwt-bearer` за [RFC 7523](https://datatracker.ietf.org/doc/html/rfc7523), і SDK реалізує обидва його боки.
|
||||
* `IdentityAssertionOAuthProvider` — ще один `httpx2.Auth`: заздалегідь зареєстрований конфіденційний клієнт, зафіксований `issuer` і один колбек `assertion_provider(audience, resource)`. Жодного браузера, жодної реєстрації, жодного токена оновлення.
|
||||
* Сервер авторизації ніколи не виявляється через сервер ресурсів. Налаштуйте `issuer` точно тим рядком, який віддає його документ метаданих; порівняння — символ за символом.
|
||||
* На боці сервера — `identity_assertion_enabled=True` плюс `exchange_identity_assertion`. SDK автентифікує клієнта й контролює доступ до гранту; перевірка ID-JAG — цілком ваша, а випущений токен прив'язаний до `resource` з ID-JAG, а не із запиту.
|
||||
|
||||
Єдина сторона, якої ця сторінка не торкнулася, — MCP-сервер. Те, що він робить із токеном, який ви щойно випустили, він уже робив на сторінці **[Авторизація](../run/authorization.md)**.
|
||||
217
i18n/uk/pages/client/index.md
Normal file
217
i18n/uk/pages/client/index.md
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
---
|
||||
translation:
|
||||
sections: [ebef1e7a0df854f4, a4c687d3d627d516, 8e79141fc2985342, b345dd05b9c3c7ab, 80ce41579825a6fa, 5f0fa90494de8f65, 83d10514eaa62fa5, 9190555aa39a5d28, 84a4c9d8bf14dddb, 927d71cf40b58c30]
|
||||
tool: 1
|
||||
---
|
||||
# Клієнт {#the-client}
|
||||
|
||||
**`Client`** — це те, через що програма на Python спілкується з MCP-сервером.
|
||||
|
||||
Це один об'єкт з одним життєвим циклом: створіть його, увійдіть в `async with`, викликайте методи. Кожна дія протоколу (перелічити інструменти, викликати один із них, прочитати ресурс, відрендерити промпт) — це його `async`-метод, що повертає типізований результат.
|
||||
|
||||
## Перший клієнт {#your-first-client}
|
||||
|
||||
```python title="client.py" hl_lines="14-18"
|
||||
--8<-- "docs_src/client/tutorial001.py"
|
||||
```
|
||||
|
||||
Сервер угорі потрібен лише для того, щоб було до чого під'єднатися. Клієнт — це п'ять виділених рядків.
|
||||
|
||||
* `Client(mcp)` отримує **сам об'єкт сервера**. Це транспорт у пам'яті: без підпроцесу, без порту, без HTTP. Саме так під'єднується кожен приклад на цій сторінці й кожен тест, який ви напишете.
|
||||
* `async with` — це **життєвий цикл**. Вхід у блок під'єднує й узгоджує параметри; вихід — від'єднує. Пари `connect()` / `close()` немає, а `Client` не можна використати повторно після завершення блоку.
|
||||
* Усередині блоку відомості про з'єднання вже доступні як звичайні властивості.
|
||||
|
||||
### Що можна передати в `Client` {#what-you-can-pass-to-client}
|
||||
|
||||
`Client` приймає один позиційний аргумент і визначає транспорт за його типом:
|
||||
|
||||
* Екземпляр `MCPServer` (або низькорівневого `Server`): під'єднання **в межах процесу**.
|
||||
* Рядок з URL (`Client("http://localhost:8000/mcp")`): Streamable HTTP, шлях для робочого розгортання.
|
||||
* **Транспорт**: будь-що, що можна використати як `async with ... as (read, write)`, наприклад `stdio_client(...)`, що обгортає підпроцес.
|
||||
|
||||
Усе інше на цій сторінці однакове для всіх трьох. Заголовки, підпроцеси, тайм-аути та протокол `Transport` мають власну сторінку: **[Транспорти клієнта](transports.md)**.
|
||||
|
||||
### Що є в під'єднаного клієнта {#whats-on-a-connected-client}
|
||||
|
||||
Чотири властивості лише для читання, заповнені в мить входу в блок:
|
||||
|
||||
* `client.server_info`: ідентичність сервера або `None` для сервера покоління 2026, який її не повідомляє (сервери python-sdk за замовчуванням повідомляють). `server_info.name` тут — `"Bookshop"`, а `server_info.version` — те, що повідомить сервер.
|
||||
* `client.server_capabilities`: що вміє сервер (`tools`, `resources`, `prompts`, `completions`, ...). Можливість, якої сервер не має, дорівнює `None`.
|
||||
* `client.protocol_version`: версія протоколу, про яку домовилися обидві сторони. Тут це `"2026-07-28"`.
|
||||
* `client.instructions`: рядок `instructions=` сервера або `None`, якщо сервер його не задав.
|
||||
|
||||
Версію протоколу ви не обирали. За замовчуванням `Client` зондує сервер і на старіших повертається до класичного рукостискання, тож один клієнт працює із сервером будь-якого покоління. Якщо потрібно цим керувати, докладніше — на сторінці **[Версії протоколу](../protocol-versions.md)**.
|
||||
|
||||
!!! tip
|
||||
`client.session` — це базова `ClientSession`, низькорівневий запасний вихід.
|
||||
Для жодної задачі на цій сторінці вона не знадобиться.
|
||||
|
||||
## Перелік інструментів {#listing-tools}
|
||||
|
||||
```python title="client.py" hl_lines="15-20"
|
||||
--8<-- "docs_src/client/tutorial002.py"
|
||||
```
|
||||
|
||||
`list_tools()` повертає `ListToolsResult`; інструменти лежать у `.tools`. Кожен із них — повне означення, яке хост передав би моделі:
|
||||
|
||||
```python
|
||||
tool.name # 'search_books'
|
||||
tool.title # 'Search the catalog'
|
||||
tool.description # 'Search the catalog by title or author.'
|
||||
```
|
||||
|
||||
а `tool.input_schema` — це JSON Schema, яку сервер вивів з анотацій типів функції:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {"title": "Query", "type": "string"},
|
||||
"limit": {"default": 10, "title": "Limit", "type": "integer"}
|
||||
},
|
||||
"required": ["query"],
|
||||
"title": "search_booksArguments"
|
||||
}
|
||||
```
|
||||
|
||||
Ця схема — усе, що потрібно UI, щоб показати форму аргументів, і все, що потрібно моделі, щоб сформувати коректні аргументи.
|
||||
|
||||
!!! tip
|
||||
`title` необов'язковий, тож UI, що показує інструменти людині, має обирати: `title`, якщо він є,
|
||||
і `name`, якщо немає. `from mcp.shared.metadata_utils import get_display_name` робить саме це —
|
||||
для інструментів, ресурсів, шаблонів ресурсів і промптів.
|
||||
|
||||
## Виклик інструмента {#calling-a-tool}
|
||||
|
||||
`call_tool(name, arguments)` запускає інструмент і повертає `CallToolResult`.
|
||||
|
||||
```python title="client.py" hl_lines="26-33"
|
||||
--8<-- "docs_src/client/tutorial003.py"
|
||||
```
|
||||
|
||||
Серверний `lookup_book` повертає Pydantic-модель `Book`. Ось що бачить клієнт:
|
||||
|
||||
```python
|
||||
result.content # [TextContent(type='text', text='{\n "title": "Dune",\n "author": "Frank Herbert",\n "year": 1965\n}')]
|
||||
result.structured_content # {'title': 'Dune', 'author': 'Frank Herbert', 'year': 1965}
|
||||
result.is_error # False
|
||||
```
|
||||
|
||||
Одне повернене значення, три речі для читання. У кожної свій споживач.
|
||||
|
||||
### `content`: що читає модель {#content-what-the-model-reads}
|
||||
|
||||
`content` — це `list` **блоків вмісту**, а блок вмісту — це об'єднання типів: `TextContent`, `ImageContent`, `AudioContent`, `ResourceLink` або `EmbeddedResource`. Інструмент може повернути кілька блоків, різних видів.
|
||||
|
||||
Саме тому `main` звужує тип через `isinstance(block, TextContent)`, перш ніж звертатися до `block.text`. Зверніть увагу: поза `isinstance` немає жодного `.text` — перевірка типів цього не дозволить, бо `ImageContent` має `.data`, а не `.text`. Об'єднання чесно показує, що інструменту дозволено вам надіслати; ваш код має бути таким самим чесним.
|
||||
|
||||
### `structured_content`: що читає ваш застосунок {#structured_content-what-your-application-reads}
|
||||
|
||||
`structured_content` — це повернене значення інструмента у вигляді JSON, що відповідає оголошеній `output_schema` інструмента. Жодного розбору рядків, жодних здогадок.
|
||||
|
||||
Коли є обидва, вони навмисно кажуть те саме двічі: `content` — для моделі, `structured_content` — для коду. Звідки береться структурована половина і як нею керувати — на сторінці **[Структурований вивід](../servers/structured-output.md)**.
|
||||
|
||||
### `is_error`: чи завершився інструмент помилкою {#is_error-whether-the-tool-failed}
|
||||
|
||||
Інструмент, що викидає виняток, **не** викидає його у вашому клієнті. Він повертається як звичайний результат з `is_error=True`.
|
||||
|
||||
!!! check
|
||||
Попросіть у `lookup_book` `"Solaris"` (назву, якої немає в каталозі) — і функція викине
|
||||
`ValueError`. Виклик усе одно повернеться нормально:
|
||||
|
||||
```python
|
||||
result.is_error # True
|
||||
result.content # [TextContent(type='text', text="Error executing tool lookup_book: No book titled 'Solaris' in the catalog.")]
|
||||
result.structured_content # None
|
||||
```
|
||||
|
||||
Повідомлення винятку потрапило в `content`, де його може прочитати **модель** і спробувати ще раз. Це
|
||||
навмисно: помилка інструмента — частина розмови, а не аварія. Завжди дивіться на `is_error`,
|
||||
перш ніж довіряти `structured_content`.
|
||||
|
||||
!!! warning
|
||||
`is_error=True` охоплює більше, ніж ваш власний `raise`. Попросіть інструмент, якого в сервера
|
||||
взагалі немає (`call_tool("does_not_exist", {})`), — і нічого не викидається. Повертається та сама форма:
|
||||
`is_error=True` з `Unknown tool: does_not_exist` у `content`. Метод `Client` викидає
|
||||
`MCPError` лише тоді, коли сервер відповідає **помилкою** JSON-RPC замість результату, а коли
|
||||
сервер повертає що саме — описано на сторінці **[Обробка помилок](../servers/handling-errors.md)**.
|
||||
|
||||
## Ресурси {#resources}
|
||||
|
||||
Дії з ресурсами йдуть парами: два способи перелічити, один спосіб прочитати.
|
||||
|
||||
```python title="client.py" hl_lines="22-31"
|
||||
--8<-- "docs_src/client/tutorial004.py"
|
||||
```
|
||||
|
||||
* `list_resources()` повертає **конкретні** ресурси — ті, що мають фіксований URI. Тут: `['catalog://genres']`.
|
||||
* `list_resource_templates()` повертає **параметризовані**. Тут: `['catalog://genres/{genre}']`. Це два різні списки, бо шаблон не можна прочитати, доки його не заповнено.
|
||||
* `read_resource(uri)` приймає URI як звичайний `str` і працює з обома: передайте `"catalog://genres/poetry"` — і сервер зіставить його з шаблоном.
|
||||
|
||||
`read_resource` повертає `contents` — список `TextResourceContents` або `BlobResourceContents`. Та сама ідея, що й із вмістом інструментів: звузьте тип через `isinstance`, потім читайте `.text` (або `.blob`).
|
||||
|
||||
Клієнта також можна сповіщати про зміни ресурсу. На з'єднаннях покоління 2025 це `subscribe_resource(uri)` / `unsubscribe_resource(uri)` — пара методів, яку `MCPServer` не реалізує, тож у протоколі 2026-07-28 (де цих дій уже немає) запит повертає `-32601`, *Method not found*. Заміна у версії 2026 — потік `subscriptions/listen`, який `MCPServer` *таки* обслуговує — `server_capabilities.resources.subscribe` там дорівнює `True` — а як споживати його через `client.listen(...)`, описано на сторінці **[Підписки](subscriptions.md)** цього розділу.
|
||||
|
||||
## Промпти {#prompts}
|
||||
|
||||
```python title="client.py" hl_lines="15-20"
|
||||
--8<-- "docs_src/client/tutorial005.py"
|
||||
```
|
||||
|
||||
`list_prompts()` повідомляє, що пропонує сервер і що потрібно кожному промпту:
|
||||
|
||||
```python
|
||||
prompt.name # 'recommend'
|
||||
prompt.title # 'Recommend a book'
|
||||
prompt.arguments # [PromptArgument(name='genre', required=True)]
|
||||
```
|
||||
|
||||
`get_prompt(name, arguments)` рендерить його. Словник аргументів — `str -> str`: аргументи промпту завжди рядки. Результат — `messages`, список `PromptMessage`, кожне з `role` і блоком `content`:
|
||||
|
||||
```python
|
||||
message.role # 'user'
|
||||
message.content # TextContent(type='text', text='Recommend one poetry book from the catalog and say why.')
|
||||
```
|
||||
|
||||
Хост передає ці повідомлення прямо моделі. Оце й уся можливість.
|
||||
|
||||
## Автодоповнення {#completions}
|
||||
|
||||
Сервер з обробником автодоповнення може доповнювати аргументи промптів і шаблонів ресурсів, поки користувач друкує.
|
||||
|
||||
```python title="client.py" hl_lines="27-31"
|
||||
--8<-- "docs_src/client/tutorial006.py"
|
||||
```
|
||||
|
||||
* `ref` вказує, *який* промпт чи шаблон ви заповнюєте: `PromptReference` або `ResourceTemplateReference`.
|
||||
* `argument` — це `{"name": ..., "value": ...}`: аргумент і те, що користувач уже встиг набрати.
|
||||
|
||||
Відповідь — у `result.completion.values`. Наберіть `"p"` — і сервер поверне `['poetry']`. Серверний бік, а також те, як обробник використовує *інші*, уже заповнені аргументи, щоб звузити свої пропозиції, — на сторінці **[Автодоповнення](../servers/completions.md)**.
|
||||
|
||||
## Пагінація {#pagination}
|
||||
|
||||
Кожен метод `list_*` приймає іменований аргумент `cursor=`, а кожен результат містить `next_cursor`. Коли `next_cursor` дорівнює `None`, у вас є все.
|
||||
|
||||
```python title="client.py" hl_lines="22-30"
|
||||
--8<-- "docs_src/client/tutorial007.py"
|
||||
```
|
||||
|
||||
Цей цикл коректний для будь-якого сервера. `MCPServer` повертає все однією сторінкою, тож `next_cursor` дорівнює `None` і цикл виконується один раз — саме тому більшість коду його ніколи не пише. Сервери, що справді розбивають результати на сторінки, і правила, яким підкоряються курсори, — на сторінці **[Пагінація](../advanced/pagination.md)**.
|
||||
|
||||
## У тестах {#in-tests}
|
||||
|
||||
`Client(mcp)` без процесу й без порту — це вже тестова обв'язка для вашого сервера.
|
||||
|
||||
Саме для цього є один прапорець конструктора: `Client(mcp, raise_exceptions=True)`. Він діє лише на з'єднаннях у пам'яті, а пояснює його й будує навколо нього весь підхід сторінка **[Тестування](../get-started/testing.md)**.
|
||||
|
||||
## Підсумки {#recap}
|
||||
|
||||
* `Client(x)` під'єднується в пам'яті до об'єкта сервера, через Streamable HTTP — до рядка з URL і через транспорт — до всього іншого.
|
||||
* `async with` — це весь життєвий цикл. Усередині нього `server_capabilities` і `protocol_version` уже заповнені; `server_info` та `instructions` — теж, якщо сервер їх надає.
|
||||
* `list_tools()` дає `name`, `title`, `description` та `input_schema` кожного інструмента.
|
||||
* `call_tool()` повертає `content` для моделі, `structured_content` для вашого коду та `is_error`. Інструмент, що викидає виняток, — це результат, а не виняток.
|
||||
* `content` — об'єднання типів блоків; звужуйте тип через `isinstance`, перш ніж читати.
|
||||
* `list_resources` / `list_resource_templates` / `read_resource`, `list_prompts` / `get_prompt` і `complete` доповнюють набір дій.
|
||||
* Кожен `list_*` приймає `cursor=`; повторюйте цикл, доки `next_cursor` не стане `None`.
|
||||
|
||||
Про що сервер може попросити *клієнта* і як на це відповідати — на сторінці **[Колбеки клієнта](callbacks.md)**.
|
||||
152
i18n/uk/pages/client/oauth-clients.md
Normal file
152
i18n/uk/pages/client/oauth-clients.md
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
---
|
||||
translation:
|
||||
sections: [c6899d3892bd9fa0, 79372cff3cc48a88, 63878d29e87c3e73, 13175843d3588af4, e7e2b9fd516f77de, 758f06399b513c1f, a05d7278487d610b]
|
||||
tool: 1
|
||||
---
|
||||
# OAuth-клієнти {#oauth-clients}
|
||||
|
||||
Деякі MCP-сервери захищені. Надішліть їм запит без токена — і у відповідь прийде `401 Unauthorized`.
|
||||
|
||||
Отримати токен допомагає **`OAuthClientProvider`**. Це взагалі не об'єкт MCP. Це `httpx2.Auth` — стандартний хук httpx2 на кшталт «зроби щось із кожним запитом». Його під'єднують до `httpx2.AsyncClient`, передають цей клієнт транспорту Streamable HTTP — і більше про нього не згадують.
|
||||
|
||||
Ця сторінка — про клієнтський бік. Як змусити власний сервер вимагати токен — на сторінці **[Авторизація](../run/authorization.md)**.
|
||||
|
||||
## Провайдер {#the-provider}
|
||||
|
||||
```python title="client.py" hl_lines="44-54"
|
||||
--8<-- "docs_src/oauth_clients/tutorial001.py"
|
||||
```
|
||||
|
||||
Йому передають чотири речі:
|
||||
|
||||
* `server_url`: MCP-ендпоінт, до якого ви під'єднуєтеся. Усе інше провайдер виявляє за ним сам.
|
||||
* `client_metadata`: те, що ви ввели б у форму «зареєструвати застосунок» на сервері авторизації.
|
||||
* `storage`: де токени зберігаються між запусками.
|
||||
* `redirect_handler` і `callback_handler`: два моменти, коли потрібна участь людини.
|
||||
|
||||
Більше ніде у файлі OAuth не згадується. `main()` ніколи не бачить токена.
|
||||
|
||||
### Метадані клієнта {#client-metadata}
|
||||
|
||||
`OAuthClientMetadata` — це справжній реєстраційний документ із [RFC 7591](https://datatracker.ietf.org/doc/html/rfc7591), оформлений як модель Pydantic.
|
||||
|
||||
Ви задаєте три поля. Решту заповнюють типові значення: `grant_types` уже дорівнює `["authorization_code", "refresh_token"]`, а `response_types` — `["code"]`, і це саме той потік, який виконує цей провайдер.
|
||||
|
||||
!!! check
|
||||
Оскільки це модель Pydantic, вона проходить валідацію **ще до того, як у мережу піде бодай один байт**.
|
||||
Пропустіть `redirect_uris` — і створення об'єкта одразу завершиться `ValidationError`, який
|
||||
називає поле:
|
||||
|
||||
```text
|
||||
redirect_uris
|
||||
Field required [type=missing, input_value={'client_name': 'Bookshop Agent'}, input_type=dict]
|
||||
```
|
||||
|
||||
Браузер не відкрився, на сервері авторизації не залишилося напівзавершеної реєстрації.
|
||||
|
||||
### Сховище токенів {#token-storage}
|
||||
|
||||
**`TokenStorage`** — це `Protocol` із чотирма асинхронними методами. Успадковуватися ні від чого не потрібно: напишіть ці методи — і будь-який клас стає сховищем токенів:
|
||||
|
||||
* `get_tokens` / `set_tokens` зберігають `OAuthToken`: токен доступу, токен оновлення, термін дії, область дії.
|
||||
* `get_client_info` / `set_client_info` зберігають `OAuthClientInformationFull`, який сервер авторизації видав, коли провайдер вас зареєстрував, — разом із вашим `client_id`.
|
||||
|
||||
Наведена вище версія в пам'яті працює. Але вона все забуває, щойно процес завершується, тож наступний запуск повторює всю процедуру з початку. Збережіть дані у файл або в системне сховище ключів вашої платформи — і наступний запуск пройде тихо.
|
||||
|
||||
!!! tip
|
||||
Зберігайте `client_info`, а не лише токени. Провайдер реєструється динамічно, коли вперше
|
||||
не знаходить збереженого `client_info`. Викиньте його — і кожен запуск створюватиме нову реєстрацію.
|
||||
|
||||
### Два обробники {#the-two-handlers}
|
||||
|
||||
Потоку authorization code людина потрібна рівно один раз: хтось має увійти й натиснути «Дозволити».
|
||||
|
||||
* **`redirect_handler`** викликається через await із повністю зібраним URL авторизації. `client_id`, `redirect_uri`, `state` і PKCE challenge уже в ньому. Ваше єдине завдання — відкрити його в браузері. Настільний застосунок викликає `webbrowser.open`; цей файл просто друкує його.
|
||||
* **`callback_handler`** очікується наступним. Він чекає, доки користувач повернеться на ваш `redirect_uri`, і повертає параметри запиту цього перенаправлення як `AuthorizationCodeResult`.
|
||||
|
||||
Справжній клієнт замість виклику `input()` запускає невеликий локальний HTTP-сервер на redirect URI. Форма та сама: отримати перенаправлення, повернути `code`, `state` та `iss`.
|
||||
|
||||
!!! warning
|
||||
Передавайте `state` та `iss` далі точно такими, якими вони надійшли. Провайдер порівнює `state`
|
||||
з тим, що згенерував сам, а `iss` — із виявленим видавцем, і відхиляє розбіжність. Це захист
|
||||
від CSRF і від атак із підміною сервера (mix-up).
|
||||
|
||||
### Усередину `Client` {#into-the-client}
|
||||
|
||||
Погляньте на `main()`. Провайдер чіпляється до **клієнта httpx2**, клієнт httpx2 передається у `streamable_http_client(url, http_client=...)`, а цей транспорт — у `Client`.
|
||||
|
||||
У `streamable_http_client` немає іменованого аргументу `auth=`. Усе, що стосується рівня HTTP (автентифікація, заголовки, тайм-аути, проксі), належить до `httpx2.AsyncClient`, який ви приносите самі. Про це розшарування — на сторінці **[Транспорти клієнта](transports.md)**.
|
||||
|
||||
## Що провайдер робить за вас {#what-the-provider-does-for-you}
|
||||
|
||||
Коли `Client` уперше надсилає запит, сервер відповідає `401`. Далі справу бере на себе провайдер:
|
||||
|
||||
1. **Виявлення.** Він читає заголовок `WWW-Authenticate`, завантажує Protected Resource Metadata сервера з `/.well-known/oauth-protected-resource`, дізнається, який сервер авторизації захищає цей ресурс, і завантажує метадані *того* сервера.
|
||||
2. **Реєстрація.** У сховищі порожньо? Він динамічно реєструє вас із вашими `OAuthClientMetadata` і зберігає результат.
|
||||
3. **Авторизація.** Він генерує пару PKCE і `state`, будує URL авторизації, очікує ваш `redirect_handler`, а потім — ваш `callback_handler`, щоб отримати код.
|
||||
4. **Обмін.** Він міняє код на `OAuthToken`, зберігає його й повторює ваш початковий запит із `Authorization: Bearer ...`.
|
||||
|
||||
Після цього все тихо. Токени беруться зі сховища, прострочений токен доступу оновлюється за допомогою токена оновлення, і лише коли ніщо з цього не спрацьовує, провайдер запускає потік знову.
|
||||
|
||||
Нічого з цього ви не писали. Лишаються ще два іменовані аргументи (`client_metadata_url` і `validate_resource_url`), і цьому файлу не потрібен жоден. Знати варто про `client_metadata_url`; йому присвячено окремий розділ нижче.
|
||||
|
||||
### Спробуйте самі {#try-it}
|
||||
|
||||
Більшість прикладів у цій документації можна перевірити за допомогою `Client(server)` у пам'яті. Цей — ні: уся суть потоку в HTTP-відповіді `401`, а між клієнтом у пам'яті та його сервером HTTP немає.
|
||||
|
||||
У репозиторії є жива версія. `examples/servers/simple-auth/` запускає окремий сервер авторизації та захищений MCP-сервер; `examples/clients/simple-auth-client/` — це клієнт із цієї сторінки, що виріс у невеликий CLI. У його README є дві команди: запустіть сервери, запустіть клієнт проти них — і спостерігайте, як проходять усі чотири кроки.
|
||||
|
||||
## Client ID Metadata Documents {#client-id-metadata-documents}
|
||||
|
||||
Ревізія специфікації 2026-07-28 оголошує динамічну реєстрацію клієнтів застарілою на користь **Client ID Metadata Documents** (CIMD). Замість надсилати POST із новою реєстрацією на кожен сервер авторизації, що трапляється на шляху, ваш клієнт публікує один JSON-документ про себе за стабільним HTTPS URL, і цей URL *і є* його `client_id`. Документ завантажує сервер авторизації; провайдер його ніколи не торкається.
|
||||
|
||||
SDK це вже підтримує: передайте URL як `client_metadata_url=`, коли створюєте провайдер. Якщо метадані сервера авторизації оголошують `client_id_metadata_document_supported: true`, провайдер повністю пропускає запит `/register`: URL іде в потік як `client_id`, а `client_secret` немає взагалі. Якщо сервер цього не оголошує (більшість поки що ні) або ви так і не передали URL, провайдер **мовчки** повертається до динамічної реєстрації, і все описане вище працює саме так, як описано. Збережений `client_info` і далі має пріоритет над обома варіантами.
|
||||
|
||||
URL має бути HTTPS із некореневим шляхом; усе інше — це `ValueError` під час створення, ще до будь-якого звернення до мережі. Приклад `examples/clients/simple-auth-client/` із репозиторію приймає його через змінну середовища `MCP_CLIENT_METADATA_URL`.
|
||||
|
||||
## Взаємодія між машинами {#machine-to-machine}
|
||||
|
||||
Нічне завдання, крок CI, інший сервіс. Браузера немає, і натиснути «Дозволити» нікому. Це грант **client credentials**: у вас уже є `client_id` і `client_secret`, а весь потік зводиться до ендпоінта токенів.
|
||||
|
||||
`ClientCredentialsOAuthProvider` — це той самий `httpx2.Auth`, тільки без людини:
|
||||
|
||||
```python title="client.py" hl_lines="4 27-33"
|
||||
--8<-- "docs_src/oauth_clients/tutorial002.py"
|
||||
```
|
||||
|
||||
Що змінилося:
|
||||
|
||||
* Немає `OAuthClientMetadata`, немає обробників. Ви передаєте `client_id` і `client_secret`; провайдер будує навколо них мінімальну реєстрацію `client_credentials` і повністю пропускає динамічну реєстрацію.
|
||||
* `scope` — це рядок, розділений пробілами: такий формат OAuth використовує в переданих даних.
|
||||
* Усе далі ідентичне: той самий `TokenStorage`, той самий `httpx2.AsyncClient(auth=...)`, той самий `streamable_http_client`.
|
||||
|
||||
За замовчуванням секрет передається як HTTP Basic auth у запиті на токен (`client_secret_basic`). Передайте `token_endpoint_auth_method="client_secret_post"`, щоб натомість помістити його в тіло форми. Деякі сервери авторизації приймають лише один із двох варіантів.
|
||||
|
||||
!!! tip
|
||||
Читайте `client_secret` зі змінних середовища або менеджера секретів, ніколи не з системи контролю версій.
|
||||
|
||||
!!! info
|
||||
Ще один провайдер живе в `mcp.client.auth.extensions.client_credentials`:
|
||||
**`PrivateKeyJWTOAuthProvider`** — для клієнтів, які автентифікуються за допомогою JWT замість
|
||||
спільного секрету (`private_key_jwt`, варіант із парою ключів та workload identity). Він дотримується
|
||||
тієї ж схеми: створіть екземпляр і передайте в `auth=`. У тому ж модулі є
|
||||
`SignedJWTParameters` і `static_assertion_provider` — два допоміжні засоби, що будують для нього assertion.
|
||||
|
||||
Є ще одна ситуація без людини: клієнт належить підприємству, де провайдер ідентичності, а не користувач, вирішує, до яких MCP-серверів він може звертатися. Це інший грант із власною моделлю довіри та власною сторінкою — **[Твердження про ідентичність](identity-assertion.md)**.
|
||||
|
||||
## Коли щось іде не так {#when-it-fails}
|
||||
|
||||
Коли в OAuth-потоці щось іде не так, провайдер викидає `OAuthFlowError` з `mcp.client.auth`. У нього два підкласи. `OAuthRegistrationError` означає, що реєстрація не дала клієнта, яким можна скористатися: сервер авторизації відмовився вас реєструвати або таки зареєстрував, але з обліковими даними, які цей потік не може використати (наприклад, із методом автентифікації, якого він не реалізує). `OAuthTokenError` означає, що токен отримати не вдалося: ендпоінт токенів відмовив, або збережений запис клієнта містить метод автентифікації, який цей клієнт не може застосувати, — про це повідомляється ще під час побудови запиту на токен, а не після надсилання. Один `except OAuthFlowError:` охоплює виявлення, реєстрацію, авторизацію та обмін.
|
||||
|
||||
Не все є помилкою потоку. Мережа, як і раніше, може підвести; це звичайні винятки `httpx2`, і вони проходять наскрізь без змін.
|
||||
|
||||
## Підсумки {#recap}
|
||||
|
||||
* `OAuthClientProvider` — це `httpx2.Auth`. Під'єднайте його до `httpx2.AsyncClient`, передайте той у `streamable_http_client(url, http_client=...)` — і `Client` ніколи не дізнається, що відбувся OAuth.
|
||||
* Ви надаєте чотири речі: URL сервера, `OAuthClientMetadata`, `TokenStorage` і пару обробників redirect/callback.
|
||||
* `TokenStorage` — це `Protocol`: чотири асинхронні методи, жодного базового класу. Зберігайте `client_info` так само, як і токени.
|
||||
* Виявлення, реєстрація (динамічна або через **Client ID Metadata Document**), PKCE, перевірки `state` та `iss` і оновлення токенів — робота провайдера, а не ваша.
|
||||
* `ClientCredentialsOAuthProvider` — версія без людини: `client_id` + `client_secret`, без обробників, без браузера.
|
||||
* Кожен збій OAuth — це `OAuthFlowError`; `OAuthRegistrationError` і `OAuthTokenError` — його підкласи.
|
||||
|
||||
Друга половина цього рукостискання — змусити ваш *сервер* вимагати токен — на сторінці **[Авторизація](../run/authorization.md)**.
|
||||
87
i18n/uk/pages/client/session-groups.md
Normal file
87
i18n/uk/pages/client/session-groups.md
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
---
|
||||
translation:
|
||||
sections: [09c857a25a9dc37a, 43bc6a76a243a50e, 0a716022a88768df, 4b7f78042bfcfff7, c112662e61b03315, 58974ba1f489a8b4, d18adbdbb835ea73]
|
||||
tool: 1
|
||||
---
|
||||
# Групи сесій {#session-groups}
|
||||
|
||||
`Client` під'єднується до одного сервера. Реальним застосункам часто потрібно кілька (сервер пошуку, сервер бази даних, внутрішній API), і зрештою доводиться жонглювати окремим з'єднанням і списком інструментів для кожного.
|
||||
|
||||
**`ClientSessionGroup`** — це один об'єкт, який тримає багато з'єднань і зводить усе, що вони надають, в єдине представлення.
|
||||
|
||||
## Два сервери {#two-servers}
|
||||
|
||||
Почнімо з двох звичайних серверів. Вони ніяк не пов'язані між собою, тож обидва, природно, назвали свій інструмент `search`:
|
||||
|
||||
```python title="library_server.py" hl_lines="7"
|
||||
--8<-- "docs_src/session_groups/tutorial001.py"
|
||||
```
|
||||
|
||||
```python title="web_server.py" hl_lines="7"
|
||||
--8<-- "docs_src/session_groups/tutorial002.py"
|
||||
```
|
||||
|
||||
## Одна група {#one-group}
|
||||
|
||||
Створіть `ClientSessionGroup` і викличте **`connect_to_server`** один раз для кожного сервера:
|
||||
|
||||
```python title="client.py" hl_lines="10-12"
|
||||
--8<-- "docs_src/session_groups/tutorial003.py"
|
||||
```
|
||||
|
||||
* `connect_to_server` приймає параметри транспорту, а не об'єкт сервера: `StdioServerParameters` (з `mcp`), щоб запустити підпроцес, або `StreamableHttpParameters` / `SseServerParameters` (з `mcp.client.session_group`) для сервера, що вже слухає на якомусь URL.
|
||||
* `group.tools` — це `dict[str, Tool]` з інструментами всіх під'єднаних серверів. `group.resources` і `group.prompts` мають таку саму форму.
|
||||
* `group.call_tool(name, arguments)` шукає ім'я, знаходить сесію, якій воно належить, і пересилає виклик. Указувати сервер не потрібно ніколи.
|
||||
|
||||
!!! check
|
||||
Покладіть `client.py` поруч із двома серверами й запустіть його. Другий `connect_to_server` відмовляє:
|
||||
|
||||
```text
|
||||
mcp.shared.exceptions.MCPError: {'search'} already exist in group tools.
|
||||
```
|
||||
|
||||
Це `MCPError`, викинутий ще до того, як щось із другого сервера буде зареєстровано. Ім'я має
|
||||
бути унікальним у межах **усієї** групи, а два сервери, які ви не контролюєте, рано чи пізно зіткнуться.
|
||||
|
||||
## `component_name_hook` {#component_name_hook}
|
||||
|
||||
Виправляти це слід у групі, а не на серверах. Передайте функцію від `(name, server_info)`, і група застосує її до кожного імені, яке реєструє:
|
||||
|
||||
```python title="client.py" hl_lines="7-8 15"
|
||||
--8<-- "docs_src/session_groups/tutorial004.py"
|
||||
```
|
||||
|
||||
Запустіть знову. `print(sorted(group.tools))` тепер показує обидва:
|
||||
|
||||
```text
|
||||
['Library.search', 'Web.search']
|
||||
```
|
||||
|
||||
* **Ключ** — ваш. `by_server` побудував його з `server_info.name` — імені, з яким було створено кожен `MCPServer(...)`.
|
||||
* `Tool` усередині лишається незмінним: `group.tools["Web.search"].name` — усе ще `"search"`, і саме це ім'я `call_tool` надсилає мережею. Префікс ніколи не виходить за межі вашого процесу.
|
||||
* Це стосується не лише інструментів. Ресурс бібліотеки `hours` зареєстровано як `Library.hours`.
|
||||
|
||||
!!! tip
|
||||
Хук виконується для **кожного** імені з **кожного** сервера, а не лише за конфліктів: режиму
|
||||
«префікс лише за зіткнення» немає. Оберіть одну схему й дайте їй діяти всюди.
|
||||
|
||||
## Додавання й видалення серверів {#adding-and-removing-servers}
|
||||
|
||||
`connect_to_server` повертає `ClientSession`, яку він відкрив. Збережіть її, якщо колись захочете позбутися цього сервера: `await group.disconnect_from_server(session)` видаляє його інструменти, ресурси й промпти з групи.
|
||||
|
||||
Якщо вже маєте під'єднану `ClientSession` (наприклад, `Client.session`), передайте її в `await group.connect_with_session(server_info, session)` замість того, щоб відкривати новий транспорт. Агрегація відбувається так само. Група ніколи не закриває сесію, яку не відкривала. `server_info` задає ім'я сервера для префіксів компонентів; на з'єднанні покоління 2026 `client.server_info` може бути `None` (ідентичність необов'язкова), тож у такому разі передайте власний `Implementation(name=..., version=...)`.
|
||||
|
||||
## Класичне рукостискання {#the-classic-handshake}
|
||||
|
||||
`ClientSessionGroup` побудовано на `ClientSession`, а не на `Client`. Кожен `connect_to_server` виконує класичне рукостискання `initialize`. Він ніколи не надсилає зонд `server/discover`, описаний на сторінці **[Версії протоколу](../protocol-versions.md)**. Це рукостискання розуміє кожен MCP-сервер, тож сумісності це ні з чим не коштує; це лише означає, що група йде старішим і повільнішим шляхом до сервера, який міг би краще.
|
||||
|
||||
## Підсумки {#recap}
|
||||
|
||||
* `ClientSessionGroup` тримає багато з'єднань із серверами й зводить їхні інструменти, ресурси й промпти в один `dict` кожного виду.
|
||||
* `connect_to_server(params)` для кожного сервера. Він приймає параметри транспорту й ніколи — об'єкт сервера чи URL, як `Client`.
|
||||
* `group.call_tool(name, arguments)` сам спрямовує виклик на сервер-власник.
|
||||
* Імена мають бути унікальними в межах усієї групи; два сервери з інструментом `search` самі по собі співіснувати не можуть.
|
||||
* `component_name_hook=` переписує кожне зареєстроване ім'я. Змінюється ключ словника, а не ім'я в переданих даних.
|
||||
* `connect_with_session` додає сесію, яку ви вже маєте; `disconnect_from_server` видаляє сесію.
|
||||
|
||||
Рукостисканню, яким говорить група (і швидшому, якому віддає перевагу `Client`), присвячено сторінку **[Версії протоколу](../protocol-versions.md)**.
|
||||
91
i18n/uk/pages/client/subscriptions.md
Normal file
91
i18n/uk/pages/client/subscriptions.md
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
---
|
||||
translation:
|
||||
sections: [8f9558e57f29eee1, a88c587739e0465c, 46ebfd5b325ed041, 4d10b00b57ce4bd9, 2cdb0edd1f59b3e2]
|
||||
tool: 1
|
||||
---
|
||||
# Підписки {#subscriptions}
|
||||
|
||||
Каталог сервера не є сталим. Інструменти з'являються під час роботи, а вміст за URI ресурсу змінюється. Клієнт дізнається про це через `client.listen(...)`: один запит `subscriptions/listen`, відповідь на який *і є* потоком. Він лишається відкритим і несе сповіщення про зміни, які клієнт попросив.
|
||||
|
||||
Ця сторінка — про клієнтський бік: як відкрити потік, стежити за ним поруч з основним потоком виконання й обробляти його завершення. Публікація змін, фільтрація та обслуговування методу — серверний бік історії, розказаний на сторінці **[Підписки](../handlers/subscriptions.md)** у розділі *Усередині обробника*. Приклади тут спілкуються із сервером спринт-дошки, побудованим там.
|
||||
|
||||
## Стеження за потоком {#watching-the-stream}
|
||||
|
||||
Підписка — це один контекстний менеджер. Вхід у нього надсилає запит із вашими іменованими аргументами як фільтром підписки й чекає на підтвердження від сервера, тож до початку блока потік уже активний.
|
||||
|
||||
```python title="client.py" hl_lines="15 18 28"
|
||||
--8<-- "docs_src/subscriptions/tutorial003.py"
|
||||
```
|
||||
|
||||
Ітерація повертає чотири типізовані події: `ToolsListChanged`, `PromptsListChanged`, `ResourcesListChanged` і `ResourceUpdated(uri=...)`.
|
||||
|
||||
Подія каже, *що* змінилося, і ніколи — *як*. Саме тому `follow_board` викликає `read_resource` і `list_tools`: подія — це сигнал перечитати дані. Читайте `event.uri`, а не припускайте, який ресурс змінився: фільтр може називати кілька URI, а сервер може повідомити про зміну підресурсу одного з них.
|
||||
|
||||
Дублікати подій, що чекають на споживання, згортаються в одну, а повторне читання все одно дає поточний стан. Згортаються лише ідентичні події: два `ResourceUpdated` для різних URI — це дві події.
|
||||
|
||||
Ще дві властивості дескриптора:
|
||||
|
||||
* `sub.honored` — фільтр, який підтвердив сервер: `SubscriptionFilter` із полями, що ви передали, доступними як атрибути (`sub.honored.prompts_list_changed`). `MCPServer` задовольняє кожен вид, який ви просите, тож повертає ваш запит як є. Сервер, що підтримує менше видів, підтверджує менше, а підтверджений вид усе одно може ніколи не спрацювати. Сервер також може відхилити весь запит замість того, щоб підтвердити його (див. [Хто може стежити](../handlers/subscriptions.md#deciding-who-may-watch) на серверній сторінці), що проявляється як помилка запиту.
|
||||
* `sub.subscription_id` — ідентифікатор запиту listen, той самий, що проставлений на кожному кадрі цього потоку. Одночасно може бути відкрито кілька підписок, і кожна демультиплексується за власним ідентифікатором.
|
||||
|
||||
## Стеження без блокування {#watching-without-blocking}
|
||||
|
||||
`follow_board` працює, доки сервер не закриє потік, а цього може не статися ніколи, тож сама по собі вона захоплює всю програму. Реальним клієнтам спостерігач потрібен *поруч* з основним потоком виконання: агент викликає інструменти, а спостерігач тим часом підтримує кеш чи інтерфейс актуальними.
|
||||
|
||||
Спершу відкрийте підписку, потім запустіть спостерігача й продовжуйте свою роботу.
|
||||
|
||||
=== "asyncio"
|
||||
|
||||
```python title="app.py" hl_lines="18 20"
|
||||
--8<-- "docs_src/subscriptions/tutorial004_asyncio.py"
|
||||
```
|
||||
|
||||
=== "trio"
|
||||
|
||||
```python title="app.py" hl_lines="18 21"
|
||||
--8<-- "docs_src/subscriptions/tutorial004_trio.py"
|
||||
```
|
||||
|
||||
=== "anyio"
|
||||
|
||||
```python title="app.py" hl_lines="18 21"
|
||||
--8<-- "docs_src/subscriptions/tutorial004_anyio.py"
|
||||
```
|
||||
|
||||
!!! note
|
||||
`app.py` імпортує `BOARD` і `read_board` з першого прикладу, який у цьому репозиторії
|
||||
збережено як `tutorial003.py`. Якщо ви зберігаєте показані файли поруч як `client.py` і `app.py`,
|
||||
напишіть натомість `from client import BOARD, read_board`. Приклад `watch.py` нижче
|
||||
імпортує `read_board` так само.
|
||||
|
||||
Уся суть — у порядку. Нічого не відтворюється повторно, тож подію, опубліковану до появи вашого потоку, буде пропущено. Вхід у `client.listen(...)` чекає на підтвердження, тому кожна зміна від цієї миті доходить до спостерігача, а знімок, зроблений усередині блока, не може жодної пропустити.
|
||||
|
||||
Запити вільно виконуються поруч із відкритим потоком — із завдання спостерігача чи будь-якого іншого, на тому самому клієнті. Оскільки *дублікати* неспожитих подій зливаються, завантажений основний потік виконання може дати одне повторне читання замість трьох. Події, що відрізняються, не зливаються: фільтр, який називає багато URI, ставить у чергу по одній відкладеній події на кожен URI.
|
||||
|
||||
Щоб припинити стеження, вийдіть із блока: виклику `unsubscribe` немає. Скасування завдання, якому належить блок, робить це за вас, а SDK скасовує запит listen так, як очікує транспорт: через Streamable HTTP — закриваючи потік цього запиту. Спостерігач, що працює весь час життя застосунку, сам ніколи не повертається, тож скасуйте його або область його групи завдань під час завершення роботи.
|
||||
|
||||
## Потоки закінчуються {#streams-end}
|
||||
|
||||
Потік закінчується одним із двох способів, і обидва — звичайний хід виконання. Коректне закриття з боку сервера завершує `async for`; раптовий обрив викидає `SubscriptionLost`.
|
||||
|
||||
Різниця — діагностична, а не в тому, що робити далі: потоку вже немає, нічого не відтворено повторно, і спостерігач, якому це досі важливо, підписується знову й перечитує дані.
|
||||
|
||||
```python title="watch.py" hl_lines="16 20"
|
||||
--8<-- "docs_src/subscriptions/tutorial005.py"
|
||||
```
|
||||
|
||||
Сервери коректно закривають потоки з власних причин, зокрема щоб позбутися підписника, чий беклог занадто виріс, тож чисте завершення — не сигнал припиняти стеження. Витримайте паузу, перш ніж підписуватися знову.
|
||||
|
||||
`SubscriptionLost` має й одну локальну причину. Клієнт тримає щонайбільше 1024 неспожиті події, і споживач, який відстав настільки, втрачає підписку, замість того щоб рости без меж. Тримайте тіло `async for` коротким, а повільну роботу виконуйте деінде.
|
||||
|
||||
`keep_following` перехоплює лише `SubscriptionLost`. Вхід у `listen()` може також викинути `MCPError` (з'єднання не вдалося або сервер не обслуговує цей метод), `TimeoutError` (підтвердження не надійшло) і `ListenNotSupportedError` (з'єднання до версії 2026). Вирішіть, які з них ваш спостерігач має повторювати: остання ніколи не минає сама.
|
||||
|
||||
## Підсумки {#recap}
|
||||
|
||||
* Увійдіть у `async with client.listen(...)`; вхід чекає на підтвердження, тож нічого опублікованого після нього не буде пропущено.
|
||||
* Ітеруйте через `async for event in sub`. Події — це сигнали перечитати дані, а не корисне навантаження.
|
||||
* Відкрийте підписку, потім запустіть спостерігача як завдання — і виклики інструментів ідуть далі поруч із ним.
|
||||
* Чисте завершення зупиняє цикл; обрив викидає `SubscriptionLost`. У будь-якому разі: підпишіться знову, перечитайте дані, але спершу витримайте паузу.
|
||||
* Вихід із блока — це і є відписка.
|
||||
|
||||
Публікація цих подій, звуження фільтра та масштабування за межі одного процесу — історія сервера: **[Підписки](../handlers/subscriptions.md)**. Ці самі події також підтримують актуальність клієнтського кешу, і наступна сторінка — **[Кешування](caching.md)**.
|
||||
128
i18n/uk/pages/client/transports.md
Normal file
128
i18n/uk/pages/client/transports.md
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
---
|
||||
translation:
|
||||
sections: [9cac816674181eb0, 0700f337babcd4dd, 2bde0dd58cdf00f5, ff7401df479af877, 3d0832f39b0d7059, d4bf7e4479637768, 05e20c0a798860e7]
|
||||
tool: 1
|
||||
---
|
||||
# Транспорти клієнта {#client-transports}
|
||||
|
||||
Кожен `Client` спілкується зі своїм сервером через **транспорт** — те, що власне й переносить повідомлення.
|
||||
|
||||
Окремо його налаштовувати не доводиться. `Client` приймає один позиційний аргумент і визначає транспорт за його типом.
|
||||
|
||||
*Серверний* бік кожного з них (що робить `mcp.run()` і що ви розгортаєте) описано на сторінці **[Запуск сервера](../run/index.md)**.
|
||||
|
||||
## У пам'яті {#in-memory}
|
||||
|
||||
Передайте сам об'єкт сервера:
|
||||
|
||||
```python title="client.py" hl_lines="14"
|
||||
--8<-- "docs_src/client_transports/tutorial001.py"
|
||||
```
|
||||
|
||||
Жодного підпроцесу, жодного порту, жодних байтів у мережі. Клієнт і сервер — це два об'єкти в одному процесі, а виклик усе одно проходить через справжній протокольний рівень: `search_books` перелічується, валідується й викликається точнісінько так само, як це було б через HTTP.
|
||||
|
||||
Тож це одразу дві речі:
|
||||
|
||||
* **Тестовий стенд.** Кожен приклад у цій документації перевіряється саме так, а сторінка **[Тестування](../get-started/testing.md)** будує довкола цього весь підхід.
|
||||
* **API для вбудовування.** Застосунку, який сам створює сервер, не потрібен мережевий перехід, щоб викликати його інструменти.
|
||||
|
||||
## Streamable HTTP {#streamable-http}
|
||||
|
||||
Передайте рядок з URL — і отримаєте **Streamable HTTP**, транспорт, за яким розгортають сервер:
|
||||
|
||||
```python title="client.py" hl_lines="5"
|
||||
--8<-- "docs_src/client_transports/tutorial002.py"
|
||||
```
|
||||
|
||||
Оце й увесь продакшен-клієнт. `Client` сам загортає URL у `streamable_http_client(...)` поверх `httpx2.AsyncClient`, налаштованого так, як потрібно MCP: `follow_redirects=True`, 30-секундний тайм-аут на connect/write/pool і 300-секундний тайм-аут на читання, бо сервер може тримати потік відповіді відкритим.
|
||||
|
||||
!!! check
|
||||
Створений `Client` ще **не** під'єднаний. Конструктор лише обирає транспорт;
|
||||
відкриває його `async with`. Спробуйте звернутися до з'єднання до входу в блок — і SDK про це скаже:
|
||||
|
||||
```text
|
||||
RuntimeError: Client must be used within an async context manager
|
||||
```
|
||||
|
||||
Коли ви написали `Client("http://...")`, нічого не розв'язувалося, не завантажувалося й не запускалося. Цей рядок нічого не коштує.
|
||||
|
||||
### Власний `httpx2.AsyncClient` {#bring-your-own-httpx2asyncclient}
|
||||
|
||||
Щойно знадобиться заголовок `Authorization`, cookie, проксі, mTLS чи інший тайм-аут — створіть `httpx2.AsyncClient` самостійно й передайте його в `streamable_http_client`:
|
||||
|
||||
```python title="client.py" hl_lines="8-14"
|
||||
--8<-- "docs_src/client_transports/tutorial003.py"
|
||||
```
|
||||
|
||||
Зверніть увагу на дві речі:
|
||||
|
||||
* `httpx2.AsyncClient` належить вам, тож саме **ви** входите в нього й виходите з нього. SDK ніколи не закриває клієнт, якого не створював.
|
||||
* `streamable_http_client(url, http_client=...)` повертає транспорт, а `Client(transport)` приймає його, як і будь-що інше.
|
||||
|
||||
Одне зауваження щодо TLS: `httpx2` перевіряє сертифікати за сховищем довіри операційної системи (через
|
||||
[`truststore`](https://pypi.org/project/truststore/)), а не за вбудованим списком CA. У середовищі
|
||||
без придатного системного сховища CA (деякі мінімальні контейнери) задайте стандартні змінні середовища `SSL_CERT_FILE`/`SSL_CERT_DIR`
|
||||
або передайте явний `verify=ssl_context` у свій `httpx2.AsyncClient`
|
||||
(подробиці — у розділі
|
||||
[`httpx` і `httpx-sse` замінено на `httpx2`](../migration.md#httpx-and-httpx-sse-replaced-by-httpx2)).
|
||||
|
||||
!!! warning
|
||||
Раніше `streamable_http_client` приймав `headers=` і `timeout=` напряму. Більше ні:
|
||||
його єдині параметри — `url`, `http_client` і `terminate_on_close`. Напишете `headers=`
|
||||
за звичкою — і отримаєте:
|
||||
|
||||
```text
|
||||
TypeError: streamable_http_client() got an unexpected keyword argument 'headers'
|
||||
```
|
||||
|
||||
Усе, що стосується HTTP, тепер живе на тому єдиному `httpx2.AsyncClient`, який ви передаєте.
|
||||
|
||||
!!! info
|
||||
`httpx2` зберігає знайомий API `httpx`, тож якщо ви знаєте `httpx`, то вже вмієте робити тут автентифікацію,
|
||||
проксі, хуки подій, повторні спроби й обмеження з'єднань. SDK нічого не додає зверху й нічого не
|
||||
забирає. Саме сюди під'єднується й OAuth:
|
||||
`httpx2.AsyncClient(auth=OAuthClientProvider(...))`. Увесь цей процес описано на сторінці **[OAuth-клієнти](oauth-clients.md)**.
|
||||
|
||||
## stdio {#stdio}
|
||||
|
||||
Сервер **stdio** — це підпроцес. Клієнт запускає його, пише JSON-RPC в його stdin і читає JSON-RPC з його stdout. Саме так десктопний хост запускає сервер на вашій машині: хост і *є* цим кодом плюс UI, а сторінка **[Під'єднання до справжнього хоста](../get-started/real-host.md)** показує ті самі стосунки з боку хоста — як конфігураційний файл.
|
||||
|
||||
Опишіть процес за допомогою `StdioServerParameters`, перетворіть його на транспорт через `stdio_client` і передайте *це* в `Client`:
|
||||
|
||||
```python title="client.py" hl_lines="4-8 12"
|
||||
--8<-- "docs_src/client_transports/tutorial004.py"
|
||||
```
|
||||
|
||||
`Client` не приймає сам об'єкт параметрів. `StdioServerParameters` — це конфігурація; `stdio_client(server)` — транспорт, який уміє запустити з неї процес. Завжди загортайте.
|
||||
|
||||
Вихід із блоку `async with` також завершує підпроцес: закриває stdin, чекає, вбиває, якщо той затримується. Прибирати за ним самостійно ніколи не доведеться.
|
||||
|
||||
!!! warning
|
||||
Дочірній процес **не** успадковує ваше середовище. Він отримує мінімальний список дозволених змінних (`HOME`, `LOGNAME`,
|
||||
`PATH`, `SHELL`, `TERM` і `USER` на POSIX), щоб нічого чутливого не просочилося в процес,
|
||||
який, можливо, писали не ви.
|
||||
|
||||
Сервер, якому потрібен ключ API, там його не знайде. Передайте його явно через `env=`; ці
|
||||
змінні накладаються поверх списку дозволених. Саме це й робить `BOOKSHOP_API_KEY` вище.
|
||||
|
||||
## SSE {#sse}
|
||||
|
||||
`sse_client(url)` з `mcp.client.sse` — це HTTP-транспорт, на зміну якому прийшов Streamable HTTP. Загортайте його так само, `Client(sse_client("http://localhost:8000/sse"))`, щоб говорити із сервером, який досі ним користується, — і не будуйте на ньому нічого нового.
|
||||
|
||||
## Протокол `Transport` {#the-transport-protocol}
|
||||
|
||||
Для `Client` усе перелічене вище — одне й те саме.
|
||||
|
||||
**Транспорт** — це будь-який асинхронний контекстний менеджер, що повертає пару потоків повідомлень `(read, write)`: формально — протокол `Transport` у `mcp.client`. `Client` розв'язує свій аргумент за типом: об'єкт сервера під'єднується в межах процесу, `str` стає `streamable_http_client(url)`, а в будь-що інше він входить безпосередньо як у транспорт. Саме завдяки останньому правилу `stdio_client(...)`, `streamable_http_client(...)` і `sse_client(...)` стають на одне й те саме місце — і саме тому можна написати власний.
|
||||
|
||||
## Підсумки {#recap}
|
||||
|
||||
* `Client(mcp)` (об'єкт сервера) під'єднується в пам'яті. Використовуйте для тестів і для вбудовування.
|
||||
* `Client("http://.../mcp")` (URL) під'єднується через Streamable HTTP, продакшен-транспорт.
|
||||
* Заголовки, автентифікація, проксі й тайм-аути належать `httpx2.AsyncClient`, який ви передаєте в `streamable_http_client(url, http_client=...)`. Іменованого аргументу `headers=` немає.
|
||||
* stdio — це `Client(stdio_client(StdioServerParameters(...)))`, ніколи не сам об'єкт параметрів.
|
||||
* Підпроцес отримує середовище зі списку дозволених, а не ваше; `env=` його доповнює.
|
||||
* Транспорт — це будь-що, з чим можна зробити `async with x as (read, write)`. Усе, що не є об'єктом сервера чи URL, `Client` передає прямо цьому протоколу.
|
||||
* Створення `Client` обирає транспорт. `async with` його відкриває.
|
||||
|
||||
Щойно транспорт відкрито, обидві сторони мають домовитися про версію протоколу. Зазвичай про це не думаєш; а коли доводиться — є сторінка **[Версії протоколу](../protocol-versions.md)**.
|
||||
98
i18n/uk/pages/deprecated.md
Normal file
98
i18n/uk/pages/deprecated.md
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
---
|
||||
translation:
|
||||
sections: [20541a40dbdd5980, 01262a123ad9501d, 429db5b574a2ac08, 56b2d49da412cb28, 6a1717123fe4513c]
|
||||
tool: 1
|
||||
---
|
||||
# Застарілі можливості {#deprecated-features}
|
||||
|
||||
Специфікація 2026-07-28 виводить з ужитку п'ять речей. SDK і далі реалізує кожну з них, і кожна тепер супроводжується **попередженням про застарілість**.
|
||||
|
||||
Таблиця нижче називає кожну застарілу можливість, пояснює, чому вона зникає, і вказує заміну, на яку варто спиратися.
|
||||
|
||||
## Що застаріло {#what-is-deprecated}
|
||||
|
||||
| Застаріле | Чому | Що робити натомість |
|
||||
|---|---|---|
|
||||
| **Кореневі каталоги (roots)**: `ctx.session.list_roots()`, `client.send_roots_list_changed()`, колбек `list_roots_callback=`, який передають у `Client(...)` | [SEP-2577](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2577) виводить цю можливість з ужитку. | Приймайте шляхи як звичайні аргументи інструмента чи URI ресурсів або вбудуйте `ListRootsRequest` в `InputRequiredResult` (див. **[Багатораундові запити (multi-round-trip)](handlers/multi-round-trip.md)**). |
|
||||
| **Семплювання (sampling) з ініціативи сервера**: `ctx.session.create_message()`, колбек `sampling_callback=`, який передають у `Client(...)` | SEP-2577 виводить цю можливість з ужитку. | Повертайте `InputRequiredResult`, і нехай клієнт повторить виклик (див. **[Багатораундові запити](handlers/multi-round-trip.md)**). |
|
||||
| **Протокольне логування**: `ctx.log()`, `ctx.debug()`, `ctx.info()`, `ctx.warning()`, `ctx.error()`, `ctx.session.send_log_message()`, `client.set_logging_level()` | SEP-2577 виводить цю можливість з ужитку. У протоколі її ніщо не замінює. | Звичайний `import logging` у stderr (див. **[Логування](handlers/logging.md)**). |
|
||||
| **`ping`**: `client.send_ping()` | **Вилучено** з протоколу, а не просто оголошено застарілим. У 2026-07-28 методу `ping` немає. | Нічого. Він працює лише на з'єднанні з `mode="legacy"`. |
|
||||
| **Перебіг виконання від клієнта до сервера**: `client.send_progress_notification()` | У 2026-07-28 перебіг виконання передається лише від сервера до клієнта. | Надсилати нічого. Про перебіг виконання звітує ваш *сервер* через `ctx.report_progress()` (див. **[Перебіг виконання](handlers/progress.md)**). |
|
||||
|
||||
З цієї таблиці випливають три речі:
|
||||
|
||||
* Кореневі каталоги, семплювання й логування йдуть разом. Одна пропозиція, **SEP-2577**, оголошує застарілими всі три можливості одразу.
|
||||
* Семплювання й кореневі каталоги мають спільну глибшу проблему: це місця, де **сервер** надсилає **запит** **клієнту**. Саме цей напрямок цілком 2026-07-28 замінює на **[багатораундові запити](handlers/multi-round-trip.md)**. Зникли окремі RPC-методи (`sampling/createMessage`, `roots/list` і push-варіант `elicitation/create`); типи корисного навантаження `CreateMessageRequest` / `ListRootsRequest` / `ElicitRequest` залишаються — вбудовані в `InputRequiredResult.input_requests`, а на клієнті вони потрапляють у ті самі колбеки.
|
||||
* `ping` стоїть осторонь. Протокол не оголошує його застарілим, а вилучає. Метод SDK і далі попереджає (у його повідомленні сказано *removed*, а не *deprecated*), а виклик на сучасному з'єднанні отримує у відповідь *«Method not found»*.
|
||||
|
||||
## Застарілість має рекомендаційний характер {#deprecated-is-advisory}
|
||||
|
||||
Сьогодні нічого не ламається.
|
||||
|
||||
Кожен із наведених методів і далі працює з будь-якою сесією, що узгодила **2025-11-25 або ранішу версію**. Зафіксуйте `mode="legacy"` на клієнті — і отримаєте точнісінько ту поведінку, що була до 2026. У переданих даних нічого не змінюється, узгодження можливостей теж без змін.
|
||||
|
||||
Змінюється те, що під час першого виконання кожного з них з'являється помітне попередження:
|
||||
|
||||
```text
|
||||
MCPDeprecationWarning: The logging capability is deprecated as of 2026-07-28 (SEP-2577).
|
||||
```
|
||||
|
||||
`MCPDeprecationWarning` успадковує `UserWarning`, а **не** `DeprecationWarning`. Це навмисно: типовий фільтр Python показує `DeprecationWarning` лише в коді, запущеному безпосередньо як `__main__`, — саме так бібліотеки оголошують щось застарілим, і два роки ніхто цього не помічає. Це попередження видно всюди, без жодного прапорця `-W`.
|
||||
|
||||
!!! warning
|
||||
«Рекомендаційний характер» закінчується на рівні переданих даних. Семплювання й кореневі
|
||||
каталоги — це *запити* від сервера до клієнта, а сесія 2026-07-28 не має каналу, яким
|
||||
їх можна передати. Викличте `ctx.session.create_message()` усередині інструмента на
|
||||
сучасному з'єднанні — попередження все одно спрацює, а потім надсилання завершиться
|
||||
помилкою:
|
||||
|
||||
```text
|
||||
Cannot send 'sampling/createMessage': this transport context has no back-channel
|
||||
for server-initiated requests.
|
||||
```
|
||||
|
||||
Два сигнали, саме в такому порядку. `MCPDeprecationWarning` спрацьовує тієї ж миті, коли
|
||||
ви викликаєте метод, на будь-якому з'єднанні. Помилка — це те, що повертається, коли SDK
|
||||
потім намагається надіслати запит. Ці два методи працюють від початку до кінця лише на
|
||||
з'єднанні з `mode="legacy"`, клієнт якого зареєстрував відповідний колбек.
|
||||
|
||||
## Приглушення попередження {#silencing-the-warning}
|
||||
|
||||
У новому коді — не робіть цього.
|
||||
|
||||
Але сервер, який ви підтримуєте і який справді обслуговує клієнтів до 2026, має повне право на тихий лог. Відфільтруйте категорію до того, як виконається перший застарілий виклик:
|
||||
|
||||
```python
|
||||
import warnings
|
||||
|
||||
from mcp import MCPDeprecationWarning
|
||||
|
||||
warnings.filterwarnings("ignore", category=MCPDeprecationWarning)
|
||||
```
|
||||
|
||||
Оце й увесь API. Перемикача для окремих методів немає, і він вам не потрібен: сенс однієї категорії в тому, що один рядок її приглушує, а один рядок повертає.
|
||||
|
||||
!!! check
|
||||
Розверніть фільтр у зворотний бік — і отримаєте безкоштовний регресійний тест. Додайте
|
||||
`"error::mcp.MCPDeprecationWarning"` до налаштування `filterwarnings` у конфігурації
|
||||
pytest — і застарілий виклик **викидатиме виняток** замість попередження. Інструмент
|
||||
з назвою `old_log`, який досі викликає `ctx.info()`, перестає проходити тест і починає
|
||||
повідомляти:
|
||||
|
||||
```text
|
||||
Error executing tool old_log: The logging capability is deprecated as of 2026-07-28 (SEP-2577).
|
||||
```
|
||||
|
||||
Один рядок конфігурації pytest — і застарілий виклик більше ніколи не прокрадеться назад
|
||||
у вашу кодову базу, не проваливши тест.
|
||||
|
||||
## Підсумки {#recap}
|
||||
|
||||
* Специфікація 2026-07-28 оголошує застарілими **кореневі каталоги**, **семплювання** з ініціативи сервера та протокольне **логування** (усе — [SEP-2577](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2577)), обмежує **перебіг виконання** напрямком від сервера до клієнта й вилучає **`ping`**.
|
||||
* Стовпець із замінами вказує, куди рухатися далі: **[Багатораундові запити](handlers/multi-round-trip.md)** для семплювання й кореневих каталогів, **[Логування](handlers/logging.md)** для логування, **[Перебіг виконання](handlers/progress.md)** для перебігу виконання. `ping` не потребує взагалі нічого.
|
||||
* Застарілість має рекомендаційний характер: жодних змін у переданих даних, усе й далі працює із сесіями до 2026, а ви отримуєте помітне попередження `MCPDeprecationWarning` (це `UserWarning`, тож воно ввімкнене за замовчуванням).
|
||||
* Семплювання й кореневі каталоги додатково потребують зворотного каналу (back-channel), якого сесія 2026-07-28 не має. На сучасному з'єднанні вони попереджають, а потім викидають виняток.
|
||||
* `warnings.filterwarnings("ignore", category=MCPDeprecationWarning)` приглушує всю категорію; `"error::mcp.MCPDeprecationWarning"` у pytest перетворює її на провал тесту.
|
||||
* Новий код не варто будувати на жодній із цих можливостей.
|
||||
|
||||
Усі інші сторінки цієї документації навчають чинного API.
|
||||
144
i18n/uk/pages/get-started/first-steps.md
Normal file
144
i18n/uk/pages/get-started/first-steps.md
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
---
|
||||
translation:
|
||||
sections: [0d6c05bcbf836bf3, 59a7b14eeefc68c1, 7114d8d6daba203f, e8bbb56a98ba7bc9, 5138010f6159901c, f78da7c7c363d4c6, 220a939cab348686]
|
||||
tool: 1
|
||||
---
|
||||
# Перші кроки {#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` **шаблоном ресурсу**: `{name}` в URI — це параметр функції.
|
||||
* `@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 є по одній вкладці на кожен примітив; пройдіть їх по черзі.
|
||||
|
||||
**Tools.** Один запис: `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.** Один запис: `summarize` з єдиним обов'язковим аргументом `text`. Отримайте його з якимось текстом — і повернеться одне повідомлення з `role: user` та вашим відрендереним рядком як вмістом. Оце й увесь промпт: функція, яка будує повідомлення.
|
||||
|
||||
Inspector запустив ваш сервер через **stdio**, один із транспортів, якими може говорити MCP-сервер. Поки що обирати транспорт не потрібно; для цього є сторінка **[Запуск сервера](../run/index.md)**.
|
||||
|
||||
## Можливості {#capabilities}
|
||||
|
||||
В Inspector ви бачили три вкладки. Звідки він знав, що їх три?
|
||||
|
||||
Коли клієнт під'єднується, сервер оголошує свої **можливості**: на які сімейства запитів він відповідатиме. Клієнт використовує це оголошення, щоб вирішити, про що взагалі просити. Ви його не писали; `MCPServer` оголошує його за вас.
|
||||
|
||||
Подивіться самі. `Client` з SDK приймає об'єкт сервера напряму й під'єднується до нього **в пам'яті** (без підпроцесу, без порту):
|
||||
|
||||
```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()`. Назва, опис і схема беруться з функції.
|
||||
* URI з `{param}` створює **шаблон** ресурсу, який показується окремо від конкретних ресурсів.
|
||||
* **Можливості** сервера оголошуються за вас, а клієнт просить лише те, що сервер оголосив.
|
||||
* `Client(mcp)` під'єднується до об'єкта сервера в пам'яті: ваш тестовий стенд із першого дня.
|
||||
|
||||
Далі — **[Під'єднання до справжнього хоста](real-host.md)**: цей сервер усередині Claude Desktop або IDE, по-справжньому. Потім **[Тестування](testing.md)**: одна сторінка, один клієнт у пам'яті — і більше ніколи не доведеться гадати, чи воно працює. Після цього кожен примітив отримує власну сторінку, починаючи з того, яким керує модель: **[Інструменти](../servers/tools.md)**.
|
||||
53
i18n/uk/pages/get-started/index.md
Normal file
53
i18n/uk/pages/get-started/index.md
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
---
|
||||
translation:
|
||||
sections: [ed4a756b4c53c585, 97e2fb315b7fe398, 4d04f1c6f4bf6c1d, 577d73078fc62baf]
|
||||
tool: 1
|
||||
---
|
||||
# Початок роботи {#get-started}
|
||||
|
||||
Уперше маєте справу з MCP або з цим SDK? Почніть звідси. Ці сторінки проведуть від нуля до робочого, протестованого сервера: [встановіть SDK](installation.md), напишіть свій [перший сервер](first-steps.md), [під'єднайте його до справжнього хоста](real-host.md) і [протестуйте](testing.md) за допомогою клієнта в пам'яті.
|
||||
|
||||
## Запуск коду {#run-the-code}
|
||||
|
||||
Усі блоки коду можна копіювати й використовувати як є: це повні робочі файли.
|
||||
|
||||
Щоб іти слідом за викладом, вставте блок у файл `server.py` і відкрийте його в MCP Inspector:
|
||||
|
||||
```console
|
||||
uv run mcp dev server.py
|
||||
```
|
||||
|
||||
**НАПОЛЕГЛИВО радимо** набрати (або скопіювати) код, відредагувати його й запустити локально. Саме робота у власному редакторі показує суть: як мало доводиться писати, автодоповнення, перевірка типів, що ловить помилки ще до запуску.
|
||||
|
||||
## Гадати не доведеться {#you-will-not-be-guessing}
|
||||
|
||||
Кожен приклад у цій документації — повний файл у каталозі [`docs_src/`](https://github.com/modelcontextprotocol/python-sdk/tree/main/docs_src) репозиторію самого SDK, і кожен із них проганяється тестовим набором SDK через **клієнт у пам'яті**:
|
||||
|
||||
```python
|
||||
import pytest
|
||||
from mcp import Client
|
||||
|
||||
from server import mcp
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_add() -> None:
|
||||
async with Client(mcp) as client:
|
||||
result = await client.call_tool("add", {"a": 1, "b": 2})
|
||||
assert result.structured_content == {"result": 3}
|
||||
```
|
||||
|
||||
Ні підпроцесу, ні порту, ні транспорту. `Client(mcp)` під'єднується безпосередньо до об'єкта сервера.
|
||||
|
||||
Якщо зміна в SDK зламає приклад на одній із цих сторінок, CI почервоніє раніше, ніж сторінка. Код, який ви тут читаєте, — це код, який виконується.
|
||||
|
||||
Ви самі скористаєтеся цим на сторінці [Тестування](testing.md): власні сервери тестують так само.
|
||||
|
||||
## Куди далі {#where-to-go-next}
|
||||
|
||||
Щойно сервер запрацює, решта цієї документації — довідник, а не курс. Кожна сторінка самодостатня, тож переходьте одразу до потрібного:
|
||||
|
||||
* Те, що сервер надає назовні (інструменти, ресурси, промпти), — це **[Сервери](../servers/index.md)**.
|
||||
* Те, що доступне всередині функцій, які ви реєструєте, — це **[Усередині обробника](../handlers/index.md)**.
|
||||
* Як донести сервер до клієнтів (stdio, HTTP, ваш наявний FastAPI-застосунок) — це **[Запуск сервера](../run/index.md)**.
|
||||
* Побудова іншої сторони — застосунку, що *використовує* MCP-сервери, — це **[Клієнти](../client/index.md)**.
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue