# MCP: мультиюзер доступ для ИИ-агентов

Сервис с MCP-функционалом — сам себе MCP-сервер; доступ — через
**персональные ключи сервиса** (не access-токены gnexus-auth), каждый ключ
принадлежит пользователю сервиса. Канон = реализация **Gnexus Synapse**
(`docs/07-mcp.md`, `app/mcp/server.py`, страница `/mcp-keys`); авторизация
на тулах — снейпшот роли владельца, вместо набора scopes.

## Правила
- Сервис предоставляет **streamable HTTP** эндпоинт (`/mcp-protocol/`, слэш
  в конце значим — реверс-прокси не буферизует и не переписывает тело).
  Legacy HTTP/SSE-адаптер — только для клиентов без streamable HTTP.
  `stdio` не используется: агент и сервис на разных хостах.
- Авторизация MCP — **Bearer-ключ сервиса** в заголовке `Authorization`
  (у Synapse: персональный `mcp_…`; исторический `/mcp` — alias к
  `/mcp-protocol/`).
- Токены выдаются в UI на **отдельной странице «MCP-ключи»** (Synapse:
  `/mcp-keys`): имя ключа («агент Navi rei»), plaintext показывается **ровно
  один раз** при создании, в БД — только `sha256`-хэш (уникальный) и
  **хвост-хинт** (последние символы — опознание ключа в списке).
- Лимит активных ключей на пользователя (Synapse: ≤10; превышение — 409
  «отзовите лишний»).
- **Ключ действует ролью владельца на момент выпуска** (снейпшот `system_role`
  в строке токена): роль `admin`+ — весь каталог тулов, роль `user` — только
  личный набор («me, свои списки»); админ-тул для user-ключа отвечает
  `{"error": 403, "detail": …}`. Смена роли на gnexus-auth **не** дотягивается
  до выпущенного ключа — старый отзывают, выпускают заново.
- Вместо архива — **ревок** (`revoked_at`); списки показывают и отозванные
  (с хвост-хинтом), plaintext никогда не возвращается.
- Админ видит ключи всех пользователей и может отозвать любой.
- **Блокировка пользователя gnexus-auth** (вебхуки `user.blocked`/`deleted`/
  `archived` → локальный флаг) гасит все его личные ключи: гард отвечает тем
  же 401, что и при неверном токене — **не раскрывать статус блокировки**.
  SSO-логаут MCP-токены не трогает — это самостоятельные креды (у Synapse
  так сделано потому, что SSO-сервер не отзывает выданные сервисом ключи).
- Гард MCP — **чистый ASGI-класс** (не BaseHTTPMiddleware — SSE-стримы не
  ломает): резолвит `Authorization: Bearer` на каждом запросе (хэш-lookup),
  401 + `www-authenticate: Bearer` при промахе, обновляет `last_used_at`.
- Тулы **не дублируют логику**: переиспользуют функции роутов сервиса (та же
  pydantic-валидация, те же коды ошибок); ошибка возвращается как данные
  (`{"error": 403|409, "detail": "…"}`) — агент читает причину и исправляет.
- Статический `MCP_TOKEN` из `.env` — **суперадмин-бэкдор**: сохраняет
  совместимость с существующими сценариями агентов. Опционален (пусто —
  работают только персональные ключи); держать в `.env` + gnexus-creds.
- Rate-limit на чувствительные операции.

## Подключение агента
```json
{
  "mcpServers": {
    "<сервис>": {
      "type": "streamable-http",
      "url": "https://<сервис>.gnexus.space/mcp-protocol/",
      "headers": { "Authorization": "Bearer mcp_<персональный ключ>" }
    }
  }
}
```
Claude Code:
```bash
claude mcp add --transport http <сервис> https://<сервис>.gnexus.space/mcp-protocol/ \
  --header "Authorization: Bearer mcp_<персональный ключ>"
```

## Схема хранения (`mcp_tokens`)
`user_id` (sub gnexus-auth владельца — ключ выпускает сам себе),
`name` (человекочитаемое назначение), `user_email` (снейпшот для списков),
`system_role` (снейпшот роли выпуска), `token_hash` (sha256, unique),
`token_hint` (хвост), `created_at`, `last_used_at`, `revoked_at`.

## Ссылки
- Канон: gnexus-synapse — `docs/07-mcp.md` (архитектура гейта и каталог тулов),
  `app/mcp/server.py` (`McpTokenGuard`), страница `/mcp-keys`,
  `resolve_personal_token`; таблица `mcp_tokens` (`docs/04-database.md`).
- Первый применённый вариант — gnexus-creds (`gnexus_creds/mcp*.py`, страница
  Tokens): те же принципы хранения, но вместо снейпшота роли — **scopes**
  (`read`/`reveal`/`write`): приемлемая альтернатива там, где нужны
  градации внутри одного класса доступа.
- hard-panel — по этой схеме: персональные `mcp_*` (страница «MCP-ключи»)
  + статический супер-токен из `.env`. Особенности: каталог тулов у всех
  ключей единый (данные панели общие); тулы — чтение плюс управление
  серверами/сервисами, запись открыта всем валидным ключам (модель доступа
  панели «любая сессия = админ»), запись лимитирована, ошибки — как данные.
  Снейпшот роли записывается в токен, но тулов «только для админа» нет:
  FastMCP 1.x не пробрасывает HTTP-контекст в тулы, поэтому роль владельца
  внутри тула не видна.