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

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

View file

@ -1,6 +1,6 @@
---
translation:
sections: [1062ef792791488a, 4be2b831547184a9, 374b049e770385f2, b72f6947089e6de0, b172c9db7831bb31, 70b9ece244ca1b0c, cba78e052898c3f6, f06bdb541cb0b469, fb82d526320b7cc3]
sections: [1062ef792791488a, 4be2b831547184a9, 374b049e770385f2, b72f6947089e6de0, b172c9db7831bb31, 10394c6f16601638, cba78e052898c3f6, f06bdb541cb0b469, 6dc898ccc5a903f9]
tool: 1
---
# Добавление в существующее приложение {#add-to-an-existing-app}
@ -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.

View file

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

View file

@ -1,6 +1,6 @@
---
translation:
sections: [28221886b198784f, f88ea1f1614f3a1d, ce926d686730b6d0, 3be24f8ad8bb5ab9, 3fad24032b2224ff, f25a7f860e579ecb, e758745df6fb7b0a]
sections: [28221886b198784f, f88ea1f1614f3a1d, 2e76f5cb9df15042, ce926d686730b6d0, 3be24f8ad8bb5ab9, 3fad24032b2224ff, f25a7f860e579ecb, 697b01d95080880d]
tool: 1
---
# Развёртывание и масштабирование {#deploy-scale}
@ -47,6 +47,22 @@ translation:
Свежеразвёрнутый сервер, который отклоняет все подключения, — это список разрешённых Host,
пока не доказано обратное. **[Устранение неполадок](../troubleshooting.md)** тоже начинается отсюда.
## За прокси, терминирующим TLS {#behind-a-tls-terminating-proxy}
Если TLS завершается на прокси (ingress, балансировщик нагрузки, Caddy, nginx), а uvicorn за ним отдаёт обычный HTTP, велите uvicorn доверять заголовкам `X-Forwarded-*` от прокси:
```console
uvicorn server:app --proxy-headers --forwarded-allow-ips='<proxy address>'
```
Без этого приложение считает, что его отдают по `http://`, и любое перенаправление, которое оно выдаёт (обычно это `/mcp` → `/mcp/`), указывает на `http://…`. Python-клиент отказывается переходить с HTTPS-эндпоинта на обычный HTTP и прямо об этом говорит:
```text
MCPError: Redirect to http://mcp.example.com/mcp/ not followed: it would downgrade this HTTPS endpoint to plain HTTP.
```
Временное решение на стороне клиента — указать точный URL, который обслуживает сервер (`https://mcp.example.com/mcp/`, со слэшем на конце), чтобы перенаправления не было вовсе. Настоящее решение — флаг выше. `FORWARDED_ALLOW_IPS` — то же самое в виде переменной окружения; `*` доверяет каждому узлу по пути, что правильно, только если до uvicorn не может добраться никто, кроме прокси.
## Рабочие процессы и кому нужна привязка {#workers-and-who-has-to-be-sticky}
Как только доменное имя отвечает, поставьте за ним больше одного рабочего процесса. В SDK для этого нет никакой ручки; приложение Starlette масштабируется так же, как любое ASGI-приложение: объект передаётся тому, кто умеет порождать процессы:
@ -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 токена по умолчанию. Одни ключи, одно имя.

View file

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

View file

@ -1,6 +1,6 @@
---
translation:
sections: [3d1663c18edc824c, d4fd37009a13f03d, af9f398a5a8b679a, 470c2dd144294d69, 8e45827e6d24e8c8, 91dfd0ce98ebb03c]
sections: [3d1663c18edc824c, 90956965ae6a1ca1, af9f398a5a8b679a, 5ce83b1f9d88da62, 0d9b5d13fffc94e5, 8e45827e6d24e8c8, 91dfd0ce98ebb03c]
tool: 1
---
# Обслуживание клиентов старого поколения {#serving-legacy-clients}
@ -21,15 +21,25 @@ SDK маршрутизирует каждый запрос по его заго
## Один обработчик, оба поколения {#one-handler-both-eras}
Вот инструмент, которому нужно кое-что спросить у пользователя, и клиенты обоих поколений, которые его вызывают:
Вот инструмент, которому нужно кое-что спросить у пользователя:
```python title="server.py" hl_lines="24 37-38"
```python title="server.py" hl_lines="21"
--8<-- "docs_src/legacy_clients/tutorial001.py"
```
Инструменту `reserve` нужно одно, чего модель не сообщила: сколько экземпляров. `Annotated[..., Resolve(ask_quantity)]` — так инструмент это объявляет (подробнее — на странице **[Зависимости](../handlers/dependencies.md)**). Ничто в `reserve` не называет версию, не проверяет возможность и не ветвится.
Оба клиента открыты **одновременно**, на одном и том же объекте `mcp`. `mode="legacy"` выполняет рукопожатие `initialize` — ровно такое подключение открывает клиент до 2026 года. Второй клиент берёт значение по умолчанию и оказывается на `2026-07-28`.
Запустите его по HTTP — и вот клиенты обоих поколений, которые его вызывают:
```console
uv run mcp run server.py --transport streamable-http
```
```python title="client.py" hl_lines="14-15"
--8<-- "docs_src/legacy_clients/tutorial001_client.py"
```
Оба клиента открыты **одновременно**, к одному и тому же работающему серверу. `mode="legacy"` выполняет рукопожатие `initialize` — ровно такое подключение открывает клиент до 2026 года. Второй клиент берёт значение по умолчанию и оказывается на `2026-07-28`. Запустите `python client.py` во втором терминале:
```text
2025-11-25 {'result': "Reserved 2 of 'Dune'."}
@ -62,6 +72,40 @@ SDK маршрутизирует каждый запрос по его заго
отправка пропущенных SSE-событий клиенту, который переподключается к *той же* сессии), а не
хранилище сессий. Сессию доступной из другого процесса он не делает никогда.
## Время жизни и лимиты сессий {#session-lifetime-and-limits}
Сессия старого поколения не живёт вечно, и один процесс не держит их неограниченное количество.
За это отвечают две настройки. Обе — именованные аргументы `run()`, `streamable_http_app()`
и `Server.streamable_http_app()`. У современных подключений (`2026-07-28`) и при `stateless_http=True`
сессий нет, так что ни одна из настроек к ним не относится.
| Настройка | По умолчанию | Что делает | Что видит клиент | Как отключить |
|---|---|---|---|---|
| `session_idle_timeout` | `1800` (30 мин) | Закрывает сессию, в которой столько времени ничего не было в работе. | `404 Session not found`. Придётся заново выполнить `initialize`. | `None` |
| `max_sessions` | `10_000` | Отказывается открывать сессии сверх этого числа. Существующие сессии не трогает и ничего не вытесняет. | `503 Too many open sessions` с кодом JSON-RPC `-32603`. | `None` |
Что считается «в работе»:
* Открытый `GET`-поток. Клиенты SDK держат такой поток открытым, поэтому сессия подключённого
клиента никогда не истекает.
* Запрос, на который ещё готовится ответ. Вызов инструмента, работающий дольше тайм-аута,
не прерывается, а обратный отсчёт начинается только после его завершения.
* Больше ничего. Между запросами часы идут. Любой запрос в сессии запускает их заново,
включая `ping`. Истёкшую сессию уже ничто не оживит.
Клиент, завершающий сессию запросом `DELETE`, освобождает её сразу. То же происходит
с клиентом, чей открывающий запрос был отклонён.
```python
mcp.run(transport="streamable-http", session_idle_timeout=None, max_sessions=50_000)
```
Оба события попадают в лог сервера. Истечение — `Session <id> idle timeout` на уровне `INFO`.
Отказ в открытии — `Refusing to open a new session: <n> sessions are already open` на уровне `WARNING`.
Лимиты действуют на процесс. При четырёх рабочих процессах потолок — четыре раза по `max_sessions`,
и каждый рабочий процесс сам отсчитывает время жизни своих сессий.
## Единственный переключатель: `stateless_http` {#the-one-knob-stateless_http}
Если привязка — цена, которую вы платить не готовы, изменить можно ровно одно.
@ -87,7 +131,7 @@ SDK маршрутизирует каждый запрос по его заго
!!! check
Сделайте заведомо неправильно. `reserve` — тот самый инструмент, который только что обслужил
оба клиента. Разверните его с `stateless_http=True`, подключите те же два клиента по HTTP и
оба клиента. Разверните его с `stateless_http=True`, подключите те же два клиента и
вызовите его из каждого.
Современный клиент по-прежнему получает `Reserved 2 of 'Dune'.` Современная ветка не изменилась.