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

Авторизация

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

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

При работе через 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

RESOURCE = "http://127.0.0.1:8000/mcp"

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


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(RESOURCE),
        required_scopes=["notes:read"],
        validate_token_resource=True,
    ),
)


@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 или вызывает эндпоинт интроспекции токенов сервера авторизации и сообщает, для кого выдан токен (его aud), в AccessToken.resource. Этот код пишете вы; SDK его только вызывает.
  • token_verifier= и auth= всегда идут в паре. Передайте один без другого — и MCPServer(...) выбросит ValueError ещё до того, как обслужит хоть один запрос.

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

  • issuer_url: сервер авторизации, который выдаёт ваши токены.
  • 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

В репозитории SDK, в examples/servers/simple-auth/, есть 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

RESOURCE = "http://127.0.0.1:8000/mcp"

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


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(RESOURCE),
        required_scopes=["notes:read"],
        validate_token_resource=True,
    ),
)


@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, которые вы прикрепили. Это и есть точка для правил на уровне инструмента: прочитайте области действия и откажите.
  • Вне аутентифицированного 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-клиенты. А клиент, который утверждает идентичность, вместо того чтобы спрашивать её у пользователя, — на странице Утверждение идентичности.