У MCP-сервера есть два способа жить. Локальный запускается прямо на машине пользователя, общается через stdio и берёт ключи из переменных окружения — там вопрос доступа решён самим фактом, что процесс запустил владелец компьютера. Remote MCP живёт по HTTPS-адресу, к нему может обратиться кто угодно из интернета, и вот тут одного API-ключа в конфиге уже мало: нужен нормальный вход с согласием пользователя, разграничением прав и отзывом доступа. Спецификация MCP отвечает на это OAuth 2.1. Разбираем, что именно придётся реализовать, если вы поднимаете такой сервер своими руками.
Почему именно OAuth, а не токен в заголовке
Соблазн понятный: сгенерировать длинную случайную строку, положить её в конфиг клиента и проверять на входе. Для сервиса на одного человека это сработает. Проблемы начинаются, когда пользователей становится больше.
- Нет согласия пользователя. Статический токен даёт всё и сразу — вы не можете показать экран «приложение просит доступ к чтению файлов».
- Нет отзыва. Утёк токен — меняете его вручную у всех клиентов.
- Нет разграничения. Один токен не отличает «прочитать заказы» от «удалить заказы».
OAuth закрывает всё это готовыми механизмами, а главное — клиенты MCP уже умеют его проходить. Реализуете спецификацию — и подключение выглядит для пользователя как обычный вход через браузер, без ручного копирования ключей.
Кто есть кто в этой схеме
В OAuth три роли, и путаница в них — источник половины ошибок при интеграции.
- MCP-клиент — приложение, в котором работает модель. Это OAuth-клиент: он ходит в браузер, получает токен и подставляет его в запросы.
- MCP-сервер — ваш сервис с инструментами. Это сервер ресурсов (resource server): он только проверяет присланный токен и отдаёт данные. Он не выдаёт токены и не показывает форму входа.
- Авторизационный сервер (authorization server, AS) — тот, кто общается с пользователем и выпускает токены. Это может быть внешний провайдер (Auth0, Keycloak, Okta, WorkOS) или ваш собственный сервис.
Ключевой вывод: писать «свой OAuth» в большинстве случаев не нужно. Ваша задача — корректно объявить, к какому авторизационному серверу идти, и правильно проверить пришедший токен.
Как проходит подключение: полный маршрут
Спецификация описывает последовательность, которую клиент проходит сам — участие пользователя нужно только на экране согласия.
1. Клиент стучится без токена и получает 401
Первый запрос к вашему /mcp прилетает пустым. Сервер обязан ответить 401 Unauthorized и заголовком WWW-Authenticate, в котором лежит адрес документа с метаданными ресурса:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource",
scope="files:read"
Параметр scope необязателен, но полезен: он подсказывает клиенту, какие именно права запрашивать, чтобы тот не просил лишнего.
2. Клиент читает метаданные защищённого ресурса
Это документ по стандарту RFC 9728. Минимально в нём должно быть поле authorization_servers — список тех, кому вы доверяете выпуск токенов:
{
"resource": "https://mcp.example.com/mcp",
"authorization_servers": ["https://auth.example.com"],
"scopes_supported": ["files:read", "files:write"],
"bearer_methods_supported": ["header"]
}
Отдавать его можно по корневому пути /.well-known/oauth-protected-resource либо с добавлением пути сервера — /.well-known/oauth-protected-resource/mcp. Клиент обязан уметь оба варианта, но сначала смотрит на адрес из WWW-Authenticate.
3. Клиент находит эндпоинты авторизационного сервера
Взяв issuer из предыдущего шага, клиент перебирает well-known адреса по порядку: сначала /.well-known/oauth-authorization-server, затем /.well-known/openid-configuration. Если у issuer есть путь (мультитенантные провайдеры), он вставляется в середину: https://auth.example.com/.well-known/oauth-authorization-server/tenant1. Получив документ, клиент обязан сверить поле issuer внутри с адресом, по которому запрашивал, — иначе подменный сервер выдаст себя за чужой.
4. Клиент получает client_id
Тут в редакции спецификации от 28 июля 2026 года произошло главное изменение за год — о нём отдельный раздел ниже.
5. Авторизация в браузере с PKCE и resource
Клиент открывает браузер, пользователь входит и подтверждает доступ. В запросе обязаны быть два параметра: code_challenge (механизм PKCE, защищает код авторизации от перехвата) и resource — канонический URI вашего сервера по RFC 8707. Именно resource заставляет авторизационный сервер выпустить токен, привязанный к вашему адресату, а не «универсальный». Канонический вид — со схемой, без фрагмента и, по возможности, без завершающего слэша: https://mcp.example.com/mcp.
6. Обмен кода на токен и работа
Клиент отдаёт код вместе с code_verifier и тем же resource, получает access-токен и дальше подставляет его в каждый HTTP-запрос: Authorization: Bearer <token>. Именно в каждый — сессии, которая «запоминала» бы авторизацию, в протоколе нет. Токен в query-строке запрещён.
Что обязан делать ваш сервер: короткий список
Если убрать всё, что делает клиент и провайдер, ваша часть сводится к трём пунктам:
- Отдавать документ метаданных защищённого ресурса с полем
authorization_servers. - Отвечать
401с заголовкомWWW-Authenticate, когда токена нет или он невалиден, и403сerror="insufficient_scope", когда прав не хватает. - Проверять каждый входящий токен: подпись, срок, издателя и — обязательно — аудиторию: что токен выпущен именно для вашего сервера.
На практике это десяток строк, потому что фреймворки уже умеют. В FastMCP, например, достаточно описать проверяльщик токенов и адрес провайдера:
from fastmcp import FastMCP
from fastmcp.server.auth import RemoteAuthProvider
from fastmcp.server.auth.providers.jwt import JWTVerifier
from pydantic import AnyHttpUrl
verifier = JWTVerifier(
jwks_uri="https://auth.example.com/.well-known/jwks.json",
issuer="https://auth.example.com",
audience="https://mcp.example.com/mcp",
)
auth = RemoteAuthProvider(
token_verifier=verifier,
authorization_servers=[AnyHttpUrl("https://auth.example.com")],
base_url="https://mcp.example.com",
)
mcp = FastMCP(name="My API", auth=auth)
Эндпоинт /.well-known/oauth-protected-resource фреймворк поднимет сам; аналогичные обёртки есть в официальных SDK. Если сервер вы ещё не написали, начните с базового гайда — как создать свой MCP-сервер, а авторизацию добавьте поверх.
CIMD: динамическая регистрация уходит в прошлое
Раньше клиент, впервые встретив незнакомый авторизационный сервер, регистрировался на лету — Dynamic Client Registration (DCR, RFC 7591): POST /register и получай client_id. Механизм рабочий, но провайдеры его не любят: базу можно засорить тысячами анонимных клиентов.
В редакции 2026-07-28 DCR помечен как устаревший, а основным способом стал Client ID Metadata Documents (CIMD). Идея простая до изящества: client_id — это просто HTTPS-ссылка на JSON-файл, который клиент публикует у себя.
{
"client_id": "https://app.example.com/oauth/client-metadata.json",
"client_name": "Example MCP Client",
"redirect_uris": [
"http://127.0.0.1:3000/callback",
"http://localhost:3000/callback"
],
"grant_types": ["authorization_code"],
"response_types": ["code"],
"token_endpoint_auth_method": "none"
}
Увидев client_id в виде URL, авторизационный сервер скачивает документ, проверяет, что поле client_id внутри совпадает с адресом, сверяет redirect_uri со списком и показывает client_name на экране согласия. Запись в базе создавать не нужно, а идентификатор переносим между провайдерами.
Поддержку CIMD провайдер объявляет флагом client_id_metadata_document_supported: true в своих метаданных. Порядок выбора для клиента такой: заранее выданные креды → CIMD → DCR как запасной вариант → ручной ввод. Устаревшее по политике MCP живёт в спецификации минимум год, так что DCR ещё какое-то время будет работать — но закладываться на него в новом коде не стоит.
Три вещи, на которых чаще всего спотыкаются
Не проверяют аудиторию токена. Самая опасная ошибка. Если сервер принимает любой валидно подписанный JWT от вашего провайдера, то токен, выданный для совсем другого сервиса, откроет доступ к вашим инструментам. Проверка aud обязательна.
Пробрасывают чужой токен дальше. Спецификация прямо запрещает передавать полученный от клиента токен в вышестоящее API. Если ваш MCP-сервер ходит, скажем, в CRM, он должен быть для неё отдельным OAuth-клиентом со своим токеном. Иначе получается классическая «путаница заместителя» (confused deputy), когда нижестоящий сервис принимает чужие права за ваши.
Возвращают 401 без заголовка. Голый 401 без WWW-Authenticate и без well-known документа — тупик: клиент не понимает, куда идти за токеном, и просто показывает ошибку подключения. Это, пожалуй, самый частый симптом «сервер не подключается» в трекерах. Как локализовать такие вещи, разбирали в заметке про отладку MCP-сервера.
Что проверить перед выкладкой
- Все эндпоинты авторизации — только по HTTPS; redirect URI — HTTPS или
localhost. - Access-токены короткоживущие; refresh-токены для публичных клиентов ротируются.
- Скоупы минимальны: в
scopes_supported— базовый набор, остальное клиент дозапрашивает через step-up при403 insufficient_scope. - Токены не попадают в логи и не уходят в query-строку.
- Права инструментов ограничены на уровне самого сервера, а не только на уровне скоупов, — общий чек-лист есть в заметке о безопасности MCP-серверов.
Вывод
Remote MCP по OAuth — это не «написать свой сервер авторизации». Это аккуратно занять место сервера ресурсов: объявить метаданные, честно отдавать 401 и 403 и придирчиво проверять токен, включая аудиторию. Тяжёлую часть — экраны входа, согласие, ротацию токенов — берёт на себя провайдер, обнаружение и PKCE делает клиент. Если вы выносите локальный сервер наружу, безопасный порядок такой: сначала внешний авторизационный сервер и проверка токенов, потом CIMD, и только затем доступ шире, чем себе. Что такое MCP вообще — в базовой заметке «Что такое MCP простыми словами».
Частые вопросы
Почти никогда. По спецификации ваш MCP-сервер — это сервер ресурсов: он только объявляет, кому доверяет выпуск токенов, и проверяет входящие токены. Выпуск токенов, экраны входа и согласие берёт на себя внешний провайдер: Auth0, Keycloak, Okta, WorkOS или любой другой OAuth 2.1-совместимый.
Три вещи. Документ метаданных защищённого ресурса по RFC 9728 (с полем authorization_servers) — по адресу /.well-known/oauth-protected-resource. Ответ 401 с заголовком WWW-Authenticate, где указан адрес этого документа. И ответ 403 с error="insufficient_scope", когда токен валиден, но прав не хватает.
Он реализует Resource Indicators (RFC 8707) и заставляет авторизационный сервер выпустить токен, привязанный именно к вашему серверу. Без такой привязки токен, выданный для другого сервиса того же провайдера, может открыть доступ к вашим инструментам. Сервер обязан проверять аудиторию токена, а клиент — всегда передавать resource.
Client ID Metadata Documents — способ, при котором client_id это HTTPS-ссылка на JSON-файл с метаданными клиента, который авторизационный сервер скачивает на лету. В редакции спецификации 2026-07-28 CIMD стал основным механизмом, а Dynamic Client Registration (RFC 7591) помечен как устаревший и оставлен для обратной совместимости.
Источники
- 1.Model Context Protocol — Authorization (спецификация 2026-07-28)https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization
- 2.MCP — Client Registration: CIMD, пре-регистрация и DCRhttps://modelcontextprotocol.io/specification/2026-07-28/basic/authorization/client-registration
- 3.MCP — Authorization Server Discoveryhttps://modelcontextprotocol.io/specification/2026-07-28/basic/authorization/authorization-server-discovery
- 4.The 2026-07-28 Specification — блог Model Context Protocolhttps://blog.modelcontextprotocol.io/posts/2026-07-28/
- 5.RFC 9728 — OAuth 2.0 Protected Resource Metadatahttps://datatracker.ietf.org/doc/html/rfc9728
- 6.RFC 8707 — Resource Indicators for OAuth 2.0https://www.rfc-editor.org/rfc/rfc8707.html
- 7.FastMCP — Remote OAuthhttps://fastmcp.wiki/en/servers/auth/remote-oauth



