Hq
FreeNot checkedRead-only MCP server for VPN business operations, integrating SHM billing and Remnawave panel into composite tools for cross-system queries.
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.
Installing Hq
This server has no published package — it is built from source. Open the repository and follow its README.
▸ github.com/qwertyhq/hq-mcpFAQ
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
Stripe
Payments, customers, subscriptions
by Stripemalamutemayhem/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
by malamutemayhemwhiteknightonhorse/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
by whiteknightonhorsetrackerfitness729-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
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
by embeddedlayerscarrierone/verilexdata-mcp
20 structured datasets (NPI healthcare, SEC filings, OFAC sanctions, crypto whales, Polymarket signals, patents, economic indicators) via x402 pay-per-query wit
by carrieronetipdotmd/tip-md-x402-mcp-server
MCP server for cryptocurrency tipping through AI interfaces using x402 payment protocol and CDP Wallet.
by tipdotmdlaundromatic/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
by laundromaticmrslbt/xendit-mcp
Xendit payment gateway for Southeast Asia. Invoices, disbursements, balance checks, and bank transfers across Indonesia, Philippines, Thailand, Vietnam, and Mal
by 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
by jiayuanliang0716-maxCompare Hq with
Not sure what to pick?
Find your stack in 60 seconds
Author?
Embed badge for your README
Browse similar
All finance MCPs
