docs: publish translated docs in twelve languages and the tool that maintains them (#3280)
This commit is contained in:
parent
14aa889992
commit
52ad0a8876
6 changed files with 89737 additions and 54 deletions
123
i18n/uk/pages/advanced/apps.md
Normal file
123
i18n/uk/pages/advanced/apps.md
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
---
|
||||
translation:
|
||||
sections: [0355618e5f4d5fe4, 1821eaf50f2d0b64, 82e0b28ebd3abf5a, 8ac39614c094f2d0, dab6ff945501ab2a, bd5565c3b2d4f959, 96819ce3d63a0487]
|
||||
tool: 1
|
||||
---
|
||||
# MCP Apps {#mcp-apps}
|
||||
|
||||
**MCP App** — це інструмент із власним обличчям: поряд із даними інструмент указує на HTML-документ, який хост відображає як інтерактивну поверхню.
|
||||
|
||||
Дві частини, завжди дві частини:
|
||||
|
||||
1. **Інструмент**, який виконує роботу й повертає дані, як будь-який інший інструмент.
|
||||
2. **Ресурс `ui://`** з HTML, який хост показує для нього.
|
||||
|
||||
Інструмент несе посилання на ресурс у `_meta.ui.resourceUri`. Хост отримує його через `resources/read`, відображає в **ізольованому iframe (sandbox)** і передає результат інструмента в цей iframe через `postMessage`. Ваш сервер ніколи не надсилає й не отримує жодних повідомлень `ui/*`: цей обмін відбувається між хостом та iframe. Ви віддаєте інструмент і HTML-документ, а всю виставу ставить хост.
|
||||
|
||||
SDK постачає це як вбудоване розширення `Apps` (`io.modelcontextprotocol/ui`). Якщо [розширення](extensions.md) для вас новинка, спершу прогляньте ту сторінку. Одна хвилина — і повертайтеся.
|
||||
|
||||
## Годинник із циферблатом {#a-clock-with-a-face}
|
||||
|
||||
```python title="server.py" hl_lines="19 22 30 32"
|
||||
--8<-- "docs_src/apps/tutorial001.py"
|
||||
```
|
||||
|
||||
Чотири кроки:
|
||||
|
||||
* `Apps()`: один екземпляр тримає ваші інструменти з прив'язаним UI та їхні ресурси.
|
||||
* `@apps.tool(resource_uri="ui://clock/app.html")`: звичайний інструмент плюс позначка `_meta.ui.resourceUri`. Усе, що приймає `@mcp.tool()` (name, title, description, ...), передається далі.
|
||||
* `apps.add_html_resource("ui://clock/app.html", CLOCK_HTML)`: відповідний ресурс, який віддається як `text/html;profile=mcp-app`. Саме цей MIME-тип каже хосту: «це застосунок, відобрази його».
|
||||
* `MCPServer("clock", extensions=[apps])`: увімкнення. Тепер сервер оголошує `io.modelcontextprotocol/ui` у `capabilities.extensions`.
|
||||
|
||||
Сам HTML слухає `postMessage` від хоста й показує результат. Для справжніх застосунків використовуйте всередині HTML офіційний браузерний SDK [`@modelcontextprotocol/ext-apps`](https://github.com/modelcontextprotocol/ext-apps). Він дає `ontoolresult`, `callServerTool`, `getHostContext` і `onhostcontextchanged` замість сирих подій повідомлень.
|
||||
|
||||
## Плавна деградація {#graceful-degradation}
|
||||
|
||||
Не кожен клієнт відображає застосунки. Специфікація прямо каже, що це означає для вас:
|
||||
|
||||
> Інструменти **МУСЯТЬ** повертати змістовний масив `content`, навіть коли UI доступний.
|
||||
|
||||
Модель читає `content`; iframe — для людей. Хост із підтримкою UI все одно передає текстовий результат моделі, а суто текстовий клієнт отримує *лише* його. Тож канонічний шаблон — один інструмент, дві відповіді. Погляньте на `get_time` ще раз:
|
||||
|
||||
```python title="server.py" hl_lines="23-27"
|
||||
--8<-- "docs_src/apps/tutorial001.py"
|
||||
```
|
||||
|
||||
`client_supports_apps(ctx)` дорівнює `True` лише тоді, коли клієнт оголосив розширення `io.modelcontextprotocol/ui` **і** вказав `text/html;profile=mcp-app` у своїх налаштуваннях `mimeTypes`. Поле обов'язкове, тож клієнт, який його пропустив, не зараховується. Саме це й оголошує `main()` у тому самому файлі: клієнтську половину узгодження — і у відповідь приходить розширений варіант.
|
||||
|
||||
!!! warning
|
||||
Ніколи не повертайте заглушку на кшталт `"[Rendered UI]"` як єдиний вміст. Якщо резервний текст марний, інструмент марний для кожного суто текстового клієнта й для самої моделі. Напишіть нормальне речення.
|
||||
|
||||
## Обмеження iframe {#locking-the-iframe-down}
|
||||
|
||||
Метадані безпеки несе ресурс: що iframe може завантажувати, які дозволи браузера йому потрібні, як його бажано вбудовувати:
|
||||
|
||||
```python title="server.py" hl_lines="9 19-22"
|
||||
--8<-- "docs_src/apps/tutorial002.py"
|
||||
```
|
||||
|
||||
`csp` і `permissions` — це **запити до хоста**, а не поведінка сервера. Хост будує з них Content-Security-Policy і Permissions-Policy для iframe й може відмовити. Перевіряйте наявність можливостей у своєму JS, а не припускайте, що дозвіл надано.
|
||||
|
||||
`ResourceCsp`, поле за полем (ім'я в Python, ключ у переданих даних, що з ним робить хост):
|
||||
|
||||
| Python | У переданих даних (`_meta.ui.csp`) | Керує |
|
||||
|---|---|---|
|
||||
| `connect_domains` | `connectDomains` | `connect-src`: куди можуть звертатися `fetch`/XHR |
|
||||
| `resource_domains` | `resourceDomains` | `img-src`, `style-src`, ...: статичні ресурси |
|
||||
| `frame_domains` | `frameDomains` | `frame-src`: вкладені iframe |
|
||||
| `base_uri_domains` | `baseUriDomains` | `base-uri`: на що може вказувати `<base>` |
|
||||
|
||||
`ResourcePermissions`: кожне поле запитує для iframe дозвіл браузера.
|
||||
|
||||
| Python | У переданих даних (`_meta.ui.permissions`) |
|
||||
|---|---|
|
||||
| `camera` | `camera` |
|
||||
| `microphone` | `microphone` |
|
||||
| `geolocation` | `geolocation` |
|
||||
| `clipboard_write` | `clipboardWrite` |
|
||||
|
||||
!!! note
|
||||
CSP і дозволи живуть на **ресурсі**, ніколи на інструменті. У метаданих інструмента за специфікацією для них немає місця, і хости їх там ігнорують. SDK робить цю помилку неможливою: `@apps.tool()` просто не має параметра `csp`.
|
||||
|
||||
### Видимість {#visibility}
|
||||
|
||||
`visibility=["app"]` на інструменті означає «це існує для iframe, а не для моделі»:
|
||||
|
||||
* `"model"`: інструмент може викликати модель.
|
||||
* `"app"`: інструмент може викликати iframe (через `callServerTool`).
|
||||
* Не вказано: обидва, це значення за замовчуванням.
|
||||
|
||||
Фільтрація — справа **хоста**. Ваш сервер перелічує інструменти лише для застосунку в `tools/list`, як і будь-які інші; хост приховує їх від моделі. Не фільтруйте на боці сервера.
|
||||
|
||||
## Правила, які контролює SDK {#the-rules-the-sdk-enforces}
|
||||
|
||||
Усе це падає під час запуску, а не в продакшені:
|
||||
|
||||
* `resource_uri` або URI ресурсу, що не має вигляду `ui://...`, дає `ValueError` під час декорування чи реєстрації.
|
||||
* Інструмент, прив'язаний до URI **без відповідного зареєстрованого ресурсу**, дає `ValueError`, коли `MCPServer(extensions=[apps])` приймає розширення. Інструмент, що оголошує HTML, який повертає 404 на `resources/read`, — це помилка конфігурації, тож сервер відмовляється створюватися.
|
||||
* `meta={"ui": ...}` у `@apps.tool()` дає `ValueError`. Декоратор володіє `_meta["ui"]`; висловлюйте це через `resource_uri=` і `visibility=`. Інші ключі `meta=` спокійно зливаються поруч.
|
||||
|
||||
Ані TypeScript SDK ext-apps, ані FastMCP сьогодні нічого з цього не ловлять; краще дізнатися про це раніше, ніж дізнається хост.
|
||||
|
||||
## Не тільки вбудований HTML {#beyond-inline-html}
|
||||
|
||||
`add_html_resource` покриває типовий випадок: рядок з HTML. Для всього іншого — HTML на диску чи згенерованого вмісту — побудуйте ресурс самі й передайте його:
|
||||
|
||||
```python title="server.py" hl_lines="12 18"
|
||||
--8<-- "docs_src/apps/tutorial003.py"
|
||||
```
|
||||
|
||||
`add_resource` підставляє MIME-тип `text/html;profile=mcp-app`, коли ресурс не задає його явно, і відхиляє явну невідповідність: ресурс `ui://` з будь-яким іншим MIME-типом не відобразить жоден хост.
|
||||
|
||||
!!! tip
|
||||
Орієнтуєтеся на хост до GA-версії, який досі читає застарілий плоский ключ `_meta["ui/resourceUri"]`? Додайте його самі:
|
||||
`@apps.tool(resource_uri="ui://x", meta={"ui/resourceUri": "ui://x"})`.
|
||||
Вкладений об'єкт `ui` — це форма за специфікацією; плоский ключ доживає своє.
|
||||
|
||||
## Приклад у дії {#see-it-run}
|
||||
|
||||
Історія `apps` в `examples/stories/` — це ця сторінка у вигляді пари, яку можна запустити: сервер з інструментом-годинником із прив'язаним UI та клієнт, який узгоджує Apps, читає `_meta.ui.resourceUri` інструмента, отримує HTML і викликає інструмент.
|
||||
|
||||
```bash
|
||||
uv run python -m stories.apps.client
|
||||
```
|
||||
172
i18n/uk/pages/advanced/extensions.md
Normal file
172
i18n/uk/pages/advanced/extensions.md
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
---
|
||||
translation:
|
||||
sections: [05891e7cc1938a13, b3c01a6af28c51ee, 7ffc91f5e38bdfe0, 717d3f235a8333a7, f471a13b2fe5d737, ed6af2df4b656dff]
|
||||
tool: 1
|
||||
---
|
||||
# Розширення {#extensions}
|
||||
|
||||
**Розширення** — це набір поведінки MCP, який вмикається лише на явний запит і стоїть за одним ідентифікатором.
|
||||
|
||||
На сервері воно може додавати інструменти, ресурси й нові методи запитів, а також обгортати `tools/call`. На клієнті — заявляти додаткові форми результату `tools/call` і спостерігати за вендорськими сповіщеннями. Кожна сторона оголошує розширення у власному `capabilities.extensions`, і для тих, хто про це не просив, нічого не змінюється. Такий контракт ([SEP-2133](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2133)), і в нього одне золоте правило: **розширення за замовчуванням вимкнені**.
|
||||
|
||||
## Використання розширення {#using-an-extension}
|
||||
|
||||
Передайте екземпляри під час створення:
|
||||
|
||||
```python title="server.py"
|
||||
--8<-- "docs_src/extensions/tutorial001.py"
|
||||
```
|
||||
|
||||
Готово. Тепер сервер оголошує `io.modelcontextprotocol/ui` у `capabilities.extensions` і обслуговує все, що додає розширення.
|
||||
|
||||
`Apps` — вбудоване еталонне розширення, і йому присвячено окрему сторінку: **[MCP Apps](apps.md)**.
|
||||
|
||||
!!! note
|
||||
Розширення фіксуються під час створення. Методу `add_extension`, який можна було б викликати пізніше, немає: карта можливостей сервера не повинна змінюватися, поки до нього під'єднані клієнти.
|
||||
|
||||
Карта можливостей передається через `server/discover`, а це шлях версії **2026-07-28**. Рукостисканню `initialize` старого покоління нікуди її покласти, тож клієнт старого покоління просто не бачить розширення. Проєктуйте з урахуванням цього: розширення *доповнює* сервер і не повинно бути єдиним способом ним користуватися.
|
||||
|
||||
## Написання власного розширення {#writing-your-own}
|
||||
|
||||
Успадкуйте `Extension` і перевизначте лише те, що потрібно. Кожен метод має типову реалізацію.
|
||||
|
||||
### Ідентифікатор {#the-identifier}
|
||||
|
||||
```python
|
||||
--8<-- "docs_src/extensions/tutorial002.py"
|
||||
```
|
||||
|
||||
Ідентифікатор — це рядок вигляду `vendor-prefix/name`, що відповідає граматиці ключів `_meta` зі специфікації: розділені крапками мітки (кожна починається з літери й закінчується літерою або цифрою), скісна риска, потім ім'я. Він перевіряється **під час визначення класу**, тож друкарська помилка не чекає, поки сервер запуститься:
|
||||
|
||||
```text
|
||||
TypeError: Stamps.identifier must be a `vendor-prefix/name` string
|
||||
(reverse-DNS prefix required), got 'stamps'
|
||||
```
|
||||
|
||||
Як префікс використовуйте домен, яким ви керуєте. `io.modelcontextprotocol/*` призначено для розширень, які специфікує сам проєкт MCP.
|
||||
|
||||
### Додавання інструментів {#contributing-tools}
|
||||
|
||||
Найменше корисне розширення — один інструмент і карта налаштувань:
|
||||
|
||||
```python title="server.py" hl_lines="17 19-20 22-23 26"
|
||||
--8<-- "docs_src/extensions/tutorial003.py"
|
||||
```
|
||||
|
||||
* `tools()` повертає об'єкти `ToolBinding`. Сервер реєструє кожен із них точно так, ніби ви самі викликали `mcp.add_tool(...)`: те саме генерування схеми, те саме впровадження `Context`, усе те саме.
|
||||
* `settings()` — це значення, що оголошується в `capabilities.extensions["com.example/stamps"]`. Поверніть `{}` (типове значення), щоб оголосити розширення без налаштувань.
|
||||
* Розширення ніколи не отримує сервер. Воно оголошує свій внесок як дані; `MCPServer` їх споживає. Ніякого `self.server`, який можна було б змінювати, немає.
|
||||
|
||||
А `main()` — доказ: клієнт у пам'яті, під'єднаний безпосередньо до `mcp`:
|
||||
|
||||
```python title="server.py" hl_lines="29-34"
|
||||
--8<-- "docs_src/extensions/tutorial003.py"
|
||||
```
|
||||
|
||||
### Обслуговування власних методів {#serving-your-own-methods}
|
||||
|
||||
Розширення може реєструвати **нові методи запитів**: власні дієслова, які обслуговуються поруч із методами специфікації:
|
||||
|
||||
```python title="server.py" hl_lines="16-22 31 40-48"
|
||||
--8<-- "docs_src/extensions/tutorial004.py"
|
||||
```
|
||||
|
||||
* `SearchParams` успадковує `RequestParams`, тож конверт `_meta` версії 2026 розбирається однаково, а обробник отримує перевірені параметри, а не сирий словник. Обмежуйте те, чим керує клієнт: `Field(ge=1, le=100)` відхиляє безглуздий `limit`, перш ніж ваш код щось під нього виділить.
|
||||
* `require_client_extension(ctx, EXTENSION_ID)` — це шлагбаум: клієнт, який не оголосив розширення, отримує помилку `-32021` (відсутня обов'язкова можливість клієнта) з машиночитаним корисним навантаженням `requiredCapabilities`, якого вимагає специфікація.
|
||||
* `protocol_versions=frozenset({"2026-07-28"})` прив'язує метод до однієї версії протоколу. На будь-якій іншій версії клієнт отримує `METHOD_NOT_FOUND` — точно так, ніби методу там не існує. Для цього клієнта він і не існує.
|
||||
|
||||
Методи **лише додаються**. SDK забезпечує це під час створення, а не під час виконання:
|
||||
|
||||
* `MethodBinding` для методу, визначеного специфікацією (`tools/list`, `completion/complete`, ...), викидає `ValueError` під час створення прив'язки. Базові дієслова належать серверу.
|
||||
* Два розширення, що прив'язують той самий метод, викидають виняток, коли реєструється друге. «Перемагає останній запис» — саме так плагіни псують одне одного; ми цього не робимо.
|
||||
* Порожня множина `protocol_versions` теж викидає виняток: метод, який ніколи не може бути обслужений, — це помилка, а не конфігурація.
|
||||
|
||||
### Клієнтська сторона {#the-client-side}
|
||||
|
||||
`main()` у тому самому файлі — це вся клієнтська частина, обидві її половини:
|
||||
|
||||
```python title="server.py" hl_lines="54-58"
|
||||
--8<-- "docs_src/extensions/tutorial004.py"
|
||||
```
|
||||
|
||||
* `Client(..., extensions=[advertise(EXTENSION_ID)])` оголошує розширення. Оголошення стають `ClientCapabilities.extensions`: на з'єднанні версії 2026-07-28 карта подорожує в конверті `_meta` кожного запиту, тож сервер бачить її в **кожному** запиті; на з'єднанні старого покоління вона передається з рукостисканням `initialize`. Серверному коду байдуже, який саме шлях: `require_client_extension(ctx, ...)` і `ctx.session.check_client_capability(...)` читають правильне джерело в обох випадках.
|
||||
* Вендорські методи опускаються на один шар нижче, до `client.session.send_request(...)`; повноцінні методи в `Client` з'являються лише для дієслів специфікації. `send_request` приймає будь-який підклас `Request`, тож вендорський запит проходить як є.
|
||||
|
||||
### Перехоплення `tools/call` {#intercepting-toolscall}
|
||||
|
||||
Єдиний хук-перехоплювач. Перевизначте `intercept_tool_call`, щоб спостерігати за викликом інструмента, завершувати його достроково або забороняти:
|
||||
|
||||
```python title="server.py" hl_lines="17-24"
|
||||
--8<-- "docs_src/extensions/tutorial005.py"
|
||||
```
|
||||
|
||||
* `params` — це перевірений `CallToolRequestParams`: `params.name` і `params.arguments` доступні без роботи із сирим JSON. Саме він визначає, який виклик інструмента виконується: якщо передати через `call_next` переписаний контекст, зміниться те, що обробник бачить у `ctx`, а не сам виклик інструмента. Переписування запитів на рівні переданих даних — справа [Middleware](middleware.md).
|
||||
* `call_next(ctx)` виконує решту ланцюжка й повертає результат обробника. Поверніть його без змін (спостереження), поверніть щось інше (заміна) або викиньте `MCPError` (відмова). Усе, що ви повернете, серіалізується як будь-який результат обробника, разом зі штампом ідентичності `serverInfo` покоління 2026, тож перехоплювач, який завершує виклик достроково, ніколи не видасть анонімної відповіді чи відповіді не за схемою.
|
||||
* Коли розширень кілька, перехоплювачі вкладаються в порядку реєстрації: перше розширення в `extensions=[...]` — зовнішнє.
|
||||
* Типова реалізація просто пропускає виклик далі, а сервер, чиї розширення не перевизначають цей хук, зберігає голий обробник `tools/call` недоторканим. За те, чим не користуєтеся, не платите.
|
||||
|
||||
Хук обгортає `tools/call` і нічого більше. Для того, що стосується кожного повідомлення, використовуйте [Middleware](middleware.md). Саме для цього воно й існує.
|
||||
|
||||
## Використання клієнтського розширення {#using-a-client-extension}
|
||||
|
||||
**Клієнтське розширення** — той самий контракт з боку споживача: набір клієнтської поведінки за одним ідентифікатором. Передайте екземпляри в `Client(extensions=[...])` і викликайте інструменти як зазвичай:
|
||||
|
||||
```python title="client.py" hl_lines="66-68"
|
||||
--8<-- "docs_src/extensions/tutorial006.py"
|
||||
```
|
||||
|
||||
`call_tool("buy", ...)` повертає звичайний `CallToolResult`, як і будь-який інший виклик. Що змінило розширення: тепер сервер може відповісти на `buy` **формою результату** `receipt` замість остаточного результату, а `Receipts` доводить її до кінця (тут — погашаючи квитанцію наступним викликом), перш ніж `call_tool` поверне значення. У місці виклику нічого не змінюється.
|
||||
|
||||
Приберіть розширення — і нічого з цього не існує: шлагбаум сервера відмовляє клієнту, який його не оголосив (помилка -32021), а заявлена форма від сервера, що обходить шлагбаум, не проходить перевірку — точно так, як специфікація вимагає для нерозпізнаного `resultType`. Вимкнено за замовчуванням, з обох кінців з'єднання.
|
||||
|
||||
Щоб оголосити ідентифікатор **без** жодної клієнтської поведінки (сервер перевіряє можливість, клієнт нічого не робить, як у клієнті пошуку вище), використовуйте `advertise()`:
|
||||
|
||||
```python
|
||||
from mcp.client import advertise
|
||||
|
||||
client = Client(mcp, extensions=[advertise("com.example/search")])
|
||||
```
|
||||
|
||||
## Написання клієнтського розширення {#writing-a-client-extension}
|
||||
|
||||
Успадкуйте `ClientExtension` і перевизначте лише те, що потрібно. Три види внеску, кожен із типовою реалізацією: `settings()`, `claims()` і `notifications()`.
|
||||
|
||||
```python title="client.py" hl_lines="17-18 43-44 46-47"
|
||||
--8<-- "docs_src/extensions/tutorial006.py"
|
||||
```
|
||||
|
||||
* Ідентифікатор підпорядковується тій самій граматиці, що й на сервері, і перевіряється під час визначення класу.
|
||||
* `claims()` повертає об'єкти `ResultClaim`: тег у переданих даних, модель, яка його розбирає, і резолвер, який доводить його до кінця. Модель мусить зафіксувати тег через `result_type: Literal["receipt"]` і не повинна успадковувати базові типи результатів дієслова; обидві умови перевіряються під час створення заявки. Вендорські поля на кшталт `receipt_token` передаються мережею як є: підставлена форма доходить до клієнта дослівно.
|
||||
* Резолвер отримує розібрану модель і `ClaimContext`; `ctx.session` — той самий публічний дескриптор, що й `client.session`, тож подальші виклики — це звичайні виклики сесії. Він повертає звичайний для дієслова `CallToolResult`.
|
||||
* `settings()` — значення, що оголошується в `ClientCapabilities.extensions[identifier]`; воно читається один раз під час створення `Client`.
|
||||
|
||||
`notifications()` оголошує вендорські сповіщення сервера, за якими слід спостерігати:
|
||||
|
||||
```python
|
||||
def notifications(self) -> Sequence[NotificationBinding[Any]]:
|
||||
return [NotificationBinding(method="notifications/receipts", params_type=ReceiptEvent, handler=self.on_receipt)]
|
||||
```
|
||||
|
||||
Обробник отримує перевірені параметри по одному, у порядку диспетчеризації. Він спостерігає; накласти вето чи відповісти він не може.
|
||||
|
||||
Два негучні правила. Заявки діють лише на з'єднаннях версії 2026-07-28, і оголошення можливостей іде за ними: на з'єднанні старого покоління заявки розчиняються, а разом із ними з оголошення випадає й ідентифікатор, тож клієнт ніколи не оголошує розширення, чиї форми він би відхилив. А коли заявлена форма потрібна вам самим, а не резолверу, викликайте `client.session.call_tool(..., allow_claimed=True)`; без цього прапорця заявлена форма, що доходить до виклику на рівні сесії, викидає `UnexpectedClaimedResult`.
|
||||
|
||||
### Дієслова розширень {#extension-verbs}
|
||||
|
||||
Власні методи запитів розширення не потребують реєстрації на клієнті. Тип вендорського запиту успадковує `mcp.types.Request` і проходить через `client.session.send_request`, як у розділі [Обслуговування власних методів](#serving-your-own-methods). Одне доповнення: коли ключ параметрів мусить передаватися в заголовку `Mcp-Name` (специфікації розширень, як-от tasks, вимагають цього для своїх дієслів), тип запиту оголошує `name_param`:
|
||||
|
||||
```python title="client.py" hl_lines="22-25 46-47"
|
||||
--8<-- "docs_src/extensions/tutorial007.py"
|
||||
```
|
||||
|
||||
Сесія дзеркалить `params["jobId"]` у `Mcp-Name` на кожному шляху надсилання, а відсутнє значення дає гучну помилку замість того, щоб мовчки пропустити обов'язковий заголовок.
|
||||
|
||||
## Чого розширення не може {#what-an-extension-cannot-do}
|
||||
|
||||
Поверхня внеску **закрита** навмисно. На сервері: налаштування, інструменти, ресурси, методи, один перехоплювач `tools/call`. На клієнті: налаштування, заявки на результати, прив'язки сповіщень. Розширення не може:
|
||||
|
||||
* **Лізти в хост.** Воно оголошує дані; посилання на сервер чи клієнт у нього немає.
|
||||
* **Замінювати базову поведінку.** Методи специфікації та базові теги результатів відхиляються під час створення (`initialize` цілком зарезервовано за виконавцем); прив'язка сповіщення, перекрита базовим словником, натомість замовкає з попередженням.
|
||||
* **Реєструватися із запізненням.** Після того як `MCPServer(...)` чи `Client(...)` повернув керування, набір розширень уже такий, який є.
|
||||
|
||||
Якщо ви воюєте з цими стінами, ви пишете не розширення. Ви пишете форк. Стіни — це й є головна перевага: користувач, що читає `extensions=[Apps(), Stamps()]`, знає *все*, чого ці двоє могли торкнутися.
|
||||
34
i18n/uk/pages/advanced/index.md
Normal file
34
i18n/uk/pages/advanced/index.md
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
---
|
||||
translation:
|
||||
sections: [ca6988b7503cd2d3]
|
||||
tool: 1
|
||||
---
|
||||
# Розширені можливості {#advanced}
|
||||
|
||||
Усе, що потрібно звичайному серверу чи клієнту, має своє тематичне місце в розділах вище.
|
||||
Цей розділ — це запасні виходи, до яких звертаються, коли зручний шар `MCPServer`
|
||||
починає заважати:
|
||||
|
||||
* **[Низькорівневий Server](low-level-server.md)**: клас, на якому побудовано `MCPServer`.
|
||||
Схеми, написані вручну, обробники `on_*`, жодних перевірок за вас і власні
|
||||
JSON-RPC-методи.
|
||||
* **[Пагінація](pagination.md)** і **[Middleware](middleware.md)**: дві речі, які
|
||||
можна зробити *лише* на низькорівневому `Server`.
|
||||
* **[Розширення](extensions.md)** і **[Застосунки MCP](apps.md)**: поверхня розширень
|
||||
протоколу. Скомпонуйте пакети розширень у сервер або напишіть власні.
|
||||
|
||||
Кілька речей, які цілком логічно шукати тут, натомість розміщено там, де ними
|
||||
справді користуються:
|
||||
|
||||
* **Авторизація** — у розділі **[Запуск сервера](../run/index.md)**, бо сервер
|
||||
захищають там, де його розгортають.
|
||||
* **OAuth**, **підтвердження ідентичності**, під'єднання до **кількох серверів** і
|
||||
**кеш** відповідей — усе це в розділі **[Клієнти](../client/index.md)**.
|
||||
* **Багатораундові запити** і **Підписки** — у розділі
|
||||
**[Усередині обробника](../handlers/index.md)**, бо і те, й інше обробник
|
||||
саме *робить*.
|
||||
* **Шаблони URI** — у розділі **[Сервери](../servers/index.md)**, поруч із ресурсами.
|
||||
* **[Версії протоколу](../protocol-versions.md)** і
|
||||
**[Застарілі можливості](../deprecated.md)** мають власні сторінки верхнього рівня.
|
||||
|
||||
Якщо ви не впевнені, чи потрібен вам цей розділ, — він вам не потрібен.
|
||||
207
i18n/uk/pages/advanced/low-level-server.md
Normal file
207
i18n/uk/pages/advanced/low-level-server.md
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
---
|
||||
translation:
|
||||
sections: [2c79b6338e09b7ac, 7edc43b3fae11314, 1086e77ce561cd7f, a3f71823df5efc31, 9fc7109f72201cae, 7bf25983df655b66, 6330e1f4c6029683, 2f1749c8c133fa1c, b3530fcf4d11fd56, ebc33704fbd74262, cd0e9c933350390e]
|
||||
tool: 1
|
||||
---
|
||||
# Низькорівневий Server {#the-low-level-server}
|
||||
|
||||
`@mcp.tool()` — це шар. Під ним лежить другий серверний клас, `Server`, який говорить «сирим» MCP: ви передаєте йому об'єкти протоколу, а він надсилає їх мережею без змін.
|
||||
|
||||
`MCPServer` побудований поверх нього. Спускатися на рівень нижче варто тоді, коли зручний шар заважає:
|
||||
|
||||
* Потрібно віддати **точну** схему (завантажену з файлу, згенеровану з бази даних), а не виведену з сигнатури Python.
|
||||
* Потрібен повний контроль над результатом: `_meta`, `is_error`, кожен ключ `structured_content`.
|
||||
* Потрібно обробити метод, якого MCP не визначає.
|
||||
|
||||
Для всього іншого залишайтеся на `MCPServer`.
|
||||
|
||||
## Той самий інструмент, вручну {#the-same-tool-by-hand}
|
||||
|
||||
Це інструмент `search_books`, який сторінка **[Інструменти](../servers/tools.md)** пише дев'ятьма рядками `@mcp.tool()`, — тільки без синтаксичного цукру:
|
||||
|
||||
```python title="server.py" hl_lines="22 26 32"
|
||||
--8<-- "docs_src/lowlevel/tutorial001.py"
|
||||
```
|
||||
|
||||
Змінилися три речі, і вони й складають увесь низькорівневий API:
|
||||
|
||||
* **Обробники — це параметри конструктора.** `on_list_tools=` і `on_call_tool=` передаються в `Server(...)`. Декораторів тут немає, і кожен обробник має однакову форму: `async (ctx, params) -> result`.
|
||||
* **Вхідну схему пишете ви.** `Tool.input_schema` — це звичайний `dict` із JSON Schema. Ніхто не виводить її з анотацій типів, бо анотацій типів, з яких можна було б її вивести, немає.
|
||||
* **Результат будуєте ви.** `CallToolResult(content=[TextContent(...)])`, вручну. Нічого не загортається, не перетворюється й не виводиться з анотації значення, що повертається.
|
||||
|
||||
`params` — це розібраний запит: `CallToolRequestParams` дає `.name` і `.arguments`. `ctx` — це `ServerRequestContext`: `ctx.session`, щоб звертатися назад до клієнта, `ctx.lifespan_context`, `ctx.request_id` і `ctx.meta` — вхідні `_meta` запиту.
|
||||
|
||||
!!! info
|
||||
Якщо ви працювали з FastAPI, це співвідношення вам уже знайоме. `MCPServer` — це шар декораторів і анотацій типів; `Server` — це Starlette під ним. Вони не суперники: `MCPServer` створює `Server` і реєструє на ньому саме такі обробники.
|
||||
|
||||
### Спробуйте самі {#try-it}
|
||||
|
||||
Inspector тут не допоможе: `mcp dev` і `mcp run` приймають лише `MCPServer`. `Client`, що працює в пам'яті, до цього байдужий: низькорівневий `Server` він приймає так само, як і `MCPServer`:
|
||||
|
||||
```python title="main.py"
|
||||
import asyncio
|
||||
|
||||
from mcp import Client
|
||||
|
||||
from server import server
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
async with Client(server) as client:
|
||||
result = await client.call_tool("search_books", {"query": "dune", "limit": 5})
|
||||
print(result.content)
|
||||
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
```text
|
||||
[TextContent(type='text', text="Found 3 books matching 'dune' (showing up to 5).", annotations=None, meta=None)]
|
||||
```
|
||||
|
||||
Той самий текст, що його видавала версія з `@mcp.tool()`. Дві чесні відмінності:
|
||||
|
||||
* `result.structured_content` дорівнює `None`. Високорівневий сервер загортає `-> str` у `{"result": ...}` за вас; тут ніхто не будує того, чого не побудували ви.
|
||||
* `list_tools` повертає схему, яку набрали **ви**, символ у символ. У високорівневій версії на кожній властивості було `"title": "Query"`, а в корені — `"title": "search_booksArguments"`: артефакти Pydantic. Тут, якщо щось є в переданих даних, — це ви його туди поклали.
|
||||
|
||||
## Ніхто нічого не перевіряє за вас {#nothing-is-checked-for-you}
|
||||
|
||||
`MCPServer` відхиляє хибний аргумент ще до того, як ваша функція запуститься, перевіряючи виклик за схемою, яку сам згенерував (**[Інструменти](../servers/tools.md)**).
|
||||
|
||||
`Server` цього не робить. Ваша `input_schema` *оголошується* клієнтові; вона ніколи не *застосовується* до `params.arguments`.
|
||||
|
||||
!!! check
|
||||
Викличте `search_books` без `limit` — і ваш `args["limit"]` викине `KeyError`. Клієнт побачить:
|
||||
|
||||
```text
|
||||
MCPError: Internal server error
|
||||
```
|
||||
|
||||
Помилка JSON-RPC з кодом `-32603` і навмисно загальним повідомленням: SDK не видасть ваш traceback віддаленій стороні, що викликає. Модель так і не дізнається, що зробила не так, тож не зможе повторити спробу. (У тесті `raise_exceptions=True` натомість показує справжній виняток; див. **[Тестування](../get-started/testing.md)**.)
|
||||
|
||||
Це узагальнюється. Виняток, викинутий із низькорівневого обробника, — це **завжди** помилка протоколу й ніколи — результат інструмента з `is_error=True`. Якщо потрібно, щоб модель прочитала про невдачу й відновилася, перевіряйте `params.arguments` самі й повертайте `CallToolResult(content=[TextContent(...)], is_error=True)`. Обом видам невдач присвячена сторінка **[Обробка помилок](../servers/handling-errors.md)**.
|
||||
|
||||
## Два інструменти, один обробник {#two-tools-one-handler}
|
||||
|
||||
`on_call_tool` — єдина точка входу для всіх інструментів сервера. Маршрутизуєте за `params.name`:
|
||||
|
||||
```python title="server.py" hl_lines="38-43"
|
||||
--8<-- "docs_src/lowlevel/tutorial002.py"
|
||||
```
|
||||
|
||||
* `list_tools` оголошує обидва. `call_tool` диспетчеризує за іменем.
|
||||
* Гілка `else` важлива: `Server` спокійно передасть `tools/call` з іменем, якого ви ніколи не оголошували, просто у ваш обробник. Виняток там перетворює виклик на ту саму `-32603`, що й вище.
|
||||
|
||||
## Структурований вивід, вручну {#structured-output-by-hand}
|
||||
|
||||
Оголосіть `output_schema` на `Tool` і покладіть `structured_content` у результат. І те, й інше — ваше:
|
||||
|
||||
```python title="server.py" hl_lines="19-23 36"
|
||||
--8<-- "docs_src/lowlevel/tutorial003.py"
|
||||
```
|
||||
|
||||
Викличте його — і результат міститиме обидва подання:
|
||||
|
||||
```json
|
||||
{
|
||||
"content": [{"type": "text", "text": "Found 3 books matching 'dune'."}],
|
||||
"structuredContent": {"matches": 3, "query": "dune"},
|
||||
"isError": false,
|
||||
"resultType": "complete",
|
||||
"_meta": {"io.modelcontextprotocol/serverInfo": {"name": "Bookshop", "version": "2.0.0"}}
|
||||
}
|
||||
```
|
||||
|
||||
Блок `_meta` — це ідентифікаційна позначка сервера: SDK додає його до кожного результату покоління 2026 разом із `version` з конструктора (сервер, який її не задав, повідомляє порожній рядок). Сервер, який не повинен себе ідентифікувати, може прибрати цей ключ за допомогою middleware, який володіє результатами, що повертає.
|
||||
|
||||
Сервер ніколи не порівнює ці два поля. `Client` цього SDK — порівнює: поверніть `structured_content`, що не відповідає оголошеній вами `output_schema`, і `call_tool` викине `RuntimeError`, який починається з `Invalid structured content returned by tool search_books` і далі цитує помилку `jsonschema`. Пообіцяти схему легко; дотримати її — ваша справа. Уся драбина типів повернення та схем — на сторінці **[Структурований вивід](../servers/structured-output.md)**.
|
||||
|
||||
## `_meta`: для застосунку, а не для моделі {#\_meta-for-the-application-not-the-model}
|
||||
|
||||
`content` — це частина відповіді, яку читає модель. `structured_content` — та сама відповідь у вигляді типізованих даних. `_meta` — третій канал: дані, що їдуть разом із результатом для **клієнтського застосунку** і взагалі не є частиною відповіді.
|
||||
|
||||
Використовуйте його для ідентифікаторів записів, ідентифікаторів трасування — усього, що потрібно вашому UI, але не потрібно промпту:
|
||||
|
||||
```python title="server.py" hl_lines="37"
|
||||
--8<-- "docs_src/lowlevel/tutorial004.py"
|
||||
```
|
||||
|
||||
* Конструюєте його як `_meta=` — це ім'я в переданих даних. Клієнт зчитує його як `result.meta`.
|
||||
* Додавайте до ключів простір імен (`bookshop/record_ids`). Ключі `io.modelcontextprotocol/*` зарезервовано протоколом.
|
||||
|
||||
!!! warning
|
||||
`_meta` — це домовленість між вами й клієнтським застосунком, а не гарантія того, що дійде
|
||||
до моделі. Що показувати, вирішує хост. Ніколи не кладіть секрет у жодну частину результату інструмента.
|
||||
|
||||
## Можливості випливають з обробників {#capabilities-follow-your-handlers}
|
||||
|
||||
`Server` оголошує рівно ті сімейства методів, для яких ви дали йому обробники. `Bookshop` вище передає `on_list_tools` і `on_call_tool` і більше нічого, тож клієнт, що до нього під'єднується, бачить:
|
||||
|
||||
```json
|
||||
{"tools": {"listChanged": false}}
|
||||
```
|
||||
|
||||
Жодних `resources`, жодних `prompts`: за ними нічого не стоїть. Передайте `on_list_prompts` — і з'явиться `prompts`; передайте `on_completion` — і з'явиться `completions`.
|
||||
|
||||
`MCPServer` завжди оголошує інструменти, ресурси й промпти, зареєстрували ви щось чи ні, бо його менеджери існують завжди. Тут же оголошення — це *і є* виклик конструктора.
|
||||
|
||||
## Життєвий цикл як параметр типу {#the-lifespan-generic}
|
||||
|
||||
`Server` узагальнений за типом, який видає його життєвий цикл (lifespan). Анотуйте його один раз — і об'єкт буде типізованим усюди, де з'являється:
|
||||
|
||||
```python title="server.py" hl_lines="24-26 44-45 50"
|
||||
--8<-- "docs_src/lowlevel/tutorial005.py"
|
||||
```
|
||||
|
||||
* Життєвий цикл — це `Callable[[Server[Catalog]], AbstractAsyncContextManager[Catalog]]`; `@asynccontextmanager` над `async`-генератором дає саме це.
|
||||
* Усе, що він видає через `yield`, стає `ctx.lifespan_context`, а оскільки обробники анотовано як `ServerRequestContext[Catalog]`, `.search(...)` автодоповнюється й проходить перевірку типів.
|
||||
* У нього входять один раз під час старту сервера й виходять один раз під час зупинки. Запуск, завершення та версія тієї самої ідеї в `MCPServer` — на сторінці **[Життєвий цикл](../handlers/lifespan.md)**.
|
||||
|
||||
Без `lifespan=` `ctx.lifespan_context` — порожній `dict`.
|
||||
|
||||
## Власний метод {#a-method-of-your-own}
|
||||
|
||||
Конструктор покриває методи, які визначає MCP. `add_request_handler` покриває все інше:
|
||||
|
||||
```python title="server.py" hl_lines="35-36 39-40 43-44 48"
|
||||
--8<-- "docs_src/lowlevel/tutorial006.py"
|
||||
```
|
||||
|
||||
* Перший аргумент — рядок методу. Для сповіщень є близнюк — `add_notification_handler`.
|
||||
* `params_type` — це модель, за якою вхідні `params` перевіряються **до** запуску вашого обробника, тож власні методи *отримують* перевірку, якої інструменти не мають. Успадковуйтеся від `RequestParams`, щоб поле `_meta` розбиралося так само, як у кожного іншого методу.
|
||||
* Обробник повертає `BaseModel`, `dict` або `None`. SDK серіалізує це в результат JSON-RPC.
|
||||
|
||||
Одне чесне застереження: високорівневий `Client` має дієслова лише для методів, які визначає MCP, тож `client.reindex()` немає. Вендорний метод — для сторони, яка вже знає про його існування: клієнта, який ви теж постачаєте, або іншого вашого сервісу, що говорить JSON-RPC.
|
||||
|
||||
Один метод забрати собі не можна:
|
||||
|
||||
```text
|
||||
ValueError: 'initialize' is handled by the server runner and cannot be overridden;
|
||||
use Server.middleware to observe or wrap initialization
|
||||
```
|
||||
|
||||
Рукостискання належить засобу запуску сервера. `server/discover`, `ping` та всі інші вбудовані методи можна замінювати.
|
||||
|
||||
!!! tip
|
||||
`Server.middleware`, згаданий у цій помилці, обгортає **кожне** вхідне повідомлення, включно з `initialize`. Якщо мета — спостерігати за трафіком чи переписувати його, а не відповідати на новий метод, почніть із **[Middleware](middleware.md)**.
|
||||
|
||||
## Інші обробники {#the-other-handlers}
|
||||
|
||||
Кожен із них — одна ідея, для якої у вас тепер є словник; кожна має власну сторінку.
|
||||
|
||||
* `on_call_tool`, `on_get_prompt` і `on_read_resource` можуть повернути `InputRequiredResult` замість звичайного результату, щоб призупинити виклик і попросити клієнта про введення; див. **[Багатораундові запити](../handlers/multi-round-trip.md)** (multi-round-trip). Як і годиться цьому рівню, нічого не встановлюється за вас: якщо `MCPServer` за замовчуванням запечатує `requestState`, то тут заданий вами `request_state` передається мережею точно так, як написано, доки ви не ввімкнете захист через `server.middleware.append(RequestStateBoundary(RequestStateSecurity(keys=[...]), default_audience=server.name))`: один рядок (обидва імені імпортуються з `mcp.server.request_state`) — і отримуєте те саме запечатування й перевірку, що їх виконує `MCPServer` (**[Захист `requestState`](../handlers/multi-round-trip.md#protecting-requeststate)**).
|
||||
* `on_list_resources`, `on_read_resource`, `on_list_prompts`, `on_get_prompt`, `on_completion` — та сама форма `(ctx, params) -> result` для інших примітивів.
|
||||
* `on_subscriptions_listen` обслуговує потік `subscriptions/listen` версії 2026-07-28. Передайте `ListenHandler`, побудований поверх `SubscriptionBus`, і публікуйте події в шину з інших обробників; повну композицію див. на сторінці **[Підписки](../handlers/subscriptions.md)**.
|
||||
* `server.streamable_http_app()` повертає той самий Starlette-застосунок, що й у `MCPServer`; розгортайте його так, як **[Запуск сервера](../run/index.md)** розгортає будь-який інший ASGI-застосунок. `server.run(transport=...)` тут немає: `server.run(read_stream, write_stream, server.create_initialization_options())` веде одне з'єднання через пару потоків, і цей один рядок — оце й усе.
|
||||
|
||||
## Підсумки {#recap}
|
||||
|
||||
* Низькорівневий `Server` приймає обробники як **параметри конструктора** `on_*`; кожен обробник — це `async (ctx, params) -> result`.
|
||||
* Ви пишете словник `input_schema` і будуєте `CallToolResult`. Нічого не виводиться, не загортається й не перевіряється за вас.
|
||||
* Виняток в обробнику — це помилка протоколу `-32603`. Помилка інструмента, яку може прочитати модель, — це `CallToolResult` з `is_error=True`, який повертаєте **ви**.
|
||||
* `_meta` в результаті адресовано клієнтському застосунку, а не моделі.
|
||||
* `Server[T]` узагальнений за тим, що видає його життєвий цикл; `ctx.lifespan_context` — це типізований `T`.
|
||||
* `add_request_handler(method, params_type, handler)` обслуговує будь-який метод. `initialize` зарезервовано.
|
||||
* Можливості, які оголошує `Server`, виводяться з того, які обробники ви зареєстрували.
|
||||
|
||||
`Client(server)` поводився з обома серверами однаково, бо вони *і є* тим самим протоколом — у цьому й увесь сенс. Наступний шар нижче — взагалі не клас: це **[Middleware](middleware.md)**.
|
||||
128
i18n/uk/pages/advanced/middleware.md
Normal file
128
i18n/uk/pages/advanced/middleware.md
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
---
|
||||
translation:
|
||||
sections: [6048b4f308edbb8c, 068bda0f21ee9c1b, c3e565b61acd75c5, c62422b159c6ed09, 47204fab253cc45c]
|
||||
tool: 1
|
||||
---
|
||||
# Middleware {#middleware}
|
||||
|
||||
**Middleware** — це одна асинхронна функція, що огортає кожне повідомлення, яке отримує сервер.
|
||||
|
||||
Її пишуть у формі `async (ctx, call_next)` і додають до `server.middleware`. Оце й увесь API.
|
||||
|
||||
!!! warning
|
||||
Список middleware у вихідному коді позначено як **попередній** (provisional): його сигнатура
|
||||
й семантика можуть змінитися в мінорному випуску 2.x. Використовуйте його, щоб
|
||||
*спостерігати* (час виконання, логування, трасування) і щоб *відхиляти* повідомлення; не
|
||||
робіть його фундаментом, на якому тримається сервер.
|
||||
|
||||
`MCPServer` приймає список у конструкторі (`MCPServer(name, middleware=[...])`) і надає доступ
|
||||
до нього як `mcp.middleware`; низькорівневий `Server` надає той самий список як
|
||||
`server.middleware`. Приклад нижче використовує низькорівневий `Server`; якщо
|
||||
`Server(name, on_call_tool=...)` вам незнайомий, спершу прочитайте сторінку
|
||||
**[Низькорівневий Server](low-level-server.md)**.
|
||||
|
||||
## Middleware для вимірювання часу {#a-timing-middleware}
|
||||
|
||||
Один сервер, один інструмент, один шар middleware, який записує в лог, скільки часу забрало кожне повідомлення:
|
||||
|
||||
```python title="server.py" hl_lines="39-45 49"
|
||||
--8<-- "docs_src/middleware/tutorial001.py"
|
||||
```
|
||||
|
||||
* `ctx` — той самий `ServerRequestContext`, що його отримують обробники. `ctx.method` — сирий
|
||||
рядок методу; `ctx.params` — сирі параметри, **до** будь-якої валідації.
|
||||
* `call_next(ctx)` запускає решту ланцюжка: валідацію, пошук обробника, сам обробник.
|
||||
Поверніть те, що він повернув, — і відповідь залишиться недоторканою.
|
||||
* `try`/`finally` тут навмисно: обробник, що викидає виняток, однаково буде заміряно, бо збій
|
||||
доходить до middleware як виняток із `call_next`.
|
||||
* `server.middleware.append(...)` реєструє її. Список виконується від зовнішнього до
|
||||
внутрішнього, тож `middleware[0]` — найближча до мережі.
|
||||
|
||||
### Спробуйте самі {#try-it}
|
||||
|
||||
Під'єднайте клієнт, отримайте список інструментів, викличте один. У лозі буде **три** рядки:
|
||||
|
||||
```text
|
||||
server/discover took 18.3 ms
|
||||
tools/list took 0.1 ms
|
||||
tools/call took 0.1 ms
|
||||
```
|
||||
|
||||
Ви зробили два виклики, а рядків три. Перший — `server/discover`: запит, який клієнт надіслав,
|
||||
щоб установити з'єднання, ще до того, як ви щось попросили.
|
||||
|
||||
У цьому й суть. Middleware огортає **кожне** вхідне повідомлення:
|
||||
|
||||
* Установлення з'єднання: `server/discover`, або `initialize` і `notifications/initialized`
|
||||
у сесії старого покоління.
|
||||
* Кожен запит і кожне сповіщення. Для сповіщення `ctx.request_id is None`,
|
||||
`call_next(ctx)` повертає `None`, а все, що повернете ви, відкидається.
|
||||
* Навіть метод, для якого сервер не має обробника: `call_next` викидає
|
||||
`MCPError(-32601, "Method not found")` *крізь* ваш middleware на шляху до клієнта.
|
||||
|
||||
## Що можна робити всередині {#what-you-can-do-inside-one}
|
||||
|
||||
У порядку зростання того, наскільки варто вагатися:
|
||||
|
||||
* **Спостерігати.** Заміряти час, рахувати, логувати. Приклад вище.
|
||||
* **Відхиляти.** Викиньте `MCPError` *замість* виклику `call_next(ctx)` — і саме на це
|
||||
повідомлення клієнт отримає помилку JSON-RPC. З'єднання лишається живим; наступне
|
||||
повідомлення проходить. Саме так сервер обмежує `subscriptions/listen` для окремих
|
||||
викликачів: розділ **[Хто має право стежити](../handlers/subscriptions.md#deciding-who-may-watch)**
|
||||
на сторінці про підписки показує це крок за кроком.
|
||||
* **Переписувати.** `ctx` — це dataclass: `await call_next(dataclasses.replace(ctx, params=...))`
|
||||
передає решті ланцюжка інші параметри, ніж надіслав клієнт. Ніколи не робіть цього з
|
||||
`initialize`: результат, який отримає клієнт, будується з переписаних параметрів, але сервер
|
||||
фіксує стан з'єднання за первинними параметрами з мережі. Обидві сторони можуть завершити
|
||||
рукостискання, не погоджуючись щодо того, про що вони домовилися.
|
||||
* **Відповідати.** Поверніть результат, не викликаючи `call_next(ctx)`, — і він піде клієнтові
|
||||
як ваша відповідь. `call_next` віддає вам готову форму для передавання мережею, а конвеєр
|
||||
ніколи не латає те, що ви повертаєте, тож уся обгортка — ваша відповідальність: на з'єднанні
|
||||
покоління 2026 сюди входить і позначка `_meta` з `serverInfo`, яку SDK додає до результатів
|
||||
обробників, але не до ваших.
|
||||
|
||||
!!! check
|
||||
`initialize` — одна з речей, які огортає middleware, і це *єдиний* гачок, який ви для нього
|
||||
маєте. Спробуйте перехопити його через `add_request_handler` — і SDK відмовить:
|
||||
|
||||
```text
|
||||
ValueError: 'initialize' is handled by the server runner and cannot be overridden;
|
||||
use Server.middleware to observe or wrap initialization
|
||||
```
|
||||
|
||||
!!! warning
|
||||
`initialize` обробляється на місці: сервер не читає наступних вхідних повідомлень, доки
|
||||
ланцюжок middleware не поверне керування. Тому очікування запиту від сервера до клієнта
|
||||
(`ctx.session.send_request(...)`, еліцитація (elicitation)) під час обробки `initialize`
|
||||
**заблокує з'єднання намертво**: відповідь, на яку ви чекаєте, ніколи не буде прочитано.
|
||||
Сповіщення за принципом «надіслав і забув» — без проблем.
|
||||
|
||||
## Єдиний middleware, увімкнений за замовчуванням {#the-one-middleware-that-ships-on-by-default}
|
||||
|
||||
SDK постачає рівно один шар middleware, і він уже є в списку вашого сервера: той, що створює
|
||||
спан OpenTelemetry для кожного повідомлення. Його не потрібно додавати, і здебільшого про нього
|
||||
не доводиться думати. Він нічого не робить, доки ви не встановите експортер, і має власну
|
||||
сторінку: **[OpenTelemetry](../run/opentelemetry.md)**.
|
||||
|
||||
!!! info
|
||||
Якщо ви писали ASGI-middleware, ця форма вам уже знайома. `(scope, receive, send)` зі
|
||||
Starlette перетворилося на `(ctx, call_next)` і виконується *після* транспорту — над
|
||||
декодованим повідомленням, а не над сирим HTTP-запитом. Обидва поєднуються: middleware
|
||||
Starlette на `streamable_http_app()` бачить HTTP, а цей — MCP.
|
||||
|
||||
## Підсумки {#recap}
|
||||
|
||||
* Middleware — це `async (ctx, call_next) -> result`; його передають як
|
||||
`MCPServer(middleware=[...])` (або додають до `mcp.middleware`), а в низькорівневому `Server`
|
||||
додають до `server.middleware`.
|
||||
* Він огортає **кожне** вхідне повідомлення (`server/discover`, `initialize`, запити,
|
||||
сповіщення, невідомі методи) і виконується від зовнішнього до внутрішнього.
|
||||
* `ctx.request_id is None` — так відрізняють сповіщення від запиту.
|
||||
* Викиньте виняток замість виклику `call_next`, щоб відхилити одне повідомлення; з'єднання
|
||||
вціліє.
|
||||
* Власне трасування OpenTelemetry у SDK — теж middleware, і воно вже в списку. Див.
|
||||
**[OpenTelemetry](../run/opentelemetry.md)**.
|
||||
* Уся ця поверхня попередня. Спостерігайте через неї; не будуйте на ній.
|
||||
|
||||
Це все, що огортає запит. **[Авторизація](../run/authorization.md)** — те, що вирішує, чи
|
||||
запит узагалі буде виконано.
|
||||
85
i18n/uk/pages/advanced/pagination.md
Normal file
85
i18n/uk/pages/advanced/pagination.md
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
---
|
||||
translation:
|
||||
sections: [a9aba7a026c7bd85, ed32bda7ba9ae33a, 7e64cc5646abb91f, 22a0129ee78b3c63, d875373c06d8d2f9]
|
||||
tool: 1
|
||||
---
|
||||
# Пагінація {#pagination}
|
||||
|
||||
Більшості серверів вона не знадобиться ніколи.
|
||||
|
||||
`MCPServer` відповідає на кожен запит `list_*` усім, що має, однією сторінкою з `next_cursor=None`. Для кількох десятків інструментів, ресурсів чи промптів це правильна відповідь, і налаштовувати нічого не потрібно.
|
||||
|
||||
Пагінація — для сервера, у якого список ресурсів насправді є базою даних: тисячі рядків, які він відмовляється серіалізувати в одну відповідь. Відповідь протоколу — **курсор**: сервер повертає сторінку плюс непрозорий токен, а клієнт надсилає цей токен назад, щоб отримати наступну сторінку.
|
||||
|
||||
У `@mcp.resource()` немає жодного гачка для цього. Щоб розбивати на сторінки, обробник списку пишуть власноруч, на **[низькорівневому Server](low-level-server.md)**.
|
||||
|
||||
## Сервер зі сторінками {#a-server-that-pages}
|
||||
|
||||
```python title="server.py" hl_lines="12 15-16"
|
||||
--8<-- "docs_src/pagination/tutorial001.py"
|
||||
```
|
||||
|
||||
* На низькорівневому `Server` обробники — це аргументи конструктора, а не декоратори. `on_list_resources` відповідає на кожен запит `resources/list`; оце й усе під'єднання.
|
||||
* Кожен обробник зі сторінками має тип `params: PaginatedRequestParams | None`, і приклад приймає обидва варіанти. Утім, через з'єднання SDK ніколи не передає `None` (запит без члена `params` доходить до обробника як модель із типовими значеннями), тож важливий сигнал — `params.cursor is None`: **починайте з початку**.
|
||||
* Ви вирішуєте, чим курсор *є*. Тут це зсув, записаний як рядок. Мітка часу, первинний ключ, base64-блоб — будь-що, що можна видати на виході й упізнати на вході.
|
||||
* `next_cursor=None` — спосіб сказати «це була остання сторінка». Немає ні лічильника, ні загальної кількості, ні `has_more`. `None` — це весь сигнал.
|
||||
|
||||
!!! tip
|
||||
`PAGE_SIZE` у 10 робить приклад читабельним. Свій розмір обирайте для кожної кінцевої точки окремо: список
|
||||
однорядкових ресурсів може дозволити собі сторінку на 500; список важких шаблонів промптів — ні.
|
||||
Клієнт на це не впливає, і так задумано.
|
||||
|
||||
### Спробуйте самі {#try-it}
|
||||
|
||||
`Client(server)` під'єднується до низькорівневого `Server` у пам'яті так само, як і до `MCPServer`.
|
||||
|
||||
Викличте `list_resources()` без аргументів. Повертається десять ресурсів, від `book-1` до `book-10`, а `next_cursor` — рядок `"10"`.
|
||||
|
||||
Передайте його назад через `list_resources(cursor="10")` — і перший ресурс уже `book-11`, а новий `next_cursor` — `"20"`.
|
||||
|
||||
Десята сторінка приходить із `next_cursor`, що дорівнює `None`. Готово.
|
||||
|
||||
## Цикл на клієнті {#the-client-loop}
|
||||
|
||||
Кожен метод `list_*` класу `Client` (`list_tools`, `list_resources`, `list_resource_templates`, `list_prompts`) приймає іменований параметр `cursor=`. Вичерпати список зі сторінками — це один `while True`:
|
||||
|
||||
```python title="client.py" hl_lines="26-32"
|
||||
--8<-- "docs_src/pagination/tutorial002.py"
|
||||
```
|
||||
|
||||
* `cursor` починається з `None`, тому перший запит курсора не несе.
|
||||
* Розширюйте список **до** того, як дивитися на `next_cursor`: на останній сторінці теж є ресурси.
|
||||
* `next_cursor is None` — це вихід. Усе інше йде прямо назад у `cursor=`, без змін.
|
||||
|
||||
Запустіть його `main()` — і він надрукує `100 resources`: десять сторінок по десять, зшитих циклом, який так і не дізнався, що сторінок було десять.
|
||||
|
||||
Це той самий цикл, який **[Клієнт](../client/index.md)** показує для кожного дієслова `list_*`, і проти сервера без сторінок він нічого не коштує: `next_cursor` дорівнює `None` вже в першій відповіді, і цикл виконується один раз.
|
||||
|
||||
## Три правила {#the-three-rules}
|
||||
|
||||
**Курсори непрозорі.** Клієнт ніколи не повинен розбирати, будувати чи вгадувати курсор. Єдине законне джерело курсора — `next_cursor` попередньої сторінки, дослівно.
|
||||
|
||||
**Розмір сторінки обирає сервер.** У протоколі немає `limit=`. Якщо потрібен інший розмір сторінки, змінюють сервер.
|
||||
|
||||
**Клієнт, що ігнорує пагінацію, усе одно працює.** Він викликає `list_resources()` один раз, отримує перші десять і навіть не помічає `next_cursor`, який викинув. Нічого не ламається; він просто бачить менше.
|
||||
|
||||
!!! check
|
||||
Непрозорий означає непрозорий. Вигадайте курсор (`list_resources(cursor="page-2")`) — і
|
||||
протокол нічим не зможе допомогти. Цей сервер пробує `int("page-2")`, обробник викидає виняток,
|
||||
а до клієнта повертається:
|
||||
|
||||
```text
|
||||
MCPError(-32603, 'Internal server error', None)
|
||||
```
|
||||
|
||||
Курсор, отриманий не від сервера, — це помилка, а не запит на нову можливість.
|
||||
|
||||
## Підсумки {#recap}
|
||||
|
||||
* `MCPServer` повертає все однією сторінкою. Пагінацію вмикають свідомо, і роблять це на низькорівневому `Server`.
|
||||
* `on_list_resources` (а також `on_list_tools`, `on_list_prompts`, `on_list_resource_templates`) отримує `PaginatedRequestParams | None`; для першої сторінки `params.cursor` дорівнює `None`.
|
||||
* Повертається сторінка плюс `next_cursor`: будь-який рядок, який ви згодом упізнаєте, або `None`, коли більше нічого не лишилося.
|
||||
* Цикл на клієнті: передати `cursor=`, накопичити, повторювати, доки `next_cursor is None`.
|
||||
* Курсори непрозорі, розмір сторінки належить серверу, а клієнт без пагінації все одно отримує першу сторінку.
|
||||
|
||||
Решта написаного власноруч API `Server` (`on_call_tool`, словники `input_schema`, `_meta`) — на сторінці **[Низькорівневий Server](low-level-server.md)**.
|
||||
Loading…
Reference in a new issue