MCP-сервер zapret-gui — справочник для сессий S5+
SkillProductivityLets 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.
No other account needed.
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.py | HTTP: 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.py | 11 разрешений, зависимости, whitelist настроек на запись |
core/mcp/audit.py | журнал вызовов (JSONL + ротация), снимки «до», диспетчер отката |
core/mcp/redact.py | маскировка секретов (ключи, URL, сырой текст) |
core/mcp/schema.py | мини-валидатор JSON Schema + normalize_tool_schema() |
core/mcp/auth.py | bind → 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.py | stdio-мост: 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_write | CRUD стратегий, hostlist'ов, ipset'ов, lua | — |
config_write | запись в whitelisted-поддеревья настроек | — |
probes | активные пробы (трафик с роутера), blockcheck, сканер | — |
experiments | движок экспериментов | control, probes |
tunnels_write | конфиги и запуск туннелей | — |
dangerous | бинарники, автозапуск, миграции, unified-правила, ребут | — |
shell_readonly | safe-команды, чтение файлов и каталогов | — |
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.
Инструменты (обновлять каждой сессией)
| Имя | Scope | Mut. | Файл | Что делает |
|---|---|---|---|---|
system_status | read | нет | tools/status.py | платформа, аптайм, память, какие движки подняты |
nfqws_status | read | нет | tools/status.py | движок nfqws2: pid, аптайм, argv, код выхода |
config_get | read | нет | tools/config.py | настройки по точечному пути, с флагом writable |
logs_tail | read | нет | tools/logs.py | хвост журнала: source, level, search, since, limit ≤ 200 |
docs_get | read | нет | tools/docs.py | любой ресурс zapret://… постранично: uri/topic, section, offset/limit |
config_describe | read | нет | tools/docs.py | описание настройки: тип, дефолт, единица, что значит 0/пусто, writable |
strategy_list | read | нет | tools/strategies.py | стратегии (builtin+user) с is_active; фильтры protocol/level/source/featured/active_only |
strategy_get | read | нет | tools/strategies.py | одна стратегия целиком: профили, их args, techniques, blob'ы |
catalog_search | read | нет | tools/strategies.py | поиск по INI-каталогам: query, technique, protocol, level, label |
nfqws_command_preview | read | нет | tools/strategies.py | итоговый argv стратегии — через build_preview_command, как при живом запуске |
strategy_state_list | read | нет | tools/strategies.py | выученное circular'ом из state.tsv: host, group, номер, возраст |
hostlists_list | read | нет | tools/lists.py | списки доменов: сколько записей, путь, есть ли файл |
hostlist_get | read | нет | tools/lists.py | окно одного списка + search; на 50 000 доменов отдаёт окно, не дамп |
ipsets_list | read | нет | tools/lists.py | списки IP: перечень, с name — содержимое |
lists_list | read | нет | tools/lists.py | именованные списки единого слоя: домены и CIDR по списку |
blobs_list | read | нет | tools/lists.py | реестр blob'ов и существует ли файл (missing_only) |
lua_functions_list | read | нет | tools/lists.py | функции --lua-desync с этого устройства: параметры, needs_blob |
firewall_status | read | нет | tools/firewall.py | правила NFQUEUE, бэкенд, queue_numbers, conflicts |
traffic_recent | read | нет | tools/traffic.py | дошёл ли трафик до движка: домен/профиль/вердикт за N минут |
tunnels_status | read | нет | tools/tunnels.py | шесть движков одним ответом: установлен/запущен/конфиги/трафик/последняя ошибка |
diagnostics_run | read | нет | tools/diagnostics.py | окружение, конфликты, предпосылки; сетевые пробы — по разрешению probes |
dpi_report | read | нет | tools/diagnostics.py | последняя классификация DPI из blockcheck; проб не запускает |
updates_check | read | нет | tools/updates.py | версии движков и обновления; по умолчанию из кеша, refresh — по probes |
config_writable_paths | read | нет | tools/config.py | что можно менять: путь, тип, текущее значение, enum |
audit_list | read | нет | tools/audit.py | последние вызовы MCP из журнала, новые первыми, с пометкой «ещё откатывается» |
config_set | config_write | да | tools/config.py | записать ОДНУ настройку; ответ — дифф «было/стало», список заменяется целиком |
nfqws_start | control | да | tools/nfqws.py | правила перехвата + движок с активной стратегией; обратное — nfqws_stop |
nfqws_stop | control | да | tools/nfqws.py | остановить движок и снять правила |
nfqws_restart | control | да | tools/nfqws.py | перезапуск со свежесобранными аргументами активной стратегии |
nfqws_reload_lists | control | да | tools/nfqws.py | SIGHUP: перечитать списки БЕЗ перезапуска; в ответе signalled |
strategy_apply | control | да | tools/nfqws.py | применить стратегию по id; снимок вида strategy_active |
firewall_apply | control | да | tools/firewall.py | поставить правила NFQUEUE; порты управления исключаются |
firewall_remove | control | да | tools/firewall.py | снять правила: трафик пойдёт напрямую |
strategy_save | strategies_write | да | tools/strategies.py | создать/перезаписать USER-стратегию; профили заменяются целиком; validation — прогон --intercept=0 |
strategy_delete | strategies_write | да | tools/strategies.py | удалить USER-стратегию; builtin — отказ |
hostlist_edit | strategies_write | да | tools/lists.py | replace/add/remove по списку доменов; SIGHUP; пустой список — предупреждение |
ipset_edit | strategies_write | да | tools/lists.py | то же для IP/CIDR; непринятые записи перечисляются |
blob_add | strategies_write | да | tools/lists.py | записать blob из hex (≤ 64 КБ); builtin-имена — отказ |
lua_script_save | strategies_write | да | tools/lists.py | сохранить lua-скрипт; битый синтаксис — отказ, force=true перебивает |
mcp_undo_last | any_write | да | tools/audit.py | откатить последнее изменение по снимку с диска (любой вид) |
scan_status | read | нет | tools/scan.py | прогресс подбора: фаза, сколько проверено, baseline_open; job_id — опционально |
scan_results | read | нет | tools/scan.py | что нашёл подбор, лучшие первыми; failed=true — что НЕ сработало |
blockcheck_status | read | нет | tools/blockcheck.py | прогресс НАШЕГО blockcheck; вердикт — в dpi_report |
blockcheck2_status | read | нет | tools/blockcheck.py | прогон скрипта bol-van: идёт ли, код выхода, found, highlights |
blockcheck2_output | read | нет | tools/blockcheck.py | телеметрия скрипта инкрементально: offset → next_offset |
healthcheck_status | read | нет | tools/blockcheck.py | расписание, сервисы, история и fail_streak; проб не запускает |
connectivity_matrix | read | нет | tools/probes.py | матрица «цель × интерфейс»; refresh — по probes |
probe_targets | probes | нет | tools/probes.py | проба доменов DNS→TCP→TLS→HTTP; коды из PROBE_CODES, состояния не меняет |
probe_compare | probes | да | tools/probes.py | домен с обходом и без; вердикт из пяти; переключение движка требует ещё и control |
scan_start | probes | да | tools/scan.py | запустить подбор стратегий; ответ — job_id, сразу |
scan_stop | probes | да | tools/scan.py | остановить подбор; проверенное остаётся в scan_results |
blockcheck_start | probes | да | tools/blockcheck.py | наш blockcheck в фоне; отчёт потом — dpi_report |
blockcheck2_start | probes | да | tools/blockcheck.py | оригинальный скрипт zapret2 (DOMAINS/SCANLEVEL/REPEATS/…) |
blockcheck2_stop | probes | да | tools/blockcheck.py | прибить скрипт; собранная телеметрия остаётся читаемой |
healthcheck_run | probes | да | tools/blockcheck.py | разовый прогон healthcheck в фоне; результат — в healthcheck_status |
scan_apply | control | да | tools/scan.py | применить найденное: сохранить USER-стратегию и поднять движок; нужен ещё strategies_write |
strategy_experiment_start | experiments | да | tools/experiments.py | прогнать варианты стратегии с измерением; ответ — run_id, сразу |
strategy_experiment_status | experiments | нет | tools/experiments.py | фаза, номер варианта, сколько осталось до авто-отката |
strategy_experiment_result | experiments | нет | tools/experiments.py | отчёт: цифры по целям, score, дельта к baseline, лог движка, подсказки |
strategy_experiment_commit | experiments | да | tools/experiments.py | оставить вариант применённым; save_as — ещё и strategies_write |
strategy_experiment_rollback | experiments | да | tools/experiments.py | вернуть состояние к снимку немедленно |
strategy_experiment_stop | experiments | да | tools/experiments.py | остановить прогон; измеренное остаётся в отчёте |
strategy_experiment_history | experiments | нет | tools/experiments.py | прошлые прогоны этого процесса GUI, новые первыми |
strategy_compose | strategies_write | нет | tools/compose.py | описание (фильтр/payload/инстансы) → argv + команда + линтер; ничего не сохраняет |
strategy_validate | strategies_write | нет | tools/compose.py | nfqws2 --intercept=0 по strategy_id/args/profiles: опции, файлы и исполнение lua-init |
shell_exec | shell_readonly | да | tools/shell.py | команда на роутере; safe-список и argv — по shell_readonly, произвольная строка (sh -c) — по shell_full |
shell_exec_async | shell_readonly | да | tools/shell.py | то же фоном: ответ — job_id, сразу |
shell_job_status | shell_readonly | нет | tools/shell.py | состояние фоновой команды; без job_id — список всех |
shell_job_output | shell_readonly | нет | tools/shell.py | вывод фоновой команды инкрементально: offset → next_offset |
shell_job_stop | shell_readonly | да | tools/shell.py | прибить фоновую команду; собранный вывод остаётся читаемым |
shell_confirm | shell_readonly | да | tools/shell.py | второй шаг: confirm_token — исполнить, run_id — снять дедмен |
file_read | shell_readonly | нет | tools/files.py | окно файла (offset/limit_kb/tail), секреты вырезаны |
file_list | shell_readonly | нет | tools/files.py | каталог полями: имя, размер, права, mtime, тип |
package_list | shell_readonly | нет | tools/packages.py | что установлено: opkg list-installed / apk list -I |
service_list | shell_readonly | нет | tools/services.py | исполняемые скрипты /opt/etc/init.d и /etc/init.d |
service_control | shell_readonly | да | tools/services.py | status — по shell_readonly, start/stop/restart/reload — по shell_full |
file_write | shell_full | да | tools/files.py | запись внутрь allow_write_paths, атомарно, с бэкапом в аудит |
package_install | shell_full | да | tools/packages.py | поставить пакет; обратимо через mcp_undo_last |
package_remove | shell_full | да | tools/packages.py | удалить пакет — через shell_confirm; обратимо |
system_reboot | dangerous | да | tools/system.py | перезагрузка: токен → shell_confirm → core/system_control.py |
code_tree | self_edit | нет | tools/code.py | файлы GUI: путь, размер, mtime, protected, staged; маска и пагинация |
code_read | self_edit | нет | tools/code.py | окно файла: content (для точного совпадения) + нумерация по запросу |
code_search | self_edit | нет | tools/code.py | поиск по коду (подстрока/регэксп) с контекстом ±N строк |
code_check | self_edit | нет | tools/code.py | проверки без применения: разбор, импорт из слепка, полный make lint |
code_test | self_edit | нет | tools/code.py | pytest tests/ -q -k … по staging-слепку; нет pytest — available: false |
code_history | self_edit | нет | tools/code.py | снимки: id, время, файлы, размер diff, состояние, причина отката |
code_diff | self_edit | нет | tools/code.py | unified diff: staging / снимок / исходный эталон |
code_export_patch | self_edit | нет | tools/code.py | все локальные правки устройства одним диффом — чтобы перенести в репозиторий |
code_patch | self_edit | да | tools/code.py | точечная правка в staging: edits ИЛИ diff; неоднозначное совпадение — отказ |
code_write | self_edit | да | tools/code.py | файл целиком (в т.ч. новый) — тоже в staging |
code_apply | self_edit | да | tools/code.py | снимок → проверки → диск → сторож → перезапуск; рвёт соединение |
code_commit | self_edit | да | tools/code.py | подтвердить применённое; без него откат по commit_ttl_sec |
code_rollback | self_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() | фильтры + окно + total | search_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