Command Palette

Search for a command to run...

UnylyUnyly
Browse all

Hq

FreeNot checked

Read-only MCP server for VPN business operations, integrating SHM billing and Remnawave panel into composite tools for cross-system queries.

GitHubEmbed

About

Read-only MCP server for VPN business operations, integrating SHM billing and Remnawave panel into composite tools for cross-system queries.

README

Русская версия · 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.2.3",
             "runtime": { "instances": 6, "youngestUptimeSeconds": 54294 } },
  "capabilities": { "shm.filter": false, "remna.realtimeBandwidth": true,
                    "tunnel.mysql": false, "…": "…" },
  "warnings": [{ "code": "specs_are_stale", "message": "…" }]
}

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

client_resolve { "query": "[email protected]" }
{
  "shm":   { "count": 1, "matches": [{ "user_id": 4821, "email": "[email protected]",
                                      "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 добавляет пятнадцать пишущих: тринадцать меняют боевые данные, один применяет план, ещё один читает локальный журнал мутаций.

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

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

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

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

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

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

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

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

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

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

Минимум — SHM 2.18.0 и Remnawave 3.0.0. Официальный danuk/shm подходит: все маршруты, которые зовут инструменты, — апстримные, форк не нужен. Единственное место, где патч того развёртывания был виден инструменту, — четвёртый флаг GET /user/password-auth; теперь его отсутствие называется предупреждением sign_in_flag_absent, а не выдаётся за диагноз. Полный перечень маршрутов обеих систем, версия появления каждого и подробный ответ про форк — в 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

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 прямо называет, что настроено. Поэтому число инструментов зависит от установки: только панель — 16, только SHM — 18, обе — 34 (и больше в режиме 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=34 …

Без 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: одна ручка списка и одна на вызов, со своим конвертом ответа и своим потолком запросов.

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

Апстримный 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 Ноды, онлайн, трафик и хосты одной страны
node_config_audit Что профиль объявляет против того, что панель на самом деле отдала бы ноде
squads_read Оба семейства сквадов: внутренние решают доступ, внешние — как подписка подана
panel_activity Что происходит с самой панелью: сводка, дайджест за окно, какие маршруты дёргают, история обращений за подпиской
torrent_reports Улики торрент-блокера — и, отдельно, установлен ли он вообще и следит ли

За туннелем (эти два без него отказывают, называя точную 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_cleanup Удаляет хосты по явному списку uuid. Необратимо
node_manage Одна нода: enable, disable, restart, reset_traffic, update, create
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; и откат обязан пережить смену, тогда как снимки планов подметаются в течение часа.

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

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

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

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

Два профиля. 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 — рассылка всем клиентам без отмены.

Класс сузился, и каждое сужение было исправлением, а не послаблением. Чтение шаблонов было запрещено вместе с записью, хотя причина — нет гита, нет отката — говорила только про запись; ширина стоила не теоретически: 43 % уведомлений в одном боевом окне отрендерились пустыми и не отправили ничего, задача при этом отчиталась 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+ символа, выглядящих случайно) измерен на 197 боевых телах шаблонов и выключен на структурированных ответах 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.

from github.com/qwertyhq/hq-mcp

Installing Hq

This server has no published package — it is built from source. Open the repository and follow its README.

▸ github.com/qwertyhq/hq-mcp

FAQ

Is Hq MCP free?

Yes, Hq MCP is free — one-click install via Unyly at no cost.

Does Hq need an API key?

No, Hq runs without API keys or environment variables.

Is Hq hosted or self-hosted?

A hosted option is available: Unyly runs the server in the cloud, no local setup required.

How do I install Hq in Claude Desktop, Claude Code or Cursor?

Open Hq on unyly.org, pick your client tab (Claude Desktop, Claude Code, Cursor) and press Install — the config is generated automatically, no JSON editing.

Related MCPs

$5

Stripe

Payments, customers, subscriptions

Stripeby Stripe

malamutemayhem/unclick-agent-native-endpoints

110+ tools for AI agents spanning social media, finance, gaming, music, AU-specific services, and utilities. Zero-config local tools plus platform connectors. n

malamutemayhemby malamutemayhem

whiteknightonhorse/APIbase

Unified API hub for AI agents with 56+ tools across travel (Amadeus, Sabre), prediction markets (Polymarket), crypto, and weather. Pay-per-call via x402 micropa

whiteknightonhorseby whiteknightonhorse

trackerfitness729-jpg/sitelauncher-mcp-server

Deploy live HTTPS websites in seconds. Instant subdomains ($1 USDC) or custom .xyz domains ($10 USDC) on Base chain. Templates for crypto tokens and AI agent pr

trackerfitness729-jpgby trackerfitness729-jpg

embeddedlayers/mcp-analytics

Statistical analysis, forecasting, and ML for business data (Shopify, Stripe, WooCommerce, eBay, GA4, Search Console). Upload a CSV or connect live data sources

embeddedlayersby embeddedlayers

carrierone/verilexdata-mcp

20 structured datasets (NPI healthcare, SEC filings, OFAC sanctions, crypto whales, Polymarket signals, patents, economic indicators) via x402 pay-per-query wit

carrieroneby carrierone

tipdotmd/tip-md-x402-mcp-server

MCP server for cryptocurrency tipping through AI interfaces using x402 payment protocol and CDP Wallet.

tipdotmdby tipdotmd

laundromatic/shopgraph

Structured product data from the open web — Schema.org + AI extraction for e-commerce enrichment. Pay per call via Stripe. [shopgraph.dev](https://shopgraph.dev

laundromaticby laundromatic

mrslbt/xendit-mcp

Xendit payment gateway for Southeast Asia. Invoices, disbursements, balance checks, and bank transfers across Indonesia, Philippines, Thailand, Vietnam, and Mal

mrslbtby mrslbt

@arbitova/mcp-server

Non-custodial on-chain escrow + AI dispute arbitration for agent-to-agent USDC payments on Base. Seven tools covering the full EscrowV1 contract surface: create

jiayuanliang0716-maxby jiayuanliang0716-max

Compare Hq with

Not sure what to pick?

Find your stack in 60 seconds

Author?

Embed badge for your README

Browse similar

All finance MCPs