docs: publish translated docs in twelve languages and the tool that maintains them (#3280)

This commit is contained in:
Max 2026-08-14 17:07:34 +01:00 • committed by GitHub
commit 52ad0a8876
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
50 changed files with 89737 additions and 54 deletions

View file

@ -0,0 +1,123 @@
---
translation:
sections: [0355618e5f4d5fe4, 1821eaf50f2d0b64, 82e0b28ebd3abf5a, 8ac39614c094f2d0, dab6ff945501ab2a, bd5565c3b2d4f959, 96819ce3d63a0487]
tool: 1
---
# MCP Apps {#mcp-apps}
**MCP App** — це інструмент із власним обличчям: поряд із даними інструмент указує на HTML-документ, який хост відображає як інтерактивну поверхню.
Дві частини, завжди дві частини:
1. **Інструмент**, який виконує роботу й повертає дані, як будь-який інший інструмент.
2. **Ресурс `ui://`** з HTML, який хост показує для нього.
Інструмент несе посилання на ресурс у `_meta.ui.resourceUri`. Хост отримує його через `resources/read`, відображає в **ізольованому iframe (sandbox)** і передає результат інструмента в цей iframe через `postMessage`. Ваш сервер ніколи не надсилає й не отримує жодних повідомлень `ui/*`: цей обмін відбувається між хостом та iframe. Ви віддаєте інструмент і HTML-документ, а всю виставу ставить хост.
SDK постачає це як вбудоване розширення `Apps` (`io.modelcontextprotocol/ui`). Якщо [розширення](extensions.md) для вас новинка, спершу прогляньте ту сторінку. Одна хвилина — і повертайтеся.
## Годинник із циферблатом {#a-clock-with-a-face}
```python title="server.py" hl_lines="19 22 30 32"
--8<-- "docs_src/apps/tutorial001.py"
```
Чотири кроки:
* `Apps()`: один екземпляр тримає ваші інструменти з прив'язаним UI та їхні ресурси.
* `@apps.tool(resource_uri="ui://clock/app.html")`: звичайний інструмент плюс позначка `_meta.ui.resourceUri`. Усе, що приймає `@mcp.tool()` (name, title, description, ...), передається далі.
* `apps.add_html_resource("ui://clock/app.html", CLOCK_HTML)`: відповідний ресурс, який віддається як `text/html;profile=mcp-app`. Саме цей MIME-тип каже хосту: «це застосунок, відобрази його».
* `MCPServer("clock", extensions=[apps])`: увімкнення. Тепер сервер оголошує `io.modelcontextprotocol/ui` у `capabilities.extensions`.
Сам HTML слухає `postMessage` від хоста й показує результат. Для справжніх застосунків використовуйте всередині HTML офіційний браузерний SDK [`@modelcontextprotocol/ext-apps`](https://github.com/modelcontextprotocol/ext-apps). Він дає `ontoolresult`, `callServerTool`, `getHostContext` і `onhostcontextchanged` замість сирих подій повідомлень.
## Плавна деградація {#graceful-degradation}
Не кожен клієнт відображає застосунки. Специфікація прямо каже, що це означає для вас:
> Інструменти **МУСЯТЬ** повертати змістовний масив `content`, навіть коли UI доступний.
Модель читає `content`; iframe — для людей. Хост із підтримкою UI все одно передає текстовий результат моделі, а суто текстовий клієнт отримує *лише* його. Тож канонічний шаблон — один інструмент, дві відповіді. Погляньте на `get_time` ще раз:
```python title="server.py" hl_lines="23-27"
--8<-- "docs_src/apps/tutorial001.py"
```
`client_supports_apps(ctx)` дорівнює `True` лише тоді, коли клієнт оголосив розширення `io.modelcontextprotocol/ui` **і** вказав `text/html;profile=mcp-app` у своїх налаштуваннях `mimeTypes`. Поле обов'язкове, тож клієнт, який його пропустив, не зараховується. Саме це й оголошує `main()` у тому самому файлі: клієнтську половину узгодження — і у відповідь приходить розширений варіант.
!!! warning
Ніколи не повертайте заглушку на кшталт `"[Rendered UI]"` як єдиний вміст. Якщо резервний текст марний, інструмент марний для кожного суто текстового клієнта й для самої моделі. Напишіть нормальне речення.
## Обмеження iframe {#locking-the-iframe-down}
Метадані безпеки несе ресурс: що iframe може завантажувати, які дозволи браузера йому потрібні, як його бажано вбудовувати:
```python title="server.py" hl_lines="9 19-22"
--8<-- "docs_src/apps/tutorial002.py"
```
`csp` і `permissions` — це **запити до хоста**, а не поведінка сервера. Хост будує з них Content-Security-Policy і Permissions-Policy для iframe й може відмовити. Перевіряйте наявність можливостей у своєму JS, а не припускайте, що дозвіл надано.
`ResourceCsp`, поле за полем (ім'я в Python, ключ у переданих даних, що з ним робить хост):
| Python | У переданих даних (`_meta.ui.csp`) | Керує |
|---|---|---|
| `connect_domains` | `connectDomains` | `connect-src`: куди можуть звертатися `fetch`/XHR |
| `resource_domains` | `resourceDomains` | `img-src`, `style-src`, ...: статичні ресурси |
| `frame_domains` | `frameDomains` | `frame-src`: вкладені iframe |
| `base_uri_domains` | `baseUriDomains` | `base-uri`: на що може вказувати `<base>` |
`ResourcePermissions`: кожне поле запитує для iframe дозвіл браузера.
| Python | У переданих даних (`_meta.ui.permissions`) |
|---|---|
| `camera` | `camera` |
| `microphone` | `microphone` |
| `geolocation` | `geolocation` |
| `clipboard_write` | `clipboardWrite` |
!!! note
CSP і дозволи живуть на **ресурсі**, ніколи на інструменті. У метаданих інструмента за специфікацією для них немає місця, і хости їх там ігнорують. SDK робить цю помилку неможливою: `@apps.tool()` просто не має параметра `csp`.
### Видимість {#visibility}
`visibility=["app"]` на інструменті означає «це існує для iframe, а не для моделі»:
* `"model"`: інструмент може викликати модель.
* `"app"`: інструмент може викликати iframe (через `callServerTool`).
* Не вказано: обидва, це значення за замовчуванням.
Фільтрація — справа **хоста**. Ваш сервер перелічує інструменти лише для застосунку в `tools/list`, як і будь-які інші; хост приховує їх від моделі. Не фільтруйте на боці сервера.
## Правила, які контролює SDK {#the-rules-the-sdk-enforces}
Усе це падає під час запуску, а не в продакшені:
* `resource_uri` або URI ресурсу, що не має вигляду `ui://...`, дає `ValueError` під час декорування чи реєстрації.
* Інструмент, прив'язаний до URI **без відповідного зареєстрованого ресурсу**, дає `ValueError`, коли `MCPServer(extensions=[apps])` приймає розширення. Інструмент, що оголошує HTML, який повертає 404 на `resources/read`, — це помилка конфігурації, тож сервер відмовляється створюватися.
* `meta={"ui": ...}` у `@apps.tool()` дає `ValueError`. Декоратор володіє `_meta["ui"]`; висловлюйте це через `resource_uri=` і `visibility=`. Інші ключі `meta=` спокійно зливаються поруч.
Ані TypeScript SDK ext-apps, ані FastMCP сьогодні нічого з цього не ловлять; краще дізнатися про це раніше, ніж дізнається хост.
## Не тільки вбудований HTML {#beyond-inline-html}
`add_html_resource` покриває типовий випадок: рядок з HTML. Для всього іншого — HTML на диску чи згенерованого вмісту — побудуйте ресурс самі й передайте його:
```python title="server.py" hl_lines="12 18"
--8<-- "docs_src/apps/tutorial003.py"
```
`add_resource` підставляє MIME-тип `text/html;profile=mcp-app`, коли ресурс не задає його явно, і відхиляє явну невідповідність: ресурс `ui://` з будь-яким іншим MIME-типом не відобразить жоден хост.
!!! tip
Орієнтуєтеся на хост до GA-версії, який досі читає застарілий плоский ключ `_meta["ui/resourceUri"]`? Додайте його самі:
`@apps.tool(resource_uri="ui://x", meta={"ui/resourceUri": "ui://x"})`.
Вкладений об'єкт `ui` — це форма за специфікацією; плоский ключ доживає своє.
## Приклад у дії {#see-it-run}
Історія `apps` в `examples/stories/` — це ця сторінка у вигляді пари, яку можна запустити: сервер з інструментом-годинником із прив'язаним UI та клієнт, який узгоджує Apps, читає `_meta.ui.resourceUri` інструмента, отримує HTML і викликає інструмент.
```bash
uv run python -m stories.apps.client
```

View file

@ -0,0 +1,172 @@
---
translation:
sections: [05891e7cc1938a13, b3c01a6af28c51ee, 7ffc91f5e38bdfe0, 717d3f235a8333a7, f471a13b2fe5d737, ed6af2df4b656dff]
tool: 1
---
# Розширення {#extensions}
**Розширення** — це набір поведінки MCP, який вмикається лише на явний запит і стоїть за одним ідентифікатором.
На сервері воно може додавати інструменти, ресурси й нові методи запитів, а також обгортати `tools/call`. На клієнті — заявляти додаткові форми результату `tools/call` і спостерігати за вендорськими сповіщеннями. Кожна сторона оголошує розширення у власному `capabilities.extensions`, і для тих, хто про це не просив, нічого не змінюється. Такий контракт ([SEP-2133](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2133)), і в нього одне золоте правило: **розширення за замовчуванням вимкнені**.
## Використання розширення {#using-an-extension}
Передайте екземпляри під час створення:
```python title="server.py"
--8<-- "docs_src/extensions/tutorial001.py"
```
Готово. Тепер сервер оголошує `io.modelcontextprotocol/ui` у `capabilities.extensions` і обслуговує все, що додає розширення.
`Apps` — вбудоване еталонне розширення, і йому присвячено окрему сторінку: **[MCP Apps](apps.md)**.
!!! note
Розширення фіксуються під час створення. Методу `add_extension`, який можна було б викликати пізніше, немає: карта можливостей сервера не повинна змінюватися, поки до нього під'єднані клієнти.
Карта можливостей передається через `server/discover`, а це шлях версії **2026-07-28**. Рукостисканню `initialize` старого покоління нікуди її покласти, тож клієнт старого покоління просто не бачить розширення. Проєктуйте з урахуванням цього: розширення *доповнює* сервер і не повинно бути єдиним способом ним користуватися.
## Написання власного розширення {#writing-your-own}
Успадкуйте `Extension` і перевизначте лише те, що потрібно. Кожен метод має типову реалізацію.
### Ідентифікатор {#the-identifier}
```python
--8<-- "docs_src/extensions/tutorial002.py"
```
Ідентифікатор — це рядок вигляду `vendor-prefix/name`, що відповідає граматиці ключів `_meta` зі специфікації: розділені крапками мітки (кожна починається з літери й закінчується літерою або цифрою), скісна риска, потім ім'я. Він перевіряється **під час визначення класу**, тож друкарська помилка не чекає, поки сервер запуститься:
```text
TypeError: Stamps.identifier must be a `vendor-prefix/name` string
(reverse-DNS prefix required), got 'stamps'
```
Як префікс використовуйте домен, яким ви керуєте. `io.modelcontextprotocol/*` призначено для розширень, які специфікує сам проєкт MCP.
### Додавання інструментів {#contributing-tools}
Найменше корисне розширення — один інструмент і карта налаштувань:
```python title="server.py" hl_lines="17 19-20 22-23 26"
--8<-- "docs_src/extensions/tutorial003.py"
```
* `tools()` повертає об'єкти `ToolBinding`. Сервер реєструє кожен із них точно так, ніби ви самі викликали `mcp.add_tool(...)`: те саме генерування схеми, те саме впровадження `Context`, усе те саме.
* `settings()` — це значення, що оголошується в `capabilities.extensions["com.example/stamps"]`. Поверніть `{}` (типове значення), щоб оголосити розширення без налаштувань.
* Розширення ніколи не отримує сервер. Воно оголошує свій внесок як дані; `MCPServer` їх споживає. Ніякого `self.server`, який можна було б змінювати, немає.
А `main()` — доказ: клієнт у пам'яті, під'єднаний безпосередньо до `mcp`:
```python title="server.py" hl_lines="29-34"
--8<-- "docs_src/extensions/tutorial003.py"
```
### Обслуговування власних методів {#serving-your-own-methods}
Розширення може реєструвати **нові методи запитів**: власні дієслова, які обслуговуються поруч із методами специфікації:
```python title="server.py" hl_lines="16-22 31 40-48"
--8<-- "docs_src/extensions/tutorial004.py"
```
* `SearchParams` успадковує `RequestParams`, тож конверт `_meta` версії 2026 розбирається однаково, а обробник отримує перевірені параметри, а не сирий словник. Обмежуйте те, чим керує клієнт: `Field(ge=1, le=100)` відхиляє безглуздий `limit`, перш ніж ваш код щось під нього виділить.
* `require_client_extension(ctx, EXTENSION_ID)` — це шлагбаум: клієнт, який не оголосив розширення, отримує помилку `-32021` (відсутня обов'язкова можливість клієнта) з машиночитаним корисним навантаженням `requiredCapabilities`, якого вимагає специфікація.
* `protocol_versions=frozenset({"2026-07-28"})` прив'язує метод до однієї версії протоколу. На будь-якій іншій версії клієнт отримує `METHOD_NOT_FOUND` — точно так, ніби методу там не існує. Для цього клієнта він і не існує.
Методи **лише додаються**. SDK забезпечує це під час створення, а не під час виконання:
* `MethodBinding` для методу, визначеного специфікацією (`tools/list`, `completion/complete`, ...), викидає `ValueError` під час створення прив'язки. Базові дієслова належать серверу.
* Два розширення, що прив'язують той самий метод, викидають виняток, коли реєструється друге. «Перемагає останній запис» — саме так плагіни псують одне одного; ми цього не робимо.
* Порожня множина `protocol_versions` теж викидає виняток: метод, який ніколи не може бути обслужений, — це помилка, а не конфігурація.
### Клієнтська сторона {#the-client-side}
`main()` у тому самому файлі — це вся клієнтська частина, обидві її половини:
```python title="server.py" hl_lines="54-58"
--8<-- "docs_src/extensions/tutorial004.py"
```
* `Client(..., extensions=[advertise(EXTENSION_ID)])` оголошує розширення. Оголошення стають `ClientCapabilities.extensions`: на з'єднанні версії 2026-07-28 карта подорожує в конверті `_meta` кожного запиту, тож сервер бачить її в **кожному** запиті; на з'єднанні старого покоління вона передається з рукостисканням `initialize`. Серверному коду байдуже, який саме шлях: `require_client_extension(ctx, ...)` і `ctx.session.check_client_capability(...)` читають правильне джерело в обох випадках.
* Вендорські методи опускаються на один шар нижче, до `client.session.send_request(...)`; повноцінні методи в `Client` з'являються лише для дієслів специфікації. `send_request` приймає будь-який підклас `Request`, тож вендорський запит проходить як є.
### Перехоплення `tools/call` {#intercepting-toolscall}
Єдиний хук-перехоплювач. Перевизначте `intercept_tool_call`, щоб спостерігати за викликом інструмента, завершувати його достроково або забороняти:
```python title="server.py" hl_lines="17-24"
--8<-- "docs_src/extensions/tutorial005.py"
```
* `params` — це перевірений `CallToolRequestParams`: `params.name` і `params.arguments` доступні без роботи із сирим JSON. Саме він визначає, який виклик інструмента виконується: якщо передати через `call_next` переписаний контекст, зміниться те, що обробник бачить у `ctx`, а не сам виклик інструмента. Переписування запитів на рівні переданих даних — справа [Middleware](middleware.md).
* `call_next(ctx)` виконує решту ланцюжка й повертає результат обробника. Поверніть його без змін (спостереження), поверніть щось інше (заміна) або викиньте `MCPError` (відмова). Усе, що ви повернете, серіалізується як будь-який результат обробника, разом зі штампом ідентичності `serverInfo` покоління 2026, тож перехоплювач, який завершує виклик достроково, ніколи не видасть анонімної відповіді чи відповіді не за схемою.
* Коли розширень кілька, перехоплювачі вкладаються в порядку реєстрації: перше розширення в `extensions=[...]` — зовнішнє.
* Типова реалізація просто пропускає виклик далі, а сервер, чиї розширення не перевизначають цей хук, зберігає голий обробник `tools/call` недоторканим. За те, чим не користуєтеся, не платите.
Хук обгортає `tools/call` і нічого більше. Для того, що стосується кожного повідомлення, використовуйте [Middleware](middleware.md). Саме для цього воно й існує.
## Використання клієнтського розширення {#using-a-client-extension}
**Клієнтське розширення** — той самий контракт з боку споживача: набір клієнтської поведінки за одним ідентифікатором. Передайте екземпляри в `Client(extensions=[...])` і викликайте інструменти як зазвичай:
```python title="client.py" hl_lines="66-68"
--8<-- "docs_src/extensions/tutorial006.py"
```
`call_tool("buy", ...)` повертає звичайний `CallToolResult`, як і будь-який інший виклик. Що змінило розширення: тепер сервер може відповісти на `buy` **формою результату** `receipt` замість остаточного результату, а `Receipts` доводить її до кінця (тут — погашаючи квитанцію наступним викликом), перш ніж `call_tool` поверне значення. У місці виклику нічого не змінюється.
Приберіть розширення — і нічого з цього не існує: шлагбаум сервера відмовляє клієнту, який його не оголосив (помилка -32021), а заявлена форма від сервера, що обходить шлагбаум, не проходить перевірку — точно так, як специфікація вимагає для нерозпізнаного `resultType`. Вимкнено за замовчуванням, з обох кінців з'єднання.
Щоб оголосити ідентифікатор **без** жодної клієнтської поведінки (сервер перевіряє можливість, клієнт нічого не робить, як у клієнті пошуку вище), використовуйте `advertise()`:
```python
from mcp.client import advertise
client = Client(mcp, extensions=[advertise("com.example/search")])
```
## Написання клієнтського розширення {#writing-a-client-extension}
Успадкуйте `ClientExtension` і перевизначте лише те, що потрібно. Три види внеску, кожен із типовою реалізацією: `settings()`, `claims()` і `notifications()`.
```python title="client.py" hl_lines="17-18 43-44 46-47"
--8<-- "docs_src/extensions/tutorial006.py"
```
* Ідентифікатор підпорядковується тій самій граматиці, що й на сервері, і перевіряється під час визначення класу.
* `claims()` повертає об'єкти `ResultClaim`: тег у переданих даних, модель, яка його розбирає, і резолвер, який доводить його до кінця. Модель мусить зафіксувати тег через `result_type: Literal["receipt"]` і не повинна успадковувати базові типи результатів дієслова; обидві умови перевіряються під час створення заявки. Вендорські поля на кшталт `receipt_token` передаються мережею як є: підставлена форма доходить до клієнта дослівно.
* Резолвер отримує розібрану модель і `ClaimContext`; `ctx.session` — той самий публічний дескриптор, що й `client.session`, тож подальші виклики — це звичайні виклики сесії. Він повертає звичайний для дієслова `CallToolResult`.
* `settings()` — значення, що оголошується в `ClientCapabilities.extensions[identifier]`; воно читається один раз під час створення `Client`.
`notifications()` оголошує вендорські сповіщення сервера, за якими слід спостерігати:
```python
def notifications(self) -> Sequence[NotificationBinding[Any]]:
return [NotificationBinding(method="notifications/receipts", params_type=ReceiptEvent, handler=self.on_receipt)]
```
Обробник отримує перевірені параметри по одному, у порядку диспетчеризації. Він спостерігає; накласти вето чи відповісти він не може.
Два негучні правила. Заявки діють лише на з'єднаннях версії 2026-07-28, і оголошення можливостей іде за ними: на з'єднанні старого покоління заявки розчиняються, а разом із ними з оголошення випадає й ідентифікатор, тож клієнт ніколи не оголошує розширення, чиї форми він би відхилив. А коли заявлена форма потрібна вам самим, а не резолверу, викликайте `client.session.call_tool(..., allow_claimed=True)`; без цього прапорця заявлена форма, що доходить до виклику на рівні сесії, викидає `UnexpectedClaimedResult`.
### Дієслова розширень {#extension-verbs}
Власні методи запитів розширення не потребують реєстрації на клієнті. Тип вендорського запиту успадковує `mcp.types.Request` і проходить через `client.session.send_request`, як у розділі [Обслуговування власних методів](#serving-your-own-methods). Одне доповнення: коли ключ параметрів мусить передаватися в заголовку `Mcp-Name` (специфікації розширень, як-от tasks, вимагають цього для своїх дієслів), тип запиту оголошує `name_param`:
```python title="client.py" hl_lines="22-25 46-47"
--8<-- "docs_src/extensions/tutorial007.py"
```
Сесія дзеркалить `params["jobId"]` у `Mcp-Name` на кожному шляху надсилання, а відсутнє значення дає гучну помилку замість того, щоб мовчки пропустити обов'язковий заголовок.
## Чого розширення не може {#what-an-extension-cannot-do}
Поверхня внеску **закрита** навмисно. На сервері: налаштування, інструменти, ресурси, методи, один перехоплювач `tools/call`. На клієнті: налаштування, заявки на результати, прив'язки сповіщень. Розширення не може:
* **Лізти в хост.** Воно оголошує дані; посилання на сервер чи клієнт у нього немає.
* **Замінювати базову поведінку.** Методи специфікації та базові теги результатів відхиляються під час створення (`initialize` цілком зарезервовано за виконавцем); прив'язка сповіщення, перекрита базовим словником, натомість замовкає з попередженням.
* **Реєструватися із запізненням.** Після того як `MCPServer(...)` чи `Client(...)` повернув керування, набір розширень уже такий, який є.
Якщо ви воюєте з цими стінами, ви пишете не розширення. Ви пишете форк. Стіни — це й є головна перевага: користувач, що читає `extensions=[Apps(), Stamps()]`, знає *все*, чого ці двоє могли торкнутися.

View file

@ -0,0 +1,34 @@
---
translation:
sections: [ca6988b7503cd2d3]
tool: 1
---
# Розширені можливості {#advanced}
Усе, що потрібно звичайному серверу чи клієнту, має своє тематичне місце в розділах вище.
Цей розділ — це запасні виходи, до яких звертаються, коли зручний шар `MCPServer`
починає заважати:
* **[Низькорівневий Server](low-level-server.md)**: клас, на якому побудовано `MCPServer`.
Схеми, написані вручну, обробники `on_*`, жодних перевірок за вас і власні
JSON-RPC-методи.
* **[Пагінація](pagination.md)** і **[Middleware](middleware.md)**: дві речі, які
можна зробити *лише* на низькорівневому `Server`.
* **[Розширення](extensions.md)** і **[Застосунки MCP](apps.md)**: поверхня розширень
протоколу. Скомпонуйте пакети розширень у сервер або напишіть власні.
Кілька речей, які цілком логічно шукати тут, натомість розміщено там, де ними
справді користуються:
* **Авторизація** — у розділі **[Запуск сервера](../run/index.md)**, бо сервер
захищають там, де його розгортають.
* **OAuth**, **підтвердження ідентичності**, під'єднання до **кількох серверів** і
**кеш** відповідей — усе це в розділі **[Клієнти](../client/index.md)**.
* **Багатораундові запити** і **Підписки** — у розділі
**[Усередині обробника](../handlers/index.md)**, бо і те, й інше обробник
саме *робить*.
* **Шаблони URI** — у розділі **[Сервери](../servers/index.md)**, поруч із ресурсами.
* **[Версії протоколу](../protocol-versions.md)** і
**[Застарілі можливості](../deprecated.md)** мають власні сторінки верхнього рівня.
Якщо ви не впевнені, чи потрібен вам цей розділ, — він вам не потрібен.

View file

@ -0,0 +1,207 @@
---
translation:
sections: [2c79b6338e09b7ac, 7edc43b3fae11314, 1086e77ce561cd7f, a3f71823df5efc31, 9fc7109f72201cae, 7bf25983df655b66, 6330e1f4c6029683, 2f1749c8c133fa1c, b3530fcf4d11fd56, ebc33704fbd74262, cd0e9c933350390e]
tool: 1
---
# Низькорівневий Server {#the-low-level-server}
`@mcp.tool()` — це шар. Під ним лежить другий серверний клас, `Server`, який говорить «сирим» MCP: ви передаєте йому об'єкти протоколу, а він надсилає їх мережею без змін.
`MCPServer` побудований поверх нього. Спускатися на рівень нижче варто тоді, коли зручний шар заважає:
* Потрібно віддати **точну** схему (завантажену з файлу, згенеровану з бази даних), а не виведену з сигнатури Python.
* Потрібен повний контроль над результатом: `_meta`, `is_error`, кожен ключ `structured_content`.
* Потрібно обробити метод, якого MCP не визначає.
Для всього іншого залишайтеся на `MCPServer`.
## Той самий інструмент, вручну {#the-same-tool-by-hand}
Це інструмент `search_books`, який сторінка **[Інструменти](../servers/tools.md)** пише дев'ятьма рядками `@mcp.tool()`, — тільки без синтаксичного цукру:
```python title="server.py" hl_lines="22 26 32"
--8<-- "docs_src/lowlevel/tutorial001.py"
```
Змінилися три речі, і вони й складають увесь низькорівневий API:
* **Обробники — це параметри конструктора.** `on_list_tools=` і `on_call_tool=` передаються в `Server(...)`. Декораторів тут немає, і кожен обробник має однакову форму: `async (ctx, params) -> result`.
* **Вхідну схему пишете ви.** `Tool.input_schema` — це звичайний `dict` із JSON Schema. Ніхто не виводить її з анотацій типів, бо анотацій типів, з яких можна було б її вивести, немає.
* **Результат будуєте ви.** `CallToolResult(content=[TextContent(...)])`, вручну. Нічого не загортається, не перетворюється й не виводиться з анотації значення, що повертається.
`params` — це розібраний запит: `CallToolRequestParams` дає `.name` і `.arguments`. `ctx` — це `ServerRequestContext`: `ctx.session`, щоб звертатися назад до клієнта, `ctx.lifespan_context`, `ctx.request_id` і `ctx.meta` — вхідні `_meta` запиту.
!!! info
Якщо ви працювали з FastAPI, це співвідношення вам уже знайоме. `MCPServer` — це шар декораторів і анотацій типів; `Server` — це Starlette під ним. Вони не суперники: `MCPServer` створює `Server` і реєструє на ньому саме такі обробники.
### Спробуйте самі {#try-it}
Inspector тут не допоможе: `mcp dev` і `mcp run` приймають лише `MCPServer`. `Client`, що працює в пам'яті, до цього байдужий: низькорівневий `Server` він приймає так само, як і `MCPServer`:
```python title="main.py"
import asyncio
from mcp import Client
from server import server
async def main() -> None:
async with Client(server) as client:
result = await client.call_tool("search_books", {"query": "dune", "limit": 5})
print(result.content)
asyncio.run(main())
```
```text
[TextContent(type='text', text="Found 3 books matching 'dune' (showing up to 5).", annotations=None, meta=None)]
```
Той самий текст, що його видавала версія з `@mcp.tool()`. Дві чесні відмінності:
* `result.structured_content` дорівнює `None`. Високорівневий сервер загортає `-> str` у `{"result": ...}` за вас; тут ніхто не будує того, чого не побудували ви.
* `list_tools` повертає схему, яку набрали **ви**, символ у символ. У високорівневій версії на кожній властивості було `"title": "Query"`, а в корені — `"title": "search_booksArguments"`: артефакти Pydantic. Тут, якщо щось є в переданих даних, — це ви його туди поклали.
## Ніхто нічого не перевіряє за вас {#nothing-is-checked-for-you}
`MCPServer` відхиляє хибний аргумент ще до того, як ваша функція запуститься, перевіряючи виклик за схемою, яку сам згенерував (**[Інструменти](../servers/tools.md)**).
`Server` цього не робить. Ваша `input_schema` *оголошується* клієнтові; вона ніколи не *застосовується* до `params.arguments`.
!!! check
Викличте `search_books` без `limit` — і ваш `args["limit"]` викине `KeyError`. Клієнт побачить:
```text
MCPError: Internal server error
```
Помилка JSON-RPC з кодом `-32603` і навмисно загальним повідомленням: SDK не видасть ваш traceback віддаленій стороні, що викликає. Модель так і не дізнається, що зробила не так, тож не зможе повторити спробу. (У тесті `raise_exceptions=True` натомість показує справжній виняток; див. **[Тестування](../get-started/testing.md)**.)
Це узагальнюється. Виняток, викинутий із низькорівневого обробника, — це **завжди** помилка протоколу й ніколи — результат інструмента з `is_error=True`. Якщо потрібно, щоб модель прочитала про невдачу й відновилася, перевіряйте `params.arguments` самі й повертайте `CallToolResult(content=[TextContent(...)], is_error=True)`. Обом видам невдач присвячена сторінка **[Обробка помилок](../servers/handling-errors.md)**.
## Два інструменти, один обробник {#two-tools-one-handler}
`on_call_tool` — єдина точка входу для всіх інструментів сервера. Маршрутизуєте за `params.name`:
```python title="server.py" hl_lines="38-43"
--8<-- "docs_src/lowlevel/tutorial002.py"
```
* `list_tools` оголошує обидва. `call_tool` диспетчеризує за іменем.
* Гілка `else` важлива: `Server` спокійно передасть `tools/call` з іменем, якого ви ніколи не оголошували, просто у ваш обробник. Виняток там перетворює виклик на ту саму `-32603`, що й вище.
## Структурований вивід, вручну {#structured-output-by-hand}
Оголосіть `output_schema` на `Tool` і покладіть `structured_content` у результат. І те, й інше — ваше:
```python title="server.py" hl_lines="19-23 36"
--8<-- "docs_src/lowlevel/tutorial003.py"
```
Викличте його — і результат міститиме обидва подання:
```json
{
"content": [{"type": "text", "text": "Found 3 books matching 'dune'."}],
"structuredContent": {"matches": 3, "query": "dune"},
"isError": false,
"resultType": "complete",
"_meta": {"io.modelcontextprotocol/serverInfo": {"name": "Bookshop", "version": "2.0.0"}}
}
```
Блок `_meta` — це ідентифікаційна позначка сервера: SDK додає його до кожного результату покоління 2026 разом із `version` з конструктора (сервер, який її не задав, повідомляє порожній рядок). Сервер, який не повинен себе ідентифікувати, може прибрати цей ключ за допомогою middleware, який володіє результатами, що повертає.
Сервер ніколи не порівнює ці два поля. `Client` цього SDK — порівнює: поверніть `structured_content`, що не відповідає оголошеній вами `output_schema`, і `call_tool` викине `RuntimeError`, який починається з `Invalid structured content returned by tool search_books` і далі цитує помилку `jsonschema`. Пообіцяти схему легко; дотримати її — ваша справа. Уся драбина типів повернення та схем — на сторінці **[Структурований вивід](../servers/structured-output.md)**.
## `_meta`: для застосунку, а не для моделі {#\_meta-for-the-application-not-the-model}
`content` — це частина відповіді, яку читає модель. `structured_content` — та сама відповідь у вигляді типізованих даних. `_meta` — третій канал: дані, що їдуть разом із результатом для **клієнтського застосунку** і взагалі не є частиною відповіді.
Використовуйте його для ідентифікаторів записів, ідентифікаторів трасування — усього, що потрібно вашому UI, але не потрібно промпту:
```python title="server.py" hl_lines="37"
--8<-- "docs_src/lowlevel/tutorial004.py"
```
* Конструюєте його як `_meta=` — це ім'я в переданих даних. Клієнт зчитує його як `result.meta`.
* Додавайте до ключів простір імен (`bookshop/record_ids`). Ключі `io.modelcontextprotocol/*` зарезервовано протоколом.
!!! warning
`_meta` — це домовленість між вами й клієнтським застосунком, а не гарантія того, що дійде
до моделі. Що показувати, вирішує хост. Ніколи не кладіть секрет у жодну частину результату інструмента.
## Можливості випливають з обробників {#capabilities-follow-your-handlers}
`Server` оголошує рівно ті сімейства методів, для яких ви дали йому обробники. `Bookshop` вище передає `on_list_tools` і `on_call_tool` і більше нічого, тож клієнт, що до нього під'єднується, бачить:
```json
{"tools": {"listChanged": false}}
```
Жодних `resources`, жодних `prompts`: за ними нічого не стоїть. Передайте `on_list_prompts` — і з'явиться `prompts`; передайте `on_completion` — і з'явиться `completions`.
`MCPServer` завжди оголошує інструменти, ресурси й промпти, зареєстрували ви щось чи ні, бо його менеджери існують завжди. Тут же оголошення — це *і є* виклик конструктора.
## Життєвий цикл як параметр типу {#the-lifespan-generic}
`Server` узагальнений за типом, який видає його життєвий цикл (lifespan). Анотуйте його один раз — і об'єкт буде типізованим усюди, де з'являється:
```python title="server.py" hl_lines="24-26 44-45 50"
--8<-- "docs_src/lowlevel/tutorial005.py"
```
* Життєвий цикл — це `Callable[[Server[Catalog]], AbstractAsyncContextManager[Catalog]]`; `@asynccontextmanager` над `async`-генератором дає саме це.
* Усе, що він видає через `yield`, стає `ctx.lifespan_context`, а оскільки обробники анотовано як `ServerRequestContext[Catalog]`, `.search(...)` автодоповнюється й проходить перевірку типів.
* У нього входять один раз під час старту сервера й виходять один раз під час зупинки. Запуск, завершення та версія тієї самої ідеї в `MCPServer` — на сторінці **[Життєвий цикл](../handlers/lifespan.md)**.
Без `lifespan=` `ctx.lifespan_context` — порожній `dict`.
## Власний метод {#a-method-of-your-own}
Конструктор покриває методи, які визначає MCP. `add_request_handler` покриває все інше:
```python title="server.py" hl_lines="35-36 39-40 43-44 48"
--8<-- "docs_src/lowlevel/tutorial006.py"
```
* Перший аргумент — рядок методу. Для сповіщень є близнюк — `add_notification_handler`.
* `params_type` — це модель, за якою вхідні `params` перевіряються **до** запуску вашого обробника, тож власні методи *отримують* перевірку, якої інструменти не мають. Успадковуйтеся від `RequestParams`, щоб поле `_meta` розбиралося так само, як у кожного іншого методу.
* Обробник повертає `BaseModel`, `dict` або `None`. SDK серіалізує це в результат JSON-RPC.
Одне чесне застереження: високорівневий `Client` має дієслова лише для методів, які визначає MCP, тож `client.reindex()` немає. Вендорний метод — для сторони, яка вже знає про його існування: клієнта, який ви теж постачаєте, або іншого вашого сервісу, що говорить JSON-RPC.
Один метод забрати собі не можна:
```text
ValueError: 'initialize' is handled by the server runner and cannot be overridden;
use Server.middleware to observe or wrap initialization
```
Рукостискання належить засобу запуску сервера. `server/discover`, `ping` та всі інші вбудовані методи можна замінювати.
!!! tip
`Server.middleware`, згаданий у цій помилці, обгортає **кожне** вхідне повідомлення, включно з `initialize`. Якщо мета — спостерігати за трафіком чи переписувати його, а не відповідати на новий метод, почніть із **[Middleware](middleware.md)**.
## Інші обробники {#the-other-handlers}
Кожен із них — одна ідея, для якої у вас тепер є словник; кожна має власну сторінку.
* `on_call_tool`, `on_get_prompt` і `on_read_resource` можуть повернути `InputRequiredResult` замість звичайного результату, щоб призупинити виклик і попросити клієнта про введення; див. **[Багатораундові запити](../handlers/multi-round-trip.md)** (multi-round-trip). Як і годиться цьому рівню, нічого не встановлюється за вас: якщо `MCPServer` за замовчуванням запечатує `requestState`, то тут заданий вами `request_state` передається мережею точно так, як написано, доки ви не ввімкнете захист через `server.middleware.append(RequestStateBoundary(RequestStateSecurity(keys=[...]), default_audience=server.name))`: один рядок (обидва імені імпортуються з `mcp.server.request_state`) — і отримуєте те саме запечатування й перевірку, що їх виконує `MCPServer` (**[Захист `requestState`](../handlers/multi-round-trip.md#protecting-requeststate)**).
* `on_list_resources`, `on_read_resource`, `on_list_prompts`, `on_get_prompt`, `on_completion` — та сама форма `(ctx, params) -> result` для інших примітивів.
* `on_subscriptions_listen` обслуговує потік `subscriptions/listen` версії 2026-07-28. Передайте `ListenHandler`, побудований поверх `SubscriptionBus`, і публікуйте події в шину з інших обробників; повну композицію див. на сторінці **[Підписки](../handlers/subscriptions.md)**.
* `server.streamable_http_app()` повертає той самий Starlette-застосунок, що й у `MCPServer`; розгортайте його так, як **[Запуск сервера](../run/index.md)** розгортає будь-який інший ASGI-застосунок. `server.run(transport=...)` тут немає: `server.run(read_stream, write_stream, server.create_initialization_options())` веде одне з'єднання через пару потоків, і цей один рядок — оце й усе.
## Підсумки {#recap}
* Низькорівневий `Server` приймає обробники як **параметри конструктора** `on_*`; кожен обробник — це `async (ctx, params) -> result`.
* Ви пишете словник `input_schema` і будуєте `CallToolResult`. Нічого не виводиться, не загортається й не перевіряється за вас.
* Виняток в обробнику — це помилка протоколу `-32603`. Помилка інструмента, яку може прочитати модель, — це `CallToolResult` з `is_error=True`, який повертаєте **ви**.
* `_meta` в результаті адресовано клієнтському застосунку, а не моделі.
* `Server[T]` узагальнений за тим, що видає його життєвий цикл; `ctx.lifespan_context` — це типізований `T`.
* `add_request_handler(method, params_type, handler)` обслуговує будь-який метод. `initialize` зарезервовано.
* Можливості, які оголошує `Server`, виводяться з того, які обробники ви зареєстрували.
`Client(server)` поводився з обома серверами однаково, бо вони *і є* тим самим протоколом — у цьому й увесь сенс. Наступний шар нижче — взагалі не клас: це **[Middleware](middleware.md)**.

View file

@ -0,0 +1,128 @@
---
translation:
sections: [6048b4f308edbb8c, 068bda0f21ee9c1b, c3e565b61acd75c5, c62422b159c6ed09, 47204fab253cc45c]
tool: 1
---
# Middleware {#middleware}
**Middleware** — це одна асинхронна функція, що огортає кожне повідомлення, яке отримує сервер.
Її пишуть у формі `async (ctx, call_next)` і додають до `server.middleware`. Оце й увесь API.
!!! warning
Список middleware у вихідному коді позначено як **попередній** (provisional): його сигнатура
й семантика можуть змінитися в мінорному випуску 2.x. Використовуйте його, щоб
*спостерігати* (час виконання, логування, трасування) і щоб *відхиляти* повідомлення; не
робіть його фундаментом, на якому тримається сервер.
`MCPServer` приймає список у конструкторі (`MCPServer(name, middleware=[...])`) і надає доступ
до нього як `mcp.middleware`; низькорівневий `Server` надає той самий список як
`server.middleware`. Приклад нижче використовує низькорівневий `Server`; якщо
`Server(name, on_call_tool=...)` вам незнайомий, спершу прочитайте сторінку
**[Низькорівневий Server](low-level-server.md)**.
## Middleware для вимірювання часу {#a-timing-middleware}
Один сервер, один інструмент, один шар middleware, який записує в лог, скільки часу забрало кожне повідомлення:
```python title="server.py" hl_lines="39-45 49"
--8<-- "docs_src/middleware/tutorial001.py"
```
* `ctx` — той самий `ServerRequestContext`, що його отримують обробники. `ctx.method` — сирий
рядок методу; `ctx.params` — сирі параметри, **до** будь-якої валідації.
* `call_next(ctx)` запускає решту ланцюжка: валідацію, пошук обробника, сам обробник.
Поверніть те, що він повернув, — і відповідь залишиться недоторканою.
* `try`/`finally` тут навмисно: обробник, що викидає виняток, однаково буде заміряно, бо збій
доходить до middleware як виняток із `call_next`.
* `server.middleware.append(...)` реєструє її. Список виконується від зовнішнього до
внутрішнього, тож `middleware[0]` — найближча до мережі.
### Спробуйте самі {#try-it}
Під'єднайте клієнт, отримайте список інструментів, викличте один. У лозі буде **три** рядки:
```text
server/discover took 18.3 ms
tools/list took 0.1 ms
tools/call took 0.1 ms
```
Ви зробили два виклики, а рядків три. Перший — `server/discover`: запит, який клієнт надіслав,
щоб установити з'єднання, ще до того, як ви щось попросили.
У цьому й суть. Middleware огортає **кожне** вхідне повідомлення:
* Установлення з'єднання: `server/discover`, або `initialize` і `notifications/initialized`
у сесії старого покоління.
* Кожен запит і кожне сповіщення. Для сповіщення `ctx.request_id is None`,
`call_next(ctx)` повертає `None`, а все, що повернете ви, відкидається.
* Навіть метод, для якого сервер не має обробника: `call_next` викидає
`MCPError(-32601, "Method not found")` *крізь* ваш middleware на шляху до клієнта.
## Що можна робити всередині {#what-you-can-do-inside-one}
У порядку зростання того, наскільки варто вагатися:
* **Спостерігати.** Заміряти час, рахувати, логувати. Приклад вище.
* **Відхиляти.** Викиньте `MCPError` *замість* виклику `call_next(ctx)` — і саме на це
повідомлення клієнт отримає помилку JSON-RPC. З'єднання лишається живим; наступне
повідомлення проходить. Саме так сервер обмежує `subscriptions/listen` для окремих
викликачів: розділ **[Хто має право стежити](../handlers/subscriptions.md#deciding-who-may-watch)**
на сторінці про підписки показує це крок за кроком.
* **Переписувати.** `ctx` — це dataclass: `await call_next(dataclasses.replace(ctx, params=...))`
передає решті ланцюжка інші параметри, ніж надіслав клієнт. Ніколи не робіть цього з
`initialize`: результат, який отримає клієнт, будується з переписаних параметрів, але сервер
фіксує стан з'єднання за первинними параметрами з мережі. Обидві сторони можуть завершити
рукостискання, не погоджуючись щодо того, про що вони домовилися.
* **Відповідати.** Поверніть результат, не викликаючи `call_next(ctx)`, — і він піде клієнтові
як ваша відповідь. `call_next` віддає вам готову форму для передавання мережею, а конвеєр
ніколи не латає те, що ви повертаєте, тож уся обгортка — ваша відповідальність: на з'єднанні
покоління 2026 сюди входить і позначка `_meta` з `serverInfo`, яку SDK додає до результатів
обробників, але не до ваших.
!!! check
`initialize` — одна з речей, які огортає middleware, і це *єдиний* гачок, який ви для нього
маєте. Спробуйте перехопити його через `add_request_handler` — і SDK відмовить:
```text
ValueError: 'initialize' is handled by the server runner and cannot be overridden;
use Server.middleware to observe or wrap initialization
```
!!! warning
`initialize` обробляється на місці: сервер не читає наступних вхідних повідомлень, доки
ланцюжок middleware не поверне керування. Тому очікування запиту від сервера до клієнта
(`ctx.session.send_request(...)`, еліцитація (elicitation)) під час обробки `initialize`
**заблокує з'єднання намертво**: відповідь, на яку ви чекаєте, ніколи не буде прочитано.
Сповіщення за принципом «надіслав і забув» — без проблем.
## Єдиний middleware, увімкнений за замовчуванням {#the-one-middleware-that-ships-on-by-default}
SDK постачає рівно один шар middleware, і він уже є в списку вашого сервера: той, що створює
спан OpenTelemetry для кожного повідомлення. Його не потрібно додавати, і здебільшого про нього
не доводиться думати. Він нічого не робить, доки ви не встановите експортер, і має власну
сторінку: **[OpenTelemetry](../run/opentelemetry.md)**.
!!! info
Якщо ви писали ASGI-middleware, ця форма вам уже знайома. `(scope, receive, send)` зі
Starlette перетворилося на `(ctx, call_next)` і виконується *після* транспорту — над
декодованим повідомленням, а не над сирим HTTP-запитом. Обидва поєднуються: middleware
Starlette на `streamable_http_app()` бачить HTTP, а цей — MCP.
## Підсумки {#recap}
* Middleware — це `async (ctx, call_next) -> result`; його передають як
`MCPServer(middleware=[...])` (або додають до `mcp.middleware`), а в низькорівневому `Server`
додають до `server.middleware`.
* Він огортає **кожне** вхідне повідомлення (`server/discover`, `initialize`, запити,
сповіщення, невідомі методи) і виконується від зовнішнього до внутрішнього.
* `ctx.request_id is None` — так відрізняють сповіщення від запиту.
* Викиньте виняток замість виклику `call_next`, щоб відхилити одне повідомлення; з'єднання
вціліє.
* Власне трасування OpenTelemetry у SDK — теж middleware, і воно вже в списку. Див.
**[OpenTelemetry](../run/opentelemetry.md)**.
* Уся ця поверхня попередня. Спостерігайте через неї; не будуйте на ній.
Це все, що огортає запит. **[Авторизація](../run/authorization.md)** — те, що вирішує, чи
запит узагалі буде виконано.

View file

@ -0,0 +1,85 @@
---
translation:
sections: [a9aba7a026c7bd85, ed32bda7ba9ae33a, 7e64cc5646abb91f, 22a0129ee78b3c63, d875373c06d8d2f9]
tool: 1
---
# Пагінація {#pagination}
Більшості серверів вона не знадобиться ніколи.
`MCPServer` відповідає на кожен запит `list_*` усім, що має, однією сторінкою з `next_cursor=None`. Для кількох десятків інструментів, ресурсів чи промптів це правильна відповідь, і налаштовувати нічого не потрібно.
Пагінація — для сервера, у якого список ресурсів насправді є базою даних: тисячі рядків, які він відмовляється серіалізувати в одну відповідь. Відповідь протоколу — **курсор**: сервер повертає сторінку плюс непрозорий токен, а клієнт надсилає цей токен назад, щоб отримати наступну сторінку.
У `@mcp.resource()` немає жодного гачка для цього. Щоб розбивати на сторінки, обробник списку пишуть власноруч, на **[низькорівневому Server](low-level-server.md)**.
## Сервер зі сторінками {#a-server-that-pages}
```python title="server.py" hl_lines="12 15-16"
--8<-- "docs_src/pagination/tutorial001.py"
```
* На низькорівневому `Server` обробники — це аргументи конструктора, а не декоратори. `on_list_resources` відповідає на кожен запит `resources/list`; оце й усе під'єднання.
* Кожен обробник зі сторінками має тип `params: PaginatedRequestParams | None`, і приклад приймає обидва варіанти. Утім, через з'єднання SDK ніколи не передає `None` (запит без члена `params` доходить до обробника як модель із типовими значеннями), тож важливий сигнал — `params.cursor is None`: **починайте з початку**.
* Ви вирішуєте, чим курсор *є*. Тут це зсув, записаний як рядок. Мітка часу, первинний ключ, base64-блоб — будь-що, що можна видати на виході й упізнати на вході.
* `next_cursor=None` — спосіб сказати «це була остання сторінка». Немає ні лічильника, ні загальної кількості, ні `has_more`. `None` — це весь сигнал.
!!! tip
`PAGE_SIZE` у 10 робить приклад читабельним. Свій розмір обирайте для кожної кінцевої точки окремо: список
однорядкових ресурсів може дозволити собі сторінку на 500; список важких шаблонів промптів — ні.
Клієнт на це не впливає, і так задумано.
### Спробуйте самі {#try-it}
`Client(server)` під'єднується до низькорівневого `Server` у пам'яті так само, як і до `MCPServer`.
Викличте `list_resources()` без аргументів. Повертається десять ресурсів, від `book-1` до `book-10`, а `next_cursor` — рядок `"10"`.
Передайте його назад через `list_resources(cursor="10")` — і перший ресурс уже `book-11`, а новий `next_cursor` — `"20"`.
Десята сторінка приходить із `next_cursor`, що дорівнює `None`. Готово.
## Цикл на клієнті {#the-client-loop}
Кожен метод `list_*` класу `Client` (`list_tools`, `list_resources`, `list_resource_templates`, `list_prompts`) приймає іменований параметр `cursor=`. Вичерпати список зі сторінками — це один `while True`:
```python title="client.py" hl_lines="26-32"
--8<-- "docs_src/pagination/tutorial002.py"
```
* `cursor` починається з `None`, тому перший запит курсора не несе.
* Розширюйте список **до** того, як дивитися на `next_cursor`: на останній сторінці теж є ресурси.
* `next_cursor is None` — це вихід. Усе інше йде прямо назад у `cursor=`, без змін.
Запустіть його `main()` — і він надрукує `100 resources`: десять сторінок по десять, зшитих циклом, який так і не дізнався, що сторінок було десять.
Це той самий цикл, який **[Клієнт](../client/index.md)** показує для кожного дієслова `list_*`, і проти сервера без сторінок він нічого не коштує: `next_cursor` дорівнює `None` вже в першій відповіді, і цикл виконується один раз.
## Три правила {#the-three-rules}
**Курсори непрозорі.** Клієнт ніколи не повинен розбирати, будувати чи вгадувати курсор. Єдине законне джерело курсора — `next_cursor` попередньої сторінки, дослівно.
**Розмір сторінки обирає сервер.** У протоколі немає `limit=`. Якщо потрібен інший розмір сторінки, змінюють сервер.
**Клієнт, що ігнорує пагінацію, усе одно працює.** Він викликає `list_resources()` один раз, отримує перші десять і навіть не помічає `next_cursor`, який викинув. Нічого не ламається; він просто бачить менше.
!!! check
Непрозорий означає непрозорий. Вигадайте курсор (`list_resources(cursor="page-2")`) — і
протокол нічим не зможе допомогти. Цей сервер пробує `int("page-2")`, обробник викидає виняток,
а до клієнта повертається:
```text
MCPError(-32603, 'Internal server error', None)
```
Курсор, отриманий не від сервера, — це помилка, а не запит на нову можливість.
## Підсумки {#recap}
* `MCPServer` повертає все однією сторінкою. Пагінацію вмикають свідомо, і роблять це на низькорівневому `Server`.
* `on_list_resources` (а також `on_list_tools`, `on_list_prompts`, `on_list_resource_templates`) отримує `PaginatedRequestParams | None`; для першої сторінки `params.cursor` дорівнює `None`.
* Повертається сторінка плюс `next_cursor`: будь-який рядок, який ви згодом упізнаєте, або `None`, коли більше нічого не лишилося.
* Цикл на клієнті: передати `cursor=`, накопичити, повторювати, доки `next_cursor is None`.
* Курсори непрозорі, розмір сторінки належить серверу, а клієнт без пагінації все одно отримує першу сторінку.
Решта написаного власноруч API `Server` (`on_call_tool`, словники `input_schema`, `_meta`) — на сторінці **[Низькорівневий Server](low-level-server.md)**.

View file

@ -0,0 +1,122 @@
---
translation:
sections: [9e7b9a1710e5aeba, b74ca4c1d2ddddee, fa8714e61bf90c5a, 04db67a886b7271c, 857690fb8f876800]
tool: 1
---
# Підказки щодо кешування {#caching-hints}
Кожен результат, який сервер повертає для `tools/list`, `prompts/list`, `resources/list`, `resources/templates/list`, `resources/read` і `server/discover`, у протоколі 2026-07-28 містить два поля: `ttlMs` — скільки мілісекунд клієнт може вважати результат свіжим, і `cacheScope` — чи можна ділитися закешованим результатом між користувачами (`"public"`), чи він належить одному контексту авторизації (`"private"`).
Сервер нічого не кешує. Ці поля — *декларація*: «цей список інструментів однаковий для всіх і не зміниться протягом хвилини». Тоді клієнт (або шлюз перед вами) може пропустити звернення до сервера. Дотримуватися підказок чи ні — вибір клієнта; видавати їх — робота сервера, і SDK робить це за вас.
За замовчуванням кожен результат каже `ttlMs: 0, cacheScope: "private"`: одразу застарілий, ніколи не спільний. Це завжди безпечно і завжди відповідає специфікації. Якщо ваші списки справді стабільні й однакові для всіх викликачів, скажіть про це під час створення:
```python title="server.py" hl_lines="5-8"
--8<-- "docs_src/caching/tutorial001.py"
```
* Ключами мапи є **імена методів**, і шість кешованих методів — єдині допустимі ключі. Параметр має тип `Mapping[CacheableMethod, CacheHint]`, тож редактор доповнює ключі автоматично й позначає описку ще до запуску; усе, що проскочить повз перевірку типів, викине виняток під час створення.
* Метод, який ви не згадали, зберігає типові значення. Мапа — це набір перевизначень, а не маніфест.
* `CacheHint(ttl_ms=5_000)` залишив `scope` незаданим, тож він лишається `"private"`: п'ять секунд свіжості, окремо для кожного викликача. Область і TTL — незалежні рішення.
* `"server/discover"` — теж допустимий ключ, бо результат discovery кешується так само, як будь-який список.
!!! warning
`cacheScope: "public"` означає, що вашу закешовану відповідь можуть віддати *будь-кому*.
Спільний шлюз охоче передасть результат одного користувача іншому, навіть якщо запит був
автентифікований. Позначайте результат як `"public"` лише тоді, коли він однаковий для кожного
викликача, і ніколи не використовуйте `cacheScope` як контроль доступу: це мітка, а не замок.
## Перевизначення в обробнику {#per-handler-override}
На низькорівневому `Server` обробники будують результати вручну, а `ttl_ms` / `cache_scope` — звичайні поля моделей результатів. Обробник, який задає їх явно, завжди перемагає мапу конструктора, поле за полем:
```python title="server.py" hl_lines="10 16"
--8<-- "docs_src/caching/tutorial002.py"
```
Обробник сказав `ttl_ms=1_000` і нічого про область. У переданих даних: `ttlMs: 1000` (значення обробника, а не `60_000` з мапи) і `cacheScope: "public"` (з мапи, бо обробник залишив поле незаданим). Явне перемагає налаштоване, а налаштоване перемагає типове. Це діє для кожного поля окремо, тож обробник може зафіксувати одне поле, а інше залишити загальносерверній політиці.
Це також запасний вихід для динаміки, про яку конструктор знати не може: обробник, що фільтрує `resources/read` для кожного користувача, може повернути `cache_scope="private"` для одного URI на сервері, який загалом публічний.
Одне застереження щодо списків із пагінацією: протокол вимагає **однакового `cacheScope` на кожній сторінці** одного списку. Мапа конструктора забезпечує це за побудовою, бо її ключі — методи, а не сторінки. Але обробник, який перевизначає область сам, відповідає за цю узгодженість: перевизначайте її на *кожній* сторінці, а не лише коли є курсор, інакше перша й друга сторінки розійдуться.
## Що бачить клієнт {#what-the-client-sees}
У сесії 2026-07-28 `Client` дотримується підказок за вас: він має вбудований кеш відповідей, увімкнений за замовчуванням. Результат, що прийшов із `ttlMs`, зберігається, і ідентичний виклик у межах цього TTL обслуговується з кешу без звернення до сервера. Результат *без* підказки не кешується: результати без підказок отримують `CacheConfig.default_ttl_ms`, типове значення якого `0` (одразу застарілий), тож сервер, який нічого не оголошує, бачить рівно той самий трафік виклик-за-викликом, що й завжди.
```python title="client.py" hl_lines="33 35 38"
--8<-- "docs_src/caching/tutorial003.py"
```
Чотири виклики, три отримання. Другий виклик знайшов свіжий запис і не дійшов до сервера; переведення (впровадженого) годинника за межу TTL змусило третій знову звернутися до сервера; четвертий сказав `cache_mode="refresh"`. Цей іменований аргумент є в п'яти методів із кешуванням (`list_tools`, `list_prompts`, `list_resources`, `list_resource_templates`, `read_resource`):
* `"use"` (за замовчуванням) віддає свіжий запис, якщо він є, а якщо ні — звертається до сервера і зберігає результат.
* `"refresh"` ніколи не віддає з кешу: звертається до сервера і зберігає результат, замінюючи те, що було закешовано.
* `"bypass"` звертається до сервера, взагалі не торкаючись кешу: ні читання, ні запису.
Одне правило стоїть над `"use"`: **виклики з `meta` завжди доходять до сервера.** Запит із заданим `meta` (токен перебігу виконання, поля трасування) очікує запиту мережею, тому за `cache_mode="use"` його обробляють як `"refresh"`: читання з кешу пропускається, а отриманий результат усе одно замінює закешований запис. `"bypass"` і явний `"refresh"` поводяться як завжди.
Щоб повністю вимкнути кешування, створіть клієнт як `Client(server, cache=None)`: кожен виклик знову стає зверненням до сервера, а `cache_mode`, хоч і приймається далі, нічого не робить.
Область також дотримується автоматично: записи `"private"` прив'язані до *розділу* (partition) кешу (нижче), тоді як записи `"public"` можуть за бажанням ділитися ширше. А **сповіщення перемагають TTL** для конкретних записів, які вони називають: сповіщення `list_changed` витісняє відповідний закешований список, а `resources/updated` витісняє закешоване читання, збережене рівно під його URI, хоч би якими свіжими вони були. На з'єднанні 2026-07-28 ці сповіщення надходять потоком `subscriptions/listen`, який відкривають через `client.listen(...)`, і витіснення завершується до того, як ваш спостерігач побачить подію; докладніше — на сторінці **[Підписки](subscriptions.md)**.
Одне застереження щодо `resources/updated`: витіснення працює лише за точним URI. Контракт сховища не має операції перелічення чи сканування (так само, як еталонна реалізація на TypeScript), тож сповіщення з URI *під*ресурсу не витісняє закешоване читання його батьківського ресурсу. Якщо ваш сервер сигналізує про підресурси таким чином, перечитайте батьківський ресурс із `cache_mode="refresh"`.
### Налаштування: `CacheConfig` {#configuring-it-cacheconfig}
```python
from mcp.client import CacheConfig
client = Client("https://api.example.com/mcp", cache=CacheConfig(default_ttl_ms=5_000))
```
* `store`: де живуть записи. За замовчуванням — свіже сховище в пам'яті для кожного клієнта; передайте власну реалізацію `ResponseCacheStore` (скажімо, на Redis), щоб ділити кеш між клієнтами або процесами. Типи контракту (`ResponseCacheStore`, `CacheKey`, `CacheEntry` і типовий `InMemoryResponseCacheStore`) імпортуються з `mcp.client`. Один пошук може виконати до двох послідовних `get` у сховищі (приватна гілка, потім публічна), тож розраховуйте очікувану затримку віддаленого сховища відповідно. Власне сховище **вимагає** явного `partition`.
* `partition`: мітка контексту авторизації, яка не дає віддати записи `"private"` одного принципала іншому в межах спільного сховища.
* `target_id`: явна ідентичність сервера, для власних транспортів і серверів у тому самому процесі (нижче).
* `default_ttl_ms`: TTL, що застосовується до результатів без підказки `ttlMs`. Типове `0` залишає результати без підказок незакешованими.
* `share_public`: віддавати записи, які сервер оголосив `"public"`, між розділами (нижче). За замовчуванням вимкнено.
* `clock`: джерело реального часу, у секундах епохи. Впровадьте його, як у прикладі вище, і тестам на закінчення терміну не доведеться засинати.
!!! warning "Розділ = перевірений принципал"
Виводьте `partition` з **перевіреного облікового посвідчення**, наприклад із суб'єкта провалідованого токена. Ніколи не виводьте його з даних, наданих у запиті, і ніколи з URL сервера (ідентичність сервера — окрема вісь ключа). SDK — це бібліотека без власної автентифікації: якорем довіри є той, хто створює `CacheConfig`, тобто розгортання, а не орендар. Багатоорендний шлюз створює по одному `CacheConfig` на кожного автентифікованого принципала.
Розділ також фіксований на весь час життя `Client`. Якщо контекст авторизації з'єднання змінюється посеред сесії (скажімо, повторна автентифікація як інший принципал), кеш за цим не стежить; створіть новий `Client` для нового принципала.
Ключі кешу також містять **ідентичність сервера**: рядок URL, до якого ви під'єдналися, з вилученим userinfo `user:pass@` і в усьому іншому побайтово точний. Жодного зведення регістру, жодного переставляння параметрів запиту, жодного прибирання кінцевої косої риски. Недостатня нормалізація коштує лише втраченого спільного використання, тоді як надмірна могла б злити двох орендарів (`?tenant=a` проти `?tenant=b`), тому поверхово різні URL просто не ділять записи. Коли URL немає (сервер у тому самому процесі або екземпляр `Transport`), клієнт натомість отримує випадкову ідентичність для кожного екземпляра; задайте `CacheConfig.target_id`, щоб назвати сервер (із власним сховищем це обов'язково, і створення про це скаже). Ідентичність хешується sha256 перед тим, як потрапити до матеріалу ключа, тож URL із секретами в рядку запиту ніколи не з'являється в ключах сховища. І самі не логуйте дохешовану форму.
!!! warning "`share_public` довіряє серверу для всього парку"
За замовчуванням навіть записи `"public"` лишаються в межах свого розділу. `share_public=True` віддає записи, які сервер позначив `cacheScope: "public"`, **кожному** розділу, що користується сховищем, довіряючи класифікації сервера від імені їх усіх. Сервер, який ставить `"public"` на дані окремого орендаря (через помилку чи зі злого наміру), тоді витікає відповіддю одного орендаря до інших. Прапорець навмисно існує лише на рівні конструктора: `cache_mode` для окремого виклику може звузити кешування, але ніщо на рівні виклику не може розширити спільний доступ.
### Чого кеш ніколи не робить {#what-the-cache-never-does}
* **Виклики рівня сесії його оминають.** `client.session.list_tools()` і подібні завжди звертаються до сервера; кеш живе на методах `Client`.
* **`server/discover` тримається осторонь.** Результат discover доставляється один раз, під час під'єднання, і ніколи не потрапляє до кешу відповідей, навіть якщо містить `ttlMs`. Якщо ви зберігаєте його самі, щоб пропустити пробу під час повторного під'єднання ([`prior_discover`](../protocol-versions.md#reconnecting-with-prior_discover)), його свіжість — ваш облік: `DiscoverResult` містить `ttl_ms` і `cache_scope`, уже розібрані, саме для цього.
* **Сторінки продовження ніколи не кешуються.** Участь беруть лише виклики без курсора. Сторінка продовження, відхилена через протермінований курсор, таки *витісняє* закешований список, бо список під нею змінився.
* **Багатораундові читання (multi-round-trip) ніколи не кешуються.** `read_resource`, засіяний `input_responses`/`request_state`, або такий, що розв'язується через раунди введення, ніколи не потрапляє до кешу (MUST специфікації).
* **Витіснення за сповіщеннями потребує сповіщень.** Витіснення працює рівно настільки добре, наскільки транспорт їх доставляє, а сучасний шлях у тому самому процесі (`Client(server)` з типовим `mode="auto"`) сьогодні не доставляє окремих сповіщень.
* **Витіснення відбувається зрештою, а не миттєво.** Сповіщення мережевого шляху розсилаються з породжених задач, тож виклик, що змагається з надходженням сповіщення, може ще раз отримати запис до витіснення; вікно обмежене затримкою розсилання, і витіснення все одно відбувається.
* **Жодного stale-if-error.** Протермінований запис ніколи не віддається через те, що повторне отримання не вдалося; помилка поширюється далі.
* **Жодного раннього повторного отримання.** Збережений запис віддається, доки не спливе його TTL, а наступний після цього виклик платить зверненням до сервера; нічого не оновлюється у фоновому режимі.
* **Жодного об'єднання.** Два одночасні ідентичні виклики — це два отримання.
* **Жодного TTL понад 24 години.** Більший `ttlMs`, надісланий сервером чи налаштований, обрізається під час збереження (`mcp.client.caching.MAX_TTL_MS`), обмежуючи, як довго можна віддавати будь-який запис, хоч би якою щедрою була підказка.
* У **спільному сховищі** клієнти змагаються між собою. Кожен клієнт відкидає власний запис, коли витіснення випередило отримання в польоті, але клієнт-*співорендар* усе ще може записати назад запис, який прибрало витіснення, якого він не бачив; і сам облік цих перегонів обмежений: понад 4096 відстежуваних ключів захист найстарішого ключа відкидається першим. Обидва вікна прийнятні й закриваються обмеженням TTL вище.
* **Жодного обслуговування між поколіннями протоколу.** Записи прив'язані до узгодженої версії протоколу: у спільному постійному сховищі сесія ніколи не віддає запис, записаний за іншої узгодженої версії (той самий список справді відрізняється залежно від покоління, бо SDK прибирає поля 2026 для старіших сесій). Витіснення так само торкається лише записів поточного покоління; записи іншого покоління просто старіють за TTL.
### Читання підказок самостійно {#reading-the-hints-yourself}
Підказки — це також звичайні поля на кожному кешованому результаті (`result.ttl_ms` і `result.cache_scope`, уже розібрані), на випадок якщо ви хочете накласти власний облік поверх вбудованого кешу (або замість нього).
Зі **старішим сервером** (протокол до 2026) полів у переданих даних просто немає, і моделі показують свої обережні типові значення: `ttl_ms == 0` і `cache_scope == "private"`, застарілий і неспільний — правильне припущення для сервера, який нічого не оголосив. Кеш ставиться до сесії старого покоління так само: підказки там ніколи не враховуються (хоч би які ключі з'явилися в переданих даних), застосовується лише `default_ttl_ms`, а його типове `0` нічого не кешує, тож з'єднання до 2026 поводиться рівно так, як до появи кешу. Якщо потрібно відрізнити «сервер сказав 0» від «сервер нічого не сказав», перевірте `"ttl_ms" in result.model_fields_set`: воно задане лише тоді, коли поле справді прийшло.
## Старіші клієнти {#older-clients}
Клієнти на версіях протоколу до 2026 ніколи не бачать жодного з двох полів; SDK прибирає їх під час серіалізації для таких з'єднань. Налаштуйте підказки один раз; нічого специфічного для версії писати не потрібно.
## Підсумки {#recap}
* Шість методів містять `ttlMs`/`cacheScope`; SDK за замовчуванням ставить `0`/`"private"` — застарілий і неспільний, завжди безпечно.
* `cache_hints={method: CacheHint(...)}` під час створення (і `MCPServer`, і `Server`) задає загальносерверні значення для кожного методу.
* Обробник, що задає поля на своєму результаті, перевизначає мапу, поле за полем.
* `"public"` — це обіцянка, що результат однаковий для кожного викликача. Це не контроль доступу.
* `Client` дотримується підказок автоматично: його кеш відповідей увімкнений за замовчуванням, віддає свіжі записи замість повторного отримання і нічого не кешує для серверів (або сесій), які не надають підказок.
* Для окремого виклику `cache_mode="refresh"` отримує заново, а `"bypass"` оминає кеш; `cache=None` під час створення вимикає його повністю.

View file

@ -0,0 +1,154 @@
---
translation:
sections: [adf3c545b5be46b6, 916cd3ab1c03f461, e9be7a8d0eb0a456, 565890a636288ecf, 6af7e49db9129ec3, 06b0238c174186af, 90c6043be435fcb0]
tool: 1
---
# Колбеки клієнта {#client-callbacks}
Майже кожен запит у MCP іде в один бік: від клієнта до сервера.
Сервер теж може дещо попросити в **клієнта**: поставити запитання користувачеві, скористатися моделлю користувача для семплювання (sampling), отримати список папок його робочого простору. На ці запити відповідають **колбеки**, які передаються в `Client(...)`.
## Сервер, який запитує {#a-server-that-asks}
Ось сервер, інструмент якого не може завершитися самотужки:
```python title="server.py" hl_lines="16"
--8<-- "docs_src/client_callbacks/tutorial001.py"
```
* `ctx.elicit(...)` надсилає запит `elicitation/create` **клієнтові** й чекає.
* Інструмент не повертає результат, доки хтось (людина у формі або ваш код) не надасть `name`.
Це серверна половина, і вона належить сторінці **[Еліцитація](../handlers/elicitation.md)**. Ця сторінка — про інший кінець з'єднання.
## Колбек еліцитації {#the-elicitation-callback}
```python title="client.py" hl_lines="6-10 16-17"
--8<-- "docs_src/client_callbacks/tutorial002.py"
```
* Колбек еліцитації (elicitation) — це `async (context, params) -> ElicitResult`.
* `params.message` — це запитання. `params.requested_schema` — JSON Schema відповіді, яку хоче отримати сервер. Справжній клієнт будує з неї форму; цей заповнює її автоматично.
* Повертається `ElicitResult(action="accept", content={...})`, або `action="decline"`, або `action="cancel"`. Єдиний інший варіант — `ErrorData(...)`: він відхиляє запит, і весь виклик завершується помилкою.
* `context` — це `ClientRequestContext`: активна `session`, `request_id` сервера та будь-які `meta`, які він додав.
!!! tip
`params` — об'єднання двох режимів еліцитації. Тут `params.mode` дорівнює `"form"`; запит `"url"`
замість схеми несе `params.url`. Обидва обробляє один колбек; розгалужуйтеся за `params.mode`.
Повний шаблон показано на сторінці **[Еліцитація](../handlers/elicitation.md)**.
### Спробуйте самі {#try-it}
Викличте `issue_card` і простежте за обома кінцями.
Колбек отримує запитання сервера, уже розібране:
```python
params.mode # 'form'
params.message # 'What name should go on the card?'
params.requested_schema # {'properties': {'name': {'title': 'Name', 'type': 'string'}},
# 'required': ['name'], 'title': 'CardHolder', 'type': 'object'}
```
Він відповідає, `ctx.elicit(...)` усередині інструмента відновлює роботу, й інструмент завершується:
```python
result.content # [TextContent(type='text', text='Card issued to Ada Lovelace.')]
```
Один `tools/call` від вас, один `elicitation/create` у відповідь від сервера, на який відповіла ваша функція, — і все це всередині одного виклику інструмента.
!!! info
`mode="legacy"` у виклику `Client(...)` стоїть не просто так. За замовчуванням `Client(...)` узгоджує сучасний
шлях протоколу, а на ньому немає зворотного каналу (back-channel) для запитів від сервера до клієнта: `ctx.elicit`
завершується помилкою ще до того, як запуститься колбек. Вирішує це не транспорт, а узгоджений
протокол — однаково і в пам'яті, і за URL. Фіксуйте `mode="legacy"` щоразу, коли клієнт має
відповідати на такий запит; так робить кожен тест за цією сторінкою. Докладніше — на сторінці **[Версії протоколу](../protocol-versions.md)**.
У сесії 2026-07-28 колбек не зникає, він просто отримує дані інакше: коли інструмент повертає
`InputRequiredResult` з `ElicitRequest` усередині, `Client` передає цей запис тому самому
`elicitation_callback` і повторює виклик за вас. Цей сценарій описано на сторінці **[Багатораундові запити](../handlers/multi-round-trip.md)** (multi-round-trip).
## Колбек — це можливість {#a-callback-is-a-capability}
Ви ніде не повідомляли серверу, що ваш клієнт уміє відповідати на запити еліцитації. Це зробив SDK.
Під'єднуючись, клієнт оголошує свої `capabilities` — дзеркальне відображення серверних. Цей об'єкт ви не пишете. **Реєстрація колбека і є оголошенням.**
| що передається | що оголошує клієнт |
| --- | --- |
| `elicitation_callback=` | `"elicitation": {"form": {}, "url": {}}` |
| `sampling_callback=` | `"sampling": {}` |
| `list_roots_callback=` | `"roots": {"listChanged": true}` |
| жодного з них | `{}` |
Єдине уточнення — підможливості семплювання: передавайте `sampling_capabilities=SamplingCapability(tools=SamplingToolsCapability())` разом із `sampling_callback`, якщо ваш семплер обробляє параметри `tools` / `tool_choice`. Сервери мають побачити оголошену `sampling.tools`, перш ніж надсилати їх.
`logging_callback` і `message_handler` у таблиці немає. Вони обробляють сповіщення, а сповіщенням можливість не потрібна.
Сервер зчитує оголошення методом `ctx.session.check_client_capability(...)`. Додайте інструмент, який це робить:
```python title="server.py" hl_lines="23-31"
--8<-- "docs_src/client_callbacks/tutorial003.py"
```
Під'єднайтеся лише з `elicitation_callback` і викличте його:
```python
result.structured_content # {'result': ['elicitation']}
```
Передайте всі три колбеки — отримаєте `['elicitation', 'sampling', 'roots']`. Не передайте жодного — отримаєте `[]`.
!!! check
Тепер зробіть неправильно: під'єднайтеся **без** `elicitation_callback` і все одно викличте `issue_card`.
Запит сервера `elicitation/create` все одно доходить до клієнта, і SDK відповідає на нього за
вас — помилкою, бо ви ніде не сказали, що можете його обробити. Ця помилка топить весь виклик.
`call_tool` не повертає результат із `is_error`; він викидає виняток:
```text
MCPError: Elicitation not supported
```
Це помилка протоколу (`-32600`, *invalid request*), а не помилка інструмента: моделі тут нічого
прочитати й повторити. Саме тому `client_features` варто мати: чемний сервер
перевіряє, перш ніж питати.
## Застаріла пара {#the-deprecated-pair}
`sampling_callback` відповідає на `sampling/createMessage`: сервер просить *вашу* модель щось доповнити. `list_roots_callback` відповідає на `roots/list`: сервер питає, у яких каталогах йому можна працювати.
Обидва працюють. Обидва дотримуються правила вище. І обидва обслуговують RPC, які **специфікація 2026-07-28 вилучає**: сучасний сервер не звертається до клієнта посеред запиту, а повертає запит вам як частину результату інструмента (**[Багатораундові запити](../handlers/multi-round-trip.md)**). Самі колбеки нікуди не зникають. Коли `InputRequiredResult` несе `CreateMessageRequest` або `ListRootsRequest`, автоматичний цикл `Client` передає його тому самому `sampling_callback` чи `list_roots_callback`, який ви зареєстрували тут. Повний список — на сторінці **[Застарілі можливості](../deprecated.md)**.
Колбеки досі потрібні, щоб спілкуватися із серверами, які ще не перейшли. Сигнатури:
```python title="client.py"
--8<-- "docs_src/client_callbacks/tutorial004.py"
```
* Колбек семплювання отримує повний `CreateMessageRequestParams` (`messages`, `model_preferences`, `max_tokens`) і повертає `CreateMessageResult`. Модель запускаєте *ви*, як завгодно; SDK лише переносить запит.
* Колбек кореневих каталогів (roots) не приймає жодних параметрів і повертає `ListRootsResult`.
* Кожен із них натомість може повернути `ErrorData(...)`, щоб відмовити.
Передавайте їх у `Client(...)` так само, як `elicitation_callback`.
## Колбеки сповіщень {#the-notification-callbacks}
Ще два. Жоден нічого не оголошує.
`logging_callback` отримує `notifications/message`, які надсилає сервер, у вигляді `LoggingMessageNotificationParams` (`level`, `logger`, `data`). Протокольне логування саме оголошене застарілим у специфікації 2026-07-28 (що робити натомість — на сторінці **[Логування](../handlers/logging.md)**), тож цей колбек існує для серверів, які досі його надсилають. На з'єднанні покоління 2026 самого колбека недостатньо, бо сервери 2026 надсилають лог-повідомлення лише у відповідь на запити, які на це погодилися: передайте `log_level="info"` (або інший рівень) у `Client(...)`, щоб проставляти цю згоду в кожному запиті й отримувати повідомлення цього рівня та вище. Сервери до 2026 ігнорують її й зберігають свою поведінку `logging/setLevel`.
`message_handler` — універсальний приймач: до нього доходить кожне сповіщення сервера, яке сесія передає назовні (на додачу до свого спеціального колбека), а на транспорті на основі потоку — ще й кожен `Exception` транспортного рівня. Два ніколи не доходять: `notifications/cancelled` SDK застосовує сам, а не передає назовні, а підтвердження підписки для активного потоку `listen()` споживає сам цей потік. Анотуйте параметр типом `IncomingMessage` (`ServerNotification | Exception`, експортується з `mcp.client`). Єдиний шаблон, який варто знати, — `if isinstance(message, Exception): raise message`, щоб розірване з'єднання падало гучно, а не зникало безслідно.
## Підсумки {#recap}
* Сервер може надсилати запити клієнтові. Відповідають на них колбеки, передані в `Client(...)`.
* Колбек еліцитації — актуальний: `async (context, params) -> ElicitResult`, одна функція і для режиму форми, і для режиму URL.
* **Реєстрація колбека — це оголошення можливості.** Без нього SDK відхиляє запит сервера від вашого імені, і весь виклик завершується з `MCPError`.
* Сервер дізнається про це ще до запиту за допомогою `ctx.session.check_client_capability(...)`.
* `sampling_callback` і `list_roots_callback` працюють так само, але обслуговують застарілі можливості; сучасні сервери натомість використовують багатораундові запити.
* `logging_callback` і `message_handler` отримують сповіщення. Вони нічого не оголошують.
Перший аргумент `Client(...)` — об'єкт транспорту. Усі його різновиди описано на сторінці **[Транспорти клієнта](transports.md)**.

View file

@ -0,0 +1,152 @@
---
translation:
sections: [a91322c46111d16d, 8e6fd6d6f59bb568, e7828fd2729b2c9d, a03ec26bfc678b65, 1034c653c0bcf1b0]
tool: 1
---
# Твердження про ідентичність {#identity-assertion}
Звичайний OAuth-провайдер (**[OAuth-клієнти](oauth-clients.md)**) починає з запитання до MCP-сервера: *якому серверу авторизації ти довіряєш?* Він іде за відповіддю, куди б вона не вказувала, а далі або людина входить у систему, або замість неї це робить заздалегідь узгоджений секрет.
Підприємство не хоче, щоб ані те, ані інше вирішувалося для кожного сервера окремо. У нього вже працює провайдер ідентичності (Okta, Microsoft Entra ID, власний); користувач уже ввійшов у нього сьогодні вранці; і саме там команда безпеки хоче вирішувати, хто й до чого має доступ. [SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990), розширення **Enterprise-Managed Authorization**, переносить це рішення туди. IdP підписує короткоживучий JWT — **Identity Assertion JWT Authorization Grant**, або **ID-JAG**: твердження, що *цей користувач* через *цей клієнт* може звертатися до *цього MCP-сервера*. Клієнт обмінює його на звичайний токен доступу. Жодного браузера, жодного екрана згоди, жодної динамічної реєстрації.
Ця сторінка описує обидва боки цього обміну. Сам MCP-сервер не змінюється взагалі: це й далі сервер ресурсів зі сторінки **[Авторизація](../run/authorization.md)**, який перевіряє будь-який токен, що надходить.
## Два запити токена {#two-token-requests}
Тут діють дві різні інстанції, і навчитися розрізняти їх за назвою — це вже більша частина розуміння цієї сторінки. **Корпоративний IdP** — це провайдер ідентичності вашої організації: він знає, хто цей працівник, у ньому живе політика, і він видає ID-JAG. SDK ніколи з ним не спілкується. **Сервер авторизації MCP** — та сама сторона, що й на сторінці **[Авторизація](../run/authorization.md)**: емітент, названий у метаданих MCP-сервера, те, що випускає токени, які цей MCP-сервер приймає. У звичайному OAuth-потоці ці дві ролі зазвичай поєднані в одному вузлі. Тут їх два, і весь грант зводиться до того, що другий погоджується довіряти першому.
Клієнт робить по одному запиту токена до кожного.
1. **До корпоративного IdP.** Клієнт обмінює вхід користувача (його ID-токен OpenID Connect) на ID-JAG. Це обмін токенів за [RFC 8693](https://datatracker.ietf.org/doc/html/rfc8693), це цілком API вашого IdP, і **SDK його не виконує**. Це робите ви — всередині одного асинхронного колбека. Саме тут також ухвалюється рішення політики: IdP, який каже «ні», ніколи не видає ID-JAG, і пред'являти просто нічого.
2. **До сервера авторизації MCP.** Клієнт пред'являє ID-JAG за грантом `jwt-bearer` з [RFC 7523](https://datatracker.ietf.org/doc/html/rfc7523) (`grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer`, ID-JAG як `assertion`) й отримує токен доступу. **Саме цей запит робить SDK**, і приймати його — єдине, що ця сторінка додає до сервера авторизації.
Усе нижче стосується другого запиту: клієнта, який його надсилає, і сервера авторизації, який на нього відповідає.
## Клієнт {#the-client}
**`IdentityAssertionOAuthProvider`** живе в `mcp.client.auth.extensions.identity_assertion`. Як і кожен провайдер зі сторінки **[OAuth-клієнти](oauth-clients.md)**, це `httpx2.Auth`: створіть його, передайте в `auth=`, віддайте `httpx2.AsyncClient` транспорту.
```python title="client.py" hl_lines="49-50 53-61"
--8<-- "docs_src/identity_assertion/tutorial001.py"
```
Читайте знизу вгору.
* `main()` — стандартний `main()` OAuth-клієнта (**[OAuth-клієнти](oauth-clients.md)**), без жодних змін, рядок у рядок. У цьому й суть: щойно провайдер існує, нічого далі за течією не знає, який грант породив токен.
* Провайдер приймає те, чого інші провайдери не можуть виявити самі: `client_id` і `client_secret`, які хтось **заздалегідь зареєстрував** на сервері авторизації, `issuer` цього сервера авторизації та `assertion_provider` — асинхронний колбек, що повертає свіжий ID-JAG на вимогу.
* `storage` — той самий протокол `TokenStorage`. Викликаються лише два методи для токенів; динамічної реєстрації тут немає, тож немає й `client_info`, який треба пам'ятати.
### Провайдер твердження {#the-assertion-provider}
`fetch_id_jag(audience, resource)` — єдиний код, який ви пишете. Його очікують (await) один раз на кожен обмін токенів, ніколи під час створення провайдера, і лише *після* того, як метадані сервера авторизації отримано й перевірено, тож неправильно налаштований емітент ніколи не призведе до витоку твердження. Два його аргументи — це два з полів (claims), з якими має бути випущено ID-JAG: `audience` — це емітент сервера авторизації (поле `aud` в ID-JAG), а `resource` — канонічний ідентифікатор MCP-сервера (поле `resource` в ID-JAG). Третє ви вже маєте: поле `client_id` в ID-JAG має називати той `client_id`, який передано провайдеру, інакше сервер авторизації відмовить в обміні.
`idp_issue_id_jag` над нею — **не ваш код**. Вона заміняє провайдера ідентичності, підписуючи твердження в тому самому процесі, щоб файл був повним і можна було прочитати кожне поле, яке несе ID-JAG. Справжня `fetch_id_jag` натомість робить перший запит токена з попереднього розділу: обмін токенів за [RFC 8693](https://datatracker.ietf.org/doc/html/rfc8693) з вашим IdP, визначений чернеткою Identity Assertion JWT Authorization Grant, яку профілює [SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990). ID-токен користувача, що ввійшов, передається як `subject_token`, `requested_token_type` — власний URN ID-JAG (`urn:ietf:params:oauth:token-type:id-jag`), `audience` і `resource` проходять без змін, а відповідь містить ID-JAG. Саме цей обмін, під цими назвами, і варто шукати в документації свого IdP.
!!! tip
Свіжий ID-JAG запитується для кожного обміну, і в цьому суть: це одноразовий грант, що живе
лічені хвилини, і сервер авторизації на цій сторінці відмовляється приймати той самий двічі.
Не кешуйте його. Повторно використовується токен доступу, який за нього отримано.
### Емітент як налаштування {#the-issuer-is-configuration}
Ось де інверсія. `OAuthClientProvider` питає сервер ресурсів, який сервер авторизації використовувати, і йде за відповіддю, куди б вона не вказувала. Цей провайдер відмовляється так робити: `issuer` обов'язковий, метадані за [RFC 8414](https://datatracker.ietf.org/doc/html/rfc8414) отримуються з власного well-known-шляху цього емітента, кінцева точка токена має належати до origin цього емітента, а сервер ресурсів ніколи ні про що не питають.
Розширення цього не вимагає; це свідомо суворіший вибір. Цей клієнт несе дві речі, які варто вкрасти: заздалегідь зареєстрований секрет і твердження, прив'язане до аудиторії, — і клієнт, який дозволив би скомпрометованому MCP-серверу скерувати себе на сервер авторизації зловмисника, надіслав би туди обидві. Фіксація емітента під час створення прибирає цю розмову взагалі.
!!! warning
Налаштований `issuer` порівнюється з полем `issuer` документа метаданих простим порівнянням
рядків за RFC 8414 §3.3: символ за символом, разом із кінцевою скісною рискою, без
нормалізації. Не вгадуйте його. Отримайте `/.well-known/oauth-authorization-server` зі свого
сервера авторизації й скопіюйте значення `issuer`, яке він повертає. Для сервера авторизації
на цій сторінці це `https://auth.example.com/`, зі скісною рискою, бо його емітент побудовано
з URL-об'єкта pydantic. Розбіжність зупиняє потік на `OAuthFlowError: Authorization server metadata issuer
mismatch` ще до того, як буде надіслано бодай одні облікові дані чи твердження.
### Конфіденційний клієнт {#a-confidential-client}
`client_secret` обов'язковий; без нього конструктор викидає `ValueError`. Профіль IETF, на якому ґрунтується [SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990), відводить цей грант лише конфіденційним клієнтам, SEP-990 вимагає, щоб клієнт автентифікувався, а цей SDK забезпечує і те, і те, наполягаючи на спільному секреті. `token_endpoint_auth_method` визначає, де він передається: `client_secret_post` (за замовчуванням, у тілі форми) або `client_secret_basic` (заголовок HTTP Basic). Профіль також дозволяє `private_key_jwt`; цей провайдер його не підтримує.
!!! tip
Читайте `client_secret` із середовища або менеджера секретів, ніколи — із системи керування версіями.
### Що провайдер робить за вас {#what-the-provider-does-for-you}
Перший запит іде без автентифікації, і відповідь сервера `401` запускає потік.
1. **Виявлення.** Він отримує метадані сервера авторизації з well-known-шляху за [RFC 8414](https://datatracker.ietf.org/doc/html/rfc8414) налаштованого емітента, перевіряє, що `issuer` документа збігається, і перевіряє, що кінцева точка токена належить до origin емітента.
2. **Твердження.** Він очікує (await) ваш `assertion_provider`.
3. **Обмін.** Він надсилає грант `jwt-bearer` методом POST на кінцеву точку токена, зберігає `OAuthToken` і повторює ваш початковий запит із `Authorization: Bearer ...`.
`403`, чий `WWW-Authenticate` називає `insufficient_scope`, знову виконує кроки 2 і 3 з об'єднанням вашого `scope` і того, що вимагається у виклику. (`scope` — це завжди лише прохання; сервер авторизації цієї сторінки надає те, що сказано в ID-JAG, і нічого більше.) Токена оновлення тут немає ніде: коли токен доступу спливає, наступний `401` випускає свіжий ID-JAG і обмінює знову, і *саме це* — важіль, який тримає IdP. Збої — ті самі два винятки, що й на решті сторінки **[OAuth-клієнти](oauth-clients.md)**: `OAuthFlowError` для виявлення й перевірки, його підклас `OAuthTokenError`, коли кінцева точка токена каже «ні».
## Сервер авторизації {#the-authorization-server}
Здебільшого на цьому можна зупинитися. Сервер авторизації MCP — це чийсь чужий продукт, приймання ID-JAG — налаштування, яке вмикається в ньому, а частина [SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990), що належить SDK, — це клієнт вище.
SDK також може *бути* сервером авторизації: `create_auth_routes` повертає маршрути сервера авторизації списком, який може змонтувати будь-який Starlette-застосунок, — саме так `examples/servers/simple-auth/` у репозиторії його запускає. SEP-990 додає до цієї поверхні один прапорець і один метод:
```python title="auth_server.py" hl_lines="48-50 105-107"
--8<-- "docs_src/identity_assertion/tutorial002.py"
```
* `identity_assertion_enabled=True` керує всім. Якщо вимкнено (а це за замовчуванням), `/token` відповідає на цей грант `unsupported_grant_type`, навіть якщо хук реалізовано, а метадані про нього не згадують. Якщо ввімкнено, метадані отримують тип гранту `jwt-bearer` і вказують `urn:ietf:params:oauth:grant-profile:id-jag` у `authorization_grant_profiles_supported` — полі, яким розширення оголошує підтримку. (Клієнт цього SDK його ніколи не читає: він налаштований на одного емітента й просто запитує.)
* **`exchange_identity_assertion`** — це хук. До його запуску SDK вже автентифікував клієнта, відмовив публічним клієнтам і відмовив клієнтам, чия реєстрація не містить цього гранту. Повертається `IdentityAssertionParams` (сире `assertion`, запитані `scopes` і `resource`), а ви повертаєте звичайний `OAuthToken`.
* Динамічна реєстрація клієнтів відхиляє цей грант безумовно, тож `get_client` тут обслуговує клієнта, заведеного вручну. ID-JAG-клієнт не може зареєструвати сам себе з нічого.
* Половина класу — відмови. `OAuthAuthorizationServerProvider` — це *весь* сервер авторизації, тож він вимагає й потоку з кодом авторизації; сервер, який також виконує вхід користувачів, реалізує його по-справжньому, а в цього рівно одні двері.
!!! warning
SDK ніколи не декодує твердження: лише ваше розгортання знає, якому IdP воно довіряє і які
ключі той IdP публікує, тож усе всередині `exchange_identity_assertion` критично важливе.
Перевірте підпис за опублікованими ключами IdP (його JWKS; спільний секрет тут — лише для
демонстрації), а також `iss` і `exp` згідно з [RFC 7523](https://datatracker.ietf.org/doc/html/rfc7523) §3. Вимагайте, щоб `typ` у заголовку
JWT дорівнював `oauth-id-jag+jwt` — це запобіжник профілю проти повторного пред'явлення
якогось іншого JWT як гранту. Вимагайте, щоб `aud` був вашим власним емітентом. Вимагайте,
щоб поле `client_id` в ID-JAG дорівнювало клієнту, якого автентифікував обробник, а його поле
`resource` називало ресурс, який ви справді обслуговуєте. Відстежуйте `jti` до `exp`
твердження, щоб воно приймалося лише раз. І беріть надані області доступу та, насамперед,
`resource` випущеного токена з перевіреного ID-JAG, а не із запиту: `params.resource` — це те,
що набрав клієнт. Повні правила обробки — у
[специфікації Enterprise-Managed Authorization](https://modelcontextprotocol.io/extensions/auth/enterprise-managed-authorization).
Відхиляйте погане твердження через `TokenError("invalid_grant", ...)`. Інший код помилки в цьому потоці — `invalid_target`: ним відхиляється ID-JAG, що називає ресурс, якого ви не обслуговуєте, — саме це не дає цьому серверу випускати токени для чужого. А надані області доступу беруться з поля `scope` в ID-JAG (твердження без нього теж відхиляється); у вашому випадку вони можуть натомість відображати групи користувача.
І зверніть увагу, чого повернений `OAuthToken` не містить: токена оновлення. IdP вирішує, як довго цей користувач зберігає доступ, вирішуючи, чи видавати наступний ID-JAG. Токен оновлення, випущений тут, тихо повернув би це рішення назад.
!!! info
Сервер, який досі вбудовує свій сервер авторизації через `auth_server_provider=`, потрапляє до
того самого коду через `AuthSettings(identity_assertion_enabled=True)`. На сторінці
**[Авторизація](../run/authorization.md)** пояснено, чому новим серверам не варто з цього починати.
!!! check
З'єднайте два файли цієї сторінки — і весь грант зведеться до одного `POST /token`:
```text
grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer
assertion=eyJhbGciOiJIUzI1NiIsInR5cCI6Im9hdXRoLWlkLWphZytqd3QifQ...
client_id=finance-agent
resource=http://localhost:8001/mcp
scope=notes:read
client_secret=finance-agent-secret
HTTP/1.1 200 OK
{"access_token": "mcp_...", "token_type": "Bearer", "expires_in": 300, "scope": "notes:read"}
```
Жодного `/authorize`, жодного `/register`, жодного отримання protected-resource-metadata.
Єдині запити в мережі — той, що отримав `401`, запит well-known, цей обмін, а далі звичайний
MCP-трафік із прикріпленим bearer-токеном. А `sub`, який ваш валідатор зчитав з ID-JAG, —
саме те, що `get_access_token().subject` показує всередині інструмента.
### Спробуйте самі {#try-it}
`examples/stories/identity_assertion/` у репозиторії SDK — це ця сторінка в дії: той самий валідатор `exchange_identity_assertion`, MCP-сервер, захищений його токенами, IdP-замінник і клієнт — в одній програмі, що перевіряє сама себе. `uv run python -m stories.identity_assertion.client --http` виконує весь обмін і перевіряє (assert), що користувач, якого назвав IdP, — це користувач, якого бачить інструмент.
## Підсумки {#recap}
* [SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990) дає змогу корпоративному провайдеру ідентичності, а не кінцевому користувачу, вирішувати, до яких MCP-серверів клієнт може звертатися. IdP підписує це рішення у вигляді **ID-JAG**.
* Отримання ID-JAG — це обмін токенів за [RFC 8693](https://datatracker.ietf.org/doc/html/rfc8693) з *вашим IdP*, і SDK його не виконує. Пред'явлення його серверу авторизації MCP — це грант `jwt-bearer` за [RFC 7523](https://datatracker.ietf.org/doc/html/rfc7523), і SDK реалізує обидва його боки.
* `IdentityAssertionOAuthProvider` — ще один `httpx2.Auth`: заздалегідь зареєстрований конфіденційний клієнт, зафіксований `issuer` і один колбек `assertion_provider(audience, resource)`. Жодного браузера, жодної реєстрації, жодного токена оновлення.
* Сервер авторизації ніколи не виявляється через сервер ресурсів. Налаштуйте `issuer` точно тим рядком, який віддає його документ метаданих; порівняння — символ за символом.
* На боці сервера — `identity_assertion_enabled=True` плюс `exchange_identity_assertion`. SDK автентифікує клієнта й контролює доступ до гранту; перевірка ID-JAG — цілком ваша, а випущений токен прив'язаний до `resource` з ID-JAG, а не із запиту.
Єдина сторона, якої ця сторінка не торкнулася, — MCP-сервер. Те, що він робить із токеном, який ви щойно випустили, він уже робив на сторінці **[Авторизація](../run/authorization.md)**.

View file

@ -0,0 +1,217 @@
---
translation:
sections: [ebef1e7a0df854f4, a4c687d3d627d516, 8e79141fc2985342, b345dd05b9c3c7ab, 80ce41579825a6fa, 5f0fa90494de8f65, 83d10514eaa62fa5, 9190555aa39a5d28, 84a4c9d8bf14dddb, 927d71cf40b58c30]
tool: 1
---
# Клієнт {#the-client}
**`Client`** — це те, через що програма на Python спілкується з MCP-сервером.
Це один об'єкт з одним життєвим циклом: створіть його, увійдіть в `async with`, викликайте методи. Кожна дія протоколу (перелічити інструменти, викликати один із них, прочитати ресурс, відрендерити промпт) — це його `async`-метод, що повертає типізований результат.
## Перший клієнт {#your-first-client}
```python title="client.py" hl_lines="14-18"
--8<-- "docs_src/client/tutorial001.py"
```
Сервер угорі потрібен лише для того, щоб було до чого під'єднатися. Клієнт — це п'ять виділених рядків.
* `Client(mcp)` отримує **сам об'єкт сервера**. Це транспорт у пам'яті: без підпроцесу, без порту, без HTTP. Саме так під'єднується кожен приклад на цій сторінці й кожен тест, який ви напишете.
* `async with` — це **життєвий цикл**. Вхід у блок під'єднує й узгоджує параметри; вихід — від'єднує. Пари `connect()` / `close()` немає, а `Client` не можна використати повторно після завершення блоку.
* Усередині блоку відомості про з'єднання вже доступні як звичайні властивості.
### Що можна передати в `Client` {#what-you-can-pass-to-client}
`Client` приймає один позиційний аргумент і визначає транспорт за його типом:
* Екземпляр `MCPServer` (або низькорівневого `Server`): під'єднання **в межах процесу**.
* Рядок з URL (`Client("http://localhost:8000/mcp")`): Streamable HTTP, шлях для робочого розгортання.
* **Транспорт**: будь-що, що можна використати як `async with ... as (read, write)`, наприклад `stdio_client(...)`, що обгортає підпроцес.
Усе інше на цій сторінці однакове для всіх трьох. Заголовки, підпроцеси, тайм-аути та протокол `Transport` мають власну сторінку: **[Транспорти клієнта](transports.md)**.
### Що є в під'єднаного клієнта {#whats-on-a-connected-client}
Чотири властивості лише для читання, заповнені в мить входу в блок:
* `client.server_info`: ідентичність сервера або `None` для сервера покоління 2026, який її не повідомляє (сервери python-sdk за замовчуванням повідомляють). `server_info.name` тут — `"Bookshop"`, а `server_info.version` — те, що повідомить сервер.
* `client.server_capabilities`: що вміє сервер (`tools`, `resources`, `prompts`, `completions`, ...). Можливість, якої сервер не має, дорівнює `None`.
* `client.protocol_version`: версія протоколу, про яку домовилися обидві сторони. Тут це `"2026-07-28"`.
* `client.instructions`: рядок `instructions=` сервера або `None`, якщо сервер його не задав.
Версію протоколу ви не обирали. За замовчуванням `Client` зондує сервер і на старіших повертається до класичного рукостискання, тож один клієнт працює із сервером будь-якого покоління. Якщо потрібно цим керувати, докладніше — на сторінці **[Версії протоколу](../protocol-versions.md)**.
!!! tip
`client.session` — це базова `ClientSession`, низькорівневий запасний вихід.
Для жодної задачі на цій сторінці вона не знадобиться.
## Перелік інструментів {#listing-tools}
```python title="client.py" hl_lines="15-20"
--8<-- "docs_src/client/tutorial002.py"
```
`list_tools()` повертає `ListToolsResult`; інструменти лежать у `.tools`. Кожен із них — повне означення, яке хост передав би моделі:
```python
tool.name # 'search_books'
tool.title # 'Search the catalog'
tool.description # 'Search the catalog by title or author.'
```
а `tool.input_schema` — це JSON Schema, яку сервер вивів з анотацій типів функції:
```json
{
"type": "object",
"properties": {
"query": {"title": "Query", "type": "string"},
"limit": {"default": 10, "title": "Limit", "type": "integer"}
},
"required": ["query"],
"title": "search_booksArguments"
}
```
Ця схема — усе, що потрібно UI, щоб показати форму аргументів, і все, що потрібно моделі, щоб сформувати коректні аргументи.
!!! tip
`title` необов'язковий, тож UI, що показує інструменти людині, має обирати: `title`, якщо він є,
і `name`, якщо немає. `from mcp.shared.metadata_utils import get_display_name` робить саме це —
для інструментів, ресурсів, шаблонів ресурсів і промптів.
## Виклик інструмента {#calling-a-tool}
`call_tool(name, arguments)` запускає інструмент і повертає `CallToolResult`.
```python title="client.py" hl_lines="26-33"
--8<-- "docs_src/client/tutorial003.py"
```
Серверний `lookup_book` повертає Pydantic-модель `Book`. Ось що бачить клієнт:
```python
result.content # [TextContent(type='text', text='{\n "title": "Dune",\n "author": "Frank Herbert",\n "year": 1965\n}')]
result.structured_content # {'title': 'Dune', 'author': 'Frank Herbert', 'year': 1965}
result.is_error # False
```
Одне повернене значення, три речі для читання. У кожної свій споживач.
### `content`: що читає модель {#content-what-the-model-reads}
`content` — це `list` **блоків вмісту**, а блок вмісту — це об'єднання типів: `TextContent`, `ImageContent`, `AudioContent`, `ResourceLink` або `EmbeddedResource`. Інструмент може повернути кілька блоків, різних видів.
Саме тому `main` звужує тип через `isinstance(block, TextContent)`, перш ніж звертатися до `block.text`. Зверніть увагу: поза `isinstance` немає жодного `.text` — перевірка типів цього не дозволить, бо `ImageContent` має `.data`, а не `.text`. Об'єднання чесно показує, що інструменту дозволено вам надіслати; ваш код має бути таким самим чесним.
### `structured_content`: що читає ваш застосунок {#structured_content-what-your-application-reads}
`structured_content` — це повернене значення інструмента у вигляді JSON, що відповідає оголошеній `output_schema` інструмента. Жодного розбору рядків, жодних здогадок.
Коли є обидва, вони навмисно кажуть те саме двічі: `content` — для моделі, `structured_content` — для коду. Звідки береться структурована половина і як нею керувати — на сторінці **[Структурований вивід](../servers/structured-output.md)**.
### `is_error`: чи завершився інструмент помилкою {#is_error-whether-the-tool-failed}
Інструмент, що викидає виняток, **не** викидає його у вашому клієнті. Він повертається як звичайний результат з `is_error=True`.
!!! check
Попросіть у `lookup_book` `"Solaris"` (назву, якої немає в каталозі) — і функція викине
`ValueError`. Виклик усе одно повернеться нормально:
```python
result.is_error # True
result.content # [TextContent(type='text', text="Error executing tool lookup_book: No book titled 'Solaris' in the catalog.")]
result.structured_content # None
```
Повідомлення винятку потрапило в `content`, де його може прочитати **модель** і спробувати ще раз. Це
навмисно: помилка інструмента — частина розмови, а не аварія. Завжди дивіться на `is_error`,
перш ніж довіряти `structured_content`.
!!! warning
`is_error=True` охоплює більше, ніж ваш власний `raise`. Попросіть інструмент, якого в сервера
взагалі немає (`call_tool("does_not_exist", {})`), — і нічого не викидається. Повертається та сама форма:
`is_error=True` з `Unknown tool: does_not_exist` у `content`. Метод `Client` викидає
`MCPError` лише тоді, коли сервер відповідає **помилкою** JSON-RPC замість результату, а коли
сервер повертає що саме — описано на сторінці **[Обробка помилок](../servers/handling-errors.md)**.
## Ресурси {#resources}
Дії з ресурсами йдуть парами: два способи перелічити, один спосіб прочитати.
```python title="client.py" hl_lines="22-31"
--8<-- "docs_src/client/tutorial004.py"
```
* `list_resources()` повертає **конкретні** ресурси — ті, що мають фіксований URI. Тут: `['catalog://genres']`.
* `list_resource_templates()` повертає **параметризовані**. Тут: `['catalog://genres/{genre}']`. Це два різні списки, бо шаблон не можна прочитати, доки його не заповнено.
* `read_resource(uri)` приймає URI як звичайний `str` і працює з обома: передайте `"catalog://genres/poetry"` — і сервер зіставить його з шаблоном.
`read_resource` повертає `contents` — список `TextResourceContents` або `BlobResourceContents`. Та сама ідея, що й із вмістом інструментів: звузьте тип через `isinstance`, потім читайте `.text` (або `.blob`).
Клієнта також можна сповіщати про зміни ресурсу. На з'єднаннях покоління 2025 це `subscribe_resource(uri)` / `unsubscribe_resource(uri)` — пара методів, яку `MCPServer` не реалізує, тож у протоколі 2026-07-28 (де цих дій уже немає) запит повертає `-32601`, *Method not found*. Заміна у версії 2026 — потік `subscriptions/listen`, який `MCPServer` *таки* обслуговує — `server_capabilities.resources.subscribe` там дорівнює `True` — а як споживати його через `client.listen(...)`, описано на сторінці **[Підписки](subscriptions.md)** цього розділу.
## Промпти {#prompts}
```python title="client.py" hl_lines="15-20"
--8<-- "docs_src/client/tutorial005.py"
```
`list_prompts()` повідомляє, що пропонує сервер і що потрібно кожному промпту:
```python
prompt.name # 'recommend'
prompt.title # 'Recommend a book'
prompt.arguments # [PromptArgument(name='genre', required=True)]
```
`get_prompt(name, arguments)` рендерить його. Словник аргументів — `str -> str`: аргументи промпту завжди рядки. Результат — `messages`, список `PromptMessage`, кожне з `role` і блоком `content`:
```python
message.role # 'user'
message.content # TextContent(type='text', text='Recommend one poetry book from the catalog and say why.')
```
Хост передає ці повідомлення прямо моделі. Оце й уся можливість.
## Автодоповнення {#completions}
Сервер з обробником автодоповнення може доповнювати аргументи промптів і шаблонів ресурсів, поки користувач друкує.
```python title="client.py" hl_lines="27-31"
--8<-- "docs_src/client/tutorial006.py"
```
* `ref` вказує, *який* промпт чи шаблон ви заповнюєте: `PromptReference` або `ResourceTemplateReference`.
* `argument` — це `{"name": ..., "value": ...}`: аргумент і те, що користувач уже встиг набрати.
Відповідь — у `result.completion.values`. Наберіть `"p"` — і сервер поверне `['poetry']`. Серверний бік, а також те, як обробник використовує *інші*, уже заповнені аргументи, щоб звузити свої пропозиції, — на сторінці **[Автодоповнення](../servers/completions.md)**.
## Пагінація {#pagination}
Кожен метод `list_*` приймає іменований аргумент `cursor=`, а кожен результат містить `next_cursor`. Коли `next_cursor` дорівнює `None`, у вас є все.
```python title="client.py" hl_lines="22-30"
--8<-- "docs_src/client/tutorial007.py"
```
Цей цикл коректний для будь-якого сервера. `MCPServer` повертає все однією сторінкою, тож `next_cursor` дорівнює `None` і цикл виконується один раз — саме тому більшість коду його ніколи не пише. Сервери, що справді розбивають результати на сторінки, і правила, яким підкоряються курсори, — на сторінці **[Пагінація](../advanced/pagination.md)**.
## У тестах {#in-tests}
`Client(mcp)` без процесу й без порту — це вже тестова обв'язка для вашого сервера.
Саме для цього є один прапорець конструктора: `Client(mcp, raise_exceptions=True)`. Він діє лише на з'єднаннях у пам'яті, а пояснює його й будує навколо нього весь підхід сторінка **[Тестування](../get-started/testing.md)**.
## Підсумки {#recap}
* `Client(x)` під'єднується в пам'яті до об'єкта сервера, через Streamable HTTP — до рядка з URL і через транспорт — до всього іншого.
* `async with` — це весь життєвий цикл. Усередині нього `server_capabilities` і `protocol_version` уже заповнені; `server_info` та `instructions` — теж, якщо сервер їх надає.
* `list_tools()` дає `name`, `title`, `description` та `input_schema` кожного інструмента.
* `call_tool()` повертає `content` для моделі, `structured_content` для вашого коду та `is_error`. Інструмент, що викидає виняток, — це результат, а не виняток.
* `content` — об'єднання типів блоків; звужуйте тип через `isinstance`, перш ніж читати.
* `list_resources` / `list_resource_templates` / `read_resource`, `list_prompts` / `get_prompt` і `complete` доповнюють набір дій.
* Кожен `list_*` приймає `cursor=`; повторюйте цикл, доки `next_cursor` не стане `None`.
Про що сервер може попросити *клієнта* і як на це відповідати — на сторінці **[Колбеки клієнта](callbacks.md)**.

View file

@ -0,0 +1,152 @@
---
translation:
sections: [c6899d3892bd9fa0, 79372cff3cc48a88, 63878d29e87c3e73, 13175843d3588af4, e7e2b9fd516f77de, 758f06399b513c1f, a05d7278487d610b]
tool: 1
---
# OAuth-клієнти {#oauth-clients}
Деякі MCP-сервери захищені. Надішліть їм запит без токена — і у відповідь прийде `401 Unauthorized`.
Отримати токен допомагає **`OAuthClientProvider`**. Це взагалі не об'єкт MCP. Це `httpx2.Auth` — стандартний хук httpx2 на кшталт «зроби щось із кожним запитом». Його під'єднують до `httpx2.AsyncClient`, передають цей клієнт транспорту Streamable HTTP — і більше про нього не згадують.
Ця сторінка — про клієнтський бік. Як змусити власний сервер вимагати токен — на сторінці **[Авторизація](../run/authorization.md)**.
## Провайдер {#the-provider}
```python title="client.py" hl_lines="44-54"
--8<-- "docs_src/oauth_clients/tutorial001.py"
```
Йому передають чотири речі:
* `server_url`: MCP-ендпоінт, до якого ви під'єднуєтеся. Усе інше провайдер виявляє за ним сам.
* `client_metadata`: те, що ви ввели б у форму «зареєструвати застосунок» на сервері авторизації.
* `storage`: де токени зберігаються між запусками.
* `redirect_handler` і `callback_handler`: два моменти, коли потрібна участь людини.
Більше ніде у файлі OAuth не згадується. `main()` ніколи не бачить токена.
### Метадані клієнта {#client-metadata}
`OAuthClientMetadata` — це справжній реєстраційний документ із [RFC 7591](https://datatracker.ietf.org/doc/html/rfc7591), оформлений як модель Pydantic.
Ви задаєте три поля. Решту заповнюють типові значення: `grant_types` уже дорівнює `["authorization_code", "refresh_token"]`, а `response_types` — `["code"]`, і це саме той потік, який виконує цей провайдер.
!!! check
Оскільки це модель Pydantic, вона проходить валідацію **ще до того, як у мережу піде бодай один байт**.
Пропустіть `redirect_uris` — і створення об'єкта одразу завершиться `ValidationError`, який
називає поле:
```text
redirect_uris
Field required [type=missing, input_value={'client_name': 'Bookshop Agent'}, input_type=dict]
```
Браузер не відкрився, на сервері авторизації не залишилося напівзавершеної реєстрації.
### Сховище токенів {#token-storage}
**`TokenStorage`** — це `Protocol` із чотирма асинхронними методами. Успадковуватися ні від чого не потрібно: напишіть ці методи — і будь-який клас стає сховищем токенів:
* `get_tokens` / `set_tokens` зберігають `OAuthToken`: токен доступу, токен оновлення, термін дії, область дії.
* `get_client_info` / `set_client_info` зберігають `OAuthClientInformationFull`, який сервер авторизації видав, коли провайдер вас зареєстрував, — разом із вашим `client_id`.
Наведена вище версія в пам'яті працює. Але вона все забуває, щойно процес завершується, тож наступний запуск повторює всю процедуру з початку. Збережіть дані у файл або в системне сховище ключів вашої платформи — і наступний запуск пройде тихо.
!!! tip
Зберігайте `client_info`, а не лише токени. Провайдер реєструється динамічно, коли вперше
не знаходить збереженого `client_info`. Викиньте його — і кожен запуск створюватиме нову реєстрацію.
### Два обробники {#the-two-handlers}
Потоку authorization code людина потрібна рівно один раз: хтось має увійти й натиснути «Дозволити».
* **`redirect_handler`** викликається через await із повністю зібраним URL авторизації. `client_id`, `redirect_uri`, `state` і PKCE challenge уже в ньому. Ваше єдине завдання — відкрити його в браузері. Настільний застосунок викликає `webbrowser.open`; цей файл просто друкує його.
* **`callback_handler`** очікується наступним. Він чекає, доки користувач повернеться на ваш `redirect_uri`, і повертає параметри запиту цього перенаправлення як `AuthorizationCodeResult`.
Справжній клієнт замість виклику `input()` запускає невеликий локальний HTTP-сервер на redirect URI. Форма та сама: отримати перенаправлення, повернути `code`, `state` та `iss`.
!!! warning
Передавайте `state` та `iss` далі точно такими, якими вони надійшли. Провайдер порівнює `state`
з тим, що згенерував сам, а `iss` — із виявленим видавцем, і відхиляє розбіжність. Це захист
від CSRF і від атак із підміною сервера (mix-up).
### Усередину `Client` {#into-the-client}
Погляньте на `main()`. Провайдер чіпляється до **клієнта httpx2**, клієнт httpx2 передається у `streamable_http_client(url, http_client=...)`, а цей транспорт — у `Client`.
У `streamable_http_client` немає іменованого аргументу `auth=`. Усе, що стосується рівня HTTP (автентифікація, заголовки, тайм-аути, проксі), належить до `httpx2.AsyncClient`, який ви приносите самі. Про це розшарування — на сторінці **[Транспорти клієнта](transports.md)**.
## Що провайдер робить за вас {#what-the-provider-does-for-you}
Коли `Client` уперше надсилає запит, сервер відповідає `401`. Далі справу бере на себе провайдер:
1. **Виявлення.** Він читає заголовок `WWW-Authenticate`, завантажує Protected Resource Metadata сервера з `/.well-known/oauth-protected-resource`, дізнається, який сервер авторизації захищає цей ресурс, і завантажує метадані *того* сервера.
2. **Реєстрація.** У сховищі порожньо? Він динамічно реєструє вас із вашими `OAuthClientMetadata` і зберігає результат.
3. **Авторизація.** Він генерує пару PKCE і `state`, будує URL авторизації, очікує ваш `redirect_handler`, а потім — ваш `callback_handler`, щоб отримати код.
4. **Обмін.** Він міняє код на `OAuthToken`, зберігає його й повторює ваш початковий запит із `Authorization: Bearer ...`.
Після цього все тихо. Токени беруться зі сховища, прострочений токен доступу оновлюється за допомогою токена оновлення, і лише коли ніщо з цього не спрацьовує, провайдер запускає потік знову.
Нічого з цього ви не писали. Лишаються ще два іменовані аргументи (`client_metadata_url` і `validate_resource_url`), і цьому файлу не потрібен жоден. Знати варто про `client_metadata_url`; йому присвячено окремий розділ нижче.
### Спробуйте самі {#try-it}
Більшість прикладів у цій документації можна перевірити за допомогою `Client(server)` у пам'яті. Цей — ні: уся суть потоку в HTTP-відповіді `401`, а між клієнтом у пам'яті та його сервером HTTP немає.
У репозиторії є жива версія. `examples/servers/simple-auth/` запускає окремий сервер авторизації та захищений MCP-сервер; `examples/clients/simple-auth-client/` — це клієнт із цієї сторінки, що виріс у невеликий CLI. У його README є дві команди: запустіть сервери, запустіть клієнт проти них — і спостерігайте, як проходять усі чотири кроки.
## Client ID Metadata Documents {#client-id-metadata-documents}
Ревізія специфікації 2026-07-28 оголошує динамічну реєстрацію клієнтів застарілою на користь **Client ID Metadata Documents** (CIMD). Замість надсилати POST із новою реєстрацією на кожен сервер авторизації, що трапляється на шляху, ваш клієнт публікує один JSON-документ про себе за стабільним HTTPS URL, і цей URL *і є* його `client_id`. Документ завантажує сервер авторизації; провайдер його ніколи не торкається.
SDK це вже підтримує: передайте URL як `client_metadata_url=`, коли створюєте провайдер. Якщо метадані сервера авторизації оголошують `client_id_metadata_document_supported: true`, провайдер повністю пропускає запит `/register`: URL іде в потік як `client_id`, а `client_secret` немає взагалі. Якщо сервер цього не оголошує (більшість поки що ні) або ви так і не передали URL, провайдер **мовчки** повертається до динамічної реєстрації, і все описане вище працює саме так, як описано. Збережений `client_info` і далі має пріоритет над обома варіантами.
URL має бути HTTPS із некореневим шляхом; усе інше — це `ValueError` під час створення, ще до будь-якого звернення до мережі. Приклад `examples/clients/simple-auth-client/` із репозиторію приймає його через змінну середовища `MCP_CLIENT_METADATA_URL`.
## Взаємодія між машинами {#machine-to-machine}
Нічне завдання, крок CI, інший сервіс. Браузера немає, і натиснути «Дозволити» нікому. Це грант **client credentials**: у вас уже є `client_id` і `client_secret`, а весь потік зводиться до ендпоінта токенів.
`ClientCredentialsOAuthProvider` — це той самий `httpx2.Auth`, тільки без людини:
```python title="client.py" hl_lines="4 27-33"
--8<-- "docs_src/oauth_clients/tutorial002.py"
```
Що змінилося:
* Немає `OAuthClientMetadata`, немає обробників. Ви передаєте `client_id` і `client_secret`; провайдер будує навколо них мінімальну реєстрацію `client_credentials` і повністю пропускає динамічну реєстрацію.
* `scope` — це рядок, розділений пробілами: такий формат OAuth використовує в переданих даних.
* Усе далі ідентичне: той самий `TokenStorage`, той самий `httpx2.AsyncClient(auth=...)`, той самий `streamable_http_client`.
За замовчуванням секрет передається як HTTP Basic auth у запиті на токен (`client_secret_basic`). Передайте `token_endpoint_auth_method="client_secret_post"`, щоб натомість помістити його в тіло форми. Деякі сервери авторизації приймають лише один із двох варіантів.
!!! tip
Читайте `client_secret` зі змінних середовища або менеджера секретів, ніколи не з системи контролю версій.
!!! info
Ще один провайдер живе в `mcp.client.auth.extensions.client_credentials`:
**`PrivateKeyJWTOAuthProvider`** — для клієнтів, які автентифікуються за допомогою JWT замість
спільного секрету (`private_key_jwt`, варіант із парою ключів та workload identity). Він дотримується
тієї ж схеми: створіть екземпляр і передайте в `auth=`. У тому ж модулі є
`SignedJWTParameters` і `static_assertion_provider` — два допоміжні засоби, що будують для нього assertion.
Є ще одна ситуація без людини: клієнт належить підприємству, де провайдер ідентичності, а не користувач, вирішує, до яких MCP-серверів він може звертатися. Це інший грант із власною моделлю довіри та власною сторінкою — **[Твердження про ідентичність](identity-assertion.md)**.
## Коли щось іде не так {#when-it-fails}
Коли в OAuth-потоці щось іде не так, провайдер викидає `OAuthFlowError` з `mcp.client.auth`. У нього два підкласи. `OAuthRegistrationError` означає, що реєстрація не дала клієнта, яким можна скористатися: сервер авторизації відмовився вас реєструвати або таки зареєстрував, але з обліковими даними, які цей потік не може використати (наприклад, із методом автентифікації, якого він не реалізує). `OAuthTokenError` означає, що токен отримати не вдалося: ендпоінт токенів відмовив, або збережений запис клієнта містить метод автентифікації, який цей клієнт не може застосувати, — про це повідомляється ще під час побудови запиту на токен, а не після надсилання. Один `except OAuthFlowError:` охоплює виявлення, реєстрацію, авторизацію та обмін.
Не все є помилкою потоку. Мережа, як і раніше, може підвести; це звичайні винятки `httpx2`, і вони проходять наскрізь без змін.
## Підсумки {#recap}
* `OAuthClientProvider` — це `httpx2.Auth`. Під'єднайте його до `httpx2.AsyncClient`, передайте той у `streamable_http_client(url, http_client=...)` — і `Client` ніколи не дізнається, що відбувся OAuth.
* Ви надаєте чотири речі: URL сервера, `OAuthClientMetadata`, `TokenStorage` і пару обробників redirect/callback.
* `TokenStorage` — це `Protocol`: чотири асинхронні методи, жодного базового класу. Зберігайте `client_info` так само, як і токени.
* Виявлення, реєстрація (динамічна або через **Client ID Metadata Document**), PKCE, перевірки `state` та `iss` і оновлення токенів — робота провайдера, а не ваша.
* `ClientCredentialsOAuthProvider` — версія без людини: `client_id` + `client_secret`, без обробників, без браузера.
* Кожен збій OAuth — це `OAuthFlowError`; `OAuthRegistrationError` і `OAuthTokenError` — його підкласи.
Друга половина цього рукостискання — змусити ваш *сервер* вимагати токен — на сторінці **[Авторизація](../run/authorization.md)**.

View 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)**.

View 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)**.

View file

@ -0,0 +1,128 @@
---
translation:
sections: [9cac816674181eb0, 0700f337babcd4dd, 2bde0dd58cdf00f5, ff7401df479af877, 3d0832f39b0d7059, d4bf7e4479637768, 05e20c0a798860e7]
tool: 1
---
# Транспорти клієнта {#client-transports}
Кожен `Client` спілкується зі своїм сервером через **транспорт** — те, що власне й переносить повідомлення.
Окремо його налаштовувати не доводиться. `Client` приймає один позиційний аргумент і визначає транспорт за його типом.
*Серверний* бік кожного з них (що робить `mcp.run()` і що ви розгортаєте) описано на сторінці **[Запуск сервера](../run/index.md)**.
## У пам'яті {#in-memory}
Передайте сам об'єкт сервера:
```python title="client.py" hl_lines="14"
--8<-- "docs_src/client_transports/tutorial001.py"
```
Жодного підпроцесу, жодного порту, жодних байтів у мережі. Клієнт і сервер — це два об'єкти в одному процесі, а виклик усе одно проходить через справжній протокольний рівень: `search_books` перелічується, валідується й викликається точнісінько так само, як це було б через HTTP.
Тож це одразу дві речі:
* **Тестовий стенд.** Кожен приклад у цій документації перевіряється саме так, а сторінка **[Тестування](../get-started/testing.md)** будує довкола цього весь підхід.
* **API для вбудовування.** Застосунку, який сам створює сервер, не потрібен мережевий перехід, щоб викликати його інструменти.
## Streamable HTTP {#streamable-http}
Передайте рядок з URL — і отримаєте **Streamable HTTP**, транспорт, за яким розгортають сервер:
```python title="client.py" hl_lines="5"
--8<-- "docs_src/client_transports/tutorial002.py"
```
Оце й увесь продакшен-клієнт. `Client` сам загортає URL у `streamable_http_client(...)` поверх `httpx2.AsyncClient`, налаштованого так, як потрібно MCP: `follow_redirects=True`, 30-секундний тайм-аут на connect/write/pool і 300-секундний тайм-аут на читання, бо сервер може тримати потік відповіді відкритим.
!!! check
Створений `Client` ще **не** під'єднаний. Конструктор лише обирає транспорт;
відкриває його `async with`. Спробуйте звернутися до з'єднання до входу в блок — і SDK про це скаже:
```text
RuntimeError: Client must be used within an async context manager
```
Коли ви написали `Client("http://...")`, нічого не розв'язувалося, не завантажувалося й не запускалося. Цей рядок нічого не коштує.
### Власний `httpx2.AsyncClient` {#bring-your-own-httpx2asyncclient}
Щойно знадобиться заголовок `Authorization`, cookie, проксі, mTLS чи інший тайм-аут — створіть `httpx2.AsyncClient` самостійно й передайте його в `streamable_http_client`:
```python title="client.py" hl_lines="8-14"
--8<-- "docs_src/client_transports/tutorial003.py"
```
Зверніть увагу на дві речі:
* `httpx2.AsyncClient` належить вам, тож саме **ви** входите в нього й виходите з нього. SDK ніколи не закриває клієнт, якого не створював.
* `streamable_http_client(url, http_client=...)` повертає транспорт, а `Client(transport)` приймає його, як і будь-що інше.
Одне зауваження щодо TLS: `httpx2` перевіряє сертифікати за сховищем довіри операційної системи (через
[`truststore`](https://pypi.org/project/truststore/)), а не за вбудованим списком CA. У середовищі
без придатного системного сховища CA (деякі мінімальні контейнери) задайте стандартні змінні середовища `SSL_CERT_FILE`/`SSL_CERT_DIR`
або передайте явний `verify=ssl_context` у свій `httpx2.AsyncClient`
(подробиці — у розділі
[`httpx` і `httpx-sse` замінено на `httpx2`](../migration.md#httpx-and-httpx-sse-replaced-by-httpx2)).
!!! warning
Раніше `streamable_http_client` приймав `headers=` і `timeout=` напряму. Більше ні:
його єдині параметри — `url`, `http_client` і `terminate_on_close`. Напишете `headers=`
за звичкою — і отримаєте:
```text
TypeError: streamable_http_client() got an unexpected keyword argument 'headers'
```
Усе, що стосується HTTP, тепер живе на тому єдиному `httpx2.AsyncClient`, який ви передаєте.
!!! info
`httpx2` зберігає знайомий API `httpx`, тож якщо ви знаєте `httpx`, то вже вмієте робити тут автентифікацію,
проксі, хуки подій, повторні спроби й обмеження з'єднань. SDK нічого не додає зверху й нічого не
забирає. Саме сюди під'єднується й OAuth:
`httpx2.AsyncClient(auth=OAuthClientProvider(...))`. Увесь цей процес описано на сторінці **[OAuth-клієнти](oauth-clients.md)**.
## stdio {#stdio}
Сервер **stdio** — це підпроцес. Клієнт запускає його, пише JSON-RPC в його stdin і читає JSON-RPC з його stdout. Саме так десктопний хост запускає сервер на вашій машині: хост і *є* цим кодом плюс UI, а сторінка **[Під'єднання до справжнього хоста](../get-started/real-host.md)** показує ті самі стосунки з боку хоста — як конфігураційний файл.
Опишіть процес за допомогою `StdioServerParameters`, перетворіть його на транспорт через `stdio_client` і передайте *це* в `Client`:
```python title="client.py" hl_lines="4-8 12"
--8<-- "docs_src/client_transports/tutorial004.py"
```
`Client` не приймає сам об'єкт параметрів. `StdioServerParameters` — це конфігурація; `stdio_client(server)` — транспорт, який уміє запустити з неї процес. Завжди загортайте.
Вихід із блоку `async with` також завершує підпроцес: закриває stdin, чекає, вбиває, якщо той затримується. Прибирати за ним самостійно ніколи не доведеться.
!!! warning
Дочірній процес **не** успадковує ваше середовище. Він отримує мінімальний список дозволених змінних (`HOME`, `LOGNAME`,
`PATH`, `SHELL`, `TERM` і `USER` на POSIX), щоб нічого чутливого не просочилося в процес,
який, можливо, писали не ви.
Сервер, якому потрібен ключ API, там його не знайде. Передайте його явно через `env=`; ці
змінні накладаються поверх списку дозволених. Саме це й робить `BOOKSHOP_API_KEY` вище.
## SSE {#sse}
`sse_client(url)` з `mcp.client.sse` — це HTTP-транспорт, на зміну якому прийшов Streamable HTTP. Загортайте його так само, `Client(sse_client("http://localhost:8000/sse"))`, щоб говорити із сервером, який досі ним користується, — і не будуйте на ньому нічого нового.
## Протокол `Transport` {#the-transport-protocol}
Для `Client` усе перелічене вище — одне й те саме.
**Транспорт** — це будь-який асинхронний контекстний менеджер, що повертає пару потоків повідомлень `(read, write)`: формально — протокол `Transport` у `mcp.client`. `Client` розв'язує свій аргумент за типом: об'єкт сервера під'єднується в межах процесу, `str` стає `streamable_http_client(url)`, а в будь-що інше він входить безпосередньо як у транспорт. Саме завдяки останньому правилу `stdio_client(...)`, `streamable_http_client(...)` і `sse_client(...)` стають на одне й те саме місце — і саме тому можна написати власний.
## Підсумки {#recap}
* `Client(mcp)` (об'єкт сервера) під'єднується в пам'яті. Використовуйте для тестів і для вбудовування.
* `Client("http://.../mcp")` (URL) під'єднується через Streamable HTTP, продакшен-транспорт.
* Заголовки, автентифікація, проксі й тайм-аути належать `httpx2.AsyncClient`, який ви передаєте в `streamable_http_client(url, http_client=...)`. Іменованого аргументу `headers=` немає.
* stdio — це `Client(stdio_client(StdioServerParameters(...)))`, ніколи не сам об'єкт параметрів.
* Підпроцес отримує середовище зі списку дозволених, а не ваше; `env=` його доповнює.
* Транспорт — це будь-що, з чим можна зробити `async with x as (read, write)`. Усе, що не є об'єктом сервера чи URL, `Client` передає прямо цьому протоколу.
* Створення `Client` обирає транспорт. `async with` його відкриває.
Щойно транспорт відкрито, обидві сторони мають домовитися про версію протоколу. Зазвичай про це не думаєш; а коли доводиться — є сторінка **[Версії протоколу](../protocol-versions.md)**.

View file

@ -0,0 +1,98 @@
---
translation:
sections: [20541a40dbdd5980, 01262a123ad9501d, 429db5b574a2ac08, 56b2d49da412cb28, 6a1717123fe4513c]
tool: 1
---
# Застарілі можливості {#deprecated-features}
Специфікація 2026-07-28 виводить з ужитку п'ять речей. SDK і далі реалізує кожну з них, і кожна тепер супроводжується **попередженням про застарілість**.
Таблиця нижче називає кожну застарілу можливість, пояснює, чому вона зникає, і вказує заміну, на яку варто спиратися.
## Що застаріло {#what-is-deprecated}
| Застаріле | Чому | Що робити натомість |
|---|---|---|
| **Кореневі каталоги (roots)**: `ctx.session.list_roots()`, `client.send_roots_list_changed()`, колбек `list_roots_callback=`, який передають у `Client(...)` | [SEP-2577](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2577) виводить цю можливість з ужитку. | Приймайте шляхи як звичайні аргументи інструмента чи URI ресурсів або вбудуйте `ListRootsRequest` в `InputRequiredResult` (див. **[Багатораундові запити (multi-round-trip)](handlers/multi-round-trip.md)**). |
| **Семплювання (sampling) з ініціативи сервера**: `ctx.session.create_message()`, колбек `sampling_callback=`, який передають у `Client(...)` | SEP-2577 виводить цю можливість з ужитку. | Повертайте `InputRequiredResult`, і нехай клієнт повторить виклик (див. **[Багатораундові запити](handlers/multi-round-trip.md)**). |
| **Протокольне логування**: `ctx.log()`, `ctx.debug()`, `ctx.info()`, `ctx.warning()`, `ctx.error()`, `ctx.session.send_log_message()`, `client.set_logging_level()` | SEP-2577 виводить цю можливість з ужитку. У протоколі її ніщо не замінює. | Звичайний `import logging` у stderr (див. **[Логування](handlers/logging.md)**). |
| **`ping`**: `client.send_ping()` | **Вилучено** з протоколу, а не просто оголошено застарілим. У 2026-07-28 методу `ping` немає. | Нічого. Він працює лише на з'єднанні з `mode="legacy"`. |
| **Перебіг виконання від клієнта до сервера**: `client.send_progress_notification()` | У 2026-07-28 перебіг виконання передається лише від сервера до клієнта. | Надсилати нічого. Про перебіг виконання звітує ваш *сервер* через `ctx.report_progress()` (див. **[Перебіг виконання](handlers/progress.md)**). |
З цієї таблиці випливають три речі:
* Кореневі каталоги, семплювання й логування йдуть разом. Одна пропозиція, **SEP-2577**, оголошує застарілими всі три можливості одразу.
* Семплювання й кореневі каталоги мають спільну глибшу проблему: це місця, де **сервер** надсилає **запит** **клієнту**. Саме цей напрямок цілком 2026-07-28 замінює на **[багатораундові запити](handlers/multi-round-trip.md)**. Зникли окремі RPC-методи (`sampling/createMessage`, `roots/list` і push-варіант `elicitation/create`); типи корисного навантаження `CreateMessageRequest` / `ListRootsRequest` / `ElicitRequest` залишаються — вбудовані в `InputRequiredResult.input_requests`, а на клієнті вони потрапляють у ті самі колбеки.
* `ping` стоїть осторонь. Протокол не оголошує його застарілим, а вилучає. Метод SDK і далі попереджає (у його повідомленні сказано *removed*, а не *deprecated*), а виклик на сучасному з'єднанні отримує у відповідь *«Method not found»*.
## Застарілість має рекомендаційний характер {#deprecated-is-advisory}
Сьогодні нічого не ламається.
Кожен із наведених методів і далі працює з будь-якою сесією, що узгодила **2025-11-25 або ранішу версію**. Зафіксуйте `mode="legacy"` на клієнті — і отримаєте точнісінько ту поведінку, що була до 2026. У переданих даних нічого не змінюється, узгодження можливостей теж без змін.
Змінюється те, що під час першого виконання кожного з них з'являється помітне попередження:
```text
MCPDeprecationWarning: The logging capability is deprecated as of 2026-07-28 (SEP-2577).
```
`MCPDeprecationWarning` успадковує `UserWarning`, а **не** `DeprecationWarning`. Це навмисно: типовий фільтр Python показує `DeprecationWarning` лише в коді, запущеному безпосередньо як `__main__`, — саме так бібліотеки оголошують щось застарілим, і два роки ніхто цього не помічає. Це попередження видно всюди, без жодного прапорця `-W`.
!!! warning
«Рекомендаційний характер» закінчується на рівні переданих даних. Семплювання й кореневі
каталоги — це *запити* від сервера до клієнта, а сесія 2026-07-28 не має каналу, яким
їх можна передати. Викличте `ctx.session.create_message()` усередині інструмента на
сучасному з'єднанні — попередження все одно спрацює, а потім надсилання завершиться
помилкою:
```text
Cannot send 'sampling/createMessage': this transport context has no back-channel
for server-initiated requests.
```
Два сигнали, саме в такому порядку. `MCPDeprecationWarning` спрацьовує тієї ж миті, коли
ви викликаєте метод, на будь-якому з'єднанні. Помилка — це те, що повертається, коли SDK
потім намагається надіслати запит. Ці два методи працюють від початку до кінця лише на
з'єднанні з `mode="legacy"`, клієнт якого зареєстрував відповідний колбек.
## Приглушення попередження {#silencing-the-warning}
У новому коді — не робіть цього.
Але сервер, який ви підтримуєте і який справді обслуговує клієнтів до 2026, має повне право на тихий лог. Відфільтруйте категорію до того, як виконається перший застарілий виклик:
```python
import warnings
from mcp import MCPDeprecationWarning
warnings.filterwarnings("ignore", category=MCPDeprecationWarning)
```
Оце й увесь API. Перемикача для окремих методів немає, і він вам не потрібен: сенс однієї категорії в тому, що один рядок її приглушує, а один рядок повертає.
!!! check
Розверніть фільтр у зворотний бік — і отримаєте безкоштовний регресійний тест. Додайте
`"error::mcp.MCPDeprecationWarning"` до налаштування `filterwarnings` у конфігурації
pytest — і застарілий виклик **викидатиме виняток** замість попередження. Інструмент
з назвою `old_log`, який досі викликає `ctx.info()`, перестає проходити тест і починає
повідомляти:
```text
Error executing tool old_log: The logging capability is deprecated as of 2026-07-28 (SEP-2577).
```
Один рядок конфігурації pytest — і застарілий виклик більше ніколи не прокрадеться назад
у вашу кодову базу, не проваливши тест.
## Підсумки {#recap}
* Специфікація 2026-07-28 оголошує застарілими **кореневі каталоги**, **семплювання** з ініціативи сервера та протокольне **логування** (усе — [SEP-2577](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2577)), обмежує **перебіг виконання** напрямком від сервера до клієнта й вилучає **`ping`**.
* Стовпець із замінами вказує, куди рухатися далі: **[Багатораундові запити](handlers/multi-round-trip.md)** для семплювання й кореневих каталогів, **[Логування](handlers/logging.md)** для логування, **[Перебіг виконання](handlers/progress.md)** для перебігу виконання. `ping` не потребує взагалі нічого.
* Застарілість має рекомендаційний характер: жодних змін у переданих даних, усе й далі працює із сесіями до 2026, а ви отримуєте помітне попередження `MCPDeprecationWarning` (це `UserWarning`, тож воно ввімкнене за замовчуванням).
* Семплювання й кореневі каталоги додатково потребують зворотного каналу (back-channel), якого сесія 2026-07-28 не має. На сучасному з'єднанні вони попереджають, а потім викидають виняток.
* `warnings.filterwarnings("ignore", category=MCPDeprecationWarning)` приглушує всю категорію; `"error::mcp.MCPDeprecationWarning"` у pytest перетворює її на провал тесту.
* Новий код не варто будувати на жодній із цих можливостей.
Усі інші сторінки цієї документації навчають чинного API.

View file

@ -0,0 +1,144 @@
---
translation:
sections: [0d6c05bcbf836bf3, 59a7b14eeefc68c1, 7114d8d6daba203f, e8bbb56a98ba7bc9, 5138010f6159901c, f78da7c7c363d4c6, 220a939cab348686]
tool: 1
---
# Перші кроки {#first-steps}
**[Головна сторінка](../index.md)** рухається швидко: написати сервер, запустити його, викликати інструмент.
Ця сторінка йде повільніше: усі три речі, які може надавати сервер, і назва для всього, що трапиться дорогою.
## Хост, клієнт і сервер {#host-client-and-server}
Три слова, які ви бачитимете на кожній сторінці відтепер:
* **Хост** — це LLM-застосунок: Claude, IDE, середовище виконання агентів. Це те, з чим говорить користувач.
* **Клієнт** живе всередині хоста й говорить мовою MCP. Хост запускає по одному клієнту на кожен сервер, до якого під'єднаний.
* **Сервер** — це те, що ви створюєте за допомогою цього SDK. Він надає речі клієнтам. Він ніколи не говорить із моделлю напряму.
Ви пишете сервер. Хости — це чийсь інший продукт. SDK також дає вам `Client`. Ним ви тестуватимете свої сервери, і він з'явиться далі на цій сторінці.
## Три примітиви {#the-three-primitives}
Сервер надає рівно три види речей. Відрізняє їх те, **хто вирішує їх використати**:
| Примітив | Хто керує | Що це таке | Приклад |
|---------------|-----------------|-----------------------------------------------------------------|------------------------------------|
| **Інструменти** | Модель | Функція, яку модель викликає, щоб виконати дію | Виклик API, запис у базу даних |
| **Ресурси** | Застосунок | Дані, які хост завантажує в контекст моделі | Вміст файлу, відповідь API |
| **Промпти** | Користувач | Багаторазовий шаблон повідомлення, який користувач викликає за назвою | Слеш-команда, пункт меню |
«Хто керує» — у цьому й увесь сенс поділу. Інструмент запускається, бо **модель** вирішила його викликати. Ресурс долучається, бо **застосунок** вирішив, що він потрібен моделі. Промпт запускається, бо його обрав **користувач**.
!!! info
Якщо ви вже створювали веб-API, більша частина інтуїції у вас є: **ресурс** — це `GET`
(завантажує дані й нічого не змінює), а **інструмент** — це `POST` (виконує роботу й може мати
побічні ефекти). **Промпт** не має HTTP-аналога; він ближчий до збереженого запиту, який
користувач запускає за назвою.
## Один сервер, усі три {#one-server-all-three}
```python title="server.py" hl_lines="6 12 18"
--8<-- "docs_src/first_steps/tutorial001.py"
```
Три звичайні функції, три декоратори. Кожен декоратор — це вся реєстрація:
* `@mcp.tool()` робить `add` **інструментом**.
* `@mcp.resource("greeting://{name}")` робить `greeting` **шаблоном ресурсу**: `{name}` в URI — це параметр функції.
* `@mcp.prompt()` робить `summarize` **промптом**. Рядок, який він повертає, стає повідомленням користувача.
Усе інше (назву, опис, схему аргументів) SDK зчитує із самої функції: її назви, докстрингу, анотацій типів. Ви нічого з цього не оголошували окремо.
!!! tip
Дві половини SDK мають два шляхи імпорту: `from mcp import Client` і
`from mcp.server import MCPServer`. Шляху `from mcp import MCPServer` не існує.
### Спробуйте самі {#try-it}
Запустіть його за допомогою MCP Inspector:
```console
uv run mcp dev server.py
```
Відкрийте URL, який він надрукує. В Inspector є по одній вкладці на кожен примітив; пройдіть їх по черзі.
**Tools.** Один запис: `add` з описом *Add two numbers.* Форма має обов'язкове цілочислове поле для `a` і ще одне для `b`. Заповніть їх, викличте інструмент, і результатом буде `3`. Inspector побудував цю форму з `a: int, b: int`. Так само робить і будь-який інший клієнт.
**Resources.** Список *Resources* порожній. `greeting` розташований у **Resource Templates**, бо `greeting://{name}` має параметр: немає жодного конкретного ресурсу, який можна показати в списку, поки хтось не вкаже `name`. Передайте `World` і прочитайте:
```text
Hello, World!
```
**Prompts.** Один запис: `summarize` з єдиним обов'язковим аргументом `text`. Отримайте його з якимось текстом — і повернеться одне повідомлення з `role: user` та вашим відрендереним рядком як вмістом. Оце й увесь промпт: функція, яка будує повідомлення.
Inspector запустив ваш сервер через **stdio**, один із транспортів, якими може говорити MCP-сервер. Поки що обирати транспорт не потрібно; для цього є сторінка **[Запуск сервера](../run/index.md)**.
## Можливості {#capabilities}
В Inspector ви бачили три вкладки. Звідки він знав, що їх три?
Коли клієнт під'єднується, сервер оголошує свої **можливості**: на які сімейства запитів він відповідатиме. Клієнт використовує це оголошення, щоб вирішити, про що взагалі просити. Ви його не писали; `MCPServer` оголошує його за вас.
Подивіться самі. `Client` з SDK приймає об'єкт сервера напряму й під'єднується до нього **в пам'яті** (без підпроцесу, без порту):
```python
import asyncio
from mcp import Client
from server import mcp
async def main() -> None:
async with Client(mcp) as client:
print(client.server_capabilities.model_dump(exclude_none=True))
asyncio.run(main())
```
```text
{'prompts': {'list_changed': True}, 'resources': {'subscribe': True, 'list_changed': True}, 'tools': {'list_changed': True}}
```
Цей словник — оголошені **можливості** вашого сервера. Це перше, про що дізнається кожен клієнт, що під'єднується:
| Можливість | Клієнт тепер може викликати |
|-------------|------------------------------------------------------------|
| `tools` | `tools/list`, `tools/call` |
| `resources` | `resources/list`, `resources/templates/list`, `resources/read` |
| `prompts` | `prompts/list`, `prompts/get` |
`MCPServer` обслуговує всі три примітиви, тож усі три завжди оголошено.
Зверніть увагу на те, чого там немає. `completions` (автодоповнення аргументів для шаблонів ресурсів і промптів) потребує обробника, який пишете ви; у цього сервера його немає, тож можливість відсутня, і коректний клієнт про неї не проситиме. Це правило для всього необов'язкового: зареєструйте річ — і можливість з'явиться; **[Автодоповнення](../servers/completions.md)** це доводить.
!!! info
`Client(mcp)` — той самий клієнт у пам'яті, яким протестовано кожен приклад у цій документації, і
саме ним ви тестуватимете свої. Йому присвячено цілу сторінку: **[Тестування](testing.md)**.
## Чого ви не писали {#what-you-did-not-write}
Озирніться на цю сторінку. Ви написали три невеликі функції Python. Ви **не** писали:
* JSON Schema. `a: int, b: int` *і є* схема для `add`.
* Обробник запитів. `tools/list`, `resources/read`, `prompts/get`: усе обслуговується за вас.
* Оголошення можливостей. `MCPServer` зробив його за вас.
* Жодного рядка протоколу. Узгодження версії, обрамлення JSON-RPC, обмін можливостями: усе це відбулося всередині `mcp dev` і `Client(mcp)`, і ви цього не бачили.
У цьому співвідношенні й увесь сенс SDK.
## Підсумки {#recap}
* **Хост** — це LLM-застосунок, **клієнт** — його половина, що говорить мовою MCP, **сервер** — те, що ви створюєте.
* Інструментами керує **модель**, ресурсами — **застосунок**, промптами — **користувач**.
* Один декоратор на примітив: `@mcp.tool()`, `@mcp.resource(uri)`, `@mcp.prompt()`. Назва, опис і схема беруться з функції.
* URI з `{param}` створює **шаблон** ресурсу, який показується окремо від конкретних ресурсів.
* **Можливості** сервера оголошуються за вас, а клієнт просить лише те, що сервер оголосив.
* `Client(mcp)` під'єднується до об'єкта сервера в пам'яті: ваш тестовий стенд із першого дня.
Далі — **[Під'єднання до справжнього хоста](real-host.md)**: цей сервер усередині Claude Desktop або IDE, по-справжньому. Потім **[Тестування](testing.md)**: одна сторінка, один клієнт у пам'яті — і більше ніколи не доведеться гадати, чи воно працює. Після цього кожен примітив отримує власну сторінку, починаючи з того, яким керує модель: **[Інструменти](../servers/tools.md)**.

View file

@ -0,0 +1,53 @@
---
translation:
sections: [ed4a756b4c53c585, 97e2fb315b7fe398, 4d04f1c6f4bf6c1d, 577d73078fc62baf]
tool: 1
---
# Початок роботи {#get-started}
Уперше маєте справу з MCP або з цим SDK? Почніть звідси. Ці сторінки проведуть від нуля до робочого, протестованого сервера: [встановіть SDK](installation.md), напишіть свій [перший сервер](first-steps.md), [під'єднайте його до справжнього хоста](real-host.md) і [протестуйте](testing.md) за допомогою клієнта в пам'яті.
## Запуск коду {#run-the-code}
Усі блоки коду можна копіювати й використовувати як є: це повні робочі файли.
Щоб іти слідом за викладом, вставте блок у файл `server.py` і відкрийте його в MCP Inspector:
```console
uv run mcp dev server.py
```
**НАПОЛЕГЛИВО радимо** набрати (або скопіювати) код, відредагувати його й запустити локально. Саме робота у власному редакторі показує суть: як мало доводиться писати, автодоповнення, перевірка типів, що ловить помилки ще до запуску.
## Гадати не доведеться {#you-will-not-be-guessing}
Кожен приклад у цій документації — повний файл у каталозі [`docs_src/`](https://github.com/modelcontextprotocol/python-sdk/tree/main/docs_src) репозиторію самого SDK, і кожен із них проганяється тестовим набором SDK через **клієнт у пам'яті**:
```python
import pytest
from mcp import Client
from server import mcp
@pytest.mark.anyio
async def test_add() -> None:
async with Client(mcp) as client:
result = await client.call_tool("add", {"a": 1, "b": 2})
assert result.structured_content == {"result": 3}
```
Ні підпроцесу, ні порту, ні транспорту. `Client(mcp)` під'єднується безпосередньо до об'єкта сервера.
Якщо зміна в SDK зламає приклад на одній із цих сторінок, CI почервоніє раніше, ніж сторінка. Код, який ви тут читаєте, — це код, який виконується.
Ви самі скористаєтеся цим на сторінці [Тестування](testing.md): власні сервери тестують так само.
## Куди далі {#where-to-go-next}
Щойно сервер запрацює, решта цієї документації — довідник, а не курс. Кожна сторінка самодостатня, тож переходьте одразу до потрібного:
* Те, що сервер надає назовні (інструменти, ресурси, промпти), — це **[Сервери](../servers/index.md)**.
* Те, що доступне всередині функцій, які ви реєструєте, — це **[Усередині обробника](../handlers/index.md)**.
* Як донести сервер до клієнтів (stdio, HTTP, ваш наявний FastAPI-застосунок) — це **[Запуск сервера](../run/index.md)**.
* Побудова іншої сторони — застосунку, що *використовує* MCP-сервери, — це **[Клієнти](../client/index.md)**.

View file

@ -0,0 +1,47 @@
---
translation:
sections: [6e2f9bab94d5ed36, 8cf653388f69e28b, 6fd9ea2f65de0df6]
tool: 1
---
# Встановлення {#installation}
Python SDK опубліковано на PyPI як [`mcp`](https://pypi.org/project/mcp/). Потрібен **Python 3.10+**.
Ця документація описує **v2** — поточну стабільну лінійку випусків:
=== "uv"
```bash
uv add "mcp[cli]"
```
=== "pip"
```bash
pip install "mcp[cli]"
```
!!! note "Переходите з v1?"
v2 — мажорна версія з несумісними змінами; **[Посібник з міграції](../migration.md)**
описує кожну з них. Якщо ваш *пакет* залежить від `mcp` і ще не готовий до міграції, залиште
верхню межу `<2` (наприклад, `mcp>=1.28,<2`), щоб розв'язання залежностей без фіксованої версії залишалося на лінійці 1.x.
## Що встановлюється {#what-gets-installed}
Щоб користуватися SDK, нічого з цього знати не потрібно, але якщо цікаво, навіщо кожна залежність:
* `mcp-types`: усі типи протоколу (запити, результати, блоки вмісту) окремим пакетом, версія якого йде в ногу з SDK. Код, що залежить від `mcp`, імпортує його через псевдонім `mcp.types` (кожне `from mcp.types import ...` у цій документації); імпортуйте `mcp_types` напряму лише в проєкті, який встановлює `mcp-types` без SDK.
* [`anyio`](https://anyio.readthedocs.io/): асинхронне середовище виконання. Увесь SDK написано поверх anyio, тож він працює і на `asyncio`, і на `trio`.
* [`pydantic`](https://docs.pydantic.dev/): основа кожної моделі в `mcp.types`, а також уся генерація схем і валідація.
* [`httpx2`](https://pypi.org/project/httpx2/): HTTP-клієнт, на якому працюють *клієнтські* транспорти Streamable HTTP і SSE, із вбудованою підтримкою server-sent events.
* [`starlette`](https://www.starlette.io/), [`uvicorn`](https://www.uvicorn.org/), [`sse-starlette`](https://pypi.org/project/sse-starlette/) і [`python-multipart`](https://pypi.org/project/python-multipart/): *серверні* HTTP-транспорти.
* [`jsonschema`](https://pypi.org/project/jsonschema/): перевіряє структурований вивід інструмента на відповідність оголошеній схемі виводу.
* [`pyjwt[crypto]`](https://pyjwt.readthedocs.io/): робота з OAuth-токенами для авторизації.
* [`opentelemetry-api`](https://opentelemetry-python.readthedocs.io/): лише легкий API, тож middleware трасування в SDK нічого не коштує, доки ви самі не встановите OpenTelemetry SDK і експортер.
* [`typing-extensions`](https://typing-extensions.readthedocs.io/) і [`typing-inspection`](https://pypi.org/project/typing-inspection/): сучасні можливості типізації на Python 3.10.
* [`pywin32`](https://pypi.org/project/pywin32/): лише для Windows, використовується для керування підпроцесами `stdio`.
## Необов'язкові доповнення {#optional-extras}
* `mcp[cli]` додає [`typer`](https://typer.tiangolo.com/) і [`python-dotenv`](https://pypi.org/project/python-dotenv/) для інструмента командного рядка `mcp` (`mcp dev`, `mcp run`, `mcp install`). Під час розробки він знадобиться; на розгорнутому сервері може бути зайвим.
* `mcp[rich]` додає [`rich`](https://rich.readthedocs.io/) для охайніших логів сервера.

View file

@ -0,0 +1,184 @@
---
translation:
sections: [3c4f2f06b4e978b6, 22520eecae3d1961, f4e1709db18d635a, 2eb57992049671d9, 1ba83e9af37cc1b4, 4822586344b08d9e, 1c93afef72478992, b6b448f9eddd51dc, fe55370fd931815b]
tool: 1
---
# Підключення до справжнього хоста {#connect-to-a-real-host}
**Хост** — це застосунок, усередині якого зрештою опиняється ваш сервер: Claude Desktop, Claude Code, IDE. Саме з хостом говорить користувач. Усередині нього MCP-**клієнт** запускає ваш сервер як дочірній процес і спілкується з ним через stdin і stdout цього процесу.
Отже, підключення до хоста — це одна дія: ви повідомляєте йому **команду, яка запускає ваш сервер**. Усе на цій сторінці (дві команди CLI, три JSON-файли) — це різні місця, куди вписати ту саму команду.
## Один сервер, усі хости {#one-server-every-host}
```python title="server.py" hl_lines="3 33-34"
--8<-- "docs_src/real_host/tutorial001.py"
```
Два інструменти й один ресурс в одному файлі. Для кожного хоста нижче в цьому файлі важливі три речі:
* `mcp.run()` без аргументів запускає **stdio**-сервер: він блокує виконання, читає повідомлення протоколу зі stdin і пише їх у stdout. Саме цим транспортом говорить кожен хост на цій сторінці. Хост запускає ваш файл як дочірній процес і володіє цими двома каналами, тому підключення завжди зводиться до «ось команда». Порт обирати не потрібно, і ніщо на порту не слухає.
* `run()` стоїть під `if __name__ == "__main__":`. Усе, що нижче, **імпортує** цей файл, а не виконує його, тож незахищений `run()` запустив би сервер, щойно будь-що завантажить модуль.
* Об'єкт сервера — глобальна змінна рівня модуля з іменем `mcp`. Саме це ім'я шукає `mcp run` (`server` і `app` теж підходять). Назвете інакше — вкажіть ім'я явно: `mcp run server.py:bookshop`.
Це останній рядок Python на цій сторінці. Далі — лише налаштування хостів.
## Команда запуску {#the-launch-command}
Кожен хост нижче отримує ту саму команду:
```bash
uv run --with "mcp[cli]" mcp run /absolute/path/to/server.py
```
Одна команда для всіх, бо `uv run --with` розв'язує SDK у свіже середовище на місці: вона працює з будь-якого каталогу й не потребує ні проєкту, ні віртуального середовища, яке треба активувати. Тут це важливіше, ніж деінде, бо хост запускає ваш сервер зі *свого* робочого каталогу з майже порожнім середовищем, а не з вашої оболонки.
Це також команда, яку `mcp install` записує за вас у конфігурацію Claude Desktop (нижче), тож те, що ви набираєте вручну, і те, що генерує утиліта, збігаються — за винятком точної фіксації версії, яку додає утиліта.
!!! tip "Якщо хост не може знайти `uv`"
Хост породжує ваш сервер із мінімальним `PATH`, і `uv` у ньому може не бути. Замініть
просто `uv` абсолютним шляхом з `which uv` (macOS/Linux) або `where uv` (Windows). Саме це
й записує `mcp install`.
!!! note "Ця сторінка — про локальний сценарій"
Усе тут запускає ваш сервер на тій самій машині, де працює хост: хост запускає ваш
файл через stdio. Для особистого інструмента або інструмента на одній машині це саме те,
що треба. Щоб дати сервер людям, у яких *немає* вашого файлу, роздають **URL**, а не
команду: той самий об'єкт `mcp`, що обслуговується через Streamable HTTP. **[Запуск сервера](../run/index.md)**
зводить це рішення до однієї таблиці, а **[Розгортання й масштабування](../run/deploy.md)** —
це шлях звідти до справжнього імені хоста.
А хост — це не більше ніж застосунок з MCP-клієнтом усередині, тож роль хоста може
зіграти й ваш власний Python: сторінка **[Транспорти клієнта](../client/transports.md)** запускає
цей самий файл як підпроцес через `stdio_client(...)`, а **[Тестування](testing.md)**
підключається до нього в пам'яті взагалі без процесу.
## Claude Desktop {#claude-desktop}
Єдиний хост, який SDK може налаштувати за вас:
```bash
uv run mcp install server.py
```
От і все. `mcp install` імпортує файл, щоб прочитати ім'я сервера, знаходить файл конфігурації Claude Desktop і записує в нього команду запуску. Дорогою вона перетворює ваш шлях на абсолютний, тож робити це самим не потрібно.
Жодної магії тут немає. Ось запис, який вона створює:
```json
{
"mcpServers": {
"Bookshop": {
"command": "/absolute/path/to/uv",
"args": [
"run",
"--frozen",
"--with",
"mcp[cli]==2.0.0",
"mcp",
"run",
"/absolute/path/to/server.py"
]
}
}
}
```
Це команда запуску з розділу вище з трьома доповненнями: абсолютний шлях до `uv`, `--frozen`, щоб `uv` ніколи не переписував lock-файл, який випадково опиниться поруч, і точна фіксація встановленої у вас версії `mcp`. Запис потрапляє в `claude_desktop_config.json`, який лежить тут:
* **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
* **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
Цей файл можна написати й вручну. `mcp install` існує, щоб ви не припустилися класичної помилки (відносного шляху), поки це робите.
Повністю завершіть Claude Desktop (а не лише закрийте вікно) і відкрийте знову.
!!! warning
`mcp install` завершується помилкою `Claude app not found`, якщо *каталогу* конфігурації
Claude Desktop ще немає. Встановіть Claude Desktop і запустіть його один раз: саме це й
створює каталог.
!!! tip
Claude Desktop запускає ваш сервер у власному процесі, тож змінних середовища вашої
оболонки там немає. `uv run mcp install server.py -v API_KEY=abc123` (або `-f .env`) записує
їх у поле `env` запису. `--name` перевизначає ім'я запису; за замовчуванням це `name` сервера.
## Claude Code {#claude-code}
Файлу для редагування немає. Зареєструйте сервер через CLI `claude`; усе після `--` — це команда запуску.
```bash
claude mcp add bookshop -- uv run --with "mcp[cli]" mcp run /absolute/path/to/server.py
```
Виконайте `/mcp` у сесії Claude Code, щоб переконатися, що `bookshop` підключено, а його інструменти перелічено.
## Cursor {#cursor}
Створіть `.cursor/mcp.json` у корені проєкту.
```json
{
"mcpServers": {
"bookshop": {
"command": "uv",
"args": ["run", "--with", "mcp[cli]", "mcp", "run", "/absolute/path/to/server.py"]
}
}
}
```
Ті самі `command` та `args` під тим самим ключем `mcpServers`, що й у Claude Desktop. Сервер з'являється в налаштуваннях MCP у Cursor з обома інструментами в списку.
## VS Code {#vs-code}
Створіть `.vscode/mcp.json` у корені проєкту.
```json
{
"servers": {
"bookshop": {
"type": "stdio",
"command": "uv",
"args": ["run", "--with", "mcp[cli]", "mcp", "run", "/absolute/path/to/server.py"]
}
}
}
```
Дві відмінності від файлу Cursor, і це єдині дві: ключ-обгортка — `servers`, а не `mcpServers`, і кожен запис оголошує свій `type`. Підтвердьте запит про довіру, і команда **MCP: List Servers** у палітрі команд покаже, що `bookshop` працює.
!!! note
Потрібен VS Code 1.99 або новіший із розширенням **GitHub Copilot**, у якому виконано вхід
(достатньо Copilot Free), а Copilot Chat має бути в режимі **Agent**, бо жоден інший режим
не викликає інструменти.
## Сервер не з'являється {#it-doesnt-show-up}
Перш ніж чіпати конфігурацію будь-якого хоста, виконайте команду запуску самі:
```bash
uv run --with "mcp[cli]" mcp run /absolute/path/to/server.py
```
Нічого не виводиться, і команда не завершується. Ця тиша правильна: stdio-сервер чекає, доки хост першим заговорить у stdin (`Ctrl-C`, щоб зупинити). Справжня помилка — це трасування стека або негайний вихід, і тепер її можна прочитати, а не вгадувати через хост.
Коли ця команда сидить і чекає, залишається майже завжди одне з трьох:
* **Відносний шлях.** Хост запускає ваш сервер зі *свого* робочого каталогу, а не з того, з якого ви його реєстрували. `server.py` там, де потрібен `/absolute/path/to/server.py`, — найпоширеніша причина збою. Якщо хост не знаходить ще й `uv`, цей шлях теж має бути абсолютним.
* **Хост досі працює зі старою конфігурацією.** Хости читають конфігурацію під час запуску. Зокрема Claude Desktop треба *повністю завершити* (а не лише закрити вікно) і відкрити знову, перш ніж зміна в `claude_desktop_config.json` набуде чинності.
* **Щось потрапило в stdout поза проміжком перенаправлення.** У stdio stdout — це *і є* протокол. Під час обслуговування SDK перенаправляє скинутий (flushed) сторонній вивід у stderr, але вивід, скинутий у stdout до того (скрипт-обгортка, що робить echo, `print()` на етапі імпорту в небуферизованому процесі), або буферизований `print()`, що зливається під час завершення інтерпретатора, передає хосту зіпсоване повідомлення, і той розриває з'єднання. Пишіть логи зі стандартною конфігурацією `logging`, чий обробник stderr скидає кожен запис; власні обробники теж мають уникати stdout. Докладніше — на сторінці **[Логування](../handlers/logging.md)**.
Claude Desktop веде окремий лог для кожного сервера: `mcp-server-<NAME>.log` — це stderr вашого сервера, поруч із `mcp.log` для з'єднань, у `~/Library/Logs/Claude` на macOS і `%APPDATA%\Claude\logs` на Windows.
Для всього, що виходить за межі цих трьох випадків, є сторінка **[Усунення несправностей](../troubleshooting.md)**.
## Підсумки {#recap}
* **Хост** (Claude Desktop, IDE) виконує MCP-клієнт, який запускає ваш сервер як дочірній процес через stdio. Підключитися означає дати йому одну команду запуску.
* Ця команда — `uv run --with "mcp[cli]" mcp run /absolute/path/to/server.py`: жодного venv для активації, працює з будь-якого каталогу.
* **Claude Desktop** — єдиний хост, який `mcp install` налаштовує за вас. Вона записує ту саму команду (плюс абсолютний шлях до `uv`, `--frozen` і точну фіксацію встановленої у вас версії) у `claude_desktop_config.json`, тож вам цього робити не доведеться.
* **Claude Code** — це `claude mcp add bookshop -- <launch command>`. **Cursor** — `.cursor/mcp.json` з ключем `mcpServers`. **VS Code** — `.vscode/mcp.json` з ключем `servers`, кожен запис із `type`.
* Скрізь абсолютні шляхи, перезапуск хоста після редагування конфігурації, і ніщо, крім SDK, ніколи не пише в stdout.
Кожен хост на цій сторінці підключився до того самого файлу тією самою командою. Про те, що цей файл може *надавати*, — решта цієї документації: **[Інструменти](../servers/tools.md)**, **[Ресурси](../servers/resources.md)** і всі транспорти, крім stdio, на сторінці **[Запуск сервера](../run/index.md)**.

View file

@ -0,0 +1,114 @@
---
translation:
sections: ['4926721070127497', c52a1de2b6b32f40, 2e410b412c25f314, 627195f7159e24ef]
tool: 1
---
# Тестування {#testing}
Python SDK містить клас `Client` із **транспортом у пам'яті**: передайте йому об'єкт сервера — і він під'єднається до нього напряму.
Жодного підпроцесу. Жодного порту. Узагалі жодного транспорту. Та сама ідея, що й `TestClient` у FastAPI.
## Базове використання {#basic-usage}
Припустімо, є простий сервер з одним інструментом:
```python title="server.py"
--8<-- "docs_src/testing/tutorial001.py"
```
Щоб запустити тест нижче, знадобляться дві додаткові залежності (для розробки):
=== "uv"
```bash
uv add --dev pytest inline-snapshot
```
=== "pip"
```bash
pip install pytest inline-snapshot
```
!!! info
Ця документація припускає, що ви вже знайомі з [`pytest`](https://docs.pytest.org/en/stable/).
[`inline-snapshot`](https://15r10nk.github.io/inline-snapshot/latest/) — це те, чим тест нижче
перевіряє весь об'єкт результату одним рядком. Він записує вивід тесту у вигляді
літерала `snapshot(...)`, який ви бачите. Якщо не хочете ним користуватися, приберіть імпорт і
перевіряйте потрібні поля (`result.content[0].text == "3"`), як у будь-якому іншому тесті.
Тепер сам тест:
```python title="test_server.py"
import pytest
from inline_snapshot import snapshot
from mcp import Client
from mcp.types import CallToolResult, TextContent
from server import mcp
@pytest.fixture
def anyio_backend(): # (1)!
return "asyncio"
@pytest.fixture
async def client(): # (2)!
async with Client(mcp, raise_exceptions=True) as c:
yield c
@pytest.mark.anyio
async def test_call_add_tool(client: Client):
result = await client.call_tool("add", {"a": 1, "b": 2})
# Drop the server identity stamp in `_meta`; it is not what this test is about.
result.meta = None
assert result == snapshot(
CallToolResult(
content=[TextContent(type="text", text="3")],
structured_content={"result": 3},
)
)
```
1. Якщо ви використовуєте `trio`, поверніть натомість `"trio"`. Подробиці — у [документації anyio](https://anyio.readthedocs.io/en/stable/testing.html#specifying-the-backends-to-run-on).
2. Фікстура віддає під'єднаного клієнта. Кожен тест, що приймає `client`, отримує нове з'єднання в пам'яті з тим самим сервером.
Готово! Тепер можна розширювати тести, щоб охопити більше сценаріїв.
## Навіщо `raise_exceptions=True`? {#why-raise_exceptionstrue}
Піти не так можуть дві різні речі, і цей прапорець стосується лише однієї з них.
Виняток усередині одного з **ваших інструментів** — це не збій протоколу. Він стає звичайним результатом
з `is_error=True`, і модель читає повідомлення. `raise_exceptions` цього не змінює: з ним чи
без нього `call_tool` повертає той самий результат з `is_error=True`. Про це є ціла сторінка:
**[Обробка помилок](../servers/handling-errors.md)**.
Збій **поза** тілом інструмента — інша річ. На з'єднанні, яке дає `Client(mcp)`, сервер
замінює його загальним `"Internal server error"`, перш ніж його побачить клієнт. Ніколи не слід
розкривати подробиці неочікуваного падіння віддаленій стороні, що викликає. У тесті це саме те,
чого ви *не* хочете, і саме це змінює `raise_exceptions=True`: тест бачить справжнє повідомлення
замість узагальненого.
Залишайте його ввімкненим у тестах. У робочому коді він не має сенсу.
## У тому самому процесі за замовчуванням {#in-process-by-default}
!!! note
`Client(mcp)` під'єднується в межах процесу й за замовчуванням **нейтральний щодо покоління**: він зондує сервер і
обирає відповідний шлях протоколу. Зафіксуйте `mode="legacy"`, якщо тест перевіряє семантику, специфічну для
старого покоління — push семплювання (sampling) чи еліцитації (elicitation), `message_handler`, — і приберіть там `raise_exceptions=True`:
з'єднання старого покоління взагалі нічого не узагальнює, а прапорець повторно викидає
збій усередині завдання сервера, а не у вашому тесті.
Цей один рядок — ще й причина, чому ця документація може обіцяти, що її приклади працюють: кожен
файл прикладу проганяється власним набором тестів SDK, майже всі — саме через цей
клієнт. Ви користуєтеся тим самим інструментом, яким SDK перевіряє сам себе.
У вас є робочий, протестований сервер. Як помістити його в справжній застосунок (Claude Desktop,
IDE) — на сторінці **[Під'єднання до справжнього хоста](real-host.md)**; усі інші способи його запустити —
у розділі **[Запуск сервера](../run/index.md)**.

Some files were not shown because too many files have changed in this diff Show more