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