Skip to main content
Glama

hq-mcp

Русская версия · English

MCP-сервер, дающий ИИ-агенту доступ к VPN-бизнесу: биллинг SHM и панель Remnawave, сшитые так, чтобы на вопрос, лежащий поперёк обеих систем, можно было ответить одним вызовом.

Ни один инструмент ничего не меняет тем же вызовом, которым его попросили: пишущий сперва возвращает план, а применение — это второй вызов, несущий идентификатор этого плана.

Как это выглядит в работе

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

platform_probe {}
{
  "shm":   { "configured": true, "reachable": true, "version": "2.19.4", "live": true },
  "remna": { "configured": true, "reachable": true, "version": "3.3.2",
             "runtime": { "instances": 6, "youngestUptimeSeconds": 54294 } },
  "capabilities": { "shm.filter": false, "remna.realtimeBandwidth": false,
                    "remna.nodeIntegrations": true, "remna.sharedLists": true,
                    "tunnel.mysql": false, "…": "…" },
  "warnings": [{ "code": "realtime_route_absent", "message": "…" },
               { "code": "specs_are_stale", "message": "…" }]
}

Дальше — вопрос, на который ни одна из двух систем не отвечает в одиночку: «клиент пишет, что оплатил, а конфига нет».

client_resolve { "query": "kot@example.com" }
{
  "shm":   { "count": 1, "matches": [{ "user_id": 4821, "email": "kot@example.com",
                                      "blocked": false }] },
  "remna": { "count": 0, "ambiguous": false,
             "paths": [{ "path": "email",   "tried": true, "found": 0, "note": null },
                       { "path": "service", "tried": true, "found": 0, "note": "…" }] }
}

Панель не знает про него ничего, но count: 0 здесь — не «аккаунта нет»: paths называет каждый пройденный поиск и то, чего он не видит. Что случилось на самом деле, говорит вторая пара глаз:

provisioning_diagnose { "shm_user_id": 4821 }
{
  "verdict": "panel_user_missing",
  "services": { "items": 1, "diagnosed": [{
    "user_service_id": 90210,
    "status": "ACTIVE",
    "verdict": "panel_user_missing",
    "storage": { "name": "vpn_mrzb_90210", "present": true, "checked": true },
    "panel":   { "username": "HQVPN_90210", "id": 11274, "found": false, "checked": true },
    "spool":   { "total": 0, "stuck": 0, "failed": 0, "succeeded": 0 },
    "history": { "total": 1, "success": 1 }
  }] }
}

Услуга ACTIVE, снимок конфигурации на месте, провижининг отчитался успехом — а пользователя, которому этот успех принадлежит, в панели нет. Ни биллинг, ни панель по отдельности такого не показывают.

Инструментов, читающих обе системы, тридцать семь. Режим rw добавляет шестнадцать пишущих: четырнадцать меняют настоящие данные, один применяет план, ещё один читает локальный журнал мутаций.

Related MCP server: remnawave-mcp

Почему составные инструменты, а не прокси эндпоинтов

Очевидная конструкция — по инструменту на HTTP-эндпоинт, штук полтораста. Она была написана и выброшена, по двум причинам.

Сырой прокси обнуляет любой список запретов. Если модель умеет звать GET <любой путь>, то перечень операций, которые вы решили не давать, — украшение: до запрещённого пути одна строка. Здесь инструменты зовут поимённо названные маршруты, а сканер на этапе сборки роняет прогон, если запрещённый путь встретился литералом в исходнике.

И эндпоинт — это не вопрос. Пример выше затрагивает четыре маршрута SHM и два маршрута панели, а интересное в нём — именно стык. client_overview, sync_audit и provisioning_diagnose существуют потому, что баги живут на этом шве.

Правило, определившее всё остальное

Пустой ответ никогда не должен быть принят за доказанное отсутствие.

Когда бэкенд отказывает, инструмент деградирует: отказ уезжает в degraded, предупреждение partial_result называет недостающую половину, а любая находка, зависевшая от этой половины, подавляется, а не вычисляется из того, что уцелело. Когда список усечён, вместе с ним приезжает серверный total — чтобы «такой услуги нет» не опиралось на необъявленное окно.

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

Совместимость: заработает ли это у вас

Проверено на SHM 2.19.4 и Remnawave 3.2.3 — оба числа сняты с работающего развёртывания, а не взяты из спецификации.

Поддержка Remnawave 3.3.2 добавляет интеграции нод, общие списки, Host Mapper, GeoCheck и подтверждаемую синхронизацию. Контракты сверены с исходниками тега 3.3.2. Чтения проверены на живой панели 3.3.2; заполненные каталоги и новые записи проверяются локальными тестами с ответами API. Подробности и ограничения — в справочнике.

Минимум базовых инструментов — SHM 2.18.0 и Remnawave 3.0.0. Возможности новой группы требуют панели 3.3+. Официальный danuk/shm подходит: все маршруты, которые зовут инструменты, — апстримные, форк не нужен. Единственное место, где патч того развёртывания был виден инструменту, — четвёртый флаг GET /user/password-auth; теперь его отсутствие называется предупреждением sign_in_flag_absent, а не выдаётся за диагноз. Полный перечень маршрутов обеих систем, версия появления каждого и подробный ответ про форк — в COMPATIBILITY.md.

SHM 3.0 поддержана, но по исходникам, а не по живой установке. В 3.0 логин, почта, телефон и привязка телеграма переехали из строки клиента в таблицу accounts, а v1.cgi начал молча выбрасывать незадекларированные аргументы. Инструменты работают на ОБЕИХ схемах и определяют живую по ответу SHM, а не по номеру версии; что именно переехало, что от этого ломается и что осталось человеку — в COMPATIBILITY.md.

Проверка занимает один вызов — тот же platform_probe. Если версия ниже минимума, он отвечает предупреждением backend_version_below_minimum, называя версию, минимум и что именно отвалится. Ничего при этом не выключается: старая версия даёт громкие отказы на конкретных маршрутах, а не тихие пустые ответы.

Версия

Что пропадает

Кого это касается

SHM < 2.18.0

GET /healthcheck — единственный маршрут без авторизации

только platform_probe: shm.live остаётся null, «биллинг лежит» и «пароль не тот» перестают различаться (shm_healthcheck_route_absent). Остальные инструменты не задеты

SHM < 2.11.3

GET /admin/user/search

client_search, client_resolve — отказ, не пустой список

SHM < 2.9.0

GET /user/referrals

client_account_state теряет счётчик рефералов

SHM < 2.4.0

GET /user/email

client_account_state теряет адрес и признак подтверждения

Панель < 3.0.0

пользователь адресуется uuid, а не числовым id

client_overview, subscription_inspect, traffic_stats, provisioning_diagnose, subscription_ops: /api/users/{id} отвергается валидацией с 400

Панель < 3.0.0

нет /api/connections/*

connections_inspect — весь инструмент

Панель < 3.0.0

нет POST /api/users/{id}/actions/extend

subscription_ops теряет продление (у панели остаётся только массовое)

Панель < 3.0.0

нет /api/system/stats/digest и /stats/http

panel_activity теряет две из пяти своих выборок

Панель < 3.2.0

нет GET /api/system/configuration

только platform_probe: возможность remna.subscriptionRequestHistory остаётся unknown — намеренно, а не false

Панель < 3.3.0

нет интеграций нод, общих списков, Host Mapper и GeoCheck

новые инструменты и поля требуют 3.3+; отказ дополнительных чтений помечается отдельно от основного аудита

platform_probe проверяет remna.nodeIntegrations и remna.sharedLists по каталогам. Пустой доступный каталог означает true, 404 — false, а 401/403, лимит запросов и сбой источника — unknown с объяснением. Отсутствующий в 3.3.2 обработчик /api/bandwidth-stats/nodes/realtime отмечается кодом realtime_route_absent; исторический трафик продолжает читаться.

Remnawave 3.x ломает совместимость со всем, что писалось под 2.x, и ломает негромко. Из объекта пользователя убран uuid, а вместе с ним исчезли маршруты by-telegram-id, by-email и by-tag, причём /api/users/{uuid} отвечает 400, а не 404, — так что отказ не похож даже на «нет такого пользователя». Здесь этих маршрутов нет вовсе; там, где сервер всё-таки встречает наследный uuid (например, в старом снимке storage SHM), он говорит об этом в ответе, а не сползает молча на догадку.

Спецификации OpenAPI отстают от работающих систем, поэтому platform_probe несёт предупреждение specs_are_stale при каждом вызове; у SHM всё хуже обычного — её спека штампует info.version из конфига в рантайме, то есть описывает тот стенд, где выгрузку сделали, а не ваш. Поэтому проба не читает версии из файлов вовсе, а спрашивает их у работающих систем — и там же устанавливает, что верно для этого развёртывания: сужает ли что-нибудь серверный filter у SHM, уважает ли панель filters в листинге пользователей (обе отвечают 200 и молча выбрасывают незнакомые параметры), ведёт ли панель журнал обращений за подпиской, существует ли маршрут realtime-трафика, какие ssh-туннели открыты. И отделяет «бэкенд лежит» от «наши креды не те»: 401/403 сообщается как credentialsRejected.

Установка

Нужны Node 22.12+ и pnpm, и хотя бы одна из двух систем — SHM или Remnawave. Обе не обязательны: каждая настраивается отдельно и в одиночку является полноценной конфигурацией. Инструменты той системы, которой нет, не публикуются вовсе — не «отвечают пусто», а отсутствуют, и platform_probe прямо называет, что настроено. Поэтому число инструментов зависит от установки: только панель — 20, только SHM — 18, обе — 38 (и больше в режиме rw).

pnpm install
pnpm build
pnpm run setup

pnpm run setup, именно с run. pnpm setup — встроенная команда самого pnpm: она правит профиль вашей оболочки и до этого репозитория не доходит.

Мастер существует потому, что шаг, который он заменяет, — написать .env руками — отказывает молча: опечатка в токене панели не мешает серверу подняться и всплывает позже ошибкой инструмента посреди неродственного вопроса. Поэтому он проверяет каждый креденшл на работающей системе и различает три отказа — хост не ответил вовсе (DNS, TLS, закрытый порт), хост ответил и отверг креды, хост ответил тем, что не доказывает ничего (502, 429): чинятся они по-разному, а одно «login failed» отправило бы чинить не то.

Спрашивает он только про ту систему, которая у вас есть, и про режим доступа; всё прочее убрано за один вопрос Configure the optional settings? [y/N]. Часовой пояс читает у работающей SHM, а не угадывает: SHM пишет даты собственным локальным временем без офсета, и неверная зона молча сдвигает каждый возраст. Секретов не печатает. По умолчанию ставит ro; на rw требует написать слово rw и отдельно подтвердить — назвав перед этим, сколько инструментов появится и сколько из них пишут в настоящий биллинг и настоящую панель, посчитав по реестру в тот же момент. .env пишет с правами 0600 поверх копии прежнего, перенося переменные, о которых не спрашивал, и печатает команды подключения для Claude Code, Codex и opencode — но чужие конфиги не правит: мастер, переписывающий JSONC, однажды сломает кому-то рабочую настройку. Перезапускать его можно в любой момент, Enter сохраняет существующее значение. Без терминала он запускаться отказывается: MCP-клиент стартует сервер без TTY, и мастер, способный проснуться там, завис бы на вопросе, которого никто не видит.

Или руками

cp .env.example .env && chmod 600 .env    # и заполнить

Каждая переменная описана в .env.example. Отсутствующая или неверная роняет старт с указанием имени переменной и того, что от неё ожидается, вместо того чтобы всплыть позже непонятной ошибкой инструмента.

{
  "mcpServers": {
    "hq": {
      "command": "node",
      "args": ["/absolute/path/to/hq-mcp/apps/stdio/dist/index.js"]
    }
  }
}

Второй транспорт: MCP поверх HTTP

Тот же набор инструментов доступен по HTTP — это нужно, когда клиент не может запустить процесс сам: он в контейнере, на другой машине или их несколько. Отдельное приложение, конфигурация из того же .env:

# метка произвольная (её показывает /metrics), токен — не короче 24 символов:
# openssl rand -hex 24
HQ_MCP_HTTP_TOKENS='<label>:<token>' pnpm --filter @hq/http start
# hq-mcp http ready: url=http://127.0.0.1:42480 mode=ro profile=human tools=38 …

Без HQ_MCP_HTTP_TOKENS он не стартует вовсе, и отказывает раньше, чем соберёт клиентов к биллингу и панели. Слушает петлю; открыть его в сеть — HQ_MCP_HTTP_HOST=0.0.0.0, и об этом печатается предупреждение, потому что между сервером и сетью останется только этот токен. Порт — HQ_MCP_HTTP_PORT. Клиент подключается к /mcp, передавая токен обычным Authorization: Bearer:

{
  "mcpServers": {
    "hq": {
      "type": "http",
      "url": "http://127.0.0.1:42480/mcp",
      "headers": { "Authorization": "Bearer <тот же токен>" }
    }
  }
}

Маршрут бессессионный: Mcp-Session-Id не выдаётся и не требуется, поэтому за обратным прокси можно держать несколько копий процесса без липких соединений. Серверных сообщений у него нет, поэтому GET на SSE-поток и DELETE на закрытие сессии отвечают 405 — клиент MCP это понимает. Запрос с заголовком Origin отбивается 403: защита от DNS rebinding, см. «Ограничения».

Соседний /v1/tools — не MCP, а внутренний REST-фасад для ai-bot: одна ручка списка и одна на вызов, со своим конвертом ответа и своим потолком запросов.

Production HTTP image

Production-образ собирается только из проверенного 40-символьного lowercase commit SHA. Этот SHA запечатывается одновременно в OCI label, root-owned read-only файл и /healthz; entrypoint восстанавливает значение из файла, так что runtime-переопределение HQ_MCP_IMAGE_REVISION не меняет health evidence.

pnpm test && pnpm test:guards && pnpm typecheck && pnpm build
HQ_MCP_COMMIT_SHA="$(git rev-parse HEAD)"
test "${#HQ_MCP_COMMIT_SHA}" -eq 40
docker build --build-arg "HQ_MCP_DEPLOYMENT_REVISION=${HQ_MCP_COMMIT_SHA}" --tag "hq-mcp-http:${HQ_MCP_COMMIT_SHA}" .
scripts/http-container-smoke.sh "hq-mcp-http:${HQ_MCP_COMMIT_SHA}"

Compose-потребитель фиксирует именно этот 40-символьный tag и не объявляет host ports. Контейнер работает как UID/GID 10001, в bot+ro публикует только /healthz и REST-фасад и требует общий non-secret HQ_MCP_DEPLOYMENT_CONFIG_REVISION в формате lowercase UUID. В health входят обе revision, чтобы ai-bot мог закрыться до чтения каталога при несовпадении.

Production передаёт секреты только через три regular non-symlink файла с точным mode 0600: SHM_ADMIN_AUTH_FILE, REMNA_API_TOKEN_FILE и HQ_MCP_HTTP_TOKENS_FILE. Последний содержит только ai-bot:<dedicated token>. Это отдельный токен сервера; креды SHM и Remnawave тоже выделяются этому deployment отдельно и не переиспользуются из support bot.

Подгонка под свою установку

Апстримный danuk/shm не знает слова «Remnawave» — ни строки. Мост между биллингом и панелью живёт целиком в ваших шаблонах провижининга: один пользователь панели на user_service_id, имя <NAME_PREFIX><user_service_id>, снимок конфигурации в storage SHM под <STORAGE_PREFIX><user_service_id>. Оба префикса сервер читает в рантайме из config.remnawave вашей SHM и позволяет переопределить (HQ_MCP_STORAGE_PREFIX, HQ_MCP_PANEL_PREFIXES) — оператор знает, что в панели лежит сегодня, лучше, чем ключ конфигурации, описывающий, что SHM соберёт завтра.

Имя пользователя панели — единственный ключ связи, и префикс, не совпадающий ни с чем, не даёт ошибки: он даёт уверенный неверный ответ, в котором каждая услуга выглядит непровижиненной. Поэтому инструменты, способные это доказать, говорят кодом prefix_unverified и подавляют затронутую находку — sync_audit не возвращает корзину missingPanelUser вовсе, provisioning_diagnose помечает результат тем же кодом или panel_username_guessed. Конвенция нужна ровно трём инструментам (sync_audit, provisioning_diagnose, мутатор storage_edit); client_overview принимает remna_user_id необязательным параметром и без него просто не показывает половину панели. Если конвенции у вас нет, все остальные инструменты работают как обычно, а эти три не выдумывают находок. Разбор целиком, с порядком префиксов и наследными именами, — в COMPATIBILITY.md.

Инструменты

Тридцать семь видны в ro; режим rw добавляет шестнадцать из последней таблицы и не убирает ничего. Числа — для профиля human; что из этого видит bot, сказано в модели безопасности.

Платформа и один клиент

Инструмент

На что отвечает

platform_probe

Что живо прямо сейчас: версии, возможности, туннели и является ли отказ аварией или кредами

client_resolve

Любой идентификатор (telegram id, email, логин, id, имя в панели) в канонические id обеих систем — все совпадения, а не первое

client_search

Поиск клиентов SHM по фрагменту, с серверным числом совпадений

client_overview

Клиент целиком в обеих системах за один вызов

client_account_state

Как учётка входит: email и его подтверждение, OTP, passkey, возможен ли вход паролем, рефералы

client_billing_view

Деньги глазами клиента: предстоящее списание и те платёжные методы, что реально ему предложены

client_catalog_view

Каталог и промокоды глазами одного клиента — его скидка, его бонусы, скрытые от него тарифы

Деньги, каталог, конфигурация

Инструмент

На что отвечает

billing_ledger

Платежи, бонусы, списания и две независимые сверки (баланс и бонус — разные колонки с разными путями обновления)

autopay_inspect

Состояние автоплатежа и все удержанные комиссии — оно лежит в JSON-поле comment платёжных строк, а не в user.settings

promo_read

Промокоды и их погашения: это разные строки, и читать их с одной нельзя

catalog_read

Тарифы, прайс заказа, дочерние услуги, карта событий, категории — источник допустимых service_id

config_read

Один ключ конфигурации SHM из закрытого списка, секреты замаскированы. Чтения конфигурации целиком не существует

template_read

Список шаблонов или тело ровно одного — того файла, который и производит уведомление или скрипт провижининга

Услуги и провижининг

Инструмент

На что отвечает

service_inspect

Услуги клиента: статус, срок, запланированный следующий тариф, задачи спула по каждой

spool_inspect

Очередь провижининга: залипшие, упавшие, приостановленные и реальная глубина

provisioning_diagnose

«Оплачено, а конфига нет» — по каждой услуге, а не по клиенту

sync_audit

Пакетная сверка биллинга с панелью, обе стороны вычитываются до конца

notify_history

Сказали ли клиенту на самом деле, а если нет — почему; вердикт доставки, которого не показывает больше ничто

server_inventory

Собственные транспорты SHM и их группы (ssh, http, mail, telegram) и разрывы, молча останавливающие провижининг. Это не список нод Remnawave

Панель — сперва со стороны клиента, затем со стороны флота

Инструмент

На что отвечает

subscription_inspect

Карточка Remnawave: статус, срок, трафик, HWID-устройства, последние обращения за подпиской. Ключи — никогда

subpage_read

Что страница подписки реально показывает клиенту: платформы, приложения, шаги установки, ссылки кнопок

client_reach

До каких нод этот клиент реально дотягивается и какие сквады и теги инбаундов это дают

device_inventory

Картина HWID по всему флоту — та база, без которой число устройств одного клиента ничего не значит

traffic_stats

Трафик по дням в разрезе нод и сквадов; это временной ряд, а не счётчики карточки

connections_inspect

Кто подключён прямо сейчас. Панель отвечает на это джобом, и опрос инструмент ведёт сам

infra_map

Ноды × профили конфигурации × инбаунды × хосты × сквады и разрывы между ними

infra_costs

Сколько стоит инфраструктура, в стыке с панелью: оплаченная нода, до которой никто не доходит, — это уходящие деньги

country_health

Ноды, онлайн, трафик и хосты одной страны

server_status

Для бота: доступность серверов по странам (ok/degraded/down и почему) и имена серверов, которые клиент видит в своём приложении, по форматам подписки. Без адресов, портов и ключей

node_config_audit

Профиль и вычисленный Xray-конфиг, отдельно — интеграции ноды и ссылки на общие списки

squads_read

Оба семейства сквадов: внутренние решают доступ, внешние — как подписка подана

panel_activity

Что происходит с самой панелью: сводка, дайджест за окно, какие маршруты дёргают, история обращений за подпиской

torrent_reports

Улики торрент-блокера, его параметры и зависимости от общих списков

node_integrations_read

Каталог интеграций и порядок их привязок к нодам, без значений конфигурации

shared_lists_read

Типы и размеры общих списков, зависимости плагинов и нод, отсутствующие ссылки

node_geocheck

Запуск диагностики ноды и отдельное чтение результата по job_id; краткий отчёт без SVG и сырых данных

За туннелем (эти два без него отказывают, называя точную ssh-команду)

Инструмент

На что отвечает

abuse_report

Находки антиабуз-хука плюс топы панели. Дорого: неограниченные сканы рабочей MySQL биллинга, потолок 5 вызовов на 5 минут

sql_query

SQL только на чтение — префлайт и ничего больше, см. ниже

Пишущие (только rw, только профиль human, сначала план)

Инструмент

Что меняет

billing_adjust

Баланс или бонусы клиента SHM

billing_refund_service

Возвращает на баланс сумму, которую SHM записал снятой за текущий оплаченный период

bulk_ops

Массовые операции над клиентами панели — по названному набору id или по всему флоту

host_edit

Один хост Remnawave: подпись, адрес, порт, SNI/host/path/ALPN/fingerprint, слой безопасности, теги, включение и скрытие, Host Mapper с резервной копией

host_cleanup

Удаляет хосты по явному списку uuid; полный снимок для ручного восстановления хранится в закрытой резервной копии. Автоматического отката нет

node_manage

Одна нода: enable, disable, restart, reset_traffic, update, create; упорядоченные integration_uuids в create/update

panel_sync

Отправляет плагин или общий список подходящим подключённым нодам; подтверждение означает принятие в очередь

subscription_ops

Одна подписка в панели: enable, disable, extend, reset_traffic, revoke, set_limits, снятие устройств

service_lifecycle

Услуга клиента: give, touch, change_plan, schedule_change, stop, activate, delete

provisioning_repair

retry, resume или pause одной залипшей задачи спула

template_edit

Перезаписывает тело существующего шаблона SHM

storage_edit

Пишет пользовательский storage SHM по списку ключей, выведенному для этой установки

server_edit

Строка транспорта или группа транспортов SHM — вебхуки, ssh-точка провижининга, почтовые отправители

user_flags

Блокирует клиента или правит безопасные поля карточки (full_name, phone, comment)

ops_confirm

Применяет план по его plan_id. Пишет то, что пишет запланированный инструмент

ops_audit

Ничего. Читает локальный журнал мутаций — rw потому, что журнал есть часть мутационной поверхности

Мутации

Ничто не применяется тем вызовом, который об этом просит. Мутатор без plan_id читает текущее состояние, строит целевое и возвращает план: before, after, diff по полям, побочные эффекты, rollback там, где он есть, и идентификатор. Не пишет ничего. Применение — второй вызов:

ops_confirm { "plan_id": "…" }          # либо: тот же мутатор, ТЕ ЖЕ аргументы, плюс plan_id

План привязан к профилю, который его построил, к инструменту, под который он построен, и к хешу аргументов: погасить его нельзя ни от другого вызывающего, ни другим инструментом, ни тем же инструментом с одним изменённым числом. Живёт 10 минут. Одноразовость — атомарный rename на диске, а не «прочитать и удалить»: из двадцати одновременных подтверждений выигрывает ровно одно, остальные получают «не найдено». Отказ исправный план не сжигает — все проверки идут после захвата, и провалившаяся возвращает файл на место; сжигает его сама попытка, и если бэкенд упал, план израсходован. Это намеренно, и в этом разница между одним списанием и тремя. Перед применением инструмент перечитывает мир и сверяет его со снимком, из которого план строился: сдвинулся объект — план отвергается, а не накатывается поверх чужого изменения.

Каждая попытка журналируется в HQ_MCP_AUDIT_PATH (JSONL, права 0600): кто, чем, с какими аргументами, как объект выглядел до и после и чем кончилось — planned, applying, applied, failed или rejected; отказы наравне с успехами. applying пишется до обращения к бэкенду, и в этом весь смысл конструкции: запись без парной терминальной означает, что процесс умер посреди, снимок плана уже уничтожен, а деньги могли уйти. ops_audit ищет такие незакрытые записи по всему журналу, независимо от запрошенного окна, и сообщает о них первыми; неразобранные строки считаются, а не пропускаются молча.

Потолки держит фреймворк, а не автор инструмента. Мутация выше HQ_MCP_MAX_OP_AMOUNT отвергается до построения плана, и фреймворк отказывается зарегистрировать инструмент, который объявил денежный эндпоинт, но не сказал, как прочитать сумму из его входа. Потолок накрывает оба вида движения денег, и второй легко упустить: и платежи с бонусами, где сумму называет вызывающий, и действия жизненного цикла, тратящие баланс клиента (give, touch, change_plan, activate), где сумма — это цена тарифа из каталога. План, у которого цену прочитать не удалось, не выдаётся: незнание числа не делает списание бесплатным. HQ_MCP_MAX_BULK_USERS ограничивает, скольких клиентов панели вправе задеть одна массовая операция, и план, не сумевший установить это число у панели, отвергается, а не оценивается на глаз. Выше потолка операция отвергается целиком — никогда не усекается.

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

template_edit и storage_edit сперва пишут собственный откат в HQ_MCP_BACKUP_DIR (каталог 0700, файлы 0600); путь возвращается в ответе, restore_from кладёт байты обратно, и без снятого снимка не пишет ни один из двух. Бэкап отделён от снимка плана намеренно: тела шаблонов и снимки конфигурации несут секреты голыми подстроками, у которых нет имени поля, чтобы их замаскировать, — значит, им нельзя ехать обратно к модели внутри before/rollback; и откат обязан пережить смену, тогда как снимки планов подметаются в течение часа.

host_edit с полем mapper использует такой же закрытый каталог: значения mapper хранятся в резервной копии, а restore_from строит подтверждаемый план возврата. host_cleanup сохраняет там полные удаляемые хосты, включая mapper, и возвращает backupRef с путём и хешем. Перед удалением сверяются целостность копии и текущее содержимое хостов. Эта копия предназначена для ручного пересоздания: автоматического восстановления удалённых хостов нет.

Ещё две вещи пишущие делать отказываются. Тело с маркерами <redacted:…> не записывается никогда: это вывод читающего инструмента, и запись его заменила бы живой креденшл тем словом, которым его спрятали. И сырые блобы панели (finalMask, xhttpExtraParams, muxParams, sockoptParams) исключены из любого патча хоста — в работающей установке заметная часть хостов несёт внутри finalMask рабочий пароль Hysteria2.

Что доказано на самом деле, а что нет

host_edit — единственный мутатор, чья ветка применения прогонялась на работающей системе: на работающей панели Remnawave 3.2.3 сменили подпись хоста, проверили, что пароль в finalMask уцелел и что не изменилось ничего сверх заявленного поля, и откатили обратно. Эта проверка относится к правке подписи на 3.2.3. Новые mapper, привязки интеграций, синхронизация и резервные копии удаляемых хостов в обновлении 3.3.2 проверены локальными тестами; записи на живой панели при этой миграции не выполнялись.

Модель безопасности

Два профиля. human — доверенный оператор, и ему достаются конкретные пригодные к действию отказы, включая точную ssh-команду, когда туннель закрыт. bot — недоверенный канал: любой отказ схлопывается в одно и то же сообщение, чтобы реестр нельзя было перебрать, нащупывая, какие имена отвечают иначе. Ни один пишущий инструмент боту не предлагается никогда: в rw профиль bot видит те же двадцать читающих инструментов, что и в ro.

Запрещённый класс, отдельный от просто опасного. Эти операции не закрыты воротами — их нет, и сканер на этапе сборки роняет прогон, если их путь встретился в исходнике литералом. Маршруты identity и keygen нод (GET, у которого в теле ответа приватный ключ). Маршруты токенов, авторизации и passkey (панель отдаёт токены открытым текстом, а созданный токен — это постоянный админ мимо всех ворот). Настройки панели и подписки. Выгрузка /admin/config целиком. Ручная пометка задачи провижининга успешной — она не выполняет работу, а лишь переводит услугу в ACTIVE при по-прежнему отсутствующем пользователе в панели. Удаление платежа, бонуса или списания — голый DELETE FROM по реестру, при котором users.balance не пересчитывается. Готовые к употреблению ссылки подписки и connection-keys. restart-all, reorder, bulk-actions сквадов и PUT /admin/spool с job_users — рассылка всем клиентам без отмены.

Класс сузился, и каждое сужение было исправлением, а не послаблением. Чтение шаблонов было запрещено вместе с записью, хотя причина — нет гита, нет отката — говорила только про запись; ширина стоила не теоретически: заметная доля уведомлений за одно наблюдавшееся окно отрендерилась пустыми и не отправила ничего, задача при этом отчиталась SUCCESS, а причина молчания лежит внутри тела шаблона. Теперь чтение открыто, POST — под template_edit, который принёс с собой откат, а PUT и DELETE закрыты: у только что появившегося и у только что исчезнувшего шаблона нет предыдущего состояния, которое можно снять. Запрет на /api/sub был префиксным и заодно накрывал /api/subscription-page-configs и /api/subscription-request-history — два читающих контроллера, ключей не выдающих; теперь это exact плюс prefix на /api/sub/. Массовые операции над клиентами панели запрещались потому, что применяются ко всей базе без списка на просмотр, — верно ровно до тех пор, пока никто не считает: bulk_ops считает у панели до применения, отказывается, когда число установить не удалось или оно выше HQ_MCP_MAX_BULK_USERS, и боту не предлагается.

POST /api/users/bulk/delete-by-status остаётся запрещённым по своей форме: в его теле статус, а не список людей. Панель ставит задачу в очередь и удаляет тех, кто подойдёт в момент её выполнения, — не тех, кого просматривал оператор, — и отвечает 202 с пустым телом и без счётчика, так что учётки, истёкшие в промежутке, удаляются невидимо. Возможность сохранена как bulk_ops delete_by_status: он перечисляет конкретные id, показывает их и удаляет ровно их через bulk/delete. Массовые маршруты на других сущностях — хосты, ноды, сквады, рассылки спула — такого шага подсчёта не имеют и остаются отсутствующими.

Секреты маскируются на выходе — и по имени ключа, и по форме значения. По имени: закрытый список кредовых ключей, совпадение с token|secret|key|password|auth при явном списке исключений, хвостовая маскировка для нескольких и маскировка PII для профиля bot. Этого мало, и за один день это подвело трижды: токен Telegram-бота ехал внутри response.request.url строки спула (ключ называется url), он же лежал в колонке host транспортных строк SHM, а тела шаблонов несут креденшлы голыми подстроками, рядом с которыми имени поля нет вовсе. Поэтому обход прогоняет каждую проходящую строку ещё и через правила формы значения: JWT; NAME=<значение>, где имя обещает секрет, а значение не похоже на плейсхолдер; токены Telegram-бота с окружающим путём и без него; user:password@ внутри URL. Живёт он внутри redact, которую зовут оба HTTP-клиента на входе и исполнитель на выходе, — помнить об этом не обязан ни один отдельный инструмент.

Правила откалиброваны, а не угаданы, и калибровка объявлена прямо в исходнике: порог «непрозрачного прогона» (32+ символа, выглядящих случайно) измерен на настоящих телах шаблонов и выключен на структурированных ответах API, где его перешагивают data-URI иконки и hex uniq_id платежа — вырезав их, чистка погасила бы ровно те поля, ради которых инструмент и писали. Границей безопасности это всё равно не является, и исходник так и говорит: у секрета, написанного словами, формы нет; всё, что проходит фильтр, остаётся внутри профиля human. Те же правила использует scripts/no-secrets.test.ts, не пускающий секрет в публикуемый коммит: две копии знания о том, «как выглядит секрет», расходятся молча, и вторая продолжает выглядеть работающей.

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

Что стоит знать про ограничения

  • HTTP-транспорт говорит на MCP (/mcp, streamable HTTP) и отдаёт тот же набор инструментов, что stdio: публикует их одна функция на оба транспорта. Чего он намеренно не умеет: сессий (Mcp-Session-Id не выдаётся), server-initiated сообщений, а с ними — потока SSE на GET и возобновления по Last-Event-ID. Каждый вызов самодостаточен, поэтому сервер и транспорт создаются свежими на запрос; этого требует и сам SDK, чей бессессионный транспорт запрещено переиспользовать.

  • По маршруту /mcp два исхода исполнителя недостижимы, и счётчики /metrics видят по нему два из четырёх. Негодный вход разбирает SDK ДО инструмента и сам отвечает -32602; несуществующее имя он тоже отбивает сам, не доходя до реестра. Поэтому invalid_input и not_found по этому маршруту не появляются ни в ответе, ни в отчёте. На REST-фасаде достижимы оба.

  • Запрос к /mcp с заголовком Origin отбивается 403 без вариантов: сервер слушает петлю, а страница в браузере может увести свой домен на 127.0.0.1 и ходить сюда от имени оператора. Браузер проставляет Origin на любом POST кросс-происхождения, настоящий клиент MCP — никогда, а заголовков CORS сервер не отдаёт, поэтому браузерного клиента у него нет и быть не может. Встроенные allowedHosts/allowedOrigins для этого не годятся: в этой версии SDK они помечены устаревшими в пользу внешнего middleware, а пустой список origin-ов у них означает «проверка выключена», а не «никакой origin не годится».

  • Двум инструментам нужен туннель во внутреннюю сеть, и без него они отказывают. Видимыми они остаются намеренно: исчезнувший инструмент учит модель, что такой возможности не существует, тогда как на деле закрыт порт.

  • sync_audit вычитывает обе системы до конца и является здесь единственным дорогим вызовом — ради этого у него собственная норма запросов.

  • Размер страницы панели измеряется в рантайме, а не предполагается: API не объявляет максимума, а фактический менялся между релизами.

  • Массовые маршруты панели отвечают 202 или 204 с пустым телом и часть работы ставят в очередь, поэтому «применено» означает «принято панелью», а не «сделано для всех». Число, установленное планом заранее, — единственное честное, какое здесь вообще есть.

  • Правки, сделанные в панели, не переносятся обратно в биллинг SHM, и инструменты, которые их делают, об этом говорят. Шага сверки нет; расхождение вам потом покажет sync_audit.

Разработка

pnpm test          # модульные тесты
pnpm typecheck
pnpm test:guards   # сканер секретов и предохранители скрипта захвата фикстур

Тесты гоняются на фикстурах, повторяющих форму настоящих ответов. Там, где дефект был виден только на работающей системе, тест, который его закрепляет, так и говорит.

Лицензия

MIT.

Related MCP Connectors

Related MCP Servers

  • A
    license
    B
    quality
    F
    maintenance
    An MCP server that enables LLM clients to manage Remnawave VPN panels through 51 specialized tools for user, node, and subscription administration. It provides real-time access to panel statistics, health checks, and guided workflows for system diagnostics.
    51
    7 npm
    98
    MIT
  • A
    license
    A
    quality
    D
    maintenance
    A read-only MCP server for securely accessing Xendit payment platform data. It enables querying balances, invoices, transactions, disbursements, refunds, and virtual account payments while preventing any money-moving operations.
    13
    MIT
  • A
    license
    B
    quality
    C
    maintenance
    MCP server for the DataGate billing platform API, providing read-only tools to manage customers, invoices, products, agreements, sites, and payments.
    13
    MIT