# 07 — MCP-сервер: управление Synapse ИИ-агентом

Synapse встраивает **FastMCP** (пакет `mcp`) — тот же контейнер API, тот же
процесс uvicorn. Эндпоинты `/mcp-protocol/` (конвенция хендбука, `10-platform/mcp.md`
— слэш в конце значим для реверс-прокси) и `/mcp` (исторический alias), транспорт
**streamable-http** (stateless).

Зачем: ИИ-агент (Claude Code и совместимые) управляет Synapse целиком —
выдаёт API-ключи клиентам (сервисам-источникам), регистрирует типы, ведёт
правила маршрутизации, смотрит поток событий и доставок, меняет настройки —
без ручной работы в админке. Правка 2026-10-03; персональные ключи 2026-10-04.

## Аутентификация: три вида доступа

`/mcp` монтируется **всегда**; гейт (`McpTokenGuard`, app/mcp/server.py)
принимает два вида Bearer-ключей:

1. **Персональный `mcp_...`** — основной путь. Каждый пользователь выпускает
   себе ключ на SPA-странице **`/mcp-keys`** (или `POST /api/v1/me/mcp_tokens`,
   plaintext в ответе ровно один раз; в БД — sha256-хэш + хвост). Одновременно
   активно до 10 ключей. **Ключ действует ролью владельца на момент выпуска**
   (снейпшот `system_role` в строке; call-home по sub без токена gnexus-auth
   невозможен — после смены роли выпустите ключ заново, старый отзовите):
   - роль `admin`/`superadmin` — весь каталог тулов (35);
   - роль `user` — только личный набор: `me`, `my_events_list`,
     `my_push_subscriptions_list`, `my_push_subscription_delete`; любой
     админ-тул отвечает `{"error": 403, "detail": "…"}`.
   Админ видит ключи всех на `/mcp-keys` (или `GET /api/v1/admin/mcp_tokens`)
   и может отозвать любой (`POST /api/v1/admin/mcp_tokens/{id}/revoke`).
   SSO-логаут MCP-токены не трогает — это самостоятельные креды, как
   ключи источников. **Блокировка пользователя** (вебхуки gnexus-auth
   `user.blocked`/`deleted`/`archived` → флаг в `user_prefs`) гасит все
   его личные ключи: гард отвечает тем же 401, что и при неверном токене.
2. **Статический `MCP_TOKEN` из `.env`** — суперадмин-бэкдор: сохраняет
   совместимость с существующими сценариями агентов. Опционален (пусто —
   работают только персональные ключи); держать в `.env` + gnexus-creds.
   Генерация: `openssl rand -hex 32`.

Регистрация в Claude Code:

```bash
claude mcp add --transport http synapse http://localhost:8013/mcp \
  --header "Authorization: Bearer mcp_<персональный ключ>"
```

## Архитектура

- **Тулы не дублируют логику**: сервер переиспользует admin-route функции
  (`app/api/admin_routes.py`)/личные (`me_routes.py`) напрямую — та же
  pydantic-валидация, те же коды ошибок и тексты. Автор запросов — реальный
  владелец ключа (роль — снейпшот выпуска; статический токен = superadmin).
- Роль-гейт на тулах: `_admin_run` (`app/mcp/tools.py`) пропускает
  admin/superadmin, для user-ключа возвращает данные `{"error": 403}`.
- Ошибка тулза возвращается как данные: `{"error": 409, "detail": "…"}` —
  агент читает причину и может её исправить (например restore архивной записи).
- Синхронная SQLAlchemy-сессия на вызов; FastMCP исполняет синхронные тулы
  в threadpool (ContextVar пользователя прокидывается в тулы).
- Гейт — чистый ASGI-класс (`McpTokenGuard`, app/mcp/server.py): резолвит
  `Authorization: Bearer` (стат. токен или `mcp_*` в БД) на каждом запросе,
  401 (`www-authenticate: Bearer`) при промахе; не BaseHTTPMiddleware —
  SSE-стримы не ломает. Обновляет `last_used_at` токена.

## Каталог тулов (35)

| Группа | Тулы |
|---|---|
| Состояние | `system_status` (БД + Celery ping + SSO) · `stats_get` (счётчики по статусам, живые справочники) |
| Источники | `sources_list` (query, include_archived) · `source_create` · `source_archive` · `source_restore` |
| Ключи | `keys_list` · `key_issue` (**plaintext один раз** в `token`) · `key_revoke` |
| Типы | `types_list` (source_id, query, include_archived) · `type_register` · `type_archive` · `type_restore` |
| Цели | `targets_list` (channel, enabled, include_archived) · `target_create` · `target_patch` · `target_archive` · `target_restore` |
| Правила | `rules_list` (enabled, include_archived) · `rule_create` · `rule_patch` · `rule_archive` · `rule_restore` |
| Поток | `events_list` (status, source_name) · `event_get` · `deliveries_list` (status, channel) |
| Push | `push_subscriptions_list` (user_id) · `push_subscription_delete` (жёстко — устройства пользователей) |
| Настройки | `settings_get` (секреты write-only: value=null, факт в set) · `settings_put` (правила docs/06: "" — не менять, null — сброс; настройка push-канала — рецепт VAPID в docs/06 «Как задать VAPID», выполняется shell'ом в контейнере) |
| Контроль | `send_test_event` (source_name, subject, action, payload, priority) — полный маршрут в воркере |
| Личный (любая роль) | `me` (sub/email/роль ключа) · `my_events_list` · `my_push_subscriptions_list` · `my_push_subscription_delete` |

## Архив вместо удаления (семантика DELETE)

DELETE не удаляет — ставит `deleted_at` (архив). Действует на **sources,
notification_types, channel_targets, routing_rules**. Пуш-подписки —
исключение: устройства пользователей удаляются физически (push-нотификации
не переживают несуществующую подписку).

Что это значит:

- DELETE отвечает 204; вторая архивация того же id — 404 («не найден»).
- Списки по умолчанию живое; `include_archived=true` покажет архив
  (`deleted_at` в объектах; в UI-списках поле всегда null).
- Архивный источник: ключ получает 401 «Источник ключа в архиве» — события
  не принимаются; ключи отозвать нельзя, но и выдать новый нельзя (404
  «сначала restore»).
- Архивный тип: тройка приёма → 422 «не зарегистрирован».
- Архивная цель в живом правиле: матчится, но доставка уходит в `skipped`
  с причиной «цель в архиве» (аудит, видно в deliveries).
- Create поверх архива: имя занято архивной записью → **409** с подсказкой
  «есть в архиве (id=N) — восстанови (restore) или выбери другое имя».
- Restore: `POST /api/v1/admin/{sources|types|targets|rules}/{id}/restore`
  → 200 с объектом; конфликт имени с живой записью → 409. Restore типа
  требует живого источника (409).
- История не режется: старые события/доставки показывают имена архивных
  источников и целей.

REST-эквивалент доступен и из админ-API (те же эндпоинты), UI-клиент SPA
ничего не заметил (204-ответы прежние).

## Пример сценариев

Выдать ключ клиенту:

```
1. source_create(name="bugtrail", description="Трекер ошибок…")
2. key_issue(source_id=<id>, name="основной")
   → token: "syn_…" — передать клиенту один раз
3. type_register(source_id, subject="issue", action="created")
4. Третьесторонний сервис шлёт POST /api/v1/events с Bearer syn_…
```

Проверить правило:

```
1. rule_create(name="critical → Navi rei", actions=[{"channel":"s2s","target_id":3}],
               conditions={"priority_min":"critical"})
2. send_test_event(source_name="monitoring", subject="container", action="down",
                   payload={"container":"api"}, priority="critical")
3. events_list(source_name="monitoring") → доставed в цель
```

## Ограничения

- Stateless транспорт: без сессий между вызовами (каждый вызов независим) —
  агент просто вызывает тула последовательно.
- Секреты настроек остаются write-only (`settings_get` их не отдаёт).
- `send_test_event` и просмотр потока требуют живого воркера (Celery) —
  `system_status` покажет.
- Транспорт — внутренняя сеть/локальный порт API: наружу `/mcp-protocol/`
  (и alias `/mcp`) не публикуем.