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

Synapse встраивает **FastMCP** (пакет `mcp`) — тот же контейнер API, тот же
процесс uvicorn. Эндпоинт `/mcp`, транспорт **streamable-http** (stateless),
доступ — статический Bearer-токен `MCP_TOKEN` из `.env`.

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

## Включение

1. `MCP_TOKEN=<hex>` в `.env` (и gnexus-creds). Пусто/не задано — `/mcp`
   не существует: GET уходит в SPA, POST отвечает 405.
2. Перезапуск контейнера api.
3. Регистрация в Claude Code:

```bash
claude mcp add --transport http synapse http://localhost:8013/mcp \
  --header "Authorization: Bearer <MCP_TOKEN>"
```

Генерация токена: `openssl rand -hex 32`. Токен — как ключ прод-сервера
(внутри API полномочия MCD = superadmin), хранить в `.env` + gnexus-creds,
никогда не в репозитории.

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

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

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

| Группа | Тулы |
|---|---|
| Состояние | `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) — полный маршрут в воркере |

## Архив вместо удаления (семантика 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` не публикуем.