# Авторизация MCP-сервера: OAuth 2.1

Как закрыть удалённый сервер токенами и разграничить права инструментов

> Страница сайта MCP Docs — руководство (https://mcpdocs.ru/svoy-server/avtorizatsiya/). Тип: Руководство. Код проверен: Python 3.13, пакет mcp 2.3.0 — против имитации сервера авторизации. Ревизия спецификации 2026-07-28, сверено 13.10.2026.

Авторизация нужна удалённому серверу с закрытыми данными. В терминах OAuth ваш MCP-сервер — сервер ресурсов: он никого не впускает сам и токенов не выдаёт, а только проверяет токен в каждом запросе. Входом и выдачей токенов занимается сервер авторизации — Keycloak, Auth0, Entra ID или собственный.

_Три участника: клиент получает токен у сервера авторизации и передаёт его MCP-серверу, а тот проверяет токен при каждом запросе._

> По stdio авторизации нет: у канала нет заголовков. Граница безопасности локального сервера — права процесса, который его запустил.

## Как клиент находит, где взять токен

1. Клиент обращается к серверу без токена и получает `401` с заголовком `WWW-Authenticate`, где указан адрес метаданных.
2. По этому адресу лежат метаданные защищённого ресурса (RFC 9728): какой сервер авторизации выдаёт токены и какие нужны права.
3. Клиент проводит пользователя через вход у сервера авторизации и получает токен для этого ресурса.
4. Повторяет запрос с заголовком `Authorization: Bearer <токен>`.

SDK делает шаги 1 и 2 за вас. Требования спецификации к клиентам и серверам авторизации — в справочнике: [«Авторизация в MCP»](https://mcpprotocol.ru/authorization/).

## Код сервера

Токен проверяется у сервера авторизации через интроспекцию (RFC 7662): MCP-сервер отправляет токен и получает ответ, активен ли он, кому и с какими правами выдан.

_auth_server.py_

```python
"""MCP-сервер, закрытый OAuth 2.1: проверка токена через интроспекцию (RFC 7662)."""
import os

import httpx2
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
from mcp.server.mcpserver.exceptions import ToolError

# Публичный адрес этого MCP-сервера: ровно тот, к которому подключаются клиенты
RESOURCE = os.environ.get("MCP_RESOURCE", "http://127.0.0.1:8000/mcp")
ISSUER = os.environ.get("OAUTH_ISSUER", "http://127.0.0.1:9000")
INTROSPECT_URL = f"{ISSUER}/introspect"
# Учётные данные, с которыми MCP-сервер обращается к серверу авторизации
CLIENT_ID = os.environ.get("INTROSPECT_CLIENT_ID", "mcp-server")
CLIENT_SECRET = os.environ.get("INTROSPECT_CLIENT_SECRET", "change-me")


class IntrospectionVerifier(TokenVerifier):
    async def verify_token(self, token: str) -> AccessToken | None:
        async with httpx2.AsyncClient(timeout=10) as http:
            r = await http.post(INTROSPECT_URL, data={"token": token},
                                auth=(CLIENT_ID, CLIENT_SECRET))
        if r.status_code != 200:
            return None
        info = r.json()
        if not info.get("active"):
            return None
        aud = info.get("aud")
        if isinstance(aud, list):  # токен выпущен для нескольких ресурсов
            aud = RESOURCE if RESOURCE in aud else None
        return AccessToken(
            token=token,
            client_id=info.get("client_id", ""),
            scopes=info.get("scope", "").split(),
            expires_at=info.get("exp"),
            resource=aud,
            subject=info.get("sub"),
        )


mcp = MCPServer(
    "Заметки",
    version="1.0.0",
    token_verifier=IntrospectionVerifier(),
    auth=AuthSettings(
        issuer_url=AnyHttpUrl(ISSUER),
        resource_server_url=AnyHttpUrl(RESOURCE),
        required_scopes=["notes:read"],
        validate_token_resource=True,  # отклонять токены, выпущенные для другого сервера
    ),
)


@mcp.tool()
def whoami() -> str:
    """Кто вызывает сервер: пользователь и разрешения токена."""
    token = get_access_token()
    if token is None:
        return "аноним"
    return f"{token.subject} (клиент {token.client_id}, права: {' '.join(token.scopes)})"


@mcp.tool()
def delete_note(note_id: int) -> str:
    """Удалить заметку. Нужно право notes:write."""
    token = get_access_token()
    if token is None or "notes:write" not in token.scopes:
        raise ToolError("Недостаточно прав: нужно право notes:write")
    return f"Заметка {note_id} удалена"


if __name__ == "__main__":
    mcp.run(transport="streamable-http", host="127.0.0.1", port=8000)
```

- `TokenVerifier` — единственное, что нужно реализовать: метод получает токен из заголовка и возвращает `AccessToken` или `None`.
- `token_verifier` и `auth` передаются вместе; иначе конструктор откажется работать.
- `validate_token_resource=True` отклоняет токен, выпущенный для другого сервера, — так ваш сервер не примет токен, украденный у соседнего.
- `get_access_token()` внутри инструмента возвращает того, кто вызывает: по нему проверяются права отдельных инструментов.

## Что видит клиент

Ответы сервера из нашей проверки. Запрос без токена:

_Ответ без токена_

```http
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"
```

Метаданные по адресу из заголовка:

_GET /.well-known/oauth-protected-resource/mcp_

```json
{"resource": "http://127.0.0.1:8000/mcp",
 "authorization_servers": ["http://127.0.0.1:9000/"],
 "scopes_supported": ["notes:read"],
 "bearer_methods_supported": ["header"]}
```

Токен, выпущенный для другого сервера, и неизвестный токен получают тот же `401`. С токеном, где есть только право `notes:read`:

_Результаты вызовов_

```text
whoami       → ivanov (клиент claude, права: notes:read)
delete_note  → ошибка: Недостаточно прав: нужно право notes:write
```

## Перед запуском в работу

- **Только HTTPS.** `RESOURCE` — точный публичный адрес, с которым работают клиенты.
- **Секрет интроспекции** храните в переменных окружения или хранилище секретов, не в коде.
- **Минимальные права.** Отдельные права (`scope`) на чтение и на изменение; инструменты, которые меняют данные, проверяют право в коде.
- **Короткоживущие токены.** Отзыв доступа у сервера авторизации должен действовать быстро.
- **Регистрация клиентов.** Спецификация 2026-07-28 считает устаревшей динамическую регистрацию (DCR) и предлагает документы метаданных клиента (CIMD); проверьте, что ваш сервер авторизации их поддерживает.

> SDK не содержит страницы входа и не выдаёт токены — это задача сервера авторизации. Параметр `auth_server_provider`, встраивающий сервер авторизации внутрь MCP-сервера, документация SDK для новых серверов не рекомендует.

> Готовые серверы — в [каталоге MCP-серверов](https://mcpprotocol.ru/katalog/) справочника. Свой удалённый сервер можно проверить на соответствие ревизии 2026-07-28 [бесплатной проверкой](https://mcpprotocol.ru/mcp-check/).
