hq-mcp
Read-only MCP server for VPN business operations, integrating SHM billing and Remnawave panel into composite tools for cross-system queries.
README
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.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": "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 добавляет
пятнадцать пишущих: тринадцать меняют боевые данные, один применяет план, ещё
один читает локальный журнал мутаций.
Почему составные инструменты, а не прокси эндпоинтов
Очевидная конструкция — по инструменту на 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.
推荐服务器
Baidu Map
百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。
Playwright MCP Server
一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。
Magic Component Platform (MCP)
一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。
Audiense Insights MCP Server
通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。
VeyraX
一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。
graphlit-mcp-server
模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。
Kagi MCP Server
一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。
e2b-mcp-server
使用 MCP 通过 e2b 运行代码。
Neon MCP Server
用于与 Neon 管理 API 和数据库交互的 MCP 服务器
Exa MCP Server
模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。