Как назначать OAuth scopes для MCP-инструментов?
OAuth scopes для MCP помогают отделить чтение, изменение и администрирование, настроить scope challenge и проверить права на сервере.

MCP-инструменты опасны не потому, что модель иногда выбирает не ту функцию. Опасность появляется, когда сервер даёт одному токену право читать данные, менять их, запускать экспорт и выполнять администрирование. Тогда одна лишняя интеграция, один неверный вызов или украденный токен получает весь набор возможностей сразу.
Хорошая схема scopes не повторяет список инструментов. Она описывает разрешённые операции над бизнес-объектами, отделяет обратимые действия от необратимых и оставляет серверу право проверить контекст запроса. Это работа по проектированию доступа, а не упражнение в нейминге OAuth.
Scope должен описывать действие, а не сервер
Общий scope вроде mcp:access почти всегда означает, что команда отложила решение о правах на потом. Потом обычно наступает после первого инструмента удаления, экспорта или одобрения, когда старые токены уже выданы и зависят от них несколько клиентов.
Начинайте с вопроса: что именно может сделать вызывающая сторона, если модель выберет этот инструмент без дополнительного подтверждения? Ответ «вызвать наш MCP-сервер» ничего не говорит о риске. Ответы «посмотреть остатки», «создать заявку», «опубликовать тариф» и «изменить правила маршрутизации» говорят достаточно.
Практичная форма имени scope обычно состоит из домена и глагола:
inventory:read
inventory:write
orders:read
orders:write
orders:approve
billing:export
admin:manage
Это не универсальный словарь. Для одной команды orders:approve означает согласование лимита, для другой - выпуск возврата. Важно, чтобы значение было устойчивым: человек, policy engine и автор инструмента должны одинаково понимать, какое действие открывает строка.
Не добавляйте scope на каждый технический метод только потому, что их много. Например, inventory.get_item, inventory.search_items и inventory.list_low_stock могут требовать inventory:read, если все три только возвращают данные одного уровня чувствительности. Но inventory.export_all заслуживает отдельного inventory:export: его результат легко оказывается в файле, почте или внешнем агенте.
Есть и обратная ошибка: слишком широкие глаголы. orders:write не должен автоматически покрывать отмену оплаченного заказа, возврат денег или массовое изменение цен. Если действие меняет обязательство перед клиентом, влияет на деньги либо трудно откатывается, отделяйте его от обычного редактирования.
Инструмент, операция и ресурс не одно и то же
Команды часто пытаются решить весь контроль доступа одним списком scopes. Так не получится, потому что scope отвечает только на один вопрос: какой класс действий в принципе разрешён токену.
У каждого вызова MCP-инструмента есть как минимум четыре разных измерения:
- Инструмент определяет точку входа, например
create_refund. - Операция описывает смысл действия: читать, создавать, менять, утверждать, экспортировать, администрировать.
- Ресурс отвечает на вопрос, над чем выполняется действие: заказ, договор, конкретный клиент, проект или филиал.
- Субъект и контекст задают, кто вызывает действие, из какой организации, в какой среде и при каких дополнительных условиях.
Scope обычно покрывает операцию и домен. Проверка ресурсов и границ организации должна жить отдельно. Токен с orders:read не должен позволять сотруднику банка читать заказы другого банка только потому, что оба используют один MCP-сервер.
Вот типичная ошибка:
orders:read:tenant-482
orders:read:tenant-721
orders:read:tenant-913
Такой дизайн выглядит точным, пока клиентов десяток. Затем появляются филиалы, проекты, временные делегирования и внешние подрядчики. Набор scopes разрастается, согласие пользователя становится нечитаемым, а аудит не даёт ответа, почему именно этот ID оказался в токене.
Лучше сохранить в токене обычный orders:read, а право на конкретную организацию получить из проверяемых claims, из серверной политики или из структурированного разрешения. RFC 9396, OAuth 2.0 Rich Authorization Requests, для этого вводит параметр authorization_details: клиент передаёт машиночитаемое описание требуемого доступа, а authorization server может выдать более точное разрешение. Это полезно, когда доступ зависит от счёта, набора документов, суммы или периода, а не только от глагола.
Scope и ограничение ресурса должны работать вместе. Первый не даёт вызвать класс опасных операций. Второе не даёт применить разрешённую операцию к чужому или неподходящему объекту.
Чтение, изменение и администрирование требуют разных границ
Для MCP-сервера полезно сначала нарисовать не инструменты, а матрицу последствий. Она быстро показывает, где «write» скрывает слишком разные действия.
| Действие | Пример инструмента | Базовое право | Что проверить помимо scope |
|---|---|---|---|
| Чтение одной записи | get_customer | customer:read | принадлежность организации, маскирование полей |
| Поиск и список | search_orders | orders:read | фильтры, лимит выдачи, доступ к полям |
| Создание черновика | create_quote | quotes:write | организация, шаблон, лимиты |
| Изменение рабочего объекта | update_ticket | tickets:write | авторство или роль исполнителя |
| Утверждение | approve_discount | discounts:approve | лимит суммы, разделение обязанностей |
| Экспорт | export_customers | customer:export | формат, объём, основание, журнал |
| Администрирование | rotate_api_key | admin:manage | MFA, роль администратора, отдельный канал |
Чтение не всегда безопасно. Поиск по клиентам может вернуть персональные данные, а выгрузка из десяти тысяч строк часто опаснее единичного изменения. Поэтому read не означает «без согласия и без аудита». Он означает, что операция не меняет исходные данные. Этого недостаточно для оценки ущерба.
Административные действия держите отдельно даже в небольшом продукте. Добавление пользователя, смена политики хранения, выпуск ключа, изменение маршрута платежей и выключение аудита способны расширить доступ за пределами одного бизнес-домена. Scope admin:manage сам по себе тоже может быть слишком широким. Иногда нужны admin:identity, admin:keys и admin:policy, если эти обязанности реально разделены между разными людьми.
Не пытайтесь компенсировать плохую схему прав текстом в описании инструмента: «используй только по просьбе администратора». Модель может прочитать это описание, но сервер обязан принять решение сам. Описание влияет на вероятность вызова. Scope и серверная политика определяют, будет ли вызов выполнен.
Карта операций должна появиться раньше OAuth-кода
До настройки authorization server составьте таблицу всех MCP-инструментов. Не поручайте её полностью разработчику сервера: владелец данных и владелец процесса знают последствия вызова лучше. Эта таблица становится контрактом между командой инструмента, командой идентификации и аудитом.
Для каждой операции заполните пять полей:
| Поле | Вопрос, на который оно отвечает |
|---|---|
| Инструмент | Какой tools/call приходит на сервер? |
| Минимальный scope | Какое право требуется для класса действия? |
| Ресурсная проверка | Какие tenant, проект, владелец или роль должны совпасть? |
| Условие усиления | Когда нужна повторная авторизация, MFA или отдельное подтверждение? |
| Аудит | Что записать, чтобы восстановить решение сервера? |
Допустим, команда делает MCP-сервер для работы с закупками. Первая версия карты может выглядеть так:
operations:
purchase_order.get:
required_scopes: [procurement:read]
resource_check: same_organization
purchase_order.create_draft:
required_scopes: [procurement:write]
resource_check: requester_can_create_for_cost_center
purchase_order.submit:
required_scopes: [procurement:submit]
resource_check: requester_is_draft_owner
purchase_order.approve:
required_scopes: [procurement:approve]
resource_check: approver_limit_covers_total
step_up_if: total_exceeds_approval_limit
supplier.export:
required_scopes: [supplier:export]
resource_check: export_allowed_for_organization
Обратите внимание на submit и approve. Обе операции меняют статус документа, но последствия разные. Если назначить им один procurement:write, сотрудник, которому разрешили исправить черновик, сможет утвердить закупку. Это не тонкая архитектурная придирка. Это обычный путь к нарушению разделения обязанностей.
Карта также защищает от другой проблемы: инструмент с нейтральным названием. update_purchase_order может менять описание, сумму, получателя и статус. Если аргументы допускают действия с разными рисками, лучше разделить сам инструмент: update_purchase_order_draft, submit_purchase_order, approve_purchase_order. Отдельные точки входа проще авторизовать, тестировать и объяснять пользователю.
Сервер должен отказать до выполнения инструмента
MCP-клиент может показать пользователю consent screen. Модель может выбрать только доступные инструменты. Gateway может отфильтровать часть запросов. Ни один из этих уровней не отменяет проверку на MCP-сервере.
Проверяйте доступ после того, как сервер понял имя инструмента и разобрал аргументы, но до любого вызова базы данных, очереди, внешнего API или побочного эффекта. В этот момент сервер знает, какое действие просит клиент и над каким объектом.
Упрощённая схема проверки выглядит так:
async function authorizeToolCall(ctx, call) {
const rule = policy.forTool(call.name);
const claims = await verifyAccessToken(ctx.authorization);
requireAudience(claims, "https://mcp.example.kz");
requireScope(claims.scope, rule.requiredScopes);
const resource = await resolveResource(call.name, call.arguments);
await rule.checkResourceAccess({ claims, resource, args: call.arguments });
if (rule.requiresStepUp({ claims, resource, args: call.arguments })) {
throw insufficientScope(rule.stepUpScopes);
}
}
verifyAccessToken не должен означать только проверку подписи JWT. Серверу нужны проверка издателя (iss), аудитории (aud), времени жизни, допустимого алгоритма подписи и scopes. Если access token непрозрачный, сервер обычно делает интроспекцию или использует надёжный кеш её результата с коротким сроком.
Никогда не проверяйте право по подстроке. Код вроде scope.includes("write") пропустит profile:write туда, где нужен orders:write. Разберите строку scope как набор значений, сравните полные элементы и определите семантику нескольких обязательных прав явно.
function requireScope(scopeString, expected) {
const granted = new Set((scopeString ?? "").split(/\s+/).filter(Boolean));
const missing = expected.filter(scope => !granted.has(scope));
if (missing.length) {
throw new AuthorizationError("insufficient_scope", { missing });
}
}
Если инструмент требует два независимых права, например экспорт клиентов для конкретного подразделения, не прячьте правило в названии scope. Проверьте customer:export как разрешение действия и принадлежность подразделения как ресурсное ограничение.
Scope challenge лучше, чем доступ про запас
MCP-спецификация от 25 ноября 2025 года рекомендует серверу указывать требуемые scopes в заголовке WWW-Authenticate при ответе 401. Клиент должен считать scopes из challenge авторитетными для текущего запроса. Это даёт нормальный механизм повышения прав: клиент начинает с минимального набора и просит больше только при попытке выполнить конкретную защищённую операцию.
Для человека это выглядит понятнее, чем экран согласия с двадцатью правами при первом подключении. Для команды безопасности это лучше потому, что широкие токены не гуляют по агентам просто ради возможности когда-нибудь вызвать редкий административный инструмент.
Ответ сервера может иметь такую форму:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://mcp.example.kz/.well-known/oauth-protected-resource", scope="procurement:approve"
Content-Type: application/json
{
"jsonrpc": "2.0",
"id": 42,
"error": {
"code": -32001,
"message": "Approval permission is required"
}
}
Не путайте такой challenge с простым сообщением «доступ запрещён». Клиенту нужны машиночитаемые данные, чтобы он запросил ровно недостающее право у authorization server. При этом нельзя считать, что любой MCP-клиент идеально поддержит повышение scopes. Проверьте поведение конкретных клиентов в интеграционных тестах и оставьте понятный отказ для тех, кто не умеет повторить авторизацию.
В текущей работе MCP-сообщества это остаётся местом, где нельзя полагаться на магию SDK. На встрече рабочей группы Tool Scopes в феврале 2026 года участники прямо отметили, что спецификация уже поддерживает OAuth и scope challenge, но общих рекомендаций по определению и сопоставлению scopes с инструментами пока недостаточно, а аргументы инструмента иногда меняют требуемое право. Это подтверждает практическое правило: храните карту операций в собственном policy layer, а не рассчитывайте, что библиотека выведет её из JSON Schema.
Не выдавайте один токен и не называйте это удобством
Самая популярная рекомендация на раннем этапе звучит так: «пока дадим агенту полный доступ, а потом сузим». Она популярна, потому что первая демонстрация проходит быстрее. Она неверна, потому что сужение доступа после запуска ломает сценарии, refresh tokens и ожидания пользователей, а разрешения успевают расползтись по конфигурациям.
Есть четыре варианта, которые особенно часто приводят к лишнему доступу:
- Один scope
mcp:full_accessдля всех инструментов. writeдля операций утверждения, возврата, удаления и массового импорта.- Включение административных прав в machine-to-machine токен.
- Выдача всех
scopes_supportedклиенту, который заранее знает только одну операцию чтения.
MCP использует OAuth-подход, но OAuth не заставляет сервер быть минимально привилегированным. Политику выбирает команда. В спецификации MCP предусмотрена стратегия выбора scopes, где challenge от сервера имеет приоритет, а набор базовых scopes должен быть минимальным для обычной работы. Это полезная цель, но не оправдание для выдачи максимума при отсутствии challenge.
Для machine-to-machine сценария границы ещё важнее. OAuth client credentials подходит, когда клиент действует как приложение, а не от имени сотрудника. Официальное расширение MCP для client credentials описывает именно этот случай: автоматизированная система получает application-level credential вместо интерактивного согласия пользователя.
Такому клиенту не нужен admin:manage, если он каждую ночь синхронизирует каталог. Дайте ему catalog:read и, если требуется, catalog:write для чётко определённого направления синхронизации. Ограничьте аудиторию токена конкретным MCP-ресурсом, сократите его срок жизни и ведите отдельный client identity для каждого сервиса. Один технический клиент на все фоновые задачи экономит несколько записей в настройках и разрушает расследование инцидента.
RFC 9700 закрепляет современные рекомендации безопасности OAuth 2.0: точное сопоставление redirect URI, защита redirect-based flows и отказ от режимов, которые признаны небезопасными. Для MCP с интерактивной авторизацией это означает, что аккуратные scopes не спасут схему с плохо защищённым authorization code flow. Используйте Authorization Code с PKCE для публичных клиентов и не передавайте access tokens через URL.
Токен должен быть привязан к MCP-ресурсу
Даже идеальный список scopes не помогает, если токен для одного API принимается другим. MCP-спецификация требует использовать OAuth Resource Indicators: клиент включает параметр resource в запрос авторизации и в запрос токена, указывая канонический URI MCP-сервера. Сервер проверяет, что токен предназначен именно ему.
Это важно для организаций, где есть отдельные MCP-серверы для кадровых данных, закупок, аналитики и администрирования. Токен с employee:read, выпущенный для кадрового сервера, не должен стать универсальным пропуском к любому endpoint, который случайно принимает подпись того же issuer.
Проверка обычно сводится к двум условиям:
iss = ожидаемый authorization server
and
resource/aud = канонический URI этого MCP-сервера
Если authorization server выпускает JWT с aud, сверяйте aud. Если он использует другой способ представить целевой ресурс, зафиксируйте его в контракте и тестируйте отказ при несовпадении. Не принимайте токен только потому, что подпись валидна. Валидная подпись говорит, кто его выпустил. Она не говорит, что этот сервер должен его принимать.
Для удалённых MCP-серверов discovery тоже относится к модели доступа. Protected Resource Metadata сообщает клиенту, с каким authorization server работать, а WWW-Authenticate может направить его к metadata URL. Не подменяйте этот процесс захардкоженным issuer в каждом клиенте, если сервер должен работать с несколькими утверждёнными способами авторизации. Но и не разрешайте клиенту выбирать произвольный issuer без серверной политики доверия.
Тесты должны доказывать отказ, а не только успешный вызов
Команда часто пишет интеграционный тест «токен с orders:read читает заказ» и считает авторизацию готовой. Такой тест нужен, но он не находит опасные разрешения. Вам нужны пары: разрешённый вызов и почти такой же запрещённый вызов.
Минимальный набор тестов для каждой группы операций выглядит так:
cases:
- name: reader_can_get_own_order
token_scopes: [orders:read]
tenant: alpha
tool: get_order
args: { order_id: "alpha-104" }
expected: success
- name: reader_cannot_update_order
token_scopes: [orders:read]
tenant: alpha
tool: update_order
args: { order_id: "alpha-104", status: "cancelled" }
expected: insufficient_scope
- name: reader_cannot_read_other_tenant
token_scopes: [orders:read]
tenant: alpha
tool: get_order
args: { order_id: "beta-104" }
expected: forbidden
- name: writer_cannot_approve_order
token_scopes: [orders:write]
tenant: alpha
tool: approve_order
args: { order_id: "alpha-104" }
expected: insufficient_scope
Разделяйте insufficient_scope и forbidden. Первый ответ означает: токену не хватает права на тип операции. Второй означает: тип операции разрешён, но конкретный объект или условие запрещает выполнение. Эта разница нужна клиенту для правильного поведения и вашей команде для расследования. Если на всё возвращается «403 access denied», вы теряете смысловую часть отказа.
Проверяйте и границы аргументов. Один transfer_funds может требовать обычный payments:write до лимита и payments:approve плюс дополнительную аутентификацию выше лимита. Если policy смотрит только на имя инструмента, модель может передать большую сумму через формально разрешённый метод.
В журнале записывайте subject, client ID, issuer, audience, имя инструмента, требуемые scopes, фактически выданные scopes, тип решения и идентификатор ресурса в безопасной форме. Не записывайте access token. Не копируйте целиком аргументы с персональными данными в логи только ради удобства отладки.
Схема должна переживать новые модели и новые маршруты
Маршрутизация между моделями не меняет правила доступа. Если один агент вызывает MCP-инструменты через несколько провайдеров, право определяется токеном и серверной политикой, а не тем, какая модель сформировала JSON-RPC запрос. Это особенно важно, когда команда меняет модель из-за стоимости, задержки или качества вызовов инструментов.
AI Router может принять OpenAI-совместимый запрос и направить его к разным моделям, но MCP-сервер всё равно обязан проверять scopes в своей точке исполнения. Не переносите авторизацию в prompt, в выбор модели или в gateway, который не видит бизнес-смысл аргументов.
Стабильность схемы важнее красоты имён. Не переименовывайте orders:read в orders:view без необходимости: старые токены, consent history и политики начнут расходиться. Если смысл права действительно изменился, добавьте новый scope, поддержите период миграции и отзовите старый по плану. Не меняйте семантику существующего имени молча.
Перед запуском нового инструмента требуйте короткий review: операция, минимальный scope, ресурсная проверка, условия усиления и тесты отказа. Если автор инструмента не может заполнить эти пять пунктов, инструмент ещё не готов к продакшену. OAuth здесь не создаёт дисциплину сам, но делает её проверяемой.
Часто задаваемые вопросы
Нужен ли отдельный scope для каждого MCP-инструмента?
Нет. Один scope на сервер уместен только для очень маленького сервера, где каждая операция имеет одинаковый риск и доступна одной и той же роли. Как только рядом с поиском появляются изменение данных, экспорт или администрирование, общий токен превращает любую ошибку выбора инструмента в проблему контроля доступа.
Как понять, когда два инструмента могут использовать один scope?
Чаще всего нет. Делите права по операциям и объектным доменам, а не по внутреннему устройству кода. Несколько безопасных инструментов чтения могут использовать один scope, если они обращаются к одному классу данных и имеют одинаковую цену ошибки.
Должен ли read-инструмент требовать write scope?
Инструмент чтения должен требовать только право чтения, даже если рядом есть инструмент изменения того же объекта. Не выдавайте write «на всякий случай»: модель может вызвать доступный инструмент по ошибке, а сервер уже не сможет вернуть лишнее право обратно.
Стоит ли выносить экспорт и удаление в отдельные scopes?
Обычно нужны отдельные права. Экспорт создаёт копию данных вне обычного рабочего контекста, а удаление часто необратимо или требует отдельного срока хранения и аудита. Названия вроде customer:export и customer:delete делают это различие видимым в политике и в журнале событий.
Где MCP-сервер должен проверять scopes?
Проверяйте scope на сервере прямо перед выполнением операции, после разбора аргументов и до обращения к бизнес-API. Проверка в описании инструмента, в клиенте или в системном промпте ничего не защищает: все три слоя можно обойти или неправильно настроить.
Можно ли включать ID клиента или документа в название scope?
Scope не должен кодировать идентификаторы каждого клиента, счёта или документа. Для этого используйте атрибуты субъекта, проверку принадлежности организации, ограничения ресурса или authorization_details из Rich Authorization Requests. Иначе набор прав разрастается до тысяч строк и перестаёт быть управляемым.
Можно ли получать новые scopes через refresh token?
Обновление токена допустимо, если refresh token не расширяет исходное согласие сам по себе. Когда клиенту нужен новый опасный scope, запускайте отдельный запрос авторизации или scope challenge, чтобы пользователь и политика увидели повышение прав.
Как назначать scopes для MCP-сервиса без пользователя?
Для сервисного MCP-клиента применяйте client credentials только к действиям, которые принадлежат самому приложению, а не человеку. Такой токен должен иметь короткое время жизни, конкретную аудиторию и набор scopes, который описывает работу сервиса, например чтение каталога, но не утверждение платежей.
Нужно ли MCP-серверу обращаться к authorization server при каждом вызове?
Да, если токен содержит достаточные для проверки данные или сервер делает интроспекцию токена. Локальная подпись JWT не заменяет проверку aud, iss, срока действия и фактических scopes; иначе сервер принимает чужой или просроченный токен как действительный.
Какие логи нужны для отказов по scope?
Собирайте события отказа по требуемому scope, имени операции, типу субъекта и причине отказа, но не записывайте сам access token и чувствительные аргументы целиком. Если команда регулярно добавляет широкие права после отказов, это сигнал исправить карту операций, а не ослабить проверку.