Туториал

Как добавить OAuth 2.1 на MCP-сервер: от Authorization Server до Access Token для удалённого MCP API

Stateless MCP чинит сессии, а не то, кто может звать ваши инструменты. Когда удалённый MCP висит в открытой сети, аутентификация идёт от открытия Authorization Server до Access Token — не общий секрет, засунутый в JSON-RPC.

MCP 2026-07-28 помечает HTTP-авторизацию как необязательную; как только вы защищаете ресурс, нужно реализовать подмножество OAuth 2.1. Cursor, Claude Desktop, удалённый MCP в OpenAI Responses и свои хосты агентов начинают с запроса без токена. Сервер отвечает 401 и указывает на Protected Resource Metadata в WWW-Authenticate. Клиент затем находит Authorization Server, регистрируется, проходит authorization code + PKCE с параметром resource, получает Access Token и ставит Authorization: Bearer на каждый HTTP-прыжок. Это тот путь удалённого MCP API на практике: кто что делает, как выглядят metadata, как привязать audience, как поднять scope. Спека: MCP Authorization. Уже есть что такое MCP, Stateless MCP и inputSchema и tool calling — здесь только «кому можно звать». Не гоняйте этот браузерный танец на STDIO; берите учётные данные из окружения.

Три роли: MCP-сервер не выдаёт токены

Первая ошибка — считать MCP-сервер тем, кто выдаёт токены. В спецификации защищённый MCP-сервер — это сервер ресурсов OAuth 2.1: он принимает, проверяет и потребляет Access Token, привязанные к своей audience. MCP-клиент — клиент OAuth 2.1: он получает токен от имени владельца ресурса и вызывает инструменты с ним. Authorization Server (AS) отвечает за вход, согласие и выпуск. AS может жить вместе с сервером ресурсов или быть уже существующим IdP (Okta, Keycloak, Auth0, свой OIDC). Спека не учит строить AS; она учит MCP-сервер, как объявить, где он — см. Authorization Server Discovery.

Авторизация для MCP необязательна. HTTP-транспорт, который защищает ресурсы, ДОЛЖЕН следовать этой спеке. STDIO НЕ ДОЛЖЕН. Запихнуть общий API-ключ в params у tools/call или токен в JSON-RPC _meta — это не удалённая MCP-аутентификация 2026-07-28. Аутентификация живёт на HTTP-транспорте, не внутри конверта метода.

На фоне других слоёв: MCP — это открытие и вызов; безсостояние убрало сессию, не авторизацию; inputSchema задаёт форму arguments, и прошедшая Schema не значит, что вызывающему можно. OAuth отвечает: «этот Bearer выдан для этого ресурса и хватает ли scope?» Сам цикл агента: что такое AI-агент.

Роль Шапка OAuth 2.1 Что вы отдаёте
MCP-клиент / хостOAuth-клиентОткрытие, PKCE, хранение токенов, Bearer на каждом прыжке
MCP-серверСервер ресурсов401 + PRM, проверка токенов, привязка audience
Authorization ServerИздатель / IdPВход, согласие, код, Access Token

Найти Authorization Server: 401 и RFC 9728

Лаборатория начинается с неудачного вызова. Клиент бьёт в https://mcp.example.com/mcp с tools/list или любым JSON-RPC без Authorization. Сервер ДОЛЖЕН вернуть HTTP 401 с Bearer и resource_metadata в WWW-Authenticate. Добавьте scope, чтобы клиент знал минимум. Клиенты ДОЛЖНЫ разобрать этот заголовок: брать resource_metadata, если он есть; иначе зондировать well-known по RFC 9728.

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="mcp",
  resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource",
  scope="invoices:read"

MCP-серверы ДОЛЖНЫ реализовать OAuth 2.0 Protected Resource Metadata (RFC 9728). В документе ДОЛЖЕН быть хотя бы один issuer AS в authorization_servers. Можно задать resource (канонический URI этого сервера), scopes_supported и bearer_methods_supported. Клиент затем тянет metadata AS: без пути — /.well-known/oauth-authorization-server, затем OpenID openid-configuration; с путём тенанта — вставлять путь в порядке приоритета спеки. issuer в документе ДОЛЖЕН байт-в-байт совпасть с issuer, из которого собрали URL. Иначе это атака, документ выбросить.

{
  "resource": "https://mcp.example.com/mcp",
  "authorization_servers": [
    "https://auth.example.com"
  ],
  "bearer_methods_supported": ["header"],
  "scopes_supported": [
    "invoices:read",
    "invoices:write"
  ],
  "resource_documentation": "https://mcp.example.com/docs"
}

Выше: 401 и минимальный Protected Resource Metadata. У resource не должно быть fragment и нельзя опускать схему. Слеш в конце держите одинаковым; спека предпочитает без него, если он не несёт смысл. Если AS несколько, каждый — независимый издатель; клиенты ДОЛЖНЫ хранить состояние регистрации раздельно.

Регистрация клиента: CIMD, предрегистрация, DCR

После нахождения AS у клиента ДОЛЖЕН быть client_id до авторизации. Три механизма по приоритету спеки: ① Client ID Metadata Documents (CIMD) — client_id сам по себе HTTPS URL; AS тянет этот JSON и проверяет redirect_uris; ② предрегистрация из консоли (конфиденциальный или публичный); ③ Dynamic Client Registration (RFC 7591) POST /register. DCR устарел и оставлен только для AS без CIMD. Новым проектам — CIMD или предрегистрация.

Если в authorization_servers больше одного, каждый — отдельный AS. Храните client_id, секреты и токены по AS. Никогда не отправляйте учётные данные A на token endpoint B. Это частая дверь mix-up / confused deputy.

Публичные клиенты (десктоп-хосты, расширения) ДОЛЖНЫ делать PKCE по OAuth 2.1. Конфиденциальным тоже стоит. Не изобретайте «положить client_secret в окружение MCP-сервера и проксировать регистрацию на AS». MCP-сервер — сервер ресурсов, не клиент.

Код авторизации + PKCE + resource: получить Access Token

Только потом поток authorization code. До открытия браузера клиент ДОЛЖЕН: сгенерировать PKCE code_verifier / code_challenge; поставить resource (канонический URI MCP-сервера, RFC 8707) и на authorize, и на token; выбрать scope (сначала scope из 401, иначе scopes_supported); записать проверенный issuer AS в ту же запись запроса, что и verifier. После согласия сравнить iss в callback с этой записью простым сравнением строк (RFC 9207) — без сворачивания регистра и обрезки слеша.

Запрос токена несёт code, code_verifier и resource. Успех — JSON: access_token, token_type Bearer, expires_in, иногда refresh_token и scope. Не считайте refresh token обязательным. Не кладите offline_access в scopes_supported MCP-сервера и в scope у 401 — это просьба клиента к AS, не требование ресурса. Ключевые поля metadata AS ниже; если authorization_response_iss_parameter_supported true, callback без iss ДОЛЖЕН быть отвергнут.

{
  "issuer": "https://auth.example.com",
  "authorization_endpoint": "https://auth.example.com/oauth/authorize",
  "token_endpoint": "https://auth.example.com/oauth/token",
  "jwks_uri": "https://auth.example.com/.well-known/jwks.json",
  "registration_endpoint": "https://auth.example.com/oauth/register",
  "code_challenge_methods_supported": ["S256"],
  "grant_types_supported": ["authorization_code", "refresh_token"],
  "response_types_supported": ["code"],
  "token_endpoint_auth_methods_supported": [
    "none",
    "client_secret_basic"
  ],
  "authorization_response_iss_parameter_supported": true,
  "scopes_supported": [
    "invoices:read",
    "invoices:write",
    "offline_access"
  ]
}

Для клиента Access Token часто непрозрачен; при отладке часто JWT. Смотрите iss, aud (или resource), scope, exp, client_id. aud ДОЛЖЕН быть привязан к этому MCP-серверу. Токен для платёжного API не должен гонять tools/call. Payload ниже с верным aud; направьте его на URI другого API — сервер ресурсов ДОЛЖЕН ответить 401.

{
  "iss": "https://auth.example.com",
  "sub": "user_1842",
  "aud": "https://mcp.example.com/mcp",
  "client_id": "https://host.example.com/oauth/client.json",
  "scope": "invoices:read",
  "exp": 1790000000,
  "iat": 1789996400
}

Вызвать удалённый MCP API с Bearer и проверить audience

После токена каждый HTTP-прыжок Client → MCP-сервер ДОЛЖЕН нести Authorization: Bearer: открытие, tools/list, tools/call, resources/read. Токены НЕ ДОЛЖНЫ попадать в query. Имена методов JSON-RPC и arguments остаются в теле — заголовок auth и конверт это два слоя. У Stateless MCP нет сессии «вошёл, дальше не проверять»; каждый запрос аутентифицирует себя. См. Stateless MCP.

POST /mcp HTTP/1.1
Host: mcp.example.com
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Type: application/json
Accept: application/json, text/event-stream

{
  "jsonrpc": "2.0",
  "id": 42,
  "method": "tools/call",
  "params": {
    "name": "searchInvoices",
    "arguments": {
      "startDate": "2026-01-01",
      "endDate": "2026-01-31",
      "status": "paid"
    }
  }
}

Сервер проверяет по OAuth 2.1 §5.2: подпись или introspection, exp, iss и audience по RFC 8707. Ошибки — 401. После audience всё ещё убедитесь, что токен выдал этот AS. Серверы НЕ ДОЛЖНЫ принимать или транзитом пропускать чужие токены. Клиенты НЕ ДОЛЖНЫ слать токен другого ресурса на этот MCP-сервер. Это правило confused deputy.

Валидный токен не делает arguments валидными. Bearer отвечает «кто, какой ресурс, какие scope». Даты и перечисления searchInvoices по-прежнему требуют parse + Schema + бизнес-правила. Не делайте «токен ок» единственной дверью tools/call. Контракт формы: MCP и JSON Schema.

Scope, 403 и отладка в JSONVue

Когда токен хорош, а scope нет, сервер ДОЛЖЕН вернуть 403 с error="insufficient_scope", scope для этой операции и тем же resource_metadata в WWW-Authenticate. Все scope этой операции — в одном вызове; не капайте по одному. На step-up клиент берёт объединение старых scope и вызова, чтобы не потерять права других инструментов.

HTTP Смысл Что дальше
401Нет аутентификации или токен неверен / истёк / неверная audienceПрочитать PRM; заново авторизовать или обновить
403 + insufficient_scopeТокен верный, прав малоStep-up: слить scope и авторизовать снова
400Кривой запрос авторизацииПочинить запрос клиента; не повторять инструмент
HTTP/1.1 403 Forbidden
WWW-Authenticate: Bearer error="insufficient_scope",
  scope="invoices:write",
  resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource",
  error_description="Write scope required to create a credit note"

Лаборатория даёт минимум четыре JSON: Protected Resource Metadata, metadata AS, ответ токена и (если JWT) payload — плюс params.arguments у tools/call. У них разные работы; не мешайте в одну «универсальную» Schema. Держите по одному падающему фикстуру: PRM без authorization_servers, metadata AS с чужим issuer, JWT с неверным aud, arguments без поля.

Это можно в браузере: форматтер JSON — проверить, парсятся ли metadata; Декодер JWT — для aud / scope / exp; Валидатор JSON Schema — против контракта, который вы написали для PRM; JSON Diff — сравнить payload до и после refresh или arguments модели с params MCP. Ничего не уходит с машины.

Дальше: Что такое MCP, Stateless MCP, MCP и JSON Schema, A2A vs MCP, Что такое AI-агент.

FAQ

Нужен ли OAuth 2.1 локальному STDIO MCP?

Нет. Спека говорит не гонять этот браузерный поток на STDIO; берите учётные данные из окружения. OAuth 2.1 — для HTTP удалённого MCP. Те же инструменты могут отдать STDIO и Streamable HTTP; 401, PRM и Bearer нужны только HTTP-стороне.

Можно ли положить Access Token в JSON-RPC params или _meta?

Не как соответствующую реализацию. Access Token ДОЛЖЕН ехать в HTTP-заголовке Authorization и НЕ ДОЛЖЕН появляться в query. _meta конверта — про версию протокола, не про вход. Токен в теле попадёт в логи, прокси и контекст модели.

Если я сам выпускаю JWT, а сервер проверяет общий секрет — это MCP OAuth?

Проверки подписи мало. Клиент открывает Authorization Server, не «ключ, запечённый в сервер». Нужны metadata RFC 9728, authorization_servers, resource на authorize и token, привязка audience и вызовы 401 / 403. Свой AS может выпускать JWT; открытие и параметр resource не опциональны.

Наш API-шлюз уже проверяет JWT. Это всё ещё нужно?

Шлюз может проверить подпись и срок. Хосты MCP всё равно должны найти AS, послать resource и разобрать WWW-Authenticate. Проверять «любой валидный JWT» без привязки audience пускает токены других API. Охранять MCP как обычный REST — и хосты часто не заканчивают рукопожатие.

Итог и следующие шаги

OAuth 2.1 на удалённом MCP — это не «добавить страницу входа». Это транспортный контракт: открытие 401, metadata AS, регистрация клиента, PKCE, resource, Access Token, Bearer на каждом прыжке. MCP-сервер — сервер ресурсов. Authorization Server выдаёт токены.

Отгружайте в таком порядке: сначала 401 + Protected Resource Metadata, которые существующий MCP-клиент умеет разобрать, затем подключите AS, затем бизнес tools/call. Metadata и JWT-фикстуры держите в JSONVue отдельно от проверки Schema. Вход в протокол: что такое MCP. Безсостояние транспорта: Stateless MCP.