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
248
i18n/ru/glossary.json
Normal file
248
i18n/ru/glossary.json
Normal file
|
|
@ -0,0 +1,248 @@
|
|||
{
|
||||
"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 Russian 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). Spelling сэмплирование, not семплирование, provisional pending native review."
|
||||
},
|
||||
{
|
||||
"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 Russian term; pinned to элицитация (feminine, declinable; the word exists in Russian linguistics for eliciting data from a person), 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 способность. 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 хендлер / хэндлер."
|
||||
},
|
||||
{
|
||||
"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; grammatically inanimate when it is a program (запустить клиент). 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 хозяин."
|
||||
},
|
||||
{
|
||||
"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\" heading → Объект Context). Standard rendering."
|
||||
},
|
||||
{
|
||||
"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): отправить уведомление, уведомление о ходе выполнения. Standard term, never нотификация. Method strings such as `notifications/tools/list_changed` stay Latin."
|
||||
},
|
||||
{
|
||||
"source": "callback",
|
||||
"target": "колбэк",
|
||||
"note": "Client callbacks and OAuth redirect callbacks alike (the \"Callbacks\" page → Колбэки): колбэк, masculine, declinable, spelled this way (not коллбек, колбек, каллбэк). 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 возбуждает. 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 срок жизни or продолжительность жизни. 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 Russian 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 → аутентификация. The `Authorization` header and code identifiers stay Latin. Standard rendering."
|
||||
},
|
||||
{
|
||||
"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."
|
||||
}
|
||||
]
|
||||
}
|
||||
173
i18n/ru/instructions.md
Normal file
173
i18n/ru/instructions.md
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
# Russian (ru) — translation instructions
|
||||
|
||||
Target language: Russian (русский язык), directory and URL code `ru`, page
|
||||
language tag `ru`. 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 the neutral, literate register of good Russian developer documentation:
|
||||
closer to a well-edited technical book than to an official notice or a chat.
|
||||
|
||||
- The reader is «вы», always lowercase mid-sentence: вы, вас, вам, ваш.
|
||||
Capitalised Вы / Ваш is for a personal letter to one person and is wrong
|
||||
here. Never ты, never a mix.
|
||||
- Reach for the pronoun rarely. Russian technical prose prefers 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
|
||||
мы с вами or давайте. One page, one register: a page that drifts between
|
||||
imperatives and officialese, or between вы and Вы, is wrong even when each
|
||||
sentence is acceptable on its own.
|
||||
|
||||
## 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 living
|
||||
Russian — neither wooden nor familiar.
|
||||
|
||||
- Use concrete verbs and let them carry the sentence: запустить, передать,
|
||||
вернуть, объявить, заблокировать. 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 Russian word order; never merge,
|
||||
drop or reorder the technical claims themselves.
|
||||
- Avoid канцелярит, the bureaucratic register technical translation slides
|
||||
into by default: данный → этот; является → есть, a dash, or nothing (Хост —
|
||||
это приложение); осуществлять / производить / выполнять + noun → the verb
|
||||
itself (осуществляет отправку → отправляет); в целях → чтобы; посредством →
|
||||
с помощью, через; в случае если → если; функционал → возможности; and no
|
||||
chains of verbal nouns (для обеспечения возможности выполнения запуска →
|
||||
чтобы запустить).
|
||||
- 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 (канцелярит): Пользователю не требуется осуществлять его создание и
|
||||
конфигурирование. Необходимо лишь выполнить соответствующий запрос.
|
||||
- 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 Russian sentence in the same register; if a light phrase
|
||||
carries no information at all, keep the sentence brief rather than inventing
|
||||
a Russian joke. Never drop the technical content around it.
|
||||
- Recurring English tags get fixed renderings: "**[X](…)** has the whole
|
||||
story" / "The whole story is in **[X](…)**" → Подробнее — на странице
|
||||
**[X](…)**.; "That's 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 по проводу. Culture-bound references (sports, TV,
|
||||
holidays) → the plain meaning.
|
||||
- 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 Russian 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».
|
||||
- Use ё wherever it belongs, consistently: ещё, её, всё, объём, передаёт,
|
||||
вернётся, трёх. A page that writes все for всё is wrong.
|
||||
- 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 without
|
||||
spaces (3.10–3.14) or от 3.10 до 3.14. Never a hyphen where a dash is meant.
|
||||
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 Russian 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 Russian too.
|
||||
|
||||
## 5. Terminology pointer
|
||||
|
||||
The glossary (`glossary.json`) is injected separately and overrides this file
|
||||
on every term it covers; each entry says whether its choice is standard or
|
||||
provisional and whether it takes a first-use gloss. These conventions are what
|
||||
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-а, в `Client`'е). Let a Russian word carry the case instead: a
|
||||
hyphenated head noun (MCP-сервер, MCP-клиент, HTTP-запрос, JSON-объект,
|
||||
ASGI-приложение, OAuth-токен) or the kind of thing in front of code (класс
|
||||
`Context`, параметр `lifespan=`, метод `client.call_tool()`, команда
|
||||
`uv run`, заголовок `Mcp-Method`). Adjectives and verbs agree with that
|
||||
Russian word.
|
||||
- Programs and components are grammatically inanimate: запустить клиент,
|
||||
подключить хост (not клиента in the accusative). Provisional; apply uniformly.
|
||||
- Where an established Russian term exists, use it, not the anglicism:
|
||||
обработчик (not хендлер), запрос / ответ (not реквест / респонс),
|
||||
уведомление (not нотификация), исключение (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),
|
||||
корневые каталоги (roots) — and appears alone after that. A glossary word
|
||||
used as a wire identifier or a key in code font is code and stays Latin:
|
||||
"the `sampling` capability" → возможность `sampling`.
|
||||
- One rendering per term per page: the glossary target, every time, even
|
||||
where its note marks the choice as provisional.
|
||||
|
||||
## 6. Provisional note
|
||||
|
||||
Every decision in this file, and every entry in `glossary.json`, is
|
||||
provisional pending review by native Russian-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 under `pages/` or
|
||||
`notices.md` next to this file. The tool cannot tell a hand edit from its own
|
||||
output, so one would persist unchecked and be carried forward into later
|
||||
runs; a correction made here reaches the pages when they are regenerated with
|
||||
`translate --lang ru --pages …`.
|
||||
20
i18n/ru/notices.md
Normal file
20
i18n/ru/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/ru/pages/advanced/apps.md
Normal file
123
i18n/ru/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** (песочнице) и передаёт результат инструмента в этот 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/ru/pages/advanced/extensions.md
Normal file
172
i18n/ru/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). Одно дополнение: когда ключ из params должен передаваться в заголовке `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/ru/pages/advanced/index.md
Normal file
34
i18n/ru/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](apps.md)**: точки расширения
|
||||
протокола. Подключайте пакеты расширений к серверу или пишите свои.
|
||||
|
||||
Кое-что из того, что логично было бы искать здесь, находится там, где это
|
||||
действительно используется:
|
||||
|
||||
* **Авторизация** — в разделе **[Запуск сервера](../run/index.md)**, потому что
|
||||
сервер защищают там, где его развёртывают.
|
||||
* **OAuth**, **подтверждение идентичности**, подключение к **нескольким серверам** и
|
||||
**кеш** ответов — всё это в разделе **[Клиенты](../client/index.md)**.
|
||||
* **Многораундовые запросы** (multi-round-trip) и **Подписки** — в разделе
|
||||
**[Внутри обработчика](../handlers/index.md)**, потому что и то и другое — это то,
|
||||
что обработчик *делает*.
|
||||
* **Шаблоны URI** — в разделе **[Серверы](../servers/index.md)**, рядом с ресурсами.
|
||||
* У **[Версий протокола](../protocol-versions.md)** и
|
||||
**[Устаревших возможностей](../deprecated.md)** есть собственные страницы верхнего уровня.
|
||||
|
||||
Если вы не уверены, нужен ли вам этот раздел, — он вам не нужен.
|
||||
207
i18n/ru/pages/advanced/low-level-server.md
Normal file
207
i18n/ru/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 не станет выдавать вашу трассировку удалённому вызывающему. Модель так и не узнает, что сделала не так, и не сможет повторить попытку. (В тесте `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)**. Верные духу этого уровня, они ничего не устанавливают за вас: там, где `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)**.
|
||||
84
i18n/ru/pages/advanced/middleware.md
Normal file
84
i18n/ru/pages/advanced/middleware.md
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
---
|
||||
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 сюда входит отметка `serverInfo` в `_meta`, которую 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`.
|
||||
* Middleware оборачивает **каждое** входящее сообщение (`server/discover`, `initialize`, запросы, уведомления, неизвестные методы) и выполняется начиная с внешнего слоя.
|
||||
* `ctx.request_id is None` — так уведомление отличают от запроса.
|
||||
* Чтобы отклонить одно сообщение, выбросьте исключение вместо вызова `call_next`; подключение это переживёт.
|
||||
* Собственная трассировка OpenTelemetry в SDK — тоже middleware, уже в списке. См. **[OpenTelemetry](../run/opentelemetry.md)**.
|
||||
* Весь этот интерфейс предварительный. Наблюдайте с его помощью; не стройте на нём.
|
||||
|
||||
Это всё, что оборачивает запрос. А решает, будет ли запрос вообще выполнен, **[Авторизация](../run/authorization.md)**.
|
||||
85
i18n/ru/pages/advanced/pagination.md
Normal file
85
i18n/ru/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`: **начать с начала**.
|
||||
* Что *такое* курсор, решаете вы. Здесь это смещение, записанное строкой. Временная метка, первичный ключ, blob в 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/ru/pages/client/caching.md
Normal file
122
i18n/ru/pages/client/caching.md
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
---
|
||||
translation:
|
||||
sections: [9e7b9a1710e5aeba, b74ca4c1d2ddddee, fa8714e61bf90c5a, 04db67a886b7271c, 857690fb8f876800]
|
||||
tool: 1
|
||||
---
|
||||
# Подсказки по кэшированию {#caching-hints}
|
||||
|
||||
В протоколе 2026-07-28 каждый результат, который сервер возвращает для `tools/list`, `prompts/list`, `resources/list`, `resources/templates/list`, `resources/read` и `server/discover`, несёт два поля: `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"` — тоже допустимый ключ, поскольку результат обнаружения кэшируется так же, как любой список.
|
||||
|
||||
!!! 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/ru/pages/client/callbacks.md
Normal file
154
i18n/ru/pages/client/callbacks.md
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
---
|
||||
translation:
|
||||
sections: [adf3c545b5be46b6, 916cd3ab1c03f461, e9be7a8d0eb0a456, 565890a636288ecf, 6af7e49db9129ec3, 06b0238c174186af, 90c6043be435fcb0]
|
||||
tool: 1
|
||||
---
|
||||
# Колбэки клиента {#client-callbacks}
|
||||
|
||||
Почти все запросы в MCP идут в одну сторону: от клиента к серверу.
|
||||
|
||||
Но и сервер может о чём-то попросить **клиент**: задать вопрос пользователю, попросить модель пользователя сгенерировать ответ, получить список папок его рабочего пространства. На такие запросы отвечают **колбэки**, которые передаются в `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): передайте `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)**.
|
||||
156
i18n/ru/pages/client/identity-assertion.md
Normal file
156
i18n/ru/pages/client/identity-assertion.md
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
---
|
||||
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)` — единственный код, который вы пишете. Он вызывается один раз на каждый обмен токенов, никогда — при создании провайдера, и только *после* того, как метаданные сервера авторизации получены и проверены, так что неверно настроенный издатель никогда не приведёт к утечке утверждения. Два его аргумента — это два из полей, с которыми должен быть выпущен 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. **Утверждение.** Он вызывает ваш `assertion_provider` и дожидается результата.
|
||||
3. **Обмен.** Он отправляет POST-запрос с грантом `jwt-bearer` на конечную точку токенов, сохраняет `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`, а в `authorization_grant_profiles_supported` — поле, через которое расширение объявляет о поддержке, — указывается `urn:ietf:params:oauth:grant-profile:id-jag`. (Клиент этого 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`, ни запроса метаданных защищённого ресурса. По сети проходят
|
||||
только запрос, получивший `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` прогоняет весь обмен и проверяет, что пользователь, которого назвал 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/ru/pages/client/index.md
Normal file
217
i18n/ru/pages/client/index.md
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
---
|
||||
translation:
|
||||
sections: [ebef1e7a0df854f4, a4c687d3d627d516, 8e79141fc2985342, b345dd05b9c3c7ab, 80ce41579825a6fa, 5f0fa90494de8f65, 83d10514eaa62fa5, 9190555aa39a5d28, 84a4c9d8bf14dddb, 927d71cf40b58c30]
|
||||
tool: 1
|
||||
---
|
||||
# Объект Client {#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"
|
||||
}
|
||||
```
|
||||
|
||||
Этой схемы достаточно и интерфейсу, чтобы отрисовать форму аргументов, и модели, чтобы сформировать корректные аргументы.
|
||||
|
||||
!!! tip
|
||||
`title` необязателен, поэтому интерфейсу, показывающему инструменты человеку, приходится выбирать: `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)**.
|
||||
154
i18n/ru/pages/client/oauth-clients.md
Normal file
154
i18n/ru/pages/client/oauth-clients.md
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
---
|
||||
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`: токен доступа, токен обновления, срок действия, область доступа (scope).
|
||||
* `get_client_info` / `set_client_info` хранят `OAuthClientInformationFull`, который сервер авторизации выдал, когда провайдер вас зарегистрировал, — включая ваш `client_id`.
|
||||
|
||||
Версия в памяти из примера выше работает. Но при завершении процесса она всё забывает, так что при следующем запуске вся процедура повторяется с начала. Сохраняйте данные в файл или в связку ключей вашей платформы — и следующий запуск пройдёт тихо.
|
||||
|
||||
!!! tip
|
||||
Сохраняйте `client_info`, а не только токены. Провайдер проходит динамическую регистрацию
|
||||
в первый раз, когда не находит сохранённого `client_info`. Выбросьте его — и при каждом
|
||||
запуске будет создаваться новая регистрация.
|
||||
|
||||
### Два обработчика {#the-two-handlers}
|
||||
|
||||
Сценарию с кодом авторизации человек нужен ровно один раз: кто-то должен войти в систему и нажать «разрешить».
|
||||
|
||||
* **`redirect_handler`** асинхронно вызывается с полностью собранным URL авторизации. `client_id`, `redirect_uri`, `state` и PKCE challenge в нём уже есть. Ваша единственная задача — открыть его в браузере. Настольное приложение вызывает `webbrowser.open`; в этом файле URL просто печатается.
|
||||
* **`callback_handler`** вызывается следующим. Он ждёт, пока пользователь вернётся на ваш `redirect_uri`, и возвращает параметры запроса из этого перенаправления в виде `AuthorizationCodeResult`.
|
||||
|
||||
Настоящий клиент вместо вызова `input()` поднимает небольшой локальный HTTP-сервер на 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/ru/pages/client/session-groups.md
Normal file
87
i18n/ru/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/ru/pages/client/subscriptions.md
Normal file
91
i18n/ru/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/ru/pages/client/transports.md
Normal file
128
i18n/ru/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)`. `Client` передаёт всё, что не объект сервера и не URL, прямо в этот протокол.
|
||||
* Создание `Client` выбирает транспорт. `async with` его открывает.
|
||||
|
||||
Когда транспорт открыт, двум сторонам нужно договориться о версии протокола. Обычно думать об этом не приходится; когда всё-таки придётся, нужная страница — **[Версии протокола](../protocol-versions.md)**.
|
||||
99
i18n/ru/pages/deprecated.md
Normal file
99
i18n/ru/pages/deprecated.md
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
---
|
||||
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.
|
||||
145
i18n/ru/pages/get-started/first-steps.md
Normal file
145
i18n/ru/pages/get-started/first-steps.md
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
---
|
||||
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)**.
|
||||
56
i18n/ru/pages/get-started/index.md
Normal file
56
i18n/ru/pages/get-started/index.md
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
---
|
||||
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