docs: refresh translations, and translate pages in parallel (#3458)

This commit is contained in:
Max 2026-09-07 13:35:53 +01:00 • committed by GitHub
commit fd66270926
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 4821 additions and 2277 deletions

View file

@ -1,6 +1,6 @@
---
translation:
sections: [0355618e5f4d5fe4, 1821eaf50f2d0b64, 82e0b28ebd3abf5a, 8ac39614c094f2d0, dab6ff945501ab2a, bd5565c3b2d4f959, 96819ce3d63a0487]
sections: [0355618e5f4d5fe4, 0fefa31fb7c7b585, 5c53e7487c9c70cc, 8ac39614c094f2d0, dab6ff945501ab2a, bd5565c3b2d4f959, 96819ce3d63a0487]
tool: 1
---
# MCP Apps {#mcp-apps}
@ -18,13 +18,13 @@ SDK постачає це як вбудоване розширення `Apps` (`
## Годинник із циферблатом {#a-clock-with-a-face}
```python title="server.py" hl_lines="19 22 30 32"
```python title="server.py" hl_lines="17 20 28 30"
--8<-- "docs_src/apps/tutorial001.py"
```
Чотири кроки:
* `Apps()`: один екземпляр тримає ваші інструменти з прив'язаним UI та їхні ресурси.
* `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`.
@ -39,11 +39,31 @@ SDK постачає це як вбудоване розширення `Apps` (`
Модель читає `content`; iframe — для людей. Хост із підтримкою UI все одно передає текстовий результат моделі, а суто текстовий клієнт отримує *лише* його. Тож канонічний шаблон — один інструмент, дві відповіді. Погляньте на `get_time` ще раз:
```python title="server.py" hl_lines="23-27"
```python title="server.py" hl_lines="21-25"
--8<-- "docs_src/apps/tutorial001.py"
```
`client_supports_apps(ctx)` дорівнює `True` лише тоді, коли клієнт оголосив розширення `io.modelcontextprotocol/ui` **і** вказав `text/html;profile=mcp-app` у своїх налаштуваннях `mimeTypes`. Поле обов'язкове, тож клієнт, який його пропустив, не зараховується. Саме це й оголошує `main()` у тому самому файлі: клієнтську половину узгодження — і у відповідь приходить розширений варіант.
`client_supports_apps(ctx)` дорівнює `True` лише тоді, коли клієнт оголосив розширення `io.modelcontextprotocol/ui` **і** вказав `text/html;profile=mcp-app` у своїх налаштуваннях `mimeTypes`. Поле обов'язкове, тож клієнт, який його пропустив, не зараховується. Ось клієнтська половина узгодження:
```python title="client.py" hl_lines="8 12"
--8<-- "docs_src/apps/tutorial001_client.py"
```
Віддайте `server.py` через HTTP, а потім із другого термінала запустіть клієнт:
```console
uv run mcp run server.py --transport streamable-http
```
```console
python client.py
```
```text
2026-06-26T12:00:00Z
```
Повернулася розширена відповідь. Приберіть `extensions=[APPS_SUPPORT]` з виклику `Client` — і та сама програма натомість надрукує `The time is 2026-06-26T12:00:00Z.`, а це все, що коли-небудь бачить суто текстовий клієнт.
!!! warning
Ніколи не повертайте заглушку на кшталт `"[Rendered UI]"` як єдиний вміст. Якщо резервний текст марний, інструмент марний для кожного суто текстового клієнта й для самої моделі. Напишіть нормальне речення.

View file

@ -1,6 +1,6 @@
---
translation:
sections: [05891e7cc1938a13, b3c01a6af28c51ee, 7ffc91f5e38bdfe0, 717d3f235a8333a7, f471a13b2fe5d737, ed6af2df4b656dff]
sections: [05891e7cc1938a13, b3c01a6af28c51ee, 6eba6ce094f4417e, 125f28588c85dd7b, ddd8969b698ca11c, ed6af2df4b656dff]
tool: 1
---
# Розширення {#extensions}
@ -49,7 +49,7 @@ TypeError: Stamps.identifier must be a `vendor-prefix/name` string
Найменше корисне розширення — один інструмент і карта налаштувань:
```python title="server.py" hl_lines="17 19-20 22-23 26"
```python title="server.py" hl_lines="16 18-19 21-22 25"
--8<-- "docs_src/extensions/tutorial003.py"
```
@ -57,17 +57,23 @@ TypeError: Stamps.identifier must be a `vendor-prefix/name` string
* `settings()` — це значення, що оголошується в `capabilities.extensions["com.example/stamps"]`. Поверніть `{}` (типове значення), щоб оголосити розширення без налаштувань.
* Розширення ніколи не отримує сервер. Воно оголошує свій внесок як дані; `MCPServer` їх споживає. Ніякого `self.server`, який можна було б змінювати, немає.
А `main()` — доказ: клієнт у пам'яті, під'єднаний безпосередньо до `mcp`:
Запустіть його через HTTP, а доказом буде клієнт:
```python title="server.py" hl_lines="29-34"
--8<-- "docs_src/extensions/tutorial003.py"
```console
uv run mcp run server.py --transport streamable-http
```
```python title="client.py" hl_lines="7-11"
--8<-- "docs_src/extensions/tutorial003_client.py"
```
Кожен `server.py` на цій сторінці запускається цією командою, а кожен `client.py` працює поруч із ним: `python client.py` у другому терміналі.
### Обслуговування власних методів {#serving-your-own-methods}
Розширення може реєструвати **нові методи запитів**: власні дієслова, які обслуговуються поруч із методами специфікації:
```python title="server.py" hl_lines="16-22 31 40-48"
```python title="server.py" hl_lines="14-20 24 33-41"
--8<-- "docs_src/extensions/tutorial004.py"
```
@ -83,14 +89,15 @@ TypeError: Stamps.identifier must be a `vendor-prefix/name` string
### Клієнтська сторона {#the-client-side}
`main()` у тому самому файлі — це вся клієнтська частина, обидві її половини:
Клієнт — окрема програма, і в ній обидві половини клієнтської частини:
```python title="server.py" hl_lines="54-58"
--8<-- "docs_src/extensions/tutorial004.py"
```python title="client.py" hl_lines="21-23 27-30"
--8<-- "docs_src/extensions/tutorial004_client.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`, тож вендорський запит проходить як є.
* `SearchRequest` і дві моделі, які він несе, — це мережевий контракт розширення, тож клієнт оголошує їх для себе сам. Опубліковане розширення постачало б їх у пакеті, який імпортують обидві сторони.
### Перехоплення `tools/call` {#intercepting-toolscall}
@ -109,12 +116,18 @@ TypeError: Stamps.identifier must be a `vendor-prefix/name` string
## Використання клієнтського розширення {#using-a-client-extension}
**Клієнтське розширення** — той самий контракт з боку споживача: набір клієнтської поведінки за одним ідентифікатором. Передайте екземпляри в `Client(extensions=[...])` і викликайте інструменти як зазвичай:
**Клієнтське розширення** — той самий контракт з боку споживача: набір клієнтської поведінки за одним ідентифікатором. Сервер тут відповідає на `buy` не товаром, а квитанцією, яку треба погасити, — і лише клієнту, що оголосив розширення:
```python title="client.py" hl_lines="66-68"
```python title="server.py" hl_lines="22-25"
--8<-- "docs_src/extensions/tutorial006.py"
```
На клієнті передайте екземпляри в `Client(extensions=[...])` і викликайте інструменти як зазвичай:
```python title="client.py" hl_lines="33-35"
--8<-- "docs_src/extensions/tutorial006_client.py"
```
`call_tool("buy", ...)` повертає звичайний `CallToolResult`, як і будь-який інший виклик. Що змінило розширення: тепер сервер може відповісти на `buy` **формою результату** `receipt` замість остаточного результату, а `Receipts` доводить її до кінця (тут — погашаючи квитанцію наступним викликом), перш ніж `call_tool` поверне значення. У місці виклику нічого не змінюється.
Приберіть розширення — і нічого з цього не існує: шлагбаум сервера відмовляє клієнту, який його не оголосив (помилка -32021), а заявлена форма від сервера, що обходить шлагбаум, не проходить перевірку — точно так, як специфікація вимагає для нерозпізнаного `resultType`. Вимкнено за замовчуванням, з обох кінців з'єднання.
@ -124,15 +137,15 @@ TypeError: Stamps.identifier must be a `vendor-prefix/name` string
```python
from mcp.client import advertise
client = Client(mcp, extensions=[advertise("com.example/search")])
client = Client("http://localhost:8000/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"
```python title="client.py" hl_lines="16-17 25-26 28-29"
--8<-- "docs_src/extensions/tutorial006_client.py"
```
* Ідентифікатор підпорядковується тій самій граматиці, що й на сервері, і перевіряється під час визначення класу.
@ -153,12 +166,18 @@ def notifications(self) -> Sequence[NotificationBinding[Any]]:
### Дієслова розширень {#extension-verbs}
Власні методи запитів розширення не потребують реєстрації на клієнті. Тип вендорського запиту успадковує `mcp.types.Request` і проходить через `client.session.send_request`, як у розділі [Обслуговування власних методів](#serving-your-own-methods). Одне доповнення: коли ключ параметрів мусить передаватися в заголовку `Mcp-Name` (специфікації розширень, як-от tasks, вимагають цього для своїх дієслів), тип запиту оголошує `name_param`:
Власні методи запитів розширення не потребують реєстрації на клієнті. Тип вендорського запиту успадковує `mcp.types.Request` і проходить через `client.session.send_request`, як у розділі [Обслуговування власних методів](#serving-your-own-methods). Візьмімо сервер, розширення якого обслуговує одне дієслово про іменоване завдання:
```python title="client.py" hl_lines="22-25 46-47"
```python title="server.py" hl_lines="12-13 30"
--8<-- "docs_src/extensions/tutorial007.py"
```
Одне доповнення на клієнті: коли ключ параметрів мусить передаватися в заголовку `Mcp-Name` (специфікації розширень, як-от tasks, вимагають цього для своїх дієслів), тип запиту оголошує `name_param`:
```python title="client.py" hl_lines="20-23 28-29"
--8<-- "docs_src/extensions/tutorial007_client.py"
```
Сесія дзеркалить `params["jobId"]` у `Mcp-Name` на кожному шляху надсилання, а відсутнє значення дає гучну помилку замість того, щоб мовчки пропустити обов'язковий заголовок.
## Чого розширення не може {#what-an-extension-cannot-do}

View file

@ -1,6 +1,6 @@
---
translation:
sections: [2c79b6338e09b7ac, 7edc43b3fae11314, 1086e77ce561cd7f, a3f71823df5efc31, 9fc7109f72201cae, d50fe7faead8cf68, 7bf25983df655b66, 6330e1f4c6029683, 2f1749c8c133fa1c, 8db7116fc8ddd0ee, ebc33704fbd74262, cd0e9c933350390e]
sections: [2c79b6338e09b7ac, 9d5d10a5f0405d0a, 1086e77ce561cd7f, a3f71823df5efc31, 9fc7109f72201cae, d50fe7faead8cf68, 7bf25983df655b66, 6330e1f4c6029683, 2f1749c8c133fa1c, 8db7116fc8ddd0ee, ebc33704fbd74262, 0fde3bcea081ba3a]
tool: 1
---
# Низькорівневий Server {#the-low-level-server}
@ -36,18 +36,22 @@ translation:
### Спробуйте самі {#try-it}
Inspector тут не допоможе: `mcp dev` і `mcp run` приймають лише `MCPServer`. `Client`, що працює в пам'яті, до цього байдужий: низькорівневий `Server` він приймає так само, як і `MCPServer`:
`mcp dev` і `mcp run` приймають лише `MCPServer`, тож цей сервер доведеться запускати самостійно. Останній рядок `server.py` будує з нього звичайний ASGI-застосунок, а uvicorn його запускає:
```python title="main.py"
```console
uvicorn server:app --port 8000
```
Спрямуйте Inspector або будь-який клієнт на `http://localhost:8000/mcp`:
```python title="client.py"
import asyncio
from mcp import Client
from server import server
async def main() -> None:
async with Client(server) as client:
async with Client("http://localhost:8000/mcp") as client:
result = await client.call_tool("search_books", {"query": "dune", "limit": 5})
print(result.content)
@ -64,6 +68,8 @@ asyncio.run(main())
* `result.structured_content` дорівнює `None`. Високорівневий сервер загортає `-> str` у `{"result": ...}` за вас; тут ніхто не будує того, чого не побудували ви.
* `list_tools` повертає схему, яку набрали **ви**, символ у символ. У високорівневій версії на кожній властивості було `"title": "Query"`, а в корені — `"title": "search_booksArguments"`: артефакти Pydantic. Тут, якщо щось є в переданих даних, — це ви його туди поклали.
У тесті uvicorn і порт не потрібні: `Client(server)` приймає низькорівневий `Server` у тому ж процесі так само, як і `MCPServer`, — саме цей підхід описано на сторінці **[Тестування](../get-started/testing.md)**.
## Ніхто нічого не перевіряє за вас {#nothing-is-checked-for-you}
`MCPServer` відхиляє хибний аргумент ще до того, як ваша функція запуститься, перевіряючи виклик за схемою, яку сам згенерував (**[Інструменти](../servers/tools.md)**).
@ -215,4 +221,4 @@ use Server.middleware to observe or wrap initialization
* `add_request_handler(method, params_type, handler)` обслуговує будь-який метод. `initialize` зарезервовано.
* Можливості, які оголошує `Server`, виводяться з того, які обробники ви зареєстрували.
`Client(server)` поводився з обома серверами однаково, бо вони *і є* тим самим протоколом — у цьому й увесь сенс. Наступний шар нижче — взагалі не клас: це **[Middleware](middleware.md)**.
Клієнт поводився з обома серверами однаково, бо вони *і є* тим самим протоколом — у цьому й увесь сенс. Наступний шар нижче — взагалі не клас: це **[Middleware](middleware.md)**.

View file

@ -1,6 +1,6 @@
---
translation:
sections: [a9aba7a026c7bd85, ed32bda7ba9ae33a, 7e64cc5646abb91f, 22a0129ee78b3c63, d875373c06d8d2f9]
sections: [a9aba7a026c7bd85, 83e2a08b9d46a398, 9fd8a0aa384b3257, 22a0129ee78b3c63, d875373c06d8d2f9]
tool: 1
---
# Пагінація {#pagination}
@ -31,9 +31,13 @@ translation:
### Спробуйте самі {#try-it}
`Client(server)` під'єднується до низькорівневого `Server` у пам'яті так само, як і до `MCPServer`.
`mcp run` приймає лише `MCPServer`, тож цей сервер доведеться обслуговувати самостійно. Останній рядок `server.py` будує з `Server` звичайний ASGI-застосунок, а uvicorn його запускає:
Викличте `list_resources()` без аргументів. Повертається десять ресурсів, від `book-1` до `book-10`, а `next_cursor` — рядок `"10"`.
```console
uvicorn server:app --port 8000
```
Спрямуйте будь-який клієнт (**[Клієнт](../client/index.md)** або Inspector) на `http://localhost:8000/mcp` і викличте `list_resources()` без аргументів. Повертається десять ресурсів, від `book-1` до `book-10`, а `next_cursor` — рядок `"10"`.
Передайте його назад через `list_resources(cursor="10")` — і перший ресурс уже `book-11`, а новий `next_cursor` — `"20"`.
@ -43,7 +47,7 @@ translation:
Кожен метод `list_*` класу `Client` (`list_tools`, `list_resources`, `list_resource_templates`, `list_prompts`) приймає іменований параметр `cursor=`. Вичерпати список зі сторінками — це один `while True`:
```python title="client.py" hl_lines="26-32"
```python title="client.py" hl_lines="9-15"
--8<-- "docs_src/pagination/tutorial002.py"
```
@ -51,7 +55,7 @@ translation:
* Розширюйте список **до** того, як дивитися на `next_cursor`: на останній сторінці теж є ресурси.
* `next_cursor is None` — це вихід. Усе інше йде прямо назад у `cursor=`, без змін.
Запустіть його `main()` — і він надрукує `100 resources`: десять сторінок по десять, зшитих циклом, який так і не дізнався, що сторінок було десять.
Поки uvicorn усе ще обслуговує `server.py`, запустіть `python client.py` у другому терміналі. Він надрукує `100 resources`: десять сторінок по десять, зшитих циклом, який так і не дізнався, що сторінок було десять.
Це той самий цикл, який **[Клієнт](../client/index.md)** показує для кожного дієслова `list_*`, і проти сервера без сторінок він нічого не коштує: `next_cursor` дорівнює `None` вже в першій відповіді, і цикл виконується один раз.

View file

@ -1,6 +1,6 @@
---
translation:
sections: [9e7b9a1710e5aeba, b74ca4c1d2ddddee, fa8714e61bf90c5a, 04db67a886b7271c, 857690fb8f876800]
sections: [9e7b9a1710e5aeba, 66a2e9acc9101d54, d3d25aa802f5145a, 04db67a886b7271c, 857690fb8f876800]
tool: 1
---
# Підказки щодо кешування {#caching-hints}
@ -30,7 +30,7 @@ translation:
На низькорівневому `Server` обробники будують результати вручну, а `ttl_ms` / `cache_scope` — звичайні поля моделей результатів. Обробник, який задає їх явно, завжди перемагає мапу конструктора, поле за полем:
```python title="server.py" hl_lines="10 16"
```python title="server.py" hl_lines="11 17"
--8<-- "docs_src/caching/tutorial002.py"
```
@ -44,10 +44,24 @@ translation:
У сесії 2026-07-28 `Client` дотримується підказок за вас: він має вбудований кеш відповідей, увімкнений за замовчуванням. Результат, що прийшов із `ttlMs`, зберігається, і ідентичний виклик у межах цього TTL обслуговується з кешу без звернення до сервера. Результат *без* підказки не кешується: результати без підказок отримують `CacheConfig.default_ttl_ms`, типове значення якого `0` (одразу застарілий), тож сервер, який нічого не оголошує, бачить рівно той самий трафік виклик-за-викликом, що й завжди.
```python title="client.py" hl_lines="33 35 38"
Щоб побачити це на власні очі, запустіть `server.py` з попереднього розділу через uvicorn (його останній рядок будує ASGI-застосунок). Обробник друкує рядок щоразу, коли справді виконується:
```console
uvicorn server:app --port 8000
```
```python title="client.py" hl_lines="20 23 28"
--8<-- "docs_src/caching/tutorial003.py"
```
Запустіть `python client.py` у другому терміналі. Він надрукує підказки, які містив перший результат: `ttlMs` обробника поруч із `cacheScope` з мапи:
```text
1000 public
```
Решту розповідає термінал сервера: між логами запитів uvicorn рядок `tools/list served` з'являється тричі.
Чотири виклики, три отримання. Другий виклик знайшов свіжий запис і не дійшов до сервера; переведення (впровадженого) годинника за межу TTL змусило третій знову звернутися до сервера; четвертий сказав `cache_mode="refresh"`. Цей іменований аргумент є в п'яти методів із кешуванням (`list_tools`, `list_prompts`, `list_resources`, `list_resource_templates`, `read_resource`):
* `"use"` (за замовчуванням) віддає свіжий запис, якщо він є, а якщо ні — звертається до сервера і зберігає результат.
@ -56,7 +70,7 @@ translation:
Одне правило стоїть над `"use"`: **виклики з `meta` завжди доходять до сервера.** Запит із заданим `meta` (токен перебігу виконання, поля трасування) очікує запиту мережею, тому за `cache_mode="use"` його обробляють як `"refresh"`: читання з кешу пропускається, а отриманий результат усе одно замінює закешований запис. `"bypass"` і явний `"refresh"` поводяться як завжди.
Щоб повністю вимкнути кешування, створіть клієнт як `Client(server, cache=None)`: кожен виклик знову стає зверненням до сервера, а `cache_mode`, хоч і приймається далі, нічого не робить.
Щоб повністю вимкнути кешування, передайте `cache=None` під час створення `Client`: кожен виклик знову стає зверненням до сервера, а `cache_mode`, хоч і приймається далі, нічого не робить.
Область також дотримується автоматично: записи `"private"` прив'язані до *розділу* (partition) кешу (нижче), тоді як записи `"public"` можуть за бажанням ділитися ширше. А **сповіщення перемагають TTL** для конкретних записів, які вони називають: сповіщення `list_changed` витісняє відповідний закешований список, а `resources/updated` витісняє закешоване читання, збережене рівно під його URI, хоч би якими свіжими вони були. На з'єднанні 2026-07-28 ці сповіщення надходять потоком `subscriptions/listen`, який відкривають через `client.listen(...)`, і витіснення завершується до того, як ваш спостерігач побачить подію; докладніше — на сторінці **[Підписки](subscriptions.md)**.

View file

@ -1,6 +1,6 @@
---
translation:
sections: [adf3c545b5be46b6, 916cd3ab1c03f461, e9be7a8d0eb0a456, 565890a636288ecf, 6af7e49db9129ec3, 06b0238c174186af, 90c6043be435fcb0]
sections: [adf3c545b5be46b6, 916cd3ab1c03f461, 32ef568335dd95a7, 565890a636288ecf, 6af7e49db9129ec3, 06b0238c174186af, 0abc5ea5cb7ff6b3]
tool: 1
---
# Колбеки клієнта {#client-callbacks}
@ -63,7 +63,7 @@ result.content # [TextContent(type='text', text='Card issued to Ada Lovelace.')
`mode="legacy"` у виклику `Client(...)` стоїть не просто так. За замовчуванням `Client(...)` узгоджує сучасний
шлях протоколу, а на ньому немає зворотного каналу (back-channel) для запитів від сервера до клієнта: `ctx.elicit`
завершується помилкою ще до того, як запуститься колбек. Вирішує це не транспорт, а узгоджений
протокол — однаково і в пам'яті, і за URL. Фіксуйте `mode="legacy"` щоразу, коли клієнт має
протокол. Фіксуйте `mode="legacy"` щоразу, коли клієнт має
відповідати на такий запит; так робить кожен тест за цією сторінкою. Докладніше — на сторінці **[Версії протоколу](../protocol-versions.md)**.
У сесії 2026-07-28 колбек не зникає, він просто отримує дані інакше: коли інструмент повертає
@ -151,4 +151,4 @@ result.structured_content # {'result': ['elicitation']}
* `sampling_callback` і `list_roots_callback` працюють так само, але обслуговують застарілі можливості; сучасні сервери натомість використовують багатораундові запити.
* `logging_callback` і `message_handler` отримують сповіщення. Вони нічого не оголошують.
Перший аргумент `Client(...)` — об'єкт транспорту. Усі його різновиди описано на сторінці **[Транспорти клієнта](transports.md)**.
Перший аргумент `Client(...)` визначає транспорт. Усі його різновиди описано на сторінці **[Транспорти клієнта](transports.md)**.

View file

@ -1,6 +1,6 @@
---
translation:
sections: [ebef1e7a0df854f4, 8355cfaf1f76c9d5, 8e79141fc2985342, 46bdb07c7537e8a5, 80ce41579825a6fa, 5f0fa90494de8f65, 83d10514eaa62fa5, 9190555aa39a5d28, 84a4c9d8bf14dddb, 927d71cf40b58c30]
sections: [ebef1e7a0df854f4, 7e16449f66e7dfd6, eeb0682f7d2a1079, 5713f0196a34e6e7, 0e844597859e4248, 3a97d9195ddcc92e, 1da08c483e59c141, 84702cc6e0a1fd42, 8dee7a31c86ffc5c, 83a5bce168ef23d7]
tool: 1
---
# Клієнт {#the-client}
@ -11,13 +11,23 @@ translation:
## Перший клієнт {#your-first-client}
```python title="client.py" hl_lines="14-18"
Клієнтові потрібен сервер, з яким говорити. До цього Bookshop під'єднується кожен приклад на цій сторінці. Збережіть його як `server.py` і залиште працювати через HTTP:
```python title="server.py"
--8<-- "docs_src/client/tutorial001.py"
```
Сервер угорі потрібен лише для того, щоб було до чого під'єднатися. Клієнт — це п'ять виділених рядків.
```console
uv run mcp run server.py --transport streamable-http
```
* `Client(mcp)` отримує **сам об'єкт сервера**. Це транспорт у пам'яті: без підпроцесу, без порту, без HTTP. Саме так під'єднується кожен приклад на цій сторінці й кожен тест, який ви напишете.
Тепер сервер доступний за адресою `http://localhost:8000/mcp`. Клієнт — окрема програма. Збережіть його як `client.py` і запустіть `python client.py` у другому терміналі:
```python title="client.py" hl_lines="7-11"
--8<-- "docs_src/client/tutorial001_client.py"
```
* `Client("http://localhost:8000/mcp")` отримує **URL**, тож під'єднується через Streamable HTTP до сервера, який ви щойно запустили.
* `async with` — це **життєвий цикл**. Вхід у блок під'єднує й узгоджує параметри; вихід — від'єднує. Пари `connect()` / `close()` немає, а `Client` не можна використати повторно після завершення блоку.
* Усередині блоку відомості про з'єднання вже доступні як звичайні властивості.
@ -25,10 +35,10 @@ translation:
`Client` приймає один позиційний аргумент і визначає транспорт за його типом:
* Екземпляр `MCPServer` (або низькорівневого `Server`): під'єднання **в межах процесу**.
* Рядок з URL (`Client("http://localhost:8000/mcp")`): Streamable HTTP, шлях для робочого розгортання.
* `StdioServerParameters`: команда, яку буде запущено як **підпроцес**; спілкування з ним іде через його stdin і stdout.
* Рядок з URL (`Client("http://localhost:8000/mcp")`): Streamable HTTP, транспорт для робочого розгортання.
* `StdioServerParameters`: команда, яку буде запущено як локальний **підпроцес**; спілкування з ним іде через його stdin і stdout.
* **Транспорт**: будь-що, що можна використати як `async with ... as (read, write)`, наприклад `streamable_http_client(url, http_client=...)` навколо вашого власного HTTP-клієнта.
* Екземпляр `MCPServer` (або низькорівневого `Server`): під'єднання **в межах процесу**, без підпроцесу й без порту. Це для тестів, і на ньому побудована сторінка **[Тестування](../get-started/testing.md)**.
Усе інше на цій сторінці однакове для всіх чотирьох. Заголовки, підпроцеси, тайм-аути та протокол `Transport` мають власну сторінку: **[Транспорти клієнта](transports.md)**.
@ -49,11 +59,11 @@ translation:
## Перелік інструментів {#listing-tools}
```python title="client.py" hl_lines="15-20"
```python title="client.py" hl_lines="8-13"
--8<-- "docs_src/client/tutorial002.py"
```
`list_tools()` повертає `ListToolsResult`; інструменти лежать у `.tools`. Кожен із них — повне означення, яке хост передав би моделі:
`list_tools()` повертає `ListToolsResult`; інструменти лежать у `.tools`. Кожен із них — повне означення, яке хост передав би моделі. Ось перший:
```python
tool.name # 'search_books'
@ -77,6 +87,8 @@ tool.description # 'Search the catalog by title or author.'
Ця схема — усе, що потрібно UI, щоб показати форму аргументів, і все, що потрібно моделі, щоб сформувати коректні аргументи.
Другий інструмент, `lookup_book`, зареєстровано без `title=`, тож його `tool.title` дорівнює `None`.
!!! tip
`title` необов'язковий, тож UI, що показує інструменти людині, має обирати: `title`, якщо він є,
і `name`, якщо немає. `from mcp.shared.metadata_utils import get_display_name` робить саме це —
@ -86,7 +98,7 @@ tool.description # 'Search the catalog by title or author.'
`call_tool(name, arguments)` запускає інструмент і повертає `CallToolResult`.
```python title="client.py" hl_lines="27-34"
```python title="client.py" hl_lines="9-16"
--8<-- "docs_src/client/tutorial003.py"
```
@ -142,7 +154,7 @@ result.is_error # False
Дії з ресурсами йдуть парами: два способи перелічити, один спосіб прочитати.
```python title="client.py" hl_lines="22-31"
```python title="client.py" hl_lines="9-18"
--8<-- "docs_src/client/tutorial004.py"
```
@ -156,7 +168,7 @@ result.is_error # False
## Промпти {#prompts}
```python title="client.py" hl_lines="15-20"
```python title="client.py" hl_lines="8-13"
--8<-- "docs_src/client/tutorial005.py"
```
@ -181,7 +193,7 @@ message.content # TextContent(type='text', text='Recommend one poetry book from
Сервер з обробником автодоповнення може доповнювати аргументи промптів і шаблонів ресурсів, поки користувач друкує.
```python title="client.py" hl_lines="27-31"
```python title="client.py" hl_lines="9-13"
--8<-- "docs_src/client/tutorial006.py"
```
@ -194,21 +206,21 @@ message.content # TextContent(type='text', text='Recommend one poetry book from
Кожен метод `list_*` приймає іменований аргумент `cursor=`, а кожен результат містить `next_cursor`. Коли `next_cursor` дорівнює `None`, у вас є все.
```python title="client.py" hl_lines="22-30"
```python title="client.py" hl_lines="7-15"
--8<-- "docs_src/client/tutorial007.py"
```
Цей цикл коректний для будь-якого сервера. `MCPServer` повертає все однією сторінкою, тож `next_cursor` дорівнює `None` і цикл виконується один раз — саме тому більшість коду його ніколи не пише. Сервери, що справді розбивають результати на сторінки, і правила, яким підкоряються курсори, — на сторінці **[Пагінація](../advanced/pagination.md)**.
`list_all_tools` коректна для будь-якого сервера. `MCPServer` повертає все однією сторінкою, тож `next_cursor` дорівнює `None` і цикл виконується один раз — саме тому більшість коду його ніколи не пише. Сервери, що справді розбивають результати на сторінки, і правила, яким підкоряються курсори, — на сторінці **[Пагінація](../advanced/pagination.md)**.
## У тестах {#in-tests}
`Client(mcp)` без процесу й без порту — це вже тестова обв'язка для вашого сервера.
Кожен `client.py` на цій сторінці звертався до `server.py` через HTTP. У тесті мережу оминають і передають у `Client` сам об'єкт сервера: `from server import mcp`, потім `Client(mcp)`. Без процесу, без порту, а всі методи вище працюють так само.
Саме для цього є один прапорець конструктора: `Client(mcp, raise_exceptions=True)`. Він діє лише на з'єднаннях у пам'яті, а пояснює його й будує навколо нього весь підхід сторінка **[Тестування](../get-started/testing.md)**.
Саме для цього є один прапорець конструктора: `Client(mcp, raise_exceptions=True)`. Він діє лише на з'єднаннях у межах процесу, а пояснює його й будує навколо нього весь підхід сторінка **[Тестування](../get-started/testing.md)**.
## Підсумки {#recap}
* `Client(x)` під'єднується в пам'яті до об'єкта сервера, через Streamable HTTP — до рядка з URL і через транспорт — до всього іншого.
* `Client(x)` під'єднується через Streamable HTTP до рядка з URL, запускає підпроцес для `StdioServerParameters`, входить у транспорт напряму, а в тестах приймає сам об'єкт сервера.
* `async with` — це весь життєвий цикл. Усередині нього `server_capabilities` і `protocol_version` уже заповнені; `server_info` та `instructions` — теж, якщо сервер їх надає.
* `list_tools()` дає `name`, `title`, `description` та `input_schema` кожного інструмента.
* `call_tool()` повертає `content` для моделі, `structured_content` для вашого коду та `is_error`. Інструмент, що викидає виняток, — це результат, а не виняток.

View file

@ -1,6 +1,6 @@
---
translation:
sections: [c6899d3892bd9fa0, 79372cff3cc48a88, 63878d29e87c3e73, 13175843d3588af4, e7e2b9fd516f77de, 758f06399b513c1f, a05d7278487d610b]
sections: [c6899d3892bd9fa0, 79372cff3cc48a88, c2dae1ebe2ebd543, 13175843d3588af4, df06056fb16b3846, 758f06399b513c1f, a05d7278487d610b]
tool: 1
---
# OAuth-клієнти {#oauth-clients}
@ -81,18 +81,20 @@ translation:
Коли `Client` уперше надсилає запит, сервер відповідає `401`. Далі справу бере на себе провайдер:
1. **Виявлення.** Він читає заголовок `WWW-Authenticate`, завантажує Protected Resource Metadata сервера з `/.well-known/oauth-protected-resource`, дізнається, який сервер авторизації захищає цей ресурс, і завантажує метадані *того* сервера.
1. **Виявлення.** Він читає заголовок `WWW-Authenticate`, завантажує Protected Resource Metadata сервера з `/.well-known/oauth-protected-resource`, дізнається, який сервер авторизації захищає цей ресурс, і завантажує метадані *того* сервера. (Старіший сервер, що не публікує метаданих ресурсу, натомість запитують про метадані сервера авторизації за його власним origin.) У будь-якому разі метадані мають називати своїм `issuer` той сервер, для якого їх завантажено; усе інше відхиляється.
2. **Реєстрація.** У сховищі порожньо? Він динамічно реєструє вас із вашими `OAuthClientMetadata` і зберігає результат.
3. **Авторизація.** Він генерує пару PKCE і `state`, будує URL авторизації, очікує ваш `redirect_handler`, а потім — ваш `callback_handler`, щоб отримати код.
4. **Обмін.** Він міняє код на `OAuthToken`, зберігає його й повторює ваш початковий запит із `Authorization: Bearer ...`.
Після цього все тихо. Токени беруться зі сховища, прострочений токен доступу оновлюється за допомогою токена оновлення, і лише коли ніщо з цього не спрацьовує, провайдер запускає потік знову.
Для всіх цих запитів діє одне транспортне правило: як і MCP-запит, усередині якого вони виконуються, вони переходять за перенаправленням лише тоді, коли воно лишається на тому самому origin і зберігає метод (скажімо, 307/308 на кінцеву скісну риску), а будь-яке інше перенаправлення трактують так, ніби цей URL не відповідає.
Нічого з цього ви не писали. Лишаються ще два іменовані аргументи (`client_metadata_url` і `validate_resource_url`), і цьому файлу не потрібен жоден. Знати варто про `client_metadata_url`; йому присвячено окремий розділ нижче.
### Спробуйте самі {#try-it}
Більшість прикладів у цій документації можна перевірити за допомогою `Client(server)` у пам'яті. Цей — ні: уся суть потоку в HTTP-відповіді `401`, а між клієнтом у пам'яті та його сервером HTTP немає.
`Client(server)` у пам'яті, яким користуються ваші тести, тут не допоможе: уся суть потоку в HTTP-відповіді `401`, а між клієнтом у пам'яті та його сервером HTTP немає.
У репозиторії є жива версія. `examples/servers/simple-auth/` запускає окремий сервер авторизації та захищений MCP-сервер; `examples/clients/simple-auth-client/` — це клієнт із цієї сторінки, що виріс у невеликий CLI. У його README є дві команди: запустіть сервери, запустіть клієнт проти них — і спостерігайте, як проходять усі чотири кроки.
@ -110,13 +112,14 @@ URL має бути HTTPS із некореневим шляхом; усе ін
`ClientCredentialsOAuthProvider` — це той самий `httpx2.Auth`, тільки без людини:
```python title="client.py" hl_lines="4 27-33"
```python title="client.py" hl_lines="4 27-34"
--8<-- "docs_src/oauth_clients/tutorial002.py"
```
Що змінилося:
* Немає `OAuthClientMetadata`, немає обробників. Ви передаєте `client_id` і `client_secret`; провайдер будує навколо них мінімальну реєстрацію `client_credentials` і повністю пропускає динамічну реєстрацію.
* `issuer` називає сервер авторизації, що видав ці облікові дані; використовуйте значення `issuer`, яке повертає його документ `/.well-known/oauth-authorization-server`. Виявлення й далі виконується, як описано вище, але запити на токен будуються лише з метаданих *цього* видавця; якщо MCP-сервер указує кудись інде, потік натомість зупиняється з `OAuthFlowError`. Пропускати його — застаріла практика, і у версії 3.0 він стане обов'язковим (див. **[Застарілі можливості](../deprecated.md#deprecated-sdk-helpers)**); доти провайдер попереджає й використовує той сервер авторизації, який знайде виявлення.
* `scope` — це рядок, розділений пробілами: такий формат OAuth використовує в переданих даних.
* Усе далі ідентичне: той самий `TokenStorage`, той самий `httpx2.AsyncClient(auth=...)`, той самий `streamable_http_client`.
@ -129,7 +132,7 @@ URL має бути HTTPS із некореневим шляхом; усе ін
Ще один провайдер живе в `mcp.client.auth.extensions.client_credentials`:
**`PrivateKeyJWTOAuthProvider`** — для клієнтів, які автентифікуються за допомогою JWT замість
спільного секрету (`private_key_jwt`, варіант із парою ключів та workload identity). Він дотримується
тієї ж схеми: створіть екземпляр і передайте в `auth=`. У тому ж модулі є
тієї ж схеми: створіть екземпляр (він приймає той самий необов'язковий `issuer`) і передайте в `auth=`. У тому ж модулі є
`SignedJWTParameters` і `static_assertion_provider` — два допоміжні засоби, що будують для нього assertion.
Є ще одна ситуація без людини: клієнт належить підприємству, де провайдер ідентичності, а не користувач, вирішує, до яких MCP-серверів він може звертатися. Це інший грант із власною моделлю довіри та власною сторінкою — **[Твердження про ідентичність](identity-assertion.md)**.

View file

@ -1,6 +1,6 @@
---
translation:
sections: [09c857a25a9dc37a, 43bc6a76a243a50e, 0a716022a88768df, 4b7f78042bfcfff7, c112662e61b03315, 58974ba1f489a8b4, d18adbdbb835ea73]
sections: [09c857a25a9dc37a, 43bc6a76a243a50e, 0a716022a88768df, 4b7f78042bfcfff7, c112662e61b03315, 58974ba1f489a8b4, ed4d17e894864056]
tool: 1
---
# Групи сесій {#session-groups}
@ -78,7 +78,7 @@ translation:
## Підсумки {#recap}
* `ClientSessionGroup` тримає багато з'єднань із серверами й зводить їхні інструменти, ресурси й промпти в один `dict` кожного виду.
* `connect_to_server(params)` для кожного сервера. Він приймає параметри транспорту й ніколи — об'єкт сервера чи URL, як `Client`.
* `connect_to_server(params)` для кожного сервера. Він приймає параметри транспорту й ніколи — URL чи `Transport`, які приймає `Client`.
* `group.call_tool(name, arguments)` сам спрямовує виклик на сервер-власник.
* Імена мають бути унікальними в межах усієї групи; два сервери з інструментом `search` самі по собі співіснувати не можуть.
* `component_name_hook=` переписує кожне зареєстроване ім'я. Змінюється ключ словника, а не ім'я в переданих даних.

View file

@ -1,6 +1,6 @@
---
translation:
sections: [9cac816674181eb0, 0700f337babcd4dd, 2bde0dd58cdf00f5, 40b4916d82eaf1d4, 3d0832f39b0d7059, dfa4446556badef0, 5bd93be2ab2ecb9c]
sections: [9cac816674181eb0, 7c157764133fea1f, 40b4916d82eaf1d4, 10d151f2cc75317f, 3d0832f39b0d7059, 92742ba36533633d, 0aeca6145e7bd302]
tool: 1
---
# Транспорти клієнта {#client-transports}
@ -11,30 +11,15 @@ translation:
*Серверний* бік кожного з них (що робить `mcp.run()` і що ви розгортаєте) описано на сторінці **[Запуск сервера](../run/index.md)**.
## У пам'яті {#in-memory}
Передайте сам об'єкт сервера:
```python title="client.py" hl_lines="14"
--8<-- "docs_src/client_transports/tutorial001.py"
```
Жодного підпроцесу, жодного порту, жодних байтів у мережі. Клієнт і сервер — це два об'єкти в одному процесі, а виклик усе одно проходить через справжній протокольний рівень: `search_books` перелічується, валідується й викликається точнісінько так само, як це було б через HTTP.
Тож це одразу дві речі:
* **Тестовий стенд.** Кожен приклад у цій документації перевіряється саме так, а сторінка **[Тестування](../get-started/testing.md)** будує довкола цього весь підхід.
* **API для вбудовування.** Застосунку, який сам створює сервер, не потрібен мережевий перехід, щоб викликати його інструменти.
## Streamable HTTP {#streamable-http}
Передайте рядок з URL — і отримаєте **Streamable HTTP**, транспорт, за яким розгортають сервер:
Передайте рядок з URL — і отримаєте **Streamable HTTP**, транспорт, за яким розгортають сервер і до якого варто звертатися насамперед:
```python title="client.py" hl_lines="5"
--8<-- "docs_src/client_transports/tutorial002.py"
```
Оце й увесь продакшен-клієнт. `Client` сам загортає URL у `streamable_http_client(...)` поверх `httpx2.AsyncClient`, налаштованого так, як потрібно MCP: `follow_redirects=True`, 30-секундний тайм-аут на connect/write/pool і 300-секундний тайм-аут на читання, бо сервер може тримати потік відповіді відкритим.
Оце й увесь продакшен-клієнт. `Client` сам загортає URL у `streamable_http_client(...)` поверх `httpx2.AsyncClient`, налаштованого так, як потрібно MCP: 30-секундний тайм-аут на connect/write/pool і 300-секундний тайм-аут на читання, бо сервер може тримати потік відповіді відкритим.
!!! check
Створений `Client` ще **не** під'єднаний. Конструктор лише обирає транспорт;
@ -50,7 +35,7 @@ translation:
Щойно знадобиться заголовок `Authorization`, cookie, проксі, mTLS чи інший тайм-аут — створіть `httpx2.AsyncClient` самостійно й передайте його в `streamable_http_client`:
```python title="client.py" hl_lines="8-14"
```python title="client.py" hl_lines="8-13"
--8<-- "docs_src/client_transports/tutorial003.py"
```
@ -80,9 +65,30 @@ translation:
!!! info
`httpx2` зберігає знайомий API `httpx`, тож якщо ви знаєте `httpx`, то вже вмієте робити тут автентифікацію,
проксі, хуки подій, повторні спроби й обмеження з'єднань. SDK нічого не додає зверху й нічого не
забирає. Саме сюди під'єднується й OAuth:
забирає, окрім [обробки перенаправлень](#redirects). Саме сюди під'єднується й OAuth:
`httpx2.AsyncClient(auth=OAuthClientProvider(...))`. Увесь цей процес описано на сторінці **[OAuth-клієнти](oauth-clients.md)**.
### Перенаправлення {#redirects}
Транспорт під'єднується до URL, який ви йому передали, і лише до цього origin.
* Перенаправлення `307`/`308`, що лишається в межах тієї самої схеми, хоста й порту, виконується; так само й `http://` → `https://` на тому самому хості. Це покриває звичне перенаправлення з кінцевою скісною рискою `/mcp` → `/mcp/`.
* Перенаправлення будь-куди інде **не** виконується. Виклик завершується помилкою:
```text
MCPError: Redirect to https://other.example.com/mcp not followed; use that URL as the endpoint if it is the intended server
```
Якщо цей URL — сервер, який ви мали на увазі, впишіть його в конфігурацію. Якщо ні — сервер або проксі перед ним налаштовано неправильно.
Це стосується будь-якого `httpx2.AsyncClient`, який ви передаєте: його налаштування `follow_redirects` для MCP-запитів не враховується — в жоден бік. OAuth-провайдери SDK застосовують те саме правило до власних запитів.
!!! tip
`Redirect to http://… not followed: it would downgrade this HTTPS endpoint to plain HTTP` означає, що
сервер стоїть за проксі з термінацією TLS, про який не знає, і видає перенаправлення на `http://`.
Це виправляють на сервері (**[Розгортання та масштабування](../run/deploy.md#behind-a-tls-terminating-proxy)**)
або використанням точного URL `https://…/`, який підказує повідомлення.
## stdio {#stdio}
Сервер **stdio** — це підпроцес. Клієнт запускає його, пише JSON-RPC в його stdin і читає JSON-RPC з його stdout. Саме так десктопний хост запускає сервер на вашій машині: хост і *є* цим кодом плюс UI, а сторінка **[Під'єднання до справжнього хоста](../get-started/real-host.md)** показує ті самі стосунки з боку хоста — як конфігураційний файл.
@ -105,6 +111,18 @@ stderr дочірнього процесу йде у ваш. Щоб спряму
Сервер, якому потрібен ключ API, там його не знайде. Передайте його явно через `env=`; ці
змінні накладаються поверх списку дозволених. Саме це й робить `BOOKSHOP_API_KEY` вище.
## У пам'яті {#in-memory}
У тесті нема чого розгортати й нема чого запускати. Передайте сам об'єкт сервера:
```python hl_lines="14"
--8<-- "docs_src/client_transports/tutorial001.py"
```
Жодного підпроцесу, жодного порту, жодних байтів у мережі. Клієнт і сервер — це два об'єкти в одному процесі, а виклик усе одно проходить через справжній протокольний рівень: `search_books` перелічується, валідується й викликається точнісінько так само, як це було б через HTTP. Сторінка **[Тестування](../get-started/testing.md)** будує довкола цього весь підхід.
Ця сама форма водночас слугує API для вбудовування: застосунок, який сам створює сервер, може викликати його інструменти без мережевого переходу.
## SSE {#sse}
`sse_client(url)` з `mcp.client.sse` — це HTTP-транспорт, на зміну якому прийшов Streamable HTTP. Загортайте його так само, `Client(sse_client("http://localhost:8000/sse"))`, щоб говорити із сервером, який досі ним користується, — і не будуйте на ньому нічого нового.
@ -113,15 +131,16 @@ stderr дочірнього процесу йде у ваш. Щоб спряму
Для `Client` усе перелічене вище — одне й те саме.
**Транспорт** — це будь-який асинхронний контекстний менеджер, що повертає пару потоків повідомлень `(read, write)`: формально — протокол `Transport` у `mcp.client`. `Client` розв'язує свій аргумент за типом: об'єкт сервера під'єднується в межах процесу, `str` стає `streamable_http_client(url)`, `StdioServerParameters` стає `stdio_client(params)`, а в будь-що інше він входить безпосередньо як у транспорт. Саме завдяки останньому правилу `stdio_client(...)`, `streamable_http_client(...)` і `sse_client(...)` стають на одне й те саме місце — і саме тому можна написати власний.
**Транспорт** — це будь-який асинхронний контекстний менеджер, що повертає пару потоків повідомлень `(read, write)`: формально — протокол `Transport` у `mcp.client`. `Client` розв'язує свій аргумент за типом: `str` стає `streamable_http_client(url)`, `StdioServerParameters` стає `stdio_client(params)`, об'єкт сервера під'єднується в межах процесу, а в будь-що інше він входить безпосередньо як у транспорт. Саме завдяки останньому правилу `stdio_client(...)`, `streamable_http_client(...)` і `sse_client(...)` стають на одне й те саме місце — і саме тому можна написати власний.
## Підсумки {#recap}
* `Client(mcp)` (об'єкт сервера) під'єднується в пам'яті. Використовуйте для тестів і для вбудовування.
* `Client("http://.../mcp")` (URL) під'єднується через Streamable HTTP, продакшен-транспорт.
* Заголовки, автентифікація, проксі й тайм-аути належать `httpx2.AsyncClient`, який ви передаєте в `streamable_http_client(url, http_client=...)`. Іменованого аргументу `headers=` немає.
* Перенаправлення виконуються лише в межах власного origin цього URL (`307`/`308` із кінцевою скісною рискою), плюс `http`→`https` на тому самому хості. Усе інше завершується помилкою `Redirect to … not followed`; пропишіть у конфігурації кінцевий URL.
* stdio — це `Client(StdioServerParameters(...))`. Загортайте його в `stdio_client(...)` самостійно лише для того, щоб перенаправити stderr дочірнього процесу.
* Підпроцес отримує середовище зі списку дозволених, а не ваше; `env=` його доповнює.
* `Client(mcp)` (об'єкт сервера) під'єднується в пам'яті. Використовуйте в тестах або щоб вбудувати сервер у застосунок, який його створив.
* Транспорт — це будь-що, з чим можна зробити `async with x as (read, write)`. Усе, що не є об'єктом сервера, URL чи `StdioServerParameters`, `Client` передає прямо цьому протоколу.
* Створення `Client` обирає транспорт. `async with` його відкриває.

View file

@ -1,11 +1,11 @@
---
translation:
sections: [490237e61c3a7a44, 01262a123ad9501d, 429db5b574a2ac08, e2d0d273fbd2d74b, 64ab0331e868f3d4, 6c8878ce2d1f6d56, 4068f23e371bf0b3, eaef75b8725bc931]
sections: [4c1dea378b2b1bf7, 01262a123ad9501d, 429db5b574a2ac08, e2d0d273fbd2d74b, 64ab0331e868f3d4, 6c8878ce2d1f6d56, c3d1099701156881, e185a1b8e53669f6]
tool: 1
---
# Застарілі можливості {#deprecated-features}
Специфікація 2026-07-28 виводить з ужитку п'ять речей. SDK і далі реалізує кожну з них, і кожна тепер супроводжується **попередженням про застарілість**. Один допоміжний метод SDK оголошено застарілим окремо, і його наведено [наприкінці сторінки](#deprecated-sdk-helpers).
Специфікація 2026-07-28 виводить з ужитку п'ять речей. SDK і далі реалізує кожну з них, і кожна тепер супроводжується **попередженням про застарілість**. Кілька речей оголошено застарілими на рівні самого SDK, незалежно від специфікації; їх наведено [наприкінці сторінки](#deprecated-sdk-helpers).
Таблиця нижче називає кожну застарілу можливість, пояснює, чому вона зникає, і вказує заміну, на яку варто спиратися.
@ -136,13 +136,15 @@ warnings.filterwarnings("ignore", category=MCPDeprecationWarning)
Один рядок конфігурації pytest — і застарілий виклик більше ніколи не прокрадеться назад
у вашу кодову базу, не проваливши тест.
## Застарілі допоміжні методи SDK {#deprecated-sdk-helpers}
## Застарілі допоміжні засоби SDK {#deprecated-sdk-helpers}
Це не зміни специфікації, а лише внутрішні частини SDK, для яких є краща заміна. Вони попереджають тим самим `MCPDeprecationWarning` і будуть вилучені у версії 3.0.
Це не зміни специфікації, а лише способи використання SDK, для яких є краща заміна. Вони попереджають тим самим `MCPDeprecationWarning`, а версія 3.0 вилучає стару форму.
| Застаріле | Що робити натомість |
|---|---|
| `FuncMetadata.call_fn_with_arg_validation()` | `FuncMetadata.validate_arguments()`, а потім `FuncMetadata.call_fn()`. Його викликав лише код, що працює з `FuncMetadata` безпосередньо (скажімо, власний підклас `Tool`). |
| `AuthSettings(resource_server_url=...)` без `validate_token_resource=` | Задайте його: з `True` сервер відхиляє bearer-токени, про які ваш верифікатор не повідомляє, що їх видано для `resource_server_url`; `False` означає, що верифікатор сам перевіряє аудиторію токена (див. **[Авторизація](run/authorization.md#a-token-verifier)**). Незадане значення поводиться як `False`; у версії 3.0 `True` стане типовим значенням щоразу, коли задано `resource_server_url`. |
| `ClientCredentialsOAuthProvider(...)` або `PrivateKeyJWTOAuthProvider(...)` без `issuer=` | Передайте `issuer=` з адресою сервера авторизації, який видав облікові дані (див. **[Написання OAuth-клієнтів](client/oauth-clients.md#machine-to-machine)**). Без нього MCP-сервер вирішує, який сервер авторизації їх отримає; у версії 3.0 цей іменований аргумент стане обов'язковим. |
## Підсумки {#recap}
@ -151,7 +153,7 @@ warnings.filterwarnings("ignore", category=MCPDeprecationWarning)
* Застарілість має рекомендаційний характер: жодних змін у переданих даних, усе й далі працює із сесіями до 2026, а ви отримуєте помітне попередження `MCPDeprecationWarning` (це `UserWarning`, тож воно ввімкнене за замовчуванням).
* Семплювання й кореневі каталоги додатково потребують зворотного каналу, якого сесія 2026-07-28 не має. На сучасному з'єднанні вони попереджають, а потім викидають виняток.
* `warnings.filterwarnings("ignore", category=MCPDeprecationWarning)` приглушує всю категорію; `"error::mcp.MCPDeprecationWarning"` у pytest перетворює її на провал тесту.
* Один допоміжний метод SDK, `FuncMetadata.call_fn_with_arg_validation()`, оголошено застарілим окремо — його вилучать у версії 3.0.
* [Застарілі засоби на рівні SDK](#deprecated-sdk-helpers) підпорядковуються тому самому правилу: зараз вони попереджають, а версія 3.0 відкидає стару форму.
* Новий код не варто будувати на жодній із цих можливостей.
Усі інші сторінки цієї документації навчають чинного API.

View file

@ -1,6 +1,6 @@
---
translation:
sections: [0d6c05bcbf836bf3, 59a7b14eeefc68c1, 7114d8d6daba203f, e8bbb56a98ba7bc9, 5138010f6159901c, f78da7c7c363d4c6, 220a939cab348686]
sections: [0d6c05bcbf836bf3, 9a78b5f6b44b18ab, 7114d8d6daba203f, e8bbb56a98ba7bc9, bfd2fd1153e71dac, 1615a994ef071fdd, 65c599fae991f245]
tool: 1
---
# Перші кроки {#first-steps}
@ -17,7 +17,7 @@ translation:
* **Клієнт** живе всередині хоста й говорить мовою MCP. Хост запускає по одному клієнту на кожен сервер, до якого під'єднаний.
* **Сервер** — це те, що ви створюєте за допомогою цього SDK. Він надає речі клієнтам. Він ніколи не говорить із моделлю напряму.
Ви пишете сервер. Хости — це чийсь інший продукт. SDK також дає вам `Client`. Ним ви тестуватимете свої сервери, і він з'явиться далі на цій сторінці.
Ви пишете сервер. Хости — це чийсь інший продукт. SDK також дає вам `Client` — той самий клас, яким хост звертався б до сервера за URL або запускав би його як підпроцес. Він з'явиться далі на цій сторінці, і саме ним ви тестуватимете свої сервери.
## Три примітиви {#the-three-primitives}
@ -83,22 +83,20 @@ Inspector запустив ваш сервер через **stdio**, один і
Коли клієнт під'єднується, сервер оголошує свої **можливості**: на які сімейства запитів він відповідатиме. Клієнт використовує це оголошення, щоб вирішити, про що взагалі просити. Ви його не писали; `MCPServer` оголошує його за вас.
Подивіться самі. `Client` з SDK приймає об'єкт сервера напряму й під'єднується до нього **в пам'яті** (без підпроцесу, без порту):
Подивіться самі. Залиште `server.py` працювати через HTTP в одному терміналі:
```python
import asyncio
```console
uv run mcp run server.py --transport streamable-http
```
from mcp import Client
а з іншого спрямуйте на нього клієнта:
from server import mcp
```python title="client.py" hl_lines="7-8"
--8<-- "docs_src/first_steps/tutorial001_client.py"
```
async def main() -> None:
async with Client(mcp) as client:
print(client.server_capabilities.model_dump(exclude_none=True))
asyncio.run(main())
```console
python client.py
```
```text
@ -118,8 +116,9 @@ asyncio.run(main())
Зверніть увагу на те, чого там немає. `completions` (автодоповнення аргументів для шаблонів ресурсів і промптів) потребує обробника, який пишете ви; у цього сервера його немає, тож можливість відсутня, і коректний клієнт про неї не проситиме. Це правило для всього необов'язкового: зареєструйте річ — і можливість з'явиться; **[Автодоповнення](../servers/completions.md)** це доводить.
!!! info
`Client(mcp)` — той самий клієнт у пам'яті, яким протестовано кожен приклад у цій документації, і
саме ним ви тестуватимете свої. Йому присвячено цілу сторінку: **[Тестування](testing.md)**.
Цей `client.py` — уже повноцінний MCP-клієнт, і йому присвячено сторінку **[Клієнт](../client/index.md)**.
У тесті термінал і порт не потрібні: передайте `Client` сам об'єкт сервера —
`Client(mcp)`. Цьому теж присвячено цілу сторінку: **[Тестування](testing.md)**.
## Чого ви не писали {#what-you-did-not-write}
@ -128,7 +127,7 @@ asyncio.run(main())
* JSON Schema. `a: int, b: int` *і є* схема для `add`.
* Обробник запитів. `tools/list`, `resources/read`, `prompts/get`: усе обслуговується за вас.
* Оголошення можливостей. `MCPServer` зробив його за вас.
* Жодного рядка протоколу. Узгодження версії, обрамлення JSON-RPC, обмін можливостями: усе це відбулося всередині `mcp dev` і `Client(mcp)`, і ви цього не бачили.
* Жодного рядка протоколу. Узгодження версії, обрамлення JSON-RPC, обмін можливостями: усе це відбулося всередині `mcp dev` і `client.py`, і ви цього не бачили.
У цьому співвідношенні й увесь сенс SDK.
@ -139,6 +138,6 @@ asyncio.run(main())
* Один декоратор на примітив: `@mcp.tool()`, `@mcp.resource(uri)`, `@mcp.prompt()`. Назва, опис і схема беруться з функції.
* URI з `{param}` створює **шаблон** ресурсу, який показується окремо від конкретних ресурсів.
* **Можливості** сервера оголошуються за вас, а клієнт просить лише те, що сервер оголосив.
* `Client(mcp)` під'єднується до об'єкта сервера в пам'яті: ваш тестовий стенд із першого дня.
* `Client("http://localhost:8000/mcp")` говорить із вашим запущеним сервером. Передайте йому натомість об'єкт сервера — `Client(mcp)` — і це ваш тестовий стенд із першого дня.
Далі — **[Під'єднання до справжнього хоста](real-host.md)**: цей сервер усередині Claude Desktop або IDE, по-справжньому. Потім **[Тестування](testing.md)**: одна сторінка, один клієнт у пам'яті — і більше ніколи не доведеться гадати, чи воно працює. Після цього кожен примітив отримує власну сторінку, починаючи з того, яким керує модель: **[Інструменти](../servers/tools.md)**.

View file

@ -1,13 +1,13 @@
---
translation:
sections: ['4926721070127497', c52a1de2b6b32f40, 8e792bf8c7489ec6, 627195f7159e24ef]
sections: [5d13c2f0ba42c0d2, c52a1de2b6b32f40, 8e792bf8c7489ec6, 38552ea228b0a04f]
tool: 1
---
# Тестування {#testing}
Python SDK містить клас `Client` із **транспортом у пам'яті**: передайте йому об'єкт сервера — і він під'єднається до нього напряму.
Клас `Client` із SDK — той самий, що під'єднується за URL чи запускає підпроцес, — уміє під'єднуватися й **у пам'яті**: передайте йому об'єкт сервера, і він говоритиме з ним напряму.
Жодного підпроцесу. Жодного порту. Узагалі жодного транспорту. Та сама ідея, що й `TestClient` у FastAPI.
Жодного підпроцесу. Жодного порту. Нічого не передається мережею. Та сама ідея, що й `TestClient` у FastAPI.
## Базове використання {#basic-usage}
@ -96,12 +96,12 @@ async def test_call_add_tool(client: Client):
Залишайте його ввімкненим у тестах. У робочому коді він не має сенсу.
## У тому самому процесі за замовчуванням {#in-process-by-default}
## Нейтральність щодо покоління за замовчуванням {#era-neutral-by-default}
!!! note
`Client(mcp)` під'єднується в межах процесу й за замовчуванням **нейтральний щодо покоління**: він зондує сервер і
обирає відповідний шлях протоколу. Зафіксуйте `mode="legacy"`, якщо тест перевіряє семантику, специфічну для
старого покоління — push семплювання (sampling) чи еліцитації (elicitation), `message_handler`, — і приберіть там `raise_exceptions=True`:
обирає відповідний шлях протоколу. Зафіксуйте `mode="legacy"`, якщо тест перевіряє семантику, властиву саме
старому поколінню — push семплювання (sampling) чи еліцитації (elicitation), `message_handler`, — і приберіть там `raise_exceptions=True`:
з'єднання старого покоління взагалі нічого не узагальнює, а прапорець повторно викидає
збій усередині завдання сервера, а не у вашому тесті.

View file

@ -1,6 +1,6 @@
---
translation:
sections: [74011e683045eea9, 9b64cc175c18b6a9, 4b41be4824030397, e3b1502da786ec33, 71e41161f143c6a9, 9ec2c1eeb8c36378, 8dd027377d46448b, f81491125dcbfe8b]
sections: [74011e683045eea9, 9b64cc175c18b6a9, 4b41be4824030397, e3b1502da786ec33, 71e41161f143c6a9, 9ec2c1eeb8c36378, b47667184ca5b516, f81491125dcbfe8b]
tool: 1
---
# Багатораундові запити {#multi-round-trip-requests}
@ -164,7 +164,7 @@ TTL, прив'язка до принципала й прив'язка до за
## Результат версії 2026-07-28 {#a-2026-07-28-result}
`InputRequiredResult` існує лише у версії протоколу **2026-07-28**. `Client(server)` у пам'яті узгоджує її за вас; мережею її виявляє `mode="auto"`. Після під'єднання `client.protocol_version` покаже, що саме ви отримали.
`InputRequiredResult` існує лише у версії протоколу **2026-07-28**. Режим `mode="auto"`, типовий для `Client`, виявляє її на будь-якому з'єднанні. Після під'єднання `client.protocol_version` покаже, що саме ви отримали.
!!! warning
У сесії, старшій за 2026, `InputRequiredResult` просто нікуди покласти. Поверніть його з обробника на

View file

@ -1,6 +1,6 @@
---
translation:
sections: [5315262fe26b33e1, 9d8e98840f1b78f0, 0284b215e85366c4, 8534d8dbb4053a70, 2966fac6fe697007]
sections: [5315262fe26b33e1, 9d8e98840f1b78f0, 52d6009a07e770ea, 8534d8dbb4053a70, 2966fac6fe697007]
tool: 1
---
# Перебіг виконання {#progress}
@ -29,19 +29,17 @@ translation:
Клієнт підписується **окремо для кожного виклику**, передаючи `progress_callback=` у `call_tool`:
```python title="client.py" hl_lines="7 16"
```python title="client.py" hl_lines="5 14"
import anyio
from mcp import Client
from server import mcp
async def show(progress: float, total: float | None, message: str | None) -> None:
print(f"{message} ({progress}/{total})")
async def main() -> None:
async with Client(mcp) as client:
async with Client("http://localhost:8000/mcp") as client:
result = await client.call_tool(
"import_catalog",
{"urls": ["https://example.com/a.json", "https://example.com/b.json"]},
@ -56,28 +54,31 @@ anyio.run(main)
Колбек — це `async`-функція, яка приймає рівно те, що повідомив сервер: `progress`, `total`, `message`.
!!! info
`Client(mcp)` під'єднується безпосередньо до об'єкта сервера, у пам'яті, — це той самий клієнт, на якому
побудована сторінка **[Тестування](../get-started/testing.md)**. Параметр `progress_callback` однаковий
незалежно від транспорту, який використовує `Client`; а от *хронометраж*, який ви зараз побачите,
властивий саме з'єднанню в пам'яті. Воно запускає колбек одразу на місці, тож кожен звіт надходить до
того, як `call_tool` поверне результат. На справжньому транспорті сповіщення змагаються з результатом,
і повільний колбек може ще виконуватися після того, як `call_tool` уже повернув результат.
Параметр `progress_callback` однаковий незалежно від того, що ви передали в `Client`: URL, як тут,
`StdioServerParameters` чи об'єкт сервера в тесті. Проте зважайте на хронометраж на справжньому
транспорті. Кожне сповіщення доставляється окремо, поруч із відповіддю, тож повільний колбек може
ще виконуватися після того, як `call_tool` уже повернув результат. Лише тестове з'єднання в межах
процесу запускає колбек одразу на місці й гарантує, що кожен звіт надійде раніше.
### Спробуйте самі {#try-it}
Покладіть `client.py` поруч із `server.py` і запустіть:
Запустіть `server.py` через HTTP, а потім запустіть клієнт у другому терміналі:
```console
uv run mcp run server.py --transport streamable-http
```
```console
python client.py
```
```text
Imported https://example.com/a.json (1/2)
Imported https://example.com/b.json (2/2)
Imported https://example.com/a.json (1.0/2.0)
Imported https://example.com/b.json (2.0/2.0)
{'result': 'Imported 2 records.'}
```
Кожен `await ctx.report_progress(...)` на сервері перетворився на один виклик `show` на клієнті, у тому самому порядку, і обидва рядки надрукувалися **до** того, як `call_tool` повернув результат. Перебіг не пакується в результат — він надходить потоком, поки інструмент іще працює.
Кожен `await ctx.report_progress(...)` на сервері перетворився на один виклик `show` на клієнті, у тому самому порядку. Перебіг не пакується в результат. Він надходить потоком, поки інструмент іще працює.
!!! warning
`progress_callback` належить **виклику**, а не `Client`. Аргументу конструктора для нього немає,

View file

@ -1,6 +1,6 @@
---
translation:
sections: [478fd619e5f90ef8, aef094a00e44e248, bab8cbf3449fa7e9, df1809b15a58335b, 5f9d8c2336ed0239, f54974398e43ddef, b24443dd78584870]
sections: [d98e337bf28845e0, dd642acbba36f615, f547b3e76da19c94, 149513378588da5c, 4e149ed992046709, f54974398e43ddef, b24443dd78584870]
tool: 1
---
# Версії протоколу {#protocol-versions}
@ -11,9 +11,17 @@ translation:
Перейматися цим майже ніколи не доводиться, бо `Client` домовляється за вас. Ця сторінка — про єдиний аргумент конструктора, який цим керує, `mode=`, і про три випадки, коли його змінюють.
Кожен фрагмент на цій сторінці — це `client.py`, що розмовляє з `server.py` книгарні Bookshop зі сторінки **[Клієнт](client/index.md)**. Запустіть цей сервер в одному терміналі:
```console
uv run mcp run server.py --transport streamable-http
```
Потім запускайте кожен фрагмент у другому терміналі командою `python client.py`.
## `mode="auto"` {#modeauto}
```python title="client.py" hl_lines="14-15"
```python title="client.py" hl_lines="7-8"
--8<-- "docs_src/protocol_versions/tutorial001.py"
```
@ -31,13 +39,14 @@ translation:
Оце й уся можливість. Один `Client`, сервер будь-якого покоління, жодних розгалужень у вашому коді.
!!! info
`MCPServer` відповідає на `server/discover` на кожному транспорті — in-memory, stdio, streamable
HTTP — тож із власним сервером `auto` завжди зупиняється на `2026-07-28`. Запасний шлях
спрацьовує лише проти справжнього сервера, випущеного до 2026, — саме тоді, коли він і потрібен.
`MCPServer` відповідає на `server/discover` на кожному транспорті — Streamable HTTP, stdio та
внутрішньопроцесному з'єднанні, яким користуються ваші тести, — тож із власним сервером `auto`
завжди зупиняється на `2026-07-28`. Запасний шлях спрацьовує лише проти справжнього сервера,
випущеного до 2026, — саме тоді, коли він і потрібен.
## `mode="legacy"` {#modelegacy}
```python title="client.py" hl_lines="14"
```python title="client.py" hl_lines="7"
--8<-- "docs_src/protocol_versions/tutorial002.py"
```
@ -61,7 +70,7 @@ translation:
`mode` також приймає рядок сучасної версії протоколу. Сьогодні ця множина — рівно `["2026-07-28"]`.
```python title="client.py" hl_lines="14"
```python title="client.py" hl_lines="7"
--8<-- "docs_src/protocol_versions/tutorial003.py"
```
@ -94,7 +103,7 @@ ValueError: mode must be 'legacy', 'auto', or one of ['2026-07-28']; got '2025-0
Тож збережіть її. Після з'єднання в режимі `auto` `client.session.discover_result` містить точний `DiscoverResult`, який надіслав сервер: його `supported_versions`, `capabilities`, `instructions` і відомості про себе, які сервер записав у `_meta` результату. Наступного разу передайте його назад як `prior_discover=`:
```python title="client.py" hl_lines="15 17"
```python title="client.py" hl_lines="8 10"
--8<-- "docs_src/protocol_versions/tutorial004.py"
```

View file

@ -1,6 +1,6 @@
---
translation:
sections: [1062ef792791488a, 4be2b831547184a9, 374b049e770385f2, b72f6947089e6de0, b172c9db7831bb31, 70b9ece244ca1b0c, cba78e052898c3f6, f06bdb541cb0b469, fb82d526320b7cc3]
sections: [1062ef792791488a, 4be2b831547184a9, 374b049e770385f2, b72f6947089e6de0, b172c9db7831bb31, 10394c6f16601638, cba78e052898c3f6, f06bdb541cb0b469, 6dc898ccc5a903f9]
tool: 1
---
# Додавання до наявного застосунку {#add-to-an-existing-app}
@ -99,7 +99,7 @@ uvicorn server:app
--8<-- "docs_src/asgi/tutorial004.py"
```
Тепер клієнти під'єднуються до `/notes`, а не до `/notes/mcp`.
Тепер клієнти під'єднуються до `/notes/`, а не до `/notes/mcp`.
## CORS для браузерних клієнтів {#cors-for-browser-clients}
@ -142,4 +142,4 @@ uvicorn server:app
* Браузерним клієнтам потрібен CORS: `allow_headers` для заголовків запиту `Mcp-*`, `expose_headers=["Mcp-Session-Id"]` для відповіді.
* `@mcp.custom_route()` додає звичайні HTTP-кінцеві точки без автентифікації поруч із `/mcp`.
Щойно сервер стане досяжним за справжньою URL-адресою, **[Клієнт](../client/index.md)** під'єднається до нього за цією URL-адресою, а не через об'єкт сервера.
Щойно сервер стане досяжним за справжньою URL-адресою, **[Клієнт](../client/index.md)** під'єднається до нього за цією URL-адресою.

View file

@ -1,60 +1,64 @@
---
translation:
sections: [d62c13457fc4a534, 80e73abaca6e0652, d1dc4c54cd00ec9c, 14ad3bc7904036bb, 5225f127bc1b9c77, fe1626fdd5aad1da, 4556cb7ea1a04a31]
sections: [d62c13457fc4a534, 80e73abaca6e0652, 128a492d18295f64, 14ad3bc7904036bb, 54a6697833fcc1d9, fe1626fdd5aad1da, 811d083c1da8bcf6]
tool: 1
---
# Авторизація {#authorization}
Через Streamable HTTP ваш MCP-сервер — це звичайний вебсервіс, і захищати його слід так само, як будь-який інший вебсервіс: bearer-токенами OAuth 2.1.
Через Streamable HTTP ваш MCP-сервер — це звичайний вебсервіс, і захищати його треба так само, як будь-який вебсервіс: bearer-токенами OAuth 2.1.
У термінах OAuth ваш сервер — це **сервер ресурсів** (resource server). Він ніколи нікого не автентифікує й ніколи не видає токенів. Він робить одне: дивиться на заголовок `Authorization` у кожному запиті й вирішує, чи придатний токен у ньому.
У термінах OAuth ваш сервер — це **сервер ресурсів**. Він нікого не автентифікує і ніколи не видає токенів. Він робить одне: дивиться на заголовок `Authorization` кожного запиту й вирішує, чи дійсний токен у ньому.
Ця сторінка — про серверний бік. Клієнт, який знаходить ваш сервер авторизації й отримує токен, описано на сторінці **[Клієнти OAuth](../client/oauth-clients.md)**.
Ця сторінка — про серверну частину. Клієнт, який знаходить ваш сервер авторизації й отримує токен, описаний на сторінці **[OAuth-клієнти](../client/oauth-clients.md)**.
## Три сторони {#the-three-parties}
* **Сервер авторизації** автентифікує людей і видає токени доступу. Ви його не пишете. Це ваш постачальник ідентичності (Auth0, Keycloak, Entra, ваш власний).
* **Сервер авторизації** автентифікує людей і видає токени доступу. Його ви не пишете. Це ваш постачальник ідентичності (Auth0, Keycloak, Entra, власний).
* **Сервер ресурсів** — це ваш MCP-сервер. Він перевіряє токен у кожному запиті.
* **Клієнт** з'ясовує, якому серверу авторизації ви довіряєте, отримує від нього токен і надсилає його вам як `Authorization: Bearer <token>`.
Оце й увесь трикутник. Усе на цій сторінці стосується середнього пункту.
Оце й увесь трикутник. Усе на цій сторінці — про середній пункт.
## Верифікатор токенів {#a-token-verifier}
SDK не має власної думки про те, який вигляд має дійсний токен. Це визначаєте ви, реалізувавши **`TokenVerifier`**:
SDK не має власної думки про те, який токен вважати дійсним. Це визначаєте ви, реалізуючи **`TokenVerifier`**:
```python title="server.py" hl_lines="12-14 19-24"
```python title="server.py" hl_lines="14-16 21-27"
--8<-- "docs_src/authorization/tutorial001.py"
```
* `TokenVerifier` — це протокол з одним асинхронним методом. `verify_token` отримує сирий токен із заголовка `Authorization` і повертає **`AccessToken`**, якщо він дійсний, або `None`, якщо ні. Більше нічого реалізовувати не треба.
* Цей верифікатор шукає токен у таблиці. Справжній перевіряє підпис JWT або звертається до кінцевої точки інтроспекції токенів сервера авторизації. Цей код — ваш; SDK лише викликає його.
* `token_verifier=` і `auth=` завжди йдуть разом. Передайте одне без іншого — і `MCPServer(...)` викине `ValueError` ще до того, як обслужить хоч один запит.
* `TokenVerifier` — це протокол з одним асинхронним методом. `verify_token` отримує сирий токен із заголовка `Authorization` і повертає **`AccessToken`**, якщо токен дійсний, або `None`, якщо ні. Більше нічого реалізовувати не потрібно.
* Цей шукає токен у таблиці; кожен запис фіксує ресурс, для якого токен видано. Справжній перевіряє підпис JWT або викликає кінцеву точку інтроспекції токенів сервера авторизації і повідомляє, для кого видано токен (його `aud`), у `AccessToken.resource`. Цей код — ваш; SDK його лише викликає.
* `token_verifier=` і `auth=` завжди йдуть у парі. Передайте одне без іншого — і `MCPServer(...)` викине `ValueError`, ще не обслуживши жодного запиту.
`AuthSettings` — це публічне обличчя вашого сервера ресурсів:
`AuthSettings` — публічне обличчя вашого сервера ресурсів:
* `issuer_url`: сервер авторизації, що видає ваші токени.
* `resource_server_url`: публічний URL цієї MCP-кінцевої точки. Він указує, для *якого* ресурсу призначено токен, і саме тут розміщено документ виявлення.
* `resource_server_url`: публічна URL-адреса цієї кінцевої точки MCP. Вона вказує, для *якого* ресурсу призначено токен, і саме за нею лежить документ виявлення.
* `required_scopes`: кожен токен мусить містити їх усі.
* `validate_token_resource`: відхиляти будь-який токен, чий `AccessToken.resource` не дорівнює `resource_server_url`. Якщо залишити його незаданим, коли `resource_server_url` задано, з'являється попередження (`MCPDeprecationWarning`), а поведінка відповідає `False`; у 3.0 типовим значенням для серверів ресурсів стане `True`.
* Увімкніть його, якщо ваш сервер авторизації прив'язує токени до `resource`, який запросив клієнт, — а MCP-клієнти завжди його надсилають. `resource_server_url` має бути точно тією URL-адресою, до якої під'єднуються клієнти.
* Залиште вимкненим, якщо ваш сервер авторизації використовує власні ідентифікатори аудиторії (ідентифікатор API в Auth0, ідентифікатор застосунку в Entra), і натомість перевіряйте `aud` у своєму верифікаторі, повертаючи `None` для токена, що не призначений цьому серверу.
* Якщо `aud` — список, покладіть у `resource` той елемент, що дорівнює `resource_server_url`.
!!! tip
`examples/servers/simple-auth/` у репозиторії SDK містить `IntrospectionTokenVerifier`, який звертається
до кінцевої точки [RFC 7662](https://datatracker.ietf.org/doc/html/rfc7662) справжнього сервера авторизації. Саме таку форму мають більшість продакшн-верифікаторів.
У `examples/servers/simple-auth/` у репозиторії SDK є `IntrospectionTokenVerifier`, який викликає
кінцеву точку [RFC 7662](https://datatracker.ietf.org/doc/html/rfc7662) справжнього сервера авторизації. Саме таку форму має більшість верифікаторів у реальних розгортаннях.
## Що з'являється через HTTP {#what-you-get-over-http}
## Що ви отримуєте через HTTP {#what-you-get-over-http}
Авторизація живе в HTTP-заголовках, тож існує лише на HTTP-транспортах. Запустіть сервер на тому, який розгортаєте: `mcp.run(transport="streamable-http")` розміщує його на `http://127.0.0.1:8000/mcp`, а решта — на сторінці **[Запуск сервера](index.md)**. Тепер застосунок має два маршрути:
Авторизація живе в HTTP-заголовках, тож існує лише на HTTP-транспортах. Запускайте її на тому, який розгортаєте: `mcp.run(transport="streamable-http")` піднімає сервер на `http://127.0.0.1:8000/mcp`, а решту описано на сторінці **[Запуск сервера](index.md)**. Тепер застосунок має два маршрути:
```text
/mcp
/.well-known/oauth-protected-resource/mcp
```
Ви зареєстрували один інструмент. Другий маршрут належить SDK.
Ви зареєстрували один інструмент. Другий маршрут — від SDK.
### Виявлення {#discovery}
Зробіть `GET` на цей well-known-шлях — і отримаєте **[RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728) Protected Resource Metadata**, побудовані безпосередньо з вашого `AuthSettings`:
Зробіть `GET` на цей well-known шлях — і отримаєте **[RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728) Protected Resource Metadata**, побудовані прямо з ваших `AuthSettings`:
```json
{
@ -65,7 +69,7 @@ SDK не має власної думки про те, який вигляд м
}
```
Саме завдяки цьому документу клієнт, який ніколи не чув про ваш сервер, знаходить дорогу: він читає `authorization_servers` і йде туди по токен. Ви не написали з нього жодного рядка.
Саме завдяки цьому документу клієнт, який ніколи не чув про ваш сервер, знаходить шлях усередину: він читає `authorization_servers` і йде туди по токен. Ви не написали з нього жодного рядка.
!!! check
Зверніться до `/mcp` без токена (або з таким, для якого ваш верифікатор повернув `None`) — і запит
@ -78,26 +82,26 @@ SDK не має власної думки про те, який вигляд м
{"error": "invalid_token", "error_description": "Authentication required"}
```
Нічого не було розібрано, і жоден інструмент не виконався. А вказівник `resource_metadata` у `WWW-Authenticate` —
саме те, що робить виявлення автоматичним: 401 -> документ метаданих -> сервер авторизації -> токен -> повторна спроба.
Нічого не розібрано, жоден інструмент не виконувався. А вказівник `resource_metadata` у `WWW-Authenticate` —
це те, що робить виявлення автоматичним: 401 -> документ метаданих -> сервер авторизації -> токен -> повторна спроба.
!!! warning
Ніщо з цього не захищає `stdio`. Канал не має заголовка `Authorization`, тож `token_verifier` там ніколи
не викликається. Межа безпеки `stdio`-сервера — це процес, який його запустив. Те саме
стосується `Client(mcp)` у пам'яті, який ви використовуєте в тестах: він під'єднується безпосередньо до об'єкта сервера
й оминає HTTP-рівень, разом з авторизацією.
Нічого з цього не захищає `stdio`. Канал (pipe) не має заголовка `Authorization`, тож `token_verifier` там ніколи
не викликається. Межа безпеки `stdio`-сервера — процес, який його запустив. Те саме
стосується `Client(mcp)` у пам'яті, який використовують у тестах: він під'єднується безпосередньо до об'єкта сервера
й оминає HTTP-рівень разом з авторизацією.
## Ідентичність того, хто викликає {#the-callers-identity}
Усередині будь-якого обробника **`get_access_token()`** — це `AccessToken`, який ваш верифікатор повернув для поточного запиту:
```python title="server.py" hl_lines="4 32-35"
```python title="server.py" hl_lines="4 35-38"
--8<-- "docs_src/authorization/tutorial002.py"
```
* Це працює в інструментах, ресурсах і промптах, і нічого нікуди передавати не треба: middleware авторизації зберігає його в контекстній змінній для кожного запиту.
* Повертається **той самий об'єкт, який побудував ваш верифікатор**: `client_id`, `scopes`, `subject`, `expires_at` і будь-які додаткові `claims`, які ви додали. Це й є зачіпка для правил на рівні окремих інструментів: прочитайте scopes і відмовте.
* Поза автентифікованим HTTP-запитом він повертає `None`. У пам'яті й через `stdio` це завжди `None`.
* Це працює в інструментах, ресурсах і промптах, і нічого не треба передавати вручну: middleware авторизації зберігає його в контекстній змінній для кожного запиту.
* Повертається **той самий об'єкт, що його побудував ваш верифікатор**: `client_id`, `scopes`, `subject`, `expires_at` і будь-які додаткові `claims`, які ви додали. Це і є зачіпка для правил на рівні окремих інструментів: прочитайте scopes і відмовте.
* Поза автентифікованим HTTP-запитом функція повертає `None`. У пам'яті та через `stdio` це завжди `None`.
Викличте `whoami` з `Authorization: Bearer alice-token` — і модель прочитає:
@ -109,22 +113,22 @@ alice (scopes: notes:read)
SDK дає вам половину сервера ресурсів: перевірити, оголосити, відмовити. Він не дає сторінки входу, екрана згоди чи токена.
Щоб побачити всі три сторони в русі, запустіть `examples/servers/simple-auth/` з репозиторію SDK (невеликий сервер авторизації та сервер ресурсів, налаштований точно як на цій сторінці), а потім спрямуйте на нього `examples/clients/simple-auth-client/`, щоб пройти повний шлях виявлення й отримання токена.
Щоб побачити всі три сторони в русі, запустіть `examples/servers/simple-auth/` з репозиторію SDK (невеликий сервер авторизації та сервер ресурсів, налаштований точно як на цій сторінці), а потім спрямуйте на нього `examples/clients/simple-auth-client/` — і пройдете весь шлях від виявлення до токена.
!!! info
Є другий аргумент конструктора, `auth_server_provider=`, який вбудовує повноцінний сервер
авторизації всередину вашого MCP-сервера. Він з'явився ще до розділення AS/RS, навколо якого
побудовано специфікацію авторизації MCP. Новим серверам не слід до нього вдаватися.
Є ще другий аргумент конструктора, `auth_server_provider=`, який вбудовує повноцінний сервер авторизації
всередину вашого MCP-сервера. Він з'явився раніше за розділення AS/RS, навколо якого побудована специфікація
авторизації MCP. У нових серверах його використовувати не слід.
Сервер авторизації також може прийняти підписане твердження корпоративного постачальника ідентичності замість того, щоб користувач проходив екран згоди, і SDK підтримує обидва боки цього обміну. Про цей grant і клієнта, що його пред'являє, — на сторінці **[Твердження ідентичності](../client/identity-assertion.md)**.
Сервер авторизації може також прийняти підписане твердження від корпоративного постачальника ідентичності замість того, щоб користувач проклацував екран згоди, і SDK підтримує обидві сторони цього обміну. Цей грант і клієнт, що його пред'являє, описані на сторінці **[Твердження ідентичності](../client/identity-assertion.md)**.
## Підсумки {#recap}
* Через Streamable HTTP ваш сервер — це **сервер ресурсів** OAuth 2.1: він перевіряє токени й ніколи їх не видає.
* `TokenVerifier` — це вся поверхня інтеграції: один асинхронний метод, на вході токен, на виході `AccessToken | None`.
* `token_verifier=` і `auth=AuthSettings(issuer_url=..., resource_server_url=..., required_scopes=[...])` завжди йдуть разом.
* Через Streamable HTTP ваш сервер — **сервер ресурсів** OAuth 2.1: він перевіряє токени й ніколи їх не видає.
* `TokenVerifier` — уся поверхня інтеграції: один асинхронний метод, токен на вході, `AccessToken | None` на виході.
* `token_verifier=` і `auth=AuthSettings(issuer_url=..., resource_server_url=..., required_scopes=[...])` завжди йдуть у парі.
* SDK публікує [RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728) Protected Resource Metadata за адресою `/.well-known/oauth-protected-resource/...` і відповідає на неавтентифіковані запити кодом 401, чий заголовок `WWW-Authenticate` вказує на них. Оце й уся історія виявлення.
* `get_access_token()` у будь-якому обробнику — це той, хто викликає.
* Авторизація — справа HTTP. `stdio` та клієнт у пам'яті ніколи її не бачать.
* Авторизація — справа HTTP. `stdio` і тестовий клієнт у пам'яті ніколи її не бачать.
Клієнтська половина (виявлення вашого сервера авторизації й отримання токена за вас) — на сторінці **[Клієнти OAuth](../client/oauth-clients.md)**. А клієнт, який *стверджує* ідентичність замість того, щоб запитувати її в користувача, — на сторінці **[Твердження ідентичності](../client/identity-assertion.md)**.
Клієнтська половина (виявлення сервера авторизації й отримання токена за вас) — на сторінці **[OAuth-клієнти](../client/oauth-clients.md)**. А клієнт, який *стверджує* ідентичність замість того, щоб просити її в користувача, — на сторінці **[Твердження ідентичності](../client/identity-assertion.md)**.

View file

@ -1,6 +1,6 @@
---
translation:
sections: [28221886b198784f, f88ea1f1614f3a1d, ce926d686730b6d0, 3be24f8ad8bb5ab9, 3fad24032b2224ff, f25a7f860e579ecb, e758745df6fb7b0a]
sections: [28221886b198784f, f88ea1f1614f3a1d, 2e76f5cb9df15042, ce926d686730b6d0, 3be24f8ad8bb5ab9, 3fad24032b2224ff, f25a7f860e579ecb, 697b01d95080880d]
tool: 1
---
# Розгортання та масштабування {#deploy-scale}
@ -47,6 +47,22 @@ translation:
розгорнутий сервер, який відхиляє кожне з'єднання, — це список дозволених Host, доки не доведено протилежне.
**[Усунення несправностей](../troubleshooting.md)** теж починається звідси.
## За проксі, що завершує TLS {#behind-a-tls-terminating-proxy}
Якщо TLS завершується на проксі (ingress, балансувальник навантаження, Caddy, nginx), а uvicorn за ним обслуговує звичайний HTTP, скажіть uvicorn довіряти заголовкам `X-Forwarded-*` від проксі:
```console
uvicorn server:app --proxy-headers --forwarded-allow-ips='<proxy address>'
```
Без цього застосунок вважає, що його обслуговують через `http://`, і будь-яке перенаправлення, яке він видає (зазвичай це `/mcp` → `/mcp/`), веде на `http://…`. Python-клієнт відмовляється переходити з HTTPS-ендпоінта на звичайний HTTP і прямо про це каже:
```text
MCPError: Redirect to http://mcp.example.com/mcp/ not followed: it would downgrade this HTTPS endpoint to plain HTTP.
```
Тимчасовий обхід на боці клієнта — вказати точний URL, який обслуговує сервер (`https://mcp.example.com/mcp/`, зі скісною рискою наприкінці), щоб перенаправлення не було взагалі. Справжнє виправлення — прапорець вище. `FORWARDED_ALLOW_IPS` — той самий параметр у вигляді змінної середовища; `*` довіряє кожному проміжному вузлу, що правильно лише тоді, коли до uvicorn не може дістатися ніхто, крім проксі.
## Робочі процеси, і для кого потрібні липкі сесії {#workers-and-who-has-to-be-sticky}
Щойно ім'я хоста відповідає, поставте за ним більше ніж один робочий процес. Для цього в SDK немає жодного перемикача; Starlette-застосунок масштабують так само, як і будь-який ASGI-застосунок, — передають об'єкт чомусь, що вміє створювати дочірні процеси:
@ -170,6 +186,7 @@ python -c "import secrets; print(secrets.token_hex(32))"
## Підсумки {#recap}
* За замовчуванням застосунок відповідає лише на запити, адресовані localhost. `transport_security=TransportSecuritySettings(allowed_hosts=[...], allowed_origins=[...])` — це ворота виходу в світ: доки його не передати, кожен запит за справжнім іменем хоста — це `421`, а причина є лише в лозі сервера.
* За проксі, що завершує TLS, запускайте uvicorn з `--proxy-headers --forwarded-allow-ips=...`, інакше його перенаправлення ведуть на `http://`, і клієнт їх відхиляє.
* На 2026-07-28 немає сесії і нічого, до чого балансувальник навантаження мав би прив'язуватися. `stateless_http=True` — перемикач лише для старого покоління, бо сучасний запит маршрутизується й отримує відповідь ще до того, як цей прапорець узагалі прочитають.
* Типовий ключ `requestState` — це `os.urandom(32)`, випущений окремо в кожному процесі. Багатораундова повторна спроба, що потрапила до іншого робочого процесу, збоїть з `-32602` *«Invalid or expired requestState»*.
* Виправлення — `RequestStateSecurity(keys=[...])` **і** те саме ім'я сервера на кожному екземплярі. Ім'я — типове твердження про аудиторію токена. Ті самі ключі, те саме ім'я.

View file

@ -1,6 +1,6 @@
---
translation:
sections: [fea8d769ff9edeba, ce8e2ad42f29ef71, 0d705efb19cf99c2, 2fd7cf825e6d2b2c, 9adc400e8c88e854, 318893ad8e2e9924, 6b63ab96b34476c0]
sections: [fea8d769ff9edeba, ce8e2ad42f29ef71, 0d705efb19cf99c2, 9fd2357154a5b7e7, 9adc400e8c88e854, 318893ad8e2e9924, 6b63ab96b34476c0]
tool: 1
---
# Запуск сервера {#running-your-server}
@ -75,6 +75,11 @@ Inspector робить рівно те саме, що й справжній хо
* `max_request_body_size`: найбільший прийнятний розмір тіла запиту в байтах. За замовчуванням 4 МіБ; більші запити
отримують HTTP 413 ще до розбору чи створення сесії. Збільшуйте його лише тоді, коли легітимні MCP-повідомлення
перевищують цей розмір.
* `session_idle_timeout`: скільки секунд сесія старого покоління може простоювати без жодного запиту в роботі,
перш ніж сервер її закриє. За замовчуванням 1800. `None` вимикає це. Див.
[Час життя сесії та обмеження](legacy-clients.md#session-lifetime-and-limits).
* `max_sessions`: скільки сесій старого покоління один процес утримує одночасно. За замовчуванням 10 000. `None`
знімає обмеження. Описано в тому самому розділі.
* `event_store`, `retry_interval`, `transport_security`: відновлюваність і захист від DNS-rebinding. Вони можуть зачекати, доки ви не розгорнете сервер деінде, крім localhost; `transport_security` описано на сторінці **[Розгортання та масштабування](deploy.md)**.
!!! warning

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