---
title: "Защита MCP-сервера с помощью OAuth | FoxAuth"
description: "Сделайте MCP-сервер защищённым ресурсом OAuth 2.1 за десять минут: объявите его здесь, опубликуйте метаданные, проверьте токен и убедитесь в обоих исходах."
canonical: "https://foxauth.dev/ru/docs/get-started/protect-your-mcp-server/"
lastmod: "2026-10-08T20:16:36+03:00"
---
Защита MCP-сервера с помощью OAuth

MCP-сервер, который может вызвать кто угодно, — это набор инструментов без входной двери.
Спецификация авторизации MCP закрывает эту дыру, делая MCP-сервер защищённым ресурсом OAuth 2.1:
хост агента, обратившийся к нему без токена, получает ответ, где этот токен взять, идёт за ним и
возвращается с токеном, выпущенным для этого MCP-сервера и ни для чего больше.

Эта страница охватывает всю работу целиком. Вы объявите свой MCP-сервер здесь, опубликуете с него две
вещи, проверите один токен, а затем увидите, как он отклоняет анонимный вызов и принимает
авторизованный. Это около десяти минут, если у вас уже есть запущенный экземпляр. Если нет,
запуск через Docker Compose добавит к ним ещё примерно две.

Что нужно заранее

Запущенный экземпляр с учётной записью администратора (Создание первого
администратора) и проект с бакетом
пользователей (это описано на странице Регистрация первого
клиента). Ваш MCP-сервер может быть
заглушкой: пока ему не нужно ничего делать, кроме как отвечать по HTTP.

1. Объявите MCP-сервер как ресурсЗаголовок раздела «1. Объявите MCP-сервер как ресурс»

Объявляйте его в проекте, бакет пользователей которого имеет собственный адрес — путь вроде /acme
или имя хоста. Такой бакет — отдельный издатель (issuer), а идентификатор ресурса уникален только в
пределах своего issuer, поэтому при объявлении ничего с вашего MCP-сервера не загружается и не
проверяется. Он может находиться во внутренней сети, на localhost, пока вы разрабатываете, или быть
ещё не развёрнутым. Другой тенант с собственным бакетом может объявить тот же URL, никак не затронув
ваш.

Проект без собственного бакета обслуживается корневым issuer экземпляра, общим для всех таких
тенантов. Там объявить ресурс может только суперадминистратор, потому что собственные метаданные
ресурса не могут сказать, какому из этих тенантов он принадлежит.

В консоли откройте свой проект и выберите Resources, затем Declare a resource.

Заполните три поля.

Resource identifier (идентификатор ресурса) — канонический URI вашего MCP-сервера, ровно в том
виде, в каком его будут называть клиенты. https://mcp.example.com/mcp. Без фрагмента и без
завершающего слэша: спецификация просит реализации предпочитать форму без слэша, и этот сервер
хранит идентификатор именно так.

Name (имя) — то, что конечный пользователь видит на экране согласия.

Scopes it recognises (распознаваемые scope, то есть области доступа) — разрешения, которые
различают ваши инструменты. Начните с одного, например mcp:tools-basic.

Настройки токена не трогайте. По умолчанию это самодостаточный токен, который ваш MCP-сервер
проверяет по опубликованным ключам этого сервера, со сроком жизни пятнадцать минут. Именно
поэтому разделу 3 ниже, Проверьте токен, не нужны собственные учётные данные.

Список scope — базовый набор, а не каталог

Хост агента, не получивший указаний по scope, запрашивает все scope,
которые объявляет ваш ресурс. Так сказано в спецификации, и там же сказано,
что это задумано: универсальный клиент не может разумно выбирать среди имён,
которых не понимает. Поэтому длинный список — не меню вариантов. Это выдача
всего списка каждому клиенту, который когда-либо придёт. Называйте те scope,
которые ваши инструменты действительно различают, а остальные пусть
запрашиваются позже.

2. Опубликуйте то, что MCP-сервер обязан публиковатьЗаголовок раздела «2. Опубликуйте то, что MCP-сервер обязан публиковать»

Две вещи, обе — с вашего собственного MCP-сервера.

Метаданные защищённого ресурсаЗаголовок раздела «Метаданные защищённого ресурса»

JSON-документ по адресу /.well-known/oauth-protected-resource, сообщающий, какой сервер авторизации
вас защищает. Спецификация делает его для MCP-сервера обязательным (MUST), как и указывающий на него
заголовок WWW-Authenticate ниже и проверку audience в шаге 3:

{

"resource": "https://mcp.example.com/mcp",

"authorization_servers": ["https://auth.example.com/acme"],

"scopes_supported": ["mcp:tools-basic"],

"bearer_methods_supported": ["header"]

}

resource — идентификатор, который вы объявили на шаге 1, символ в символ. authorization_servers
называет issuer бакета пользователей, через который выполняется вход в вашем проекте: собственный
issuer бакета, если у того есть адрес, — https://auth.example.com/acme или его имя хоста, — и
ISSUER экземпляра в остальных случаях. Документ может находиться по адресу
/.well-known/oauth-protected-resource, за которым следует путь вашего ресурса, как это формирует RFC
9728, в корне, как выше, или там, куда указывает challenge ниже.

Строка ресурса в консоли показывает, подтверждает ли сейчас этот документ ваше объявление и в каком
из трёх мест он был найден. Это только диагностика: она ничего не блокирует.

Challenge, который говорит, куда идтиЗаголовок раздела «Challenge, который говорит, куда идти»

Когда запрос приходит без токена или с токеном, который вы отклоняете, отвечайте 401 с заголовком
WWW-Authenticate, указывающим на этот документ:

HTTP/1.1 401 Unauthorized

WWW-Authenticate: Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource",

scope="mcp:tools-basic"

scope — это любезность, о которой просит спецификация и которой пользуются клиенты: без него клиент
вернётся к вашему документу метаданных и запросит всё, что перечислено в scopes_supported.

3. Проверьте токенЗаголовок раздела «3. Проверьте токен»

Ваш MCP-сервер должен установить о каждом токене две вещи: что он подлинный и что он выпущен для
вас. Второе не опционально. Принять токен, выпущенный для чего-то другого, — это та самая дыра
confused deputy (посредник с чужими полномочиями действует не в интересах их владельца), для закрытия
которой и существует привязка к аудитории,
и спецификация прямо это запрещает.

С форматом токена по умолчанию в момент запроса спрашивать у этого сервера ничего не нужно. Один раз
загрузите открытые ключи issuer из jwks_uri, объявленного в его метаданных, закэшируйте их и
проверьте четыре claim. Бакет с адресом подписывает собственными ключами, поэтому его набор ключей
находится под его собственным issuer — ниже это https://auth.example.com/acme/jwks — и не содержит
ключей других тенантов:

import { createRemoteJWKSet, jwtVerify } from 'jose';

const keys = createRemoteJWKSet(new URL('https://auth.example.com/acme/jwks'));

export async function principalFor(authorization) {

const token = authorization?.replace(/^Bearer /i, '');

if (!token) return null;

const { payload } = await jwtVerify(token, keys, {

issuer: 'https://auth.example.com/acme',

// The identifier you declared. This is the check that makes the token yours.

audience: 'https://mcp.example.com/mcp'

});

return { subject: payload.sub, scopes: (payload.scope ?? '').split(' ') };

}

jwtVerify сам проверяет подпись и exp; issuer и audience — те два значения, которые вы обязаны
указать явно. Любая JWT-библиотека на любом языке выполняет те же четыре проверки. Форма выше — это
чек-лист; библиотеку выбираете вы.

Если отзыв должен действовать немедленно

Самодостаточный токен остаётся действительным до истечения срока, поэтому
время жизни по умолчанию короткое. Если вашему ресурсу нужно, чтобы отозванный
токен сразу переставал работать, объявите его с форматом токена opaque
(непрозрачный) и проверяйте каждый токен через POST /token/introspect. Этот эндпоинт требует
аутентификации, поэтому вашему MCP-серверу понадобятся собственные учётные
данные и включённый
introspection.enabled.
Таков компромисс, на который вы идёте.

4. Дайте хосту агента представитьсяЗаголовок раздела «4. Дайте хосту агента представиться»

Хост агента обычно не знает ничего, кроме URL, и в нём негде ввести идентификатор клиента. Ответ
текущей спецификации — Client ID Metadata Documents: client_id клиента является HTTPS URL,
указывающим на JSON-документ с его описанием, а этот сервер загружает и проверяет этот документ по
требованию и вообще не создаёт запись клиента.

Включите это в Settings:
clientIdMetadataDocument.enabled.
Настройка действует сразу после сохранения. Когда она включена, discovery объявляет
client_id_metadata_document_supported — это то, что клиент проверяет, прежде чем пробовать.

Старые хосты и что нужно каждой поверхности

Некоторые хосты агентов умеют только динамическую регистрацию клиентов, которую текущая
спецификация помечает как устаревшую, но сохраняет. Она работает: включите
registration.enabled, и такой клиент
зарегистрируется сам, а затем обратится к вашему ресурсу точно так же, как описано выше.
Административная плоскость MCP на /mcp строже, и об этом стоит знать до того, как вы её
попробуете. Динамически зарегистрированный клиент никогда не сможет администрировать экземпляр, как бы
ни было настроено всё остальное. Клиент, идентифицированный документом, может, но только после того,
как суперадминистратор укажет его в разделе Agent access консоли. Все остальные могут
по-прежнему использовать зарезервированный client id admin-mcp, которому запись не нужна.

5. Проверьте оба исходаЗаголовок раздела «5. Проверьте оба исхода»

Три вызова. Отказы важны не меньше успеха, потому что без них не отличить «защищён» от «до него
просто ещё не дошли».

Вызовите MCP-сервер без токена. Вы должны получить 401 и заголовок WWW-Authenticate из
шага 2. Если пришёл 200, скорее всего, ваша проверка не встроена в путь обработки запроса.

Окно терминала

curl -i https://mcp.example.com/mcp -X POST \

-H 'content-type: application/json' \

-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

Получите токен и вызовите снова. Направьте хост агента на ваш MCP-сервер и дайте ему пройти
поток: он должен прочитать ваш документ метаданных, обнаружить этот сервер, выполнить вход вашего
конечного пользователя и вернуться с токеном. Тот же вызов теперь отвечает 200.

Попробуйте токен там, где ему не место. Объявите второй ресурс, получите для него токен и
предъявите его вашему MCP-серверу. Он должен быть отклонён — claim aud не совпадёт. Именно эта
проверка не даёт токену одной интеграции работать в другой, и хотя бы раз стоит увидеть, как она
срабатывает.

Чего вам не пришлось делатьЗаголовок раздела «Чего вам не пришлось делать»

Ни строчки кода в этом сервере и никакого перезапуска ради самого ресурса: объявление — это данные,
которые читаются при каждом запросе токена. Никакого секрета клиента для вашего MCP-сервера, потому
что он проверяет токены по опубликованным ключам, ничего не спрашивая. И никакого списка разрешённых
хостов агентов для обычного ресурса: каких конечных пользователей клиент может впустить, следует из
ресурса, который он называет, поэтому ответ на этот вопрос принадлежит проекту, которому принадлежит
ресурс.

Эндпоинты, которых касается эта страница, описаны в справочнике эндпоинтов,
а каждая упомянутая выше настройка — в справочнике настроек, вместе со
значением по умолчанию и обоснованием.

ПредыдущаяПолучение первого токенаСледующаяDeploy to Fly.io
