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