Перейти к содержанию

Авторизация

Машинный перевод

Эта страница переведена с английской документации автоматически, и основной версией остаётся английская страница. Если что-то читается неправильно, на странице Переводы объясняется, как об этом сообщить.

Через Streamable HTTP MCP-сервер — это обычный веб-сервис, и защищается он так же, как любой веб-сервис: с помощью bearer-токенов OAuth 2.1.

В терминах OAuth ваш сервер — это сервер ресурсов. Он никого не аутентифицирует и не выдаёт токенов. Он делает ровно одно: смотрит на заголовок Authorization каждого запроса и решает, годится ли токен в нём.

Эта страница — о серверной стороне. Клиент, который обнаруживает ваш сервер авторизации и получает токен, описан на странице OAuth-клиенты.

Три стороны

  • Сервер авторизации аутентифицирует пользователей и выдаёт токены доступа. Его вы не пишете. Это ваш провайдер идентификации (Auth0, Keycloak, Entra или ваш собственный).
  • Сервер ресурсов — это ваш MCP-сервер. Он проверяет токен в каждом запросе.
  • Клиент выясняет, какому серверу авторизации вы доверяете, получает у него токен и присылает его вам в виде Authorization: Bearer <token>.

Вот и весь треугольник. Всё на этой странице — про средний пункт.

Верификатор токенов

SDK ничего не предполагает о том, как выглядит действительный токен. Это определяете вы, реализуя TokenVerifier:

server.py
from pydantic import AnyHttpUrl

from mcp.server import MCPServer
from mcp.server.auth.provider import AccessToken, TokenVerifier
from mcp.server.auth.settings import AuthSettings

KNOWN_TOKENS = {
    "alice-token": AccessToken(token="alice-token", client_id="alice", scopes=["notes:read"]),
}


class StaticTokenVerifier(TokenVerifier):
    async def verify_token(self, token: str) -> AccessToken | None:
        return KNOWN_TOKENS.get(token)


mcp = MCPServer(
    "Notes",
    token_verifier=StaticTokenVerifier(),
    auth=AuthSettings(
        issuer_url=AnyHttpUrl("https://auth.example.com"),
        resource_server_url=AnyHttpUrl("http://127.0.0.1:8000/mcp"),
        required_scopes=["notes:read"],
    ),
)


@mcp.tool()
def list_notes() -> list[str]:
    """List every note in the notebook."""
    return ["Buy milk", "Ship the release"]
  • TokenVerifier — это протокол с одним асинхронным методом. verify_token получает сырой токен из заголовка Authorization и возвращает AccessToken, если токен действителен, или None, если нет. Больше реализовывать нечего.
  • Этот верификатор ищет токен в таблице. Настоящий проверяет подпись JWT или обращается к эндпоинту интроспекции токенов на сервере авторизации. Этот код — ваш; SDK его только вызывает.
  • token_verifier= и auth= всегда идут в паре. Передайте один без другого — и MCPServer(...) выбросит ValueError ещё до того, как обслужит хоть один запрос.

AuthSettings — это публичное лицо вашего сервера ресурсов:

  • issuer_url: сервер авторизации, который выдаёт ваши токены.
  • resource_server_url: публичный URL этого MCP-эндпоинта. Он указывает, для какого ресурса предназначен токен, и по нему же размещается документ обнаружения.
  • required_scopes: каждый токен должен содержать их все.

Tip

В examples/servers/simple-auth/ в репозитории SDK есть IntrospectionTokenVerifier, который обращается к эндпоинту RFC 7662 настоящего сервера авторизации. Именно так устроено большинство верификаторов в продакшене.

Что вы получаете через HTTP

Авторизация живёт в HTTP-заголовках, поэтому существует только на HTTP-транспортах. Запускайте её на том транспорте, который развёртываете: mcp.run(transport="streamable-http") поднимает её на http://127.0.0.1:8000/mcp, а остальное — на странице Запуск сервера. Теперь у приложения два маршрута:

/mcp
/.well-known/oauth-protected-resource/mcp

Вы зарегистрировали один инструмент. Второй маршрут добавил SDK.

Обнаружение

Выполните GET по этому well-known-пути — и получите Protected Resource Metadata по RFC 9728, собранные прямо из ваших AuthSettings:

{
  "resource": "http://127.0.0.1:8000/mcp",
  "authorization_servers": ["https://auth.example.com/"],
  "scopes_supported": ["notes:read"],
  "bearer_methods_supported": ["header"]
}

Именно по этому документу клиент, который никогда не слышал о вашем сервере, находит к нему дорогу: читает authorization_servers и идёт туда за токеном. Ничего из этого вы не писали.

Check

Обратитесь к /mcp без токена (или с таким, для которого верификатор вернул None) — и запрос остановят на входе:

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer error="invalid_token", error_description="Authentication required", resource_metadata="http://127.0.0.1:8000/.well-known/oauth-protected-resource/mcp"

{"error": "invalid_token", "error_description": "Authentication required"}

Ничего не разбиралось, ни один инструмент не запускался. А указатель resource_metadata в WWW-Authenticate — это то, что делает обнаружение автоматическим: 401 -> документ метаданных -> сервер авторизации -> токен -> повтор.

Warning

Ничто из этого не защищает stdio. У канала нет заголовка Authorization, поэтому к token_verifier там никогда не обращаются. Граница безопасности stdio-сервера — это процесс, который его запустил. То же относится к Client(mcp) в памяти, который используется в тестах: он подключается напрямую к объекту сервера и минует HTTP-уровень вместе с авторизацией.

Личность вызывающего

Внутри любого обработчика get_access_token() — это AccessToken, который ваш верификатор вернул для текущего запроса:

server.py
from pydantic import AnyHttpUrl

from mcp.server import MCPServer
from mcp.server.auth.middleware.auth_context import get_access_token
from mcp.server.auth.provider import AccessToken, TokenVerifier
from mcp.server.auth.settings import AuthSettings

KNOWN_TOKENS = {
    "alice-token": AccessToken(token="alice-token", client_id="alice", scopes=["notes:read"]),
}


class StaticTokenVerifier(TokenVerifier):
    async def verify_token(self, token: str) -> AccessToken | None:
        return KNOWN_TOKENS.get(token)


mcp = MCPServer(
    "Notes",
    token_verifier=StaticTokenVerifier(),
    auth=AuthSettings(
        issuer_url=AnyHttpUrl("https://auth.example.com"),
        resource_server_url=AnyHttpUrl("http://127.0.0.1:8000/mcp"),
        required_scopes=["notes:read"],
    ),
)


@mcp.tool()
def whoami() -> str:
    """Report which OAuth client is calling."""
    token = get_access_token()
    if token is None:
        return "anonymous"
    return f"{token.client_id} (scopes: {', '.join(token.scopes)})"
  • Это работает в инструментах, ресурсах и промптах, и ничего передавать не нужно: middleware авторизации сохраняет его в контекстной переменной для каждого запроса.
  • Возвращается тот самый объект, который собрал ваш верификатор: client_id, scopes, subject, expires_at и любые дополнительные claims, которые вы прикрепили. Это и есть точка для правил на уровне отдельных инструментов: прочитайте scopes и откажите.
  • Вне аутентифицированного HTTP-запроса функция возвращает None. В памяти и через stdio это всегда None.

Вызовите whoami с Authorization: Bearer alice-token — и модель прочитает:

alice (scopes: notes:read)

Половина, которую SDK не делает

SDK даёт вам половину сервера ресурсов: проверить, объявить, отказать. Он не даёт страницу входа, экран согласия или токен.

Чтобы увидеть все три стороны в действии, запустите examples/servers/simple-auth/ из репозитория SDK (небольшой сервер авторизации и сервер ресурсов, настроенный ровно так, как на этой странице), а затем направьте на него examples/clients/simple-auth-client/ — и пройдите весь путь от обнаружения до токена.

Info

Есть второй аргумент конструктора, auth_server_provider=, который встраивает полноценный сервер авторизации внутрь MCP-сервера. Он появился раньше разделения AS/RS, вокруг которого построена спецификация авторизации MCP. В новых серверах к нему прибегать не следует.

Сервер авторизации также может принять подписанное утверждение от корпоративного провайдера идентификации вместо того, чтобы пользователь проходил через экран согласия, и SDK поддерживает обе стороны этого обмена. Сам грант и клиент, который его предъявляет, описаны на странице Утверждение личности.

Итоги

  • Через 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 по адресу /.well-known/oauth-protected-resource/... и отвечает на неаутентифицированные запросы кодом 401, заголовок WWW-Authenticate которого указывает на них. Это и есть вся история обнаружения.
  • get_access_token() в любом обработчике — это тот, кто вызывает.
  • Авторизация — забота HTTP. stdio и клиент в памяти её никогда не видят.

Клиентская половина (обнаружение вашего сервера авторизации и получение токена за вас) — на странице OAuth-клиенты. А клиент, который утверждает личность вместо того, чтобы запрашивать её у пользователя, — на странице Утверждение личности.