docs: refresh translations, and translate pages in parallel (#3458)
This commit is contained in:
parent
08a3bc8eaf
commit
fd66270926
6 changed files with 4821 additions and 2277 deletions
|
|
@ -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` его открывает.
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue