MCP-сервер zapret-gui — справочник для сессий S5+

SkillProductivity

Lets your agent read zapret-gui's tool-server codebase to answer questions about tools, permissions, secrets, and diagnostics.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the MCP-сервер zapret-gui — справочник для сессий S5+ skill

About this capability

Implementation of an MCP server (Model Context Protocol) inside zapret-gui: the `POST /api/mcp` endpoint, tool registry, permissions, and secret editing. Use for any tasks about: our MCP tools and their declaration (the `@tool` decorator, scope/mutating, argument schema, response format `content`+`

What this skill tells your AI

The instructions your AI receives, as published by avatardd/zapret-gui in .claude/skills/mcp/SKILL.md and read by ahel’s review.

Слепок того, как устроен MCP в этом репозитории. Читать вместо того, чтобы заново разбирать уже написанный код: контракт (docs/mcp/00-contract.md) говорит, что строим, этот файл — как оно сделано сейчас.

Обновляется каждой сессией: добавили инструмент — добавили строку в таблицу и в tests/test_mcp_tool_counts.py.

Ревизия спеки: 2025-06-18 (server.PROTOCOL_VERSION; понимаются также 2025-03-26 и 2024-11-05). Ограничение на весь пакет — только stdlib.

Карта кода

ФайлЧто в нём
api/mcp.pyHTTP: POST /api/mcp, GET/DELETE → 405, GET /api/mcp/info, legacy-SSE (GET /api/mcp/sse, POST /api/mcp/messages)
core/mcp/server.pyдиспетчер JSON-RPC, методы протокола, псевдонимы реестра
core/mcp/registry.py@tool, проверки объявления, автозагрузка, call(), tool_result()
core/mcp/permissions.py11 разрешений, зависимости, whitelist настроек на запись
core/mcp/audit.pyжурнал вызовов (JSONL + ротация), снимки «до», диспетчер отката
core/mcp/redact.pyмаскировка секретов (ключи, URL, сырой текст)
core/mcp/schema.pyмини-валидатор JSON Schema + normalize_tool_schema()
core/mcp/auth.pybind → Origin → токен → рейт-лимит, settings(), permissions()
core/mcp/resources.pyресурсы zapret://…, единая точка рендера
core/mcp/config_docs.pyописания настроек (данные, не код)
core/mcp/prompts.pyпромты-сценарии
core/mcp/session.pyсессии legacy-SSE: очередь ответов, живость потока, уборка мёртвых, рассылка tools/list_changed
core/mcp/stdio.pystdio-мост: JSON-RPC построчно, локально или прокси в чужую точку
api/mcp_ui.pyне протокол, а панель: /api/mcp/ui/* для страницы MCP — одно состояние и действия (токен, разрешения, откат, аварийный запрет shell)
web/js/pages/mcp.jsстраница «MCP-сервер»: включение, токен, разрешения, сниппеты, журнал, эксперимент, самоправка, shell
core/mcp/tools/*.pyсами инструменты, по модулю на домен
core/mcp/tools/_paging.pyобщая форма списка и окно под лимит ответа (реестр модули с _ пропускает)
core/mcp/tools/_jobs.pyасинхронная задача: job_id, опрос после конца прогона, отказ второму старту (реестр модули с _ пропускает)
core/nfqws_control.pyне в пакете MCP: старт/стоп/перезапуск/SIGHUP, применение и сброс стратегии, running(), busy() — одним кодом для UI, CLI и MCP
core/probe_runner.pyне в пакете MCP: пробы по списку целей, сравнение «с обходом и без», лимиты mcp.probes
core/nfqws_session.pyне в пакете MCP: общий мьютекс на nfqws2/firewall (acquire/holder), снимок состояния и возврат «как было»
core/strategy_experiment.pyне в пакете MCP: движок экспериментов — варианты, baseline, метрики, правила-подсказки, дедмен-свитч и снимок на диске
core/shell_exec.pyне в пакете MCP: исполнение команд — safe-список, запреты, подтверждения, дедмен-свитч, фоновые задачи
core/code_editor.pyне в пакете MCP: самоправка — границы, staging, слепок дерева, проверки, снимки, применение
core/code_guard.pyне в пакете MCP и без единого нашего импорта: сторож перезапуска, отдельный процесс, откат по health-check и по TTL
core/cli.pyне в пакете MCP: подкоманда zapret-gui mcp … — status/tools/call/token/audit/code и точка входа моста

Как объявляется инструмент

from core.mcp.registry import tool

@tool(
    name="logs_tail",           # <домен>_<действие>, snake_case
    scope="read",               # "read" или одно из permissions.PERMISSIONS
    mutating=False,             # True — меняет состояние устройства
    title="Tail GUI log",
    description=("EN first, RU after the slash / английский и русский "
                 "одной строкой, ≤ 300 символов"),
    schema={"type": "object", "properties": {...},
            "additionalProperties": False},
)
def logs_tail(args: dict) -> dict:
    """Короткий docstring по-русски."""
    from core.log_buffer import get_log_buffer   # импорт — внутри!
    return {"ok": True, "items": [...], "count": 0}

Правила, которые проверяются на импорте (registry.ToolError):

  • имя — snake_case, не занято;
  • описание непустое и ≤ 300 символов (registry.MAX_DESCRIPTION);
  • scope и mutating объявлены явно; mutating=True при scope="read" — ошибка (такой инструмент был бы доступен без разрешения);
  • схема разбирается нашим валидатором: неизвестный type, required без такого properties, не-объект в properties — ошибка.

Модуль в core/mcp/tools/ подхватывается сам (pkgutil), перечислять его нигде не надо. Импорт ленивый — при первом обращении к реестру.

Форма ответа

{"content": [{"type": "text", "text": "<тот же JSON строкой>"}],
 "structuredContent": {"ok": true, "...": "..."},
 "isError": false}
  • обработчик возвращает обычный dict; ok и elapsed_ms дописываются сами;
  • ошибка инструмента — не ошибка JSON-RPC: isError: true + error + hint, коды -32700…-32603 остаются про сам протокол;
  • одинаковые поля при успехе и неуспехе; списки — items + count + truncated; «нет данных» — честный ответ и то, что есть рядом (available, sources);
  • сериализация ровно однаregistry.tool_result(): там маскировка секретов и обрезка по mcp.limits.response_kb (вместо обрубка JSON отдаётся {truncated: true, size_bytes, limit_bytes, hint}).

Разрешения

settings.json → mcp.permissions, все по умолчанию false; чтение переключателя не имеет и доступно всегда.

КлючЧто открываетЗависит от
controlстарт/стоп/перезапуск движков, применение стратегий
strategies_writeCRUD стратегий, hostlist'ов, ipset'ов, lua
config_writeзапись в whitelisted-поддеревья настроек
probesактивные пробы (трафик с роутера), blockcheck, сканер
experimentsдвижок экспериментовcontrol, probes
tunnels_writeконфиги и запуск туннелей
dangerousбинарники, автозапуск, миграции, unified-правила, ребут
shell_readonlysafe-команды, чтение файлов и каталогов
shell_fullпроизвольная команда от root, запись файлов, пакетыоткрывает shell_readonly
self_editчтение и правка модулей GUI (13 инструментов code_*)
self_edit_coreправка защищённого ядра; своих инструментов нет, спрашивается по местуself_edit
any_write (псевдо)не переключатель: открывается ЛЮБЫМ из WRITE_PERMISSIONS. Нужен одному mcp_undo_last

IMPLIES — обратная сторона REQUIRES (S12). shell_full включает shell_readonly сам: кому отдали произвольную команду от root, тому df -h уже отдали. Без этого пользователь видел бы включённый shell_full и половину невидимых инструментов (чтение файлов, список пакетов, статус служб) — и читал бы это как поломку. Порядок в effective() значим: сначала гасим невыполненные зависимости, потом раздаём вложенные, иначе снятое зависимостью разрешение успело бы открыть своё вложенное. В describe() у такого разрешения появляется implied_by, у открывающего — opens.

Зависимость, которая не выполнена, не игнорируется молча: permissions.denial(scope, perms) возвращает error, requires, missing и hint — иначе пользователь видит включённый флаг и выключенные инструменты. /api/mcp/info отдаёт и permissions (как стоят), и permissions_effective (как действуют), и permissions_info (таблица для UI), и tools_by_scope.

Инструменты (обновлять каждой сессией)

ИмяScopeMut.ФайлЧто делает
system_statusreadнетtools/status.pyплатформа, аптайм, память, какие движки подняты
nfqws_statusreadнетtools/status.pyдвижок nfqws2: pid, аптайм, argv, код выхода
config_getreadнетtools/config.pyнастройки по точечному пути, с флагом writable
logs_tailreadнетtools/logs.pyхвост журнала: source, level, search, since, limit ≤ 200
docs_getreadнетtools/docs.pyлюбой ресурс zapret://… постранично: uri/topic, section, offset/limit
config_describereadнетtools/docs.pyописание настройки: тип, дефолт, единица, что значит 0/пусто, writable
strategy_listreadнетtools/strategies.pyстратегии (builtin+user) с is_active; фильтры protocol/level/source/featured/active_only
strategy_getreadнетtools/strategies.pyодна стратегия целиком: профили, их args, techniques, blob'ы
catalog_searchreadнетtools/strategies.pyпоиск по INI-каталогам: query, technique, protocol, level, label
nfqws_command_previewreadнетtools/strategies.pyитоговый argv стратегии — через build_preview_command, как при живом запуске
strategy_state_listreadнетtools/strategies.pyвыученное circular'ом из state.tsv: host, group, номер, возраст
hostlists_listreadнетtools/lists.pyсписки доменов: сколько записей, путь, есть ли файл
hostlist_getreadнетtools/lists.pyокно одного списка + search; на 50 000 доменов отдаёт окно, не дамп
ipsets_listreadнетtools/lists.pyсписки IP: перечень, с name — содержимое
lists_listreadнетtools/lists.pyименованные списки единого слоя: домены и CIDR по списку
blobs_listreadнетtools/lists.pyреестр blob'ов и существует ли файл (missing_only)
lua_functions_listreadнетtools/lists.pyфункции --lua-desync с этого устройства: параметры, needs_blob
firewall_statusreadнетtools/firewall.pyправила NFQUEUE, бэкенд, queue_numbers, conflicts
traffic_recentreadнетtools/traffic.pyдошёл ли трафик до движка: домен/профиль/вердикт за N минут
tunnels_statusreadнетtools/tunnels.pyшесть движков одним ответом: установлен/запущен/конфиги/трафик/последняя ошибка
diagnostics_runreadнетtools/diagnostics.pyокружение, конфликты, предпосылки; сетевые пробы — по разрешению probes
dpi_reportreadнетtools/diagnostics.pyпоследняя классификация DPI из blockcheck; проб не запускает
updates_checkreadнетtools/updates.pyверсии движков и обновления; по умолчанию из кеша, refresh — по probes
config_writable_pathsreadнетtools/config.pyчто можно менять: путь, тип, текущее значение, enum
audit_listreadнетtools/audit.pyпоследние вызовы MCP из журнала, новые первыми, с пометкой «ещё откатывается»
config_setconfig_writeдаtools/config.pyзаписать ОДНУ настройку; ответ — дифф «было/стало», список заменяется целиком
nfqws_startcontrolдаtools/nfqws.pyправила перехвата + движок с активной стратегией; обратное — nfqws_stop
nfqws_stopcontrolдаtools/nfqws.pyостановить движок и снять правила
nfqws_restartcontrolдаtools/nfqws.pyперезапуск со свежесобранными аргументами активной стратегии
nfqws_reload_listscontrolдаtools/nfqws.pySIGHUP: перечитать списки БЕЗ перезапуска; в ответе signalled
strategy_applycontrolдаtools/nfqws.pyприменить стратегию по id; снимок вида strategy_active
firewall_applycontrolдаtools/firewall.pyпоставить правила NFQUEUE; порты управления исключаются
firewall_removecontrolдаtools/firewall.pyснять правила: трафик пойдёт напрямую
strategy_savestrategies_writeдаtools/strategies.pyсоздать/перезаписать USER-стратегию; профили заменяются целиком; validation — прогон --intercept=0
strategy_deletestrategies_writeдаtools/strategies.pyудалить USER-стратегию; builtin — отказ
hostlist_editstrategies_writeдаtools/lists.pyreplace/add/remove по списку доменов; SIGHUP; пустой список — предупреждение
ipset_editstrategies_writeдаtools/lists.pyто же для IP/CIDR; непринятые записи перечисляются
blob_addstrategies_writeдаtools/lists.pyзаписать blob из hex (≤ 64 КБ); builtin-имена — отказ
lua_script_savestrategies_writeдаtools/lists.pyсохранить lua-скрипт; битый синтаксис — отказ, force=true перебивает
mcp_undo_lastany_writeдаtools/audit.pyоткатить последнее изменение по снимку с диска (любой вид)
scan_statusreadнетtools/scan.pyпрогресс подбора: фаза, сколько проверено, baseline_open; job_id — опционально
scan_resultsreadнетtools/scan.pyчто нашёл подбор, лучшие первыми; failed=true — что НЕ сработало
blockcheck_statusreadнетtools/blockcheck.pyпрогресс НАШЕГО blockcheck; вердикт — в dpi_report
blockcheck2_statusreadнетtools/blockcheck.pyпрогон скрипта bol-van: идёт ли, код выхода, found, highlights
blockcheck2_outputreadнетtools/blockcheck.pyтелеметрия скрипта инкрементально: offsetnext_offset
healthcheck_statusreadнетtools/blockcheck.pyрасписание, сервисы, история и fail_streak; проб не запускает
connectivity_matrixreadнетtools/probes.pyматрица «цель × интерфейс»; refresh — по probes
probe_targetsprobesнетtools/probes.pyпроба доменов DNS→TCP→TLS→HTTP; коды из PROBE_CODES, состояния не меняет
probe_compareprobesдаtools/probes.pyдомен с обходом и без; вердикт из пяти; переключение движка требует ещё и control
scan_startprobesдаtools/scan.pyзапустить подбор стратегий; ответ — job_id, сразу
scan_stopprobesдаtools/scan.pyостановить подбор; проверенное остаётся в scan_results
blockcheck_startprobesдаtools/blockcheck.pyнаш blockcheck в фоне; отчёт потом — dpi_report
blockcheck2_startprobesдаtools/blockcheck.pyоригинальный скрипт zapret2 (DOMAINS/SCANLEVEL/REPEATS/…)
blockcheck2_stopprobesдаtools/blockcheck.pyприбить скрипт; собранная телеметрия остаётся читаемой
healthcheck_runprobesдаtools/blockcheck.pyразовый прогон healthcheck в фоне; результат — в healthcheck_status
scan_applycontrolдаtools/scan.pyприменить найденное: сохранить USER-стратегию и поднять движок; нужен ещё strategies_write
strategy_experiment_startexperimentsдаtools/experiments.pyпрогнать варианты стратегии с измерением; ответ — run_id, сразу
strategy_experiment_statusexperimentsнетtools/experiments.pyфаза, номер варианта, сколько осталось до авто-отката
strategy_experiment_resultexperimentsнетtools/experiments.pyотчёт: цифры по целям, score, дельта к baseline, лог движка, подсказки
strategy_experiment_commitexperimentsдаtools/experiments.pyоставить вариант применённым; save_as — ещё и strategies_write
strategy_experiment_rollbackexperimentsдаtools/experiments.pyвернуть состояние к снимку немедленно
strategy_experiment_stopexperimentsдаtools/experiments.pyостановить прогон; измеренное остаётся в отчёте
strategy_experiment_historyexperimentsнетtools/experiments.pyпрошлые прогоны этого процесса GUI, новые первыми
strategy_composestrategies_writeнетtools/compose.pyописание (фильтр/payload/инстансы) → argv + команда + линтер; ничего не сохраняет
strategy_validatestrategies_writeнетtools/compose.pynfqws2 --intercept=0 по strategy_id/args/profiles: опции, файлы и исполнение lua-init
shell_execshell_readonlyдаtools/shell.pyкоманда на роутере; safe-список и argv — по shell_readonly, произвольная строка (sh -c) — по shell_full
shell_exec_asyncshell_readonlyдаtools/shell.pyто же фоном: ответ — job_id, сразу
shell_job_statusshell_readonlyнетtools/shell.pyсостояние фоновой команды; без job_id — список всех
shell_job_outputshell_readonlyнетtools/shell.pyвывод фоновой команды инкрементально: offsetnext_offset
shell_job_stopshell_readonlyдаtools/shell.pyприбить фоновую команду; собранный вывод остаётся читаемым
shell_confirmshell_readonlyдаtools/shell.pyвторой шаг: confirm_token — исполнить, run_id — снять дедмен
file_readshell_readonlyнетtools/files.pyокно файла (offset/limit_kb/tail), секреты вырезаны
file_listshell_readonlyнетtools/files.pyкаталог полями: имя, размер, права, mtime, тип
package_listshell_readonlyнетtools/packages.pyчто установлено: opkg list-installed / apk list -I
service_listshell_readonlyнетtools/services.pyисполняемые скрипты /opt/etc/init.d и /etc/init.d
service_controlshell_readonlyдаtools/services.pystatus — по shell_readonly, start/stop/restart/reload — по shell_full
file_writeshell_fullдаtools/files.pyзапись внутрь allow_write_paths, атомарно, с бэкапом в аудит
package_installshell_fullдаtools/packages.pyпоставить пакет; обратимо через mcp_undo_last
package_removeshell_fullдаtools/packages.pyудалить пакет — через shell_confirm; обратимо
system_rebootdangerousдаtools/system.pyперезагрузка: токен → shell_confirmcore/system_control.py
code_treeself_editнетtools/code.pyфайлы GUI: путь, размер, mtime, protected, staged; маска и пагинация
code_readself_editнетtools/code.pyокно файла: content (для точного совпадения) + нумерация по запросу
code_searchself_editнетtools/code.pyпоиск по коду (подстрока/регэксп) с контекстом ±N строк
code_checkself_editнетtools/code.pyпроверки без применения: разбор, импорт из слепка, полный make lint
code_testself_editнетtools/code.pypytest tests/ -q -k … по staging-слепку; нет pytest — available: false
code_historyself_editнетtools/code.pyснимки: id, время, файлы, размер diff, состояние, причина отката
code_diffself_editнетtools/code.pyunified diff: staging / снимок / исходный эталон
code_export_patchself_editнетtools/code.pyвсе локальные правки устройства одним диффом — чтобы перенести в репозиторий
code_patchself_editдаtools/code.pyточечная правка в staging: edits ИЛИ diff; неоднозначное совпадение — отказ
code_writeself_editдаtools/code.pyфайл целиком (в т.ч. новый) — тоже в staging
code_applyself_editдаtools/code.pyснимок → проверки → диск → сторож → перезапуск; рвёт соединение
code_commitself_editдаtools/code.pyподтвердить применённое; без него откат по commit_ttl_sec
code_rollbackself_editдаtools/code.pyвернуть снимок (последний или по snapshot_id)

Эталон формы — первые четыре: одинаковые имена полей, одинаковая обработка «нет данных», одинаковые лимиты. Новый инструмент делается по ним. docs_get/config_describe — эталон постраничного ответа (offset/limit/truncated/next_offset).

Списки: одна форма на всех (tools/_paging.py)

Модуль с подчёркиванием — реестр такие пропускает. Через него проходит каждый список, и он же задаёт контракт:

ПолеЧто значит
itemsокно записей
totalсколько подошло под фильтр всего, а не сколько отдано
countсколько в items
offset / limitкакое окно отдано
truncatedесть ли что-то за окном; при true — ещё и next_offset

page() меряет собранный ответ и ужимает окно под mcp.limits.response_kb (shrunk_to_fit, requested_limit): ответ сверх лимита реестр заменяет ЦЕЛИКОМ на «слишком много», и модель получает не страницу данных, а сообщение об ошибке. empty(reason, hint) — пустой список с объяснением; unavailable(what, reason, hint) — честное «этого на устройстве нет» при ok: true.

Что добавлено в core/*.py ради этих инструментов

Логика в менеджерах, а не в tools/* — поэтому доступна и UI, и CLI:

ГдеЧтоЗачем
models.CatalogEntry.desync_names()имена функций --lua-desync записи«приём» стратегии; по ним же ищет catalog_search
catalog_loader.CatalogManager.find_entries()фильтры + окно + totalsearch_entries не умеет ни уровень, ни приём, ни окно
blob_registry.list_blobs()весь реестр + exists файланет файла = ПУСТОЙ fake, «тихий 0%»
firewall.get_conflicts() / queue_numbers()расхождения правил, движка и конфигатри разные поломки выглядят снаружи одинаково
nfqws_manager.resolve_binary()путь к бинарю с откатом на дефолтcfg.get() отдавал None, и argv собирался с None в нулевом элементе
core/traffic_recent.pyтри источника «видел ли движок трафик»«настроен ли домен» ≠ «дошёл ли пакет»
strategy_scanner.compose_score() / credit_success()формула ранжирования и правило «baseline открыт — кредита нет»одна формула на сканер и эксперименты: иначе «лучший вариант» и «лучшая стратегия в UI» — разные строки
probe_runner.fold(..., latency="median")медиана вместо среднеговыброс по латентности на роутере — норма, и среднее из трёх замеров он переставляет местами
core/tunnels_overview.pyсводка по шести движкам в одной формешесть менеджеров отвечают о себе шестью способами
tunnel_monitor.iface_counters()RX/TX интерфейса из /sys/class/netсчётчики нужны не только графикам
diagnostics.check_services()обход сервисов с бюджетом времениполный прогон уходит за любой таймаут вызова
permissions.granted(name)«разрешение включено ПРЯМО СЕЙЧАС»обработчику карта разрешений не передаётся
core/probe_runner.pyпробы по списку целей, сравнение «с обходом и без», лимитыS10 берёт baseline оттуда же, а не пишет свои пробы
nfqws_control.running()«движок поднят прямо сейчас» без побочных действийсравнению нужно исходное состояние, а менеджер в тестах подменён в _managers()
nfqws_control._is_running()is_running как метод ИЛИ как propertyнаш blockcheck иначе никогда не считался занявшим движок
core/nfqws_session.pyобщий мьютекс на движок + снимок/восстановлениесканер и эксперимент иначе независимо «вернут как было», и победит второй

Туннели: одна запись движка на все шесть (S5)

Сводка собирается в core/tunnels_overview.py (не в tools/), поэтому ею пользуются и MCP, и UI, и CLI. overview(engine="", logs=True) отдаёт engines — список записей одинаковой формы, на неё будут опираться S7 (запуск туннелей) и S15 (страница MCP):

Поле записи движкаЧто значит
engine / titleключ (singbox, mihomo, awg, usque, tgproxy, opera) и человеческое имя
installed / runningесть ли бинарник; поднят ли хоть один инстанс
version / binaryчто установлено и откуда запускается
instancesконфиги, интерфейсы или подпроцессы движка
instances_count / running_countсколько их всего и сколько живых
reasonпочему не установлен / не запущен — словами
errorдвижок не удалось опросить (и это не роняет остальных)

Запись инстанса: name, running, pid, iface, config, traffic, traffic_source, last_error + свои поля движка (link_up у usque, peers/last_handshake у AWG, redirect_active у mtproto).

Shortened here. Read the whole file on GitHub.

Signals

GitHub stars
145
Forks
11
Last commit
Sep 2026

ahel review

  • K4info
    destructive

Automated review, not a security audit. Ruleset v1+k2.

Advanced
Catalog kind
skill
Gateway key
mcp-avatardd
Source
github.com/avatardd/zapret-gui