OAuth 2.1 resource-server support and the licence gate, shared by the MCP fleet
Find a file
airaneel a2d92a56b2 Арендатор в токене
Разделение данных между клиентами держится на том, ЧЕЙ это запрос, и
ответ обязан приходить от издателя. Возьми сервер арендатора из
аргумента инструмента — и любой с валидным токеном сможет назваться
чужим контуром, причём выглядеть это будет как обычная работа.

Пока арендатор один, поле не меняет ничего. Заводится оно ровно поэтому:
когда появится второй, переписывать придётся строку настройки, а не
двенадцать серверов. Обратный порядок — это разделение задним числом, то
есть в том состоянии, где чужое уже смешалось со своим.

current_tenant(required=True) отказывает и когда арендатор не назван, и
когда авторизации нет вовсе: серверу, который без арендатора не знает,
чьи данные трогать, оба случая одинаково непригодны.
2026-08-18 10:01:02 +03:00
mcp_fleet_auth Арендатор в токене 2026-08-18 10:01:02 +03:00
tests Арендатор в токене 2026-08-18 10:01:02 +03:00
.gitignore Общая обвязка OAuth и лицензии для флота MCP 2026-08-17 14:52:29 +03:00
pyproject.toml Арендатор в токене 2026-08-18 10:01:02 +03:00
README.md Арендатор в токене 2026-08-18 10:01:02 +03:00

mcp-fleet-auth

Обвязка OAuth 2.1 и гейт лицензии — одна на весь флот MCP-серверов.

Токены выпускает общий издатель mcp-auth, здесь — только проверка предъявленного bearer, публикация метаданных RFC 9728 и отказ инструмента тому, у кого нет лицензии на этот сервис.

Зачем пакет, а не файл в каждом сервере

Он и был файлом. К третьему серверу их стало три разных: excel гейтил инструменты декоратором, rusprofile — прослойкой, web-search двумя своими модулями. Одна работа, три ответа, и не сказать, какой текущий. Здесь это опаснее обычного дублирования: код решает, кому дать платный доступ, поэтому разъехавшаяся копия либо раздаёт доступ, либо запирает — и молча в обоих случаях.

Установка

Пин по тегу, чтобы обновление было осознанным:

dependencies = [
    "mcp-fleet-auth @ https://git.dev.andyjazzy.cloud/airaneel/mcp_fleet_auth/archive/v0.3.0.tar.gz",
]

Использование

Три строки в сервере:

from mcp_fleet_auth import LicenseMiddleware, auth_provider

mcp = FastMCP("Закупки ЕИС", auth=auth_provider("Zakupki MCP"))
mcp.add_middleware(LicenseMiddleware())

Всё остальное — окружение:

Переменная Смысл
MCP_AUTH_URL корень издателя. Пусто — авторизации нет вовсе (stdio, разработка)
PUBLIC_URL адрес, по которому сервер видят клиенты, без /mcp
REQUIRED_SCOPE имя сервиса: и область токена, и срез лицензии
LICENSE_URL куда идти за лицензией. Видно только внутри отказа

MCP_AUTH_URL задан, а PUBLIC_URL или REQUIRED_SCOPE нет — сервер не поднимется. Оба уезжают в метаданные, по которым клиент строит запрос авторизации, и угаданное значение сломается при подключении, где ничто не укажет обратно сюда.

За фронтом с префиксом пути

nginx на raidmen раздаёт флот по /mcp-servers/<имя>/ и проксирует метаданные, срезая префикс. Поэтому документ отдаётся по двум путям сразу: /.well-known/oauth-protected-resource/<путь ресурса> для клиента и голый /.well-known/oauth-protected-resource — в него фронт превращает первый. Один документ, не копия: алиас переиспользует тот же обработчик.

PUBLIC_URL в этом случае содержит путь целиком: https://raidmen.ru/mcp-servers/zakupki.

Проверки

pytest