OAuth-клиенты
Машинный перевод
Эта страница переведена с английской документации автоматически, и основной версией остаётся английская страница. Если что-то читается неправильно, на странице Переводы объясняется, как об этом сообщить.
Некоторые MCP-серверы защищены. Отправьте им запрос без токена — и в ответ придёт 401 Unauthorized.
OAuthClientProvider — это способ получить токен. Это вовсе не объект MCP. Это httpx2.Auth, стандартный хук httpx2 для задачи «сделать что-то с каждым запросом». Его подключают к httpx2.AsyncClient, передают этот клиент транспорту Streamable HTTP — и больше о нём не думают.
Эта страница — о клиентской стороне. Как заставить собственный сервер требовать токен, описано на странице Авторизация.
Провайдер
from urllib.parse import parse_qs, urlparse
import httpx2
from pydantic import AnyUrl
from mcp import Client
from mcp.client.auth import AuthorizationCodeResult, OAuthClientProvider
from mcp.client.streamable_http import streamable_http_client
from mcp.shared.auth import OAuthClientInformationFull, OAuthClientMetadata, OAuthToken
class InMemoryTokenStorage:
def __init__(self) -> None:
self.tokens: OAuthToken | None = None
self.client_info: OAuthClientInformationFull | None = None
async def get_tokens(self) -> OAuthToken | None:
return self.tokens
async def set_tokens(self, tokens: OAuthToken) -> None:
self.tokens = tokens
async def get_client_info(self) -> OAuthClientInformationFull | None:
return self.client_info
async def set_client_info(self, client_info: OAuthClientInformationFull) -> None:
self.client_info = client_info
async def open_browser(authorization_url: str) -> None:
print(f"Visit: {authorization_url}")
async def wait_for_callback() -> AuthorizationCodeResult:
redirect_url = input("Paste the URL you were redirected to: ")
params = parse_qs(urlparse(redirect_url).query)
return AuthorizationCodeResult(
code=params["code"][0],
state=params["state"][0],
iss=params["iss"][0] if "iss" in params else None,
)
oauth = OAuthClientProvider(
server_url="http://localhost:8001/mcp",
client_metadata=OAuthClientMetadata(
client_name="Bookshop Agent",
redirect_uris=[AnyUrl("http://localhost:3030/callback")],
scope="user",
),
storage=InMemoryTokenStorage(),
redirect_handler=open_browser,
callback_handler=wait_for_callback,
)
async def main() -> None:
async with httpx2.AsyncClient(auth=oauth, follow_redirects=True) as http_client:
transport = streamable_http_client("http://localhost:8001/mcp", http_client=http_client)
async with Client(transport) as client:
result = await client.list_tools()
print([tool.name for tool in result.tools])
Ему передают четыре вещи:
server_url: конечная точка MCP, к которой вы подключаетесь. Всё остальное провайдер выясняет по ней сам.client_metadata: то, что вы ввели бы в форму «зарегистрировать приложение» на сервере авторизации.storage: место, где токены хранятся между запусками.redirect_handlerиcallback_handler: два момента, когда участвует человек.
Больше нигде в файле OAuth не упоминается. main() токена не видит вовсе.
Метаданные клиента
OAuthClientMetadata — это настоящий регистрационный документ из RFC 7591, оформленный как модель Pydantic.
Вы задаёте три поля. Остальное заполняют значения по умолчанию: grant_types уже равно ["authorization_code", "refresh_token"], а response_types — ["code"], и это ровно тот сценарий, который выполняет провайдер.
Check
Поскольку это модель Pydantic, она проверяется ещё до того, как хоть один байт уйдёт в сеть.
Опустите redirect_uris — и создание объекта тут же завершится ошибкой ValidationError,
в которой названо поле:
redirect_uris
Field required [type=missing, input_value={'client_name': 'Bookshop Agent'}, input_type=dict]
Браузер не открылся, и на сервере авторизации не осталось недоделанной регистрации.
Хранилище токенов
TokenStorage — это Protocol с четырьмя асинхронными методами. Наследоваться ни от чего не нужно: напишите эти методы — и любой класс станет хранилищем токенов:
get_tokens/set_tokensхранятOAuthToken: токен доступа, токен обновления, срок действия, область доступа (scope).get_client_info/set_client_infoхранятOAuthClientInformationFull, который сервер авторизации выдал, когда провайдер вас зарегистрировал, — включая вашclient_id.
Версия в памяти из примера выше работает. Но при завершении процесса она всё забывает, так что при следующем запуске вся процедура повторяется с начала. Сохраняйте данные в файл или в связку ключей вашей платформы — и следующий запуск пройдёт тихо.
Tip
Сохраняйте client_info, а не только токены. Провайдер проходит динамическую регистрацию
в первый раз, когда не находит сохранённого client_info. Выбросьте его — и при каждом
запуске будет создаваться новая регистрация.
Два обработчика
Сценарию с кодом авторизации человек нужен ровно один раз: кто-то должен войти в систему и нажать «разрешить».
redirect_handlerасинхронно вызывается с полностью собранным URL авторизации.client_id,redirect_uri,stateи PKCE challenge в нём уже есть. Ваша единственная задача — открыть его в браузере. Настольное приложение вызываетwebbrowser.open; в этом файле URL просто печатается.callback_handlerвызывается следующим. Он ждёт, пока пользователь вернётся на вашredirect_uri, и возвращает параметры запроса из этого перенаправления в видеAuthorizationCodeResult.
Настоящий клиент вместо вызова input() поднимает небольшой локальный HTTP-сервер на URI перенаправления. Схема та же: принять перенаправление, вернуть code, state и iss.
Warning
Передавайте state и iss ровно в том виде, в каком они пришли. Провайдер сравнивает
state со значением, которое сгенерировал сам, а iss — с издателем, которого обнаружил,
и при несовпадении отказывает. Это защита от CSRF и от подмены сервера (mix-up).
Подключение к Client
Посмотрите на main(). Провайдер подключается к клиенту httpx2, клиент httpx2 передаётся в streamable_http_client(url, http_client=...), а этот транспорт — в Client.
У streamable_http_client нет именованного параметра auth=. Всё, что относится к уровню HTTP (аутентификация, заголовки, тайм-ауты, прокси), настраивается на httpx2.AsyncClient, который вы приносите сами. Об этом разделении уровней — на странице Транспорты клиента.
Что провайдер делает за вас
Когда Client отправляет первый запрос, сервер отвечает 401. Дальше действует провайдер:
- Обнаружение. Он читает заголовок
WWW-Authenticate, загружает метаданные защищённого ресурса (Protected Resource Metadata) сервера с/.well-known/oauth-protected-resource, узнаёт, какой сервер авторизации защищает этот ресурс, и загружает метаданные уже того сервера. - Регистрация. В хранилище пусто? Он динамически регистрирует вас с вашим
OAuthClientMetadataи сохраняет результат. - Авторизация. Он генерирует пару PKCE и
state, собирает URL авторизации, ждёт вашredirect_handler, а затем ждёт отcallback_handlerкод. - Обмен. Он обменивает код на
OAuthToken, сохраняет его и повторяет исходный запрос уже сAuthorization: Bearer ....
После этого он работает незаметно. Токены берутся из хранилища, истёкший токен доступа обновляется с помощью токена обновления, и только когда ничего из этого не срабатывает, сценарий запускается заново.
Ничего из этого вы не писали. Остаются два именованных аргумента (client_metadata_url и validate_resource_url), и этому файлу не нужен ни один из них. О client_metadata_url стоит знать — ему посвящён отдельный раздел ниже.
Попробуйте сами
Большинство примеров в этой документации можно проверить с Client(server) в памяти. Этот — нет: весь смысл сценария в HTTP-ответе 401, а между клиентом в памяти и его сервером никакого HTTP нет.
В репозитории есть живая версия. examples/servers/simple-auth/ запускает отдельный сервер авторизации и защищённый MCP-сервер; examples/clients/simple-auth-client/ — это клиент с этой страницы, выросший в небольшой CLI. В его README — две команды: запустите серверы, запустите клиент, подключив его к ним, — и наблюдайте, как проходят все четыре шага.
Client ID Metadata Documents
Ревизия спецификации 2026-07-28 объявляет динамическую регистрацию клиентов устаревшей в пользу Client ID Metadata Documents (CIMD). Вместо того чтобы отправлять POST-запросом новую регистрацию каждому встреченному серверу авторизации, клиент публикует один JSON-документ о себе по стабильному HTTPS URL — и этот URL и есть его client_id. Документ загружает сервер авторизации; провайдер его вообще не трогает.
SDK это уже умеет: передайте URL как client_metadata_url= при создании провайдера. Если метаданные сервера авторизации объявляют client_id_metadata_document_supported: true, провайдер полностью пропускает запрос /register: URL идёт в сценарий как client_id, а client_secret нет вовсе. Если сервер этого не объявляет (большинство пока не объявляет) или URL не передан, провайдер молча откатывается к динамической регистрации, и всё описанное выше работает ровно так, как описано. Сохранённый client_info по-прежнему имеет приоритет над обоими вариантами.
URL должен быть HTTPS и с некорневым путём; всё остальное — ValueError при создании, до любого обращения к сети. Поставляемый пример examples/clients/simple-auth-client/ принимает его в переменной окружения MCP_CLIENT_METADATA_URL.
Межмашинное взаимодействие
Ночное задание, шаг CI, другой сервис. Браузера нет, и нажать «разрешить» некому. Это грант client credentials: client_id и client_secret у вас уже есть, а весь сценарий сводится к конечной точке токенов.
ClientCredentialsOAuthProvider — тот же httpx2.Auth, только без человека:
import httpx2
from mcp import Client
from mcp.client.auth.extensions.client_credentials import ClientCredentialsOAuthProvider
from mcp.client.streamable_http import streamable_http_client
from mcp.shared.auth import OAuthClientInformationFull, OAuthToken
class InMemoryTokenStorage:
def __init__(self) -> None:
self.tokens: OAuthToken | None = None
self.client_info: OAuthClientInformationFull | None = None
async def get_tokens(self) -> OAuthToken | None:
return self.tokens
async def set_tokens(self, tokens: OAuthToken) -> None:
self.tokens = tokens
async def get_client_info(self) -> OAuthClientInformationFull | None:
return self.client_info
async def set_client_info(self, client_info: OAuthClientInformationFull) -> None:
self.client_info = client_info
oauth = ClientCredentialsOAuthProvider(
server_url="http://localhost:8001/mcp",
storage=InMemoryTokenStorage(),
client_id="reporting-agent",
client_secret="...",
scope="user",
)
async def main() -> None:
async with httpx2.AsyncClient(auth=oauth, follow_redirects=True) as http_client:
transport = streamable_http_client("http://localhost:8001/mcp", http_client=http_client)
async with Client(transport) as client:
result = await client.list_tools()
print([tool.name for tool in result.tools])
Что изменилось:
- Нет
OAuthClientMetadata, нет обработчиков. Вы передаётеclient_idиclient_secret; провайдер строит вокруг них минимальную регистрациюclient_credentialsи полностью пропускает динамическую регистрацию. scope— строка с разделением пробелами, формат OAuth для передачи по сети.- Всё дальше по цепочке идентично: тот же
TokenStorage, тот жеhttpx2.AsyncClient(auth=...), тот жеstreamable_http_client.
По умолчанию секрет передаётся в запросе токена через HTTP Basic auth (client_secret_basic). Передайте token_endpoint_auth_method="client_secret_post", чтобы вместо этого поместить его в тело формы. Некоторые серверы авторизации принимают только один из двух способов.
Tip
Читайте client_secret из окружения или менеджера секретов, никогда не из системы контроля версий.
Info
Ещё один провайдер находится в mcp.client.auth.extensions.client_credentials:
PrivateKeyJWTOAuthProvider — для клиентов, которые аутентифицируются с помощью JWT
вместо общего секрета (private_key_jwt, вариант с парой ключей и workload identity).
Схема та же: создайте экземпляр и передайте его в auth=. В том же модуле есть
SignedJWTParameters и static_assertion_provider — два вспомогательных средства,
которые собирают для него утверждение (assertion).
Есть ещё одна ситуация без человека: клиент принадлежит организации, чей провайдер удостоверений, а не пользователь, решает, к каким MCP-серверам он может обращаться. Это другой грант со своей моделью доверия и своей страницей — Подтверждение идентичности.
Когда возникает ошибка
Когда сценарий OAuth идёт не так, провайдер выбрасывает OAuthFlowError из mcp.client.auth. У него два подкласса. OAuthRegistrationError означает, что регистрация не дала пригодного клиента: сервер авторизации отказал в регистрации или всё же зарегистрировал вас, но с учётными данными, которые этот сценарий использовать не может (например, с методом аутентификации, который он не реализует). OAuthTokenError означает, что получить токен не удалось: конечная точка токенов ответила отказом, или в сохранённой записи клиента указан метод аутентификации, который этот клиент применить не может, — об этом сообщается при сборке запроса токена, а не после его отправки. Один except OAuthFlowError: охватывает обнаружение, регистрацию, авторизацию и обмен.
Не всё — ошибка сценария. Сеть по-прежнему может подвести; это обычные исключения httpx2, и они проходят насквозь без изменений.
Итоги
OAuthClientProvider— этоhttpx2.Auth. Подключите его кhttpx2.AsyncClient, передайте тот вstreamable_http_client(url, http_client=...)— иClientтак и не узнает, что был OAuth.- От вас нужны четыре вещи: URL сервера,
OAuthClientMetadata,TokenStorageи пара обработчиков redirect/callback. TokenStorage— этоProtocol: четыре асинхронных метода, без базового класса. Сохраняйтеclient_infoнаряду с токенами.- Обнаружение, регистрация (динамическая или через Client ID Metadata Document), PKCE, проверки
stateиissи обновление токенов — забота провайдера, а не ваша. ClientCredentialsOAuthProvider— версия без человека:client_id+client_secret, без обработчиков, без браузера.- Любой сбой OAuth — это
OAuthFlowError;OAuthRegistrationErrorиOAuthTokenError— его подклассы.
Вторая половина этого рукопожатия — как заставить ваш сервер требовать токен — на странице Авторизация.