docs: refresh translations, and translate pages in parallel (#3458)
This commit is contained in:
parent
08a3bc8eaf
commit
fd66270926
23 changed files with 4821 additions and 2277 deletions
|
|
@ -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,7 +18,7 @@ translation:
|
|||
|
||||
## Часы с циферблатом {#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"
|
||||
```
|
||||
|
||||
|
|
@ -39,11 +39,31 @@ translation:
|
|||
|
||||
Модель читает `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]"` в качестве единственного содержимого. Если запасной текст бесполезен, инструмент бесполезен для любого текстового клиента и для самой модели. Напишите нормальное предложение.
|
||||
|
|
|
|||
|
|
@ -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). Одно дополнение: когда ключ из params должен передаваться в заголовке `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"
|
||||
```
|
||||
|
||||
Одно дополнение на клиенте: когда ключ из params должен передаваться в заголовке `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}
|
||||
|
|
|
|||
|
|
@ -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)**.
|
||||
|
|
|
|||
|
|
@ -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` уже в первом ответе, и цикл выполняется один раз.
|
||||
|
||||
|
|
|
|||
|
|
@ -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)**.
|
||||
|
||||
|
|
|
|||
|
|
@ -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)**.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
---
|
||||
# Объект Client {#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.'
|
|||
|
||||
Этой схемы достаточно и интерфейсу, чтобы отрисовать форму аргументов, и модели, чтобы сформировать корректные аргументы.
|
||||
|
||||
Второй инструмент, `lookup_book`, зарегистрирован без `title=`, поэтому его `tool.title` равен `None`.
|
||||
|
||||
!!! tip
|
||||
`title` необязателен, поэтому интерфейсу, показывающему инструменты человеку, приходится выбирать: `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`. Инструмент, выбросивший исключение, — это результат, а не исключение.
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
@ -82,18 +82,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 — две команды: запустите серверы, запустите клиент, подключив его к ним, — и наблюдайте, как проходят все четыре шага.
|
||||
|
||||
|
|
@ -111,13 +113,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`.
|
||||
|
||||
|
|
@ -130,7 +133,7 @@ URL должен быть HTTPS и с некорневым путём; всё о
|
|||
Ещё один провайдер находится в `mcp.client.auth.extensions.client_credentials`:
|
||||
**`PrivateKeyJWTOAuthProvider`** — для клиентов, которые аутентифицируются с помощью JWT
|
||||
вместо общего секрета (`private_key_jwt`, вариант с парой ключей и workload identity).
|
||||
Схема та же: создайте экземпляр и передайте его в `auth=`. В том же модуле есть
|
||||
Схема та же: создайте экземпляр (он принимает тот же необязательный `issuer`) и передайте его в `auth=`. В том же модуле есть
|
||||
`SignedJWTParameters` и `static_assertion_provider` — два вспомогательных средства,
|
||||
которые собирают для него утверждение (assertion).
|
||||
|
||||
|
|
|
|||
|
|
@ -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=` переписывает каждое регистрируемое имя. Меняется ключ словаря, но не имя в передаваемых данных.
|
||||
|
|
|
|||
|
|
@ -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=` нет.
|
||||
* Редиректы выполняются только в пределах источника самого 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)`. `Client` передаёт всё, что не объект сервера, не URL и не `StdioServerParameters`, прямо в этот протокол.
|
||||
* Создание `Client` выбирает транспорт. `async with` его открывает.
|
||||
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
||||
В таблице ниже перечислены все устаревшие возможности, причина, по которой каждая уходит, и замена, на которую стоит опираться.
|
||||
|
||||
|
|
@ -137,13 +137,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}
|
||||
|
||||
|
|
@ -152,7 +154,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.
|
||||
|
|
|
|||
|
|
@ -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,9 +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}
|
||||
|
||||
|
|
@ -129,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.
|
||||
|
||||
|
|
@ -140,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)**.
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
---
|
||||
translation:
|
||||
sections: ['4926721070127497', c52a1de2b6b32f40, 8e792bf8c7489ec6, 627195f7159e24ef]
|
||||
sections: [5d13c2f0ba42c0d2, c52a1de2b6b32f40, 8e792bf8c7489ec6, 38552ea228b0a04f]
|
||||
tool: 1
|
||||
---
|
||||
# Тестирование {#testing}
|
||||
|
||||
В Python SDK есть класс `Client` со **встроенным in-memory транспортом**: передайте ему объект сервера, и он подключится к нему напрямую.
|
||||
Класс `Client` из SDK — тот самый, что подключается к URL или запускает подпроцесс, — умеет подключаться и **в памяти**: передайте ему объект сервера, и он будет говорить с ним напрямую.
|
||||
|
||||
Ни подпроцесса. Ни порта. Вообще никакого транспорта. Та же идея, что и `TestClient` в FastAPI.
|
||||
Ни подпроцесса. Ни порта. Ничего не передаётся по сети. Та же идея, что и `TestClient` в FastAPI.
|
||||
|
||||
## Базовое использование {#basic-usage}
|
||||
|
||||
|
|
@ -97,7 +97,7 @@ async def test_call_add_tool(client: Client):
|
|||
|
||||
В тестах оставляйте его включённым. В продакшен-коде он не имеет смысла.
|
||||
|
||||
## Внутри процесса по умолчанию {#in-process-by-default}
|
||||
## Без привязки к поколению протокола по умолчанию {#era-neutral-by-default}
|
||||
|
||||
!!! note
|
||||
`Client(mcp)` подключается внутри процесса и по умолчанию **не привязан к поколению
|
||||
|
|
|
|||
|
|
@ -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) {#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` некуда положить. Верните его из обработчика на
|
||||
|
|
|
|||
|
|
@ -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,29 +54,32 @@ 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`. Аргумента конструктора для него нет,
|
||||
|
|
|
|||
|
|
@ -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`, который общается с сервером Bookshop `server.py` со страницы **[Клиент](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` на любом транспорте — в памяти, 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"
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
@ -102,7 +102,7 @@ uvicorn server:app
|
|||
--8<-- "docs_src/asgi/tutorial004.py"
|
||||
```
|
||||
|
||||
Теперь клиенты подключаются к `/notes`, а не к `/notes/mcp`.
|
||||
Теперь клиенты подключаются к `/notes/`, а не к `/notes/mcp`.
|
||||
|
||||
## CORS для браузерных клиентов {#cors-for-browser-clients}
|
||||
|
||||
|
|
@ -137,7 +137,7 @@ uvicorn server:app
|
|||
## Итоги {#recap}
|
||||
|
||||
* `mcp.streamable_http_app()` возвращает приложение Starlette с одним маршрутом, `/mcp`. Запустить его может любой ASGI-сервер.
|
||||
* По умолчанию приложение отвечает только на запросы, адресованные localhost, а за настоящим именем хоста отклоняет всё кодом `421`, пока вы не передадите список разрешённых хостов в `transport_security=`. За это и за остальной путь к продакшену отвечает страница **[Развёртывание и масштабирование](deploy.md)**.
|
||||
* По умолчанию приложение отвечает только на запросы, адресованные localhost, а за настоящим именем хоста отклоняет всё кодом `421`, пока вы не передадите в `transport_security=` список разрешённых хостов. За это и за остальной путь к продакшену отвечает страница **[Развёртывание и масштабирование](deploy.md)**.
|
||||
* `Mount` (или `Host`) помещает его внутрь более крупного приложения Starlette или FastAPI.
|
||||
* **Монтирование отключает встроенный жизненный цикл.** Жизненный цикл хост-приложения должен войти в `mcp.session_manager.run()`, иначе первый запрос завершится ошибкой.
|
||||
* Несколько серверов в одном приложении — это несколько монтирований и один жизненный цикл, который входит в каждый менеджер сессий.
|
||||
|
|
@ -145,4 +145,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.
|
||||
|
|
|
|||
|
|
@ -1,19 +1,19 @@
|
|||
---
|
||||
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 ваш сервер — это **сервер ресурсов**. Он никого не аутентифицирует и не выдаёт токенов. Он делает ровно одно: смотрит на заголовок `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>`.
|
||||
|
||||
|
|
@ -21,29 +21,33 @@ translation:
|
|||
|
||||
## Верификатор токенов {#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 его только вызывает.
|
||||
* `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) настоящего сервера авторизации. Именно так устроено большинство верификаторов в продакшене.
|
||||
В репозитории SDK, в `examples/servers/simple-auth/`, есть `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
|
||||
|
|
@ -65,11 +69,11 @@ SDK ничего не предполагает о том, как выгляди
|
|||
}
|
||||
```
|
||||
|
||||
Именно по этому документу клиент, который никогда не слышал о вашем сервере, находит к нему дорогу: читает `authorization_servers` и идёт туда за токеном. Ничего из этого вы не писали.
|
||||
По этому документу клиент, никогда не слышавший о вашем сервере, находит дорогу внутрь: читает `authorization_servers` и идёт туда за токеном. Ни строчки из него вы не писали.
|
||||
|
||||
!!! check
|
||||
Обратитесь к `/mcp` без токена (или с таким, для которого верификатор вернул `None`) — и запрос
|
||||
остановят на входе:
|
||||
Вызовите `/mcp` без токена (или с токеном, для которого верификатор вернул `None`) — и запрос
|
||||
остановят на пороге:
|
||||
|
||||
```text
|
||||
HTTP/1.1 401 Unauthorized
|
||||
|
|
@ -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)` в памяти, который используется в тестах: он подключается напрямую к объекту сервера
|
||||
не обращаются. Граница безопасности `stdio`-сервера — процесс, который его запустил. То же
|
||||
относится к `Client(mcp)` в памяти, который используется в тестах: он подключается прямо к объекту сервера
|
||||
и минует HTTP-уровень вместе с авторизацией.
|
||||
|
||||
## Личность вызывающего {#the-callers-identity}
|
||||
## Личность вызывающей стороны {#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`, которые вы прикрепили. Это и есть точка для правил на уровне инструмента: прочитайте области действия и откажите.
|
||||
* Вне аутентифицированного HTTP-запроса возвращается `None`. В памяти и по `stdio` это всегда `None`.
|
||||
|
||||
Вызовите `whoami` с `Authorization: Bearer alice-token` — и модель прочитает:
|
||||
|
||||
|
|
@ -105,26 +109,26 @@ SDK ничего не предполагает о том, как выгляди
|
|||
alice (scopes: notes:read)
|
||||
```
|
||||
|
||||
## Половина, которую SDK не делает {#the-half-the-sdk-doesnt-do}
|
||||
## Половина, которой в SDK нет {#the-half-the-sdk-doesnt-do}
|
||||
|
||||
SDK даёт вам половину сервера ресурсов: проверить, объявить, отказать. Он не даёт страницу входа, экран согласия или токен.
|
||||
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 поддерживает обе стороны этого обмена. Сам грант и клиент, который его предъявляет, описаны на странице **[Утверждение личности](../client/identity-assertion.md)**.
|
||||
Сервер авторизации может также принять подписанное утверждение корпоративного провайдера идентификации вместо того, чтобы пользователь проходил экран согласия, — и SDK поддерживает обе стороны этого обмена. Сам грант и клиент, который его предъявляет, описаны на странице **[Утверждение идентичности](../client/identity-assertion.md)**.
|
||||
|
||||
## Итоги {#recap}
|
||||
|
||||
* Через Streamable HTTP ваш сервер — это **сервер ресурсов** OAuth 2.1: он проверяет токены, но никогда их не выдаёт.
|
||||
* `TokenVerifier` — вся поверхность интеграции: один асинхронный метод, токен на входе, `AccessToken | None` на выходе.
|
||||
* По Streamable HTTP ваш сервер — **сервер ресурсов** OAuth 2.1: он проверяет токены и никогда их не выдаёт.
|
||||
* `TokenVerifier` — вся поверхность интеграции: один асинхронный метод, на входе токен, на выходе `AccessToken | None`.
|
||||
* `token_verifier=` и `auth=AuthSettings(issuer_url=..., resource_server_url=..., required_scopes=[...])` всегда идут в паре.
|
||||
* SDK публикует Protected Resource Metadata по [RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728) по адресу `/.well-known/oauth-protected-resource/...` и отвечает на неаутентифицированные запросы кодом 401, заголовок `WWW-Authenticate` которого указывает на них. Это и есть вся история обнаружения.
|
||||
* SDK публикует Protected Resource Metadata по [RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728) по адресу `/.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)**.
|
||||
|
|
|
|||
|
|
@ -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-приложение: объект передаётся тому, кто умеет порождать процессы:
|
||||
|
|
@ -171,6 +187,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=[...])` **и** одно и то же имя сервера на каждом экземпляре. Имя — это audience claim токена по умолчанию. Одни ключи, одно имя.
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in a new issue