Перейти до змісту

OAuth-клієнти

Машинний переклад

Цю сторінку перекладено автоматично з англомовної документації, і основною версією є англомовна сторінка. Якщо щось читається неправильно, на сторінці Переклади пояснено, як про це повідомити.

Деякі MCP-сервери захищені. Надішліть їм запит без токена — і у відповідь прийде 401 Unauthorized.

Отримати токен допомагає OAuthClientProvider. Це взагалі не об'єкт MCP. Це httpx2.Auth — стандартний хук httpx2 на кшталт «зроби щось із кожним запитом». Його під'єднують до httpx2.AsyncClient, передають цей клієнт транспорту Streamable HTTP — і більше про нього не згадують.

Ця сторінка — про клієнтський бік. Як змусити власний сервер вимагати токен — на сторінці Авторизація.

Провайдер

client.py
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: токен доступу, токен оновлення, термін дії, область дії.
  • get_client_info / set_client_info зберігають OAuthClientInformationFull, який сервер авторизації видав, коли провайдер вас зареєстрував, — разом із вашим client_id.

Наведена вище версія в пам'яті працює. Але вона все забуває, щойно процес завершується, тож наступний запуск повторює всю процедуру з початку. Збережіть дані у файл або в системне сховище ключів вашої платформи — і наступний запуск пройде тихо.

Tip

Зберігайте client_info, а не лише токени. Провайдер реєструється динамічно, коли вперше не знаходить збереженого client_info. Викиньте його — і кожен запуск створюватиме нову реєстрацію.

Два обробники

Потоку authorization code людина потрібна рівно один раз: хтось має увійти й натиснути «Дозволити».

  • redirect_handler викликається через await із повністю зібраним URL авторизації. client_id, redirect_uri, state і PKCE challenge уже в ньому. Ваше єдине завдання — відкрити його в браузері. Настільний застосунок викликає webbrowser.open; цей файл просто друкує його.
  • callback_handler очікується наступним. Він чекає, доки користувач повернеться на ваш redirect_uri, і повертає параметри запиту цього перенаправлення як AuthorizationCodeResult.

Справжній клієнт замість виклику input() запускає невеликий локальний HTTP-сервер на redirect 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. Далі справу бере на себе провайдер:

  1. Виявлення. Він читає заголовок WWW-Authenticate, завантажує Protected Resource Metadata сервера з /.well-known/oauth-protected-resource, дізнається, який сервер авторизації захищає цей ресурс, і завантажує метадані того сервера.
  2. Реєстрація. У сховищі порожньо? Він динамічно реєструє вас із вашими OAuthClientMetadata і зберігає результат.
  3. Авторизація. Він генерує пару PKCE і state, будує URL авторизації, очікує ваш redirect_handler, а потім — ваш callback_handler, щоб отримати код.
  4. Обмін. Він міняє код на 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, тільки без людини:

client.py
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 — його підкласи.

Друга половина цього рукостискання — змусити ваш сервер вимагати токен — на сторінці Авторизація.