Newer
Older
gn-synapse / docs / 07-mcp.md

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

Synapse встраивает FastMCP (пакет mcp) — тот же контейнер API, тот же процесс uvicorn. Эндпоинт /mcp, транспорт 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-токены не трогает — это самостоятельные креды, как ключи источников.
  2. Статический MCP_TOKEN из .env — суперадмин-бэкдор: сохраняет совместимость с существующими сценариями агентов. Опционален (пусто — работают только персональные ключи); держать в .env + gnexus-creds. Генерация: openssl rand -hex 32.

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

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 не публикуем.