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