Skip to main content
Glama
covalschi

dayz-agentic-modding-mcp

dayz-agent-mcp

MCP-сервер, который позволяет агенту собрать мод для DayZ, проверить, что клиент компилируется, запустить тестовый сервер и получить структурированный вердикт вместо лога.

Сервер выполняет всю работу сам: он вызывает FileBank, инструмент подписи и диагностический исполняемый файл напрямую. Проекту не нужен собственный скрипт сборки.

Зачем нужен профиль

Сервер ничего не знает о конкретном моде. Всё специфичное хранится в профиле из двух частей:

  • dayz-mcp.toml — переносимая часть, хранится в репозитории мода: какие моды собирать, как выглядит здоровая загрузка.

  • dayz-mcp.local.toml — машинозависимая часть, не хранится в репозитории: где находится игра, инструменты и тестовый стенд.

Смешивание этих частей запрещено при загрузке — именно так репозиторий перестаёт собираться на чужой машине. Начните с dayz-mcp.example.toml.

Мод объявляется один раз, по имени: исходники в <root>/Name, результат — @Name/addons/Name.pbo. Параметр build.sources может перенаправить исходники мода в другое место (например, "." для мода, чей config.cpp лежит в корне репозитория).

Переносимая часть — dayz-mcp.toml

Ключ

Значение

project.name

как назвать этот проект

build.mods

по одной записи на мод, по имени; исходники, pbo и @folder выводятся из него

build.sources

где на самом деле живут исходники мода, относительно профиля ("." = сам корень репозитория)

build.exclude

что никогда не должно попадать в pbo. Указание этого списка полностью заменяет стандартный; по умолчанию: .git, *.blend, *.blend1, .gitignore, .gitattributes, README.md, *.ps1

build.stage

собирать отфильтрованную копию вместо отказа, когда присутствует исключённое — раскладка, которая нужна корневому моду

build.project

каталог, относительно которого разрешаются все пути моделей; указывается относительно этого файла. Обязателен для инструментов работы с моделями — и больше ни для чего. См. «Конвейер ассетов»

build.pre_script

сценарий PowerShell, запускаемый перед сборкой, для проектов, которые сначала генерируют код

expect.ready_line

строка, которую мод печатает, когда загрузка завершена — см. ниже

expect.counters

пары ключ = значение в этой строке, сравниваются численно

expect.max_warnings

допустимое число предупреждений; опустите ключ, чтобы отключить проверку

expect.forbid

подстроки, наличие которых делает запуск неудачным независимо ни от чего

expect.error_regex

регулярные выражения, помечающие ошибки скриптов, принадлежащие вам; используются при проверке компиляции клиента

expect.noise

дополнительный шум движка, который следует игнорировать, помимо встроенного списка

Машинозависимая часть — dayz-mcp.local.toml, не хранится в репозитории

Ключ

Значение

machine.game

установка игры; определяется автоматически, если не указано

machine.tools

DayZ Tools; определяются автоматически, если не указаны

machine.blender

исполняемый файл Blender, только для asset_export; определяется автоматически, если не указан, и больше ни для чего не нужен

machine.stand_root

подготовленный тестовый стенд. Сервер загружается против него, а логи читаются из <stand_root>/profiles. По умолчанию: <root>/testenv

machine.config

имя конфигурационного файла сервера внутри стенда (по умолчанию serverDZ.cfg). Это настройка, потому что стенд может содержать конфиг, который зависает после компиляции мира, и рабочий — под другим именем; должен находиться внутри stand_root

machine.port

порт, который server_start передаёт серверу (по умолчанию 2302)

mods.required

имена папок модов, разрешаемые внутри собственной папки игры !Workshop — например, required = ["@CF"] означает <game>/!Workshop/@CF

mods.extra

полные пути ко всему остальному, что нужно загрузить, для модов, которые не лежат в !Workshop

mods.server_only

имена папок, которые направляются в -serverMod вместо -mod. Сопоставление — по имени папки со всеми загружаемыми модами, откуда бы они ни взялись — mods.required, mods.extra или собственные папки @Name проекта; всё, что не перечислено, идёт в -mod. Диагностический клиент никогда не загружает серверные моды, поэтому проверка компиляции клиента их отбрасывает

Строка готовности

server_start завершается, когда expect.ready_line появляется в логе, записанном после начала загрузки. Это единственное, что сервер не может выяснить сам, и без неё меняются две вещи: ждать нечего, поэтому задание на запуск стартует сервер, через мгновение проверяет, что он ещё жив, и завершается, сообщая об этом; и у log_verdict нет строки, с которой можно считывать счётчики, поэтому expect.counters никогда не срабатывает. Ошибки, падения и лимит предупреждений по-прежнему оцениваются. Профиль без строки готовности поддерживается, а не считается сломанным — project_open сообщает об этом в своих примечаниях.

Related MCP server: DayZ API MCP Server

Инструменты

Tool

Что делает

project_open(path)

прочитать профиль, обнаружить игру и инструменты, сообщить, чего не хватает

project_status()

текущий проект, запущенный сервер, недавние задачи

mod_build()

упаковать и подписать каждый объявленный мод; возвращает идентификатор задачи. Отказывается от второй сборки того же проекта, пока одна ещё выполняется

server_start(timeout)

запустить тестовый сервер, завершить, когда он будет готов. Отказывается, если миссия, указанная в конфиге, не находится в <game>/mpmissions — движок ищет миссии рядом с запускаемым исполняемым файлом, а не рядом с -config. Возвращает pid сразу же — процесс запускается до возврата вызова, поэтому следующий же инструмент уже видит работающий сервер. Отказывается, если игровой порт уже занят кем-то другим, и отказывается на месте, если образ не может быть запущен. Готовность определяется по expect.ready_line, если он объявлен, иначе — по двум сигналам движка вместе: порт привязан И модуль миссии скомпилирован. Порт привязывается примерно за 17 секунд до скриптов, и загрузка, объявленная готовой в промежутке, слушает без миссии: она отвечает на запросы и отказывает каждому игроку. Сводка задачи называет, какой именно

server_status(pulse_seconds)

pid, жив ли процесс, растёт ли журнал (выборка с интервалом pulse_seconds), и как долго он завис

server_stop(pid)

остановить сервер, запущенный этой сессией (необязательный pid для осиротевших серверов)

server_signatures(value)

прочитать — или намеренно изменить — verifySignatures стенда. Без аргумента только сообщает. Редактирует только конфиг, который профиль называет конфигом этого стенда, отказывается от того, который разрешается за пределами machine.stand_root, отказывается, пока против него работает сервер, сохраняет комментарии и окончания строк файла, а затем считывает значение обратно из файла

client_compile_check(extra_mods, wait_seconds)

запустить диагностический клиент и прочитать его журналы

log_verdict(source, since)

пройдено/не пройдено с причинами: счётчики, запрещённые строки, бюджет предупреждений. source — это "server" (самый новый журнал в стенде) или "client" (журнал, созданный последним client_compile_check). since (метка времени эпохи, например since, возвращаемый server_start) отказывается от журнала, записанного до оцениваемого запуска, чтобы устаревший журнал от более ранней загрузки не был принят за результат этого запуска

log_tail(source, pattern, n)

последние строки, при необходимости отфильтрованные; те же два источника

job_status(job_id)

статус длительной задачи

job_wait(job_id, timeout)

ждать завершения задачи

job_artifacts(job_id)

получить результаты завершённой задачи

bridge_build()

упаковать мод-мост, исходники которого поставляются с этим сервером (bridge/), а не с вашим проектом; возвращает идентификатор задачи. Собирается без подписи — см. ниже

bridge_status(window)

продолжает ли мост внутри запущенной игры тикать: сообщает номер тика и продвинулся ли он за window секунд. Успешен только для тика, который действительно сдвинулся, или для стенда, перезапущенного в середине выборки

bridge_clear(force, probe_window)

отбросить команду, застрявшую в почтовом ящике, называя, что было выброшено. Отказывается, пока мост выглядит живым, если только force=True

world_ready(timeout)

ждать, пока мост внутри игры действительно начнёт принимать команды. Вызовите его один раз после завершения задачи загрузки, перед первой командой мира — см. «сервер готов — не значит мост готов» ниже

world_state(class_name, radius, pos)

снимок мира из публикации моста раз в секунду: игроки, позиция, здоровье, руки. Бесплатно без аргументов; с class_name также подсчитывает объекты этого класса поблизости (один цикл команды)

world_spawn(class_name, where, pos, quantity)

создать предмет на земле (без времени жизни, чтобы он не исчез в середине проверки), в руках игрока или в его инвентаре

world_teleport(pos)

переместить игрока в "x y z" — тот же формат, который сообщает world_state, поэтому прочитанную позицию можно вернуть напрямую

world_set(what, value, target)

установить health (игрок или удерживаемый предмет) или quantity (удерживаемый предмет)

world_delete(class_name, radius, pos)

удалить объекты одного класса поблизости. Требует класс; никогда не удаляет реального игрока

world_entities(class_name, radius, pos, limit)

какие объекты находятся рядом, а не сколько: класс, позиция, расстояние и здоровье для каждого. Страница, и она об этом сообщает — истинное общее количество возвращается рядом со списком

world_time_set(hour, minute, day, month, year)

переместить мировые часы. Каждое поле, оставленное на -1, сохраняет текущее значение, сначала считываемое из движка, потому что движок устанавливает дату как пять чисел одновременно

world_weather_set(what, value, seconds, duration)

изменить облачность, дождь, туман, снегопад или ветер. Подталкивание, а не блокировка: движок продолжает моделировать погоду после этого, и инструмент, и мод об этом сообщают

world_action(action_class, target_class, subject, radius, pos)

выполнить собственное действие мода через шлюз движка — см. ниже

world_exec(verb, args)

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

client_start(timeout, extra_args)

запустить игровой клиент и подключить его к стенду; возвращает идентификатор задачи. Всегда в оконном режиме. Завершается, когда мост сообщает players >= 1 — счётчик, а не таймер

| client_status() | pid, геометрия окна, свёрнуто ли окно или находится на переднем плане, настройка фона, количество игроков и подключён ли виртуальный контроллер | | client_stop() | остановить клиент, запущенный этой сессией, и отключить виртуальный контроллер | | client_shot(path) | захватить окно клиента в PNG, с lit_fraction — число, которое отличает настоящий кадр от полностью чёрного. Фокус не нужен | | client_move(x, y, seconds) | вести персонажа левым стиком. Аналоговое управление, единственный способ вообще как-то двигать персонажа. Фокус не нужен | | client_look(x, y, seconds) | поворачивать камеру правым стиком. Фокус не нужен | | client_press(button, seconds) | одна кнопка геймпада, из закрытого списка четырнадцати названий. Фокус не нужен | | client_chat(text, color) | отправить строку в чат — доставляется на стороне сервера через мост, так что не нужны ни клавиатура, ни окно, ни фокус | | client_type(text, submit) | ввод в клиентском поле ввода настоящими нажатиями клавиш. Единственный инструмент здесь, которому нужен передний план, и он сам об этом сообщает в ответе | | client_verdict(since) | судить живой клиент по его собственному .RPT — вердикт об ошибках и падениях; см. ниже | | ui_menu() | что делает интерфейс клиента: класс открытого меню, курсор, диалог. Бесплатно — перепубликуется каждый тик | | ui_tree(root, depth, limit) | дерево виджетов клиента: путь, класс, имя, видимость, прямоугольник на экране, глубина и текст. Это страница, и он сам об этом сообщает | | ui_find(name, class_name, text, root) | тот же обход, но с фильтрацией на стороне клиента, так что всё дерево никогда не приходится передавать целиком | | ui_click(path, expect_name, expect_class, via) | нажать виджет. via="script" проходит через обработчик открытого меню без фокуса; via="cursor" помещает настоящую мышь на его прямоугольник | | ui_text(path, text, expect_name) | записать текст в поле ввода и прочитать значение обратно из виджета | | mod_lint(mod, strict) | судить Enforce Script без упаковки и загрузки чего-либо. mod_build запускает его первым делом и отказывается на том, на чём отказывается он | | knowledge_build(layer, full, only) | собрать или обновить слой индекса API; возвращает id задания. only=[path] перечитывает ровно те файлы, которые вы указали | | knowledge_status() | что хранит каждый слой, насколько он старый и совпадает ли ещё с тем, что лежит на диске | | knowledge_find(name, kind, owner, layer, prefix, limit) | найти класс, метод, константу, перечисление или конфиг-класс по имени | | knowledge_show(name, ..., body) | одно объявление полностью: сигнатура, члены, цепочка наследования и сам исходник — читается напрямую из архива, если он там живёт | | knowledge_overrides(name, owner, layer) | кто переопределяет этот класс или метод | | knowledge_callers(name, kind, owner, layer) | кто вызывает этот метод или создаёт этот класс — каждое место вызова, с классом и методом, которые его сделали | | asset_export(blend, mod, source, name) | экспортировать модель из .blend в build.project_root, без головы; возвращает id задания. Необязательный первый шаг — см. ниже | | asset_build(mod, source, deploy) | бинаризовать модели мода из их MLOD-исходников, оценить результат и только потом положить его в мод; возвращает id задания | | asset_check(mod, model) | судить модели и текстуры, которые мод уже поставляет. Ничего не собирает, DayZ Tools не нужны, отвечает за миллисекунды | | asset_convert(source, output) | конвертировать одну текстуру между .png и .paa и оценить результат |

Сигнатуры, и почему собственное сообщение движка уводит вас не туда. При verifySignatures = 2 стенд отказывает каждому клиенту с кодом 118 и "missing dta\bin.pbo" — имя ванильного файла, без единого упоминания сигнатур вообще. Причина обычно в связке ключей: этот инструмент запускает диагностический исполняемый файл из установки КЛИЕНТА, поэтому движок читает keys рядом с этим исполняемым файлом, тогда как dayz.bikey — ключ, подписывающий собственные pbo игры, — поставляется с отдельной установкой DayZServer. Папка keys, которой нет или в которой лежит только ключ самого мода, оставляет сервер неспособным проверить что-либо, включая ванильные файлы. client_start отказывается и называет, какой из трёх случаев имеет место; server_start только предупреждает, потому что безголовая загрузка без клиента всё ещё полезна. Неподписанный pbo на строке -mod клиента называется тем же способом — включая собственный мост этого сервера, который упакован неподписанным намеренно.

Три ограничения, которые движок накладывает на инструменты UI, и ни одно из них не обходится: у обычного TextWidget есть SetText и нет GetText нигде в enwidgets.c, так что строку подписи вообще нельзя прочитать — что означает интерфейс мода, остаётся вопросом для моста на стороне сервера, где данные настоящие. Клик на уровне скрипта достигает только открытого скриптового меню, потому что у Widget есть SetHandler и нет GetHandler; via="cursor" существует для всего остального. И клиент должен загрузить мост: один pbo несёт обе половины, поэтому профиль, перечисляющий его в mods.server_only, держит его вне строки -mod клиента — в этом случае отказ происходит по имени, а не ответом с пустым деревом.

job_wait — это инструмент, предназначенный для ожидания, и его timeout ограничен 600 секундами, какое бы большое значение ни было передано. Два других инструмента спят: server_status сэмплирует лог дважды, с интервалом pulse_seconds, ограниченным 10 секундами — эта пауза и есть то, как он отличает медленную загрузку от зависшей, — а bridge_status сэмплирует тик моста дважды, с интервалом window, ограниченным теми же 10 секундами, по той же причине. Всё остальное возвращается немедленно; работа, занимающая минуты, происходит за id задания.

Мод моста

bridge_build упаковывает bridge/ из репозитория самого этого сервера в @DZMCP_Bridge рядом с ним. Это мод сервера, не ваш: одна копия обслуживает все проекты, в ваш репозиторий ничего не записывается, кроме записи о задании, и ни один ключ подписи проекта не используется — pbo с -serverMod никогда не передаётся клиенту на проверку, поэтому он собирается неподписанным, и его выходная папка хранится свободной от сигнатур и ключей.

Сборка не означает загрузку. Это остаётся решением вашего профиля, потому что мост — это дополнительный pbo в стенде, и запуск без него должен оставаться возможным. Чтобы подключить его, добавьте две строки в dayz-mcp.local.toml (те же две, которые печатает сводка задания bridge_build):

[mods]
extra       = ["<path printed by bridge_build>/@DZMCP_Bridge"]
server_only = ["@DZMCP_Bridge"]

server_only — это то, что направляет его в -serverMod вместо -mod. Без него стенд загружается совершенно нормально, и bridge_status сообщает, что мост никогда не записал никакого состояния, — что правда, и это легко принять за сломанный мост.

bridge_status также сообщает о почтовом ящике команд. Внутри игры его опустошает только мод, заявляя команду; на этой стороне это делают bridge_clear и очистка перед загрузкой в server_start. Так что команда, отправленная, пока стенд был выключен или до подключения моста, не отбрасывается и не истекает сама по себе — она продолжает блокировать каждую последующую отправку, и стенд, загруженный вне этих инструментов, подхватил бы её. server_start очищает оба транспортных файла перед каждой загрузкой, так что сервер, запущенный через этот инструмент, никогда не выполняет команду из предыдущей сессии; это гигиена, а не замена знанию того, что команда там. Состояние возвращается как stale_command, и bridge_clear() — это выход из него. Очистка — отдельный инструмент намеренно: выбросить поставленную в очередь команду — это решение, а не то, что проверка состояния должна делать за вашей спиной. Она отказывается, пока мост выглядит живым, если вы не передадите force=True, и в любом случае сообщает id команды, которую отбросила.

Что bridge_status умеет различать

Одного тика недостаточно, чтобы судить о мосте, потому что он перезапускается с 0 при каждой загрузке, тогда как файл состояния переживает её в каталоге профиля. Каждый ответ несёт собственный вердикт канала в heartbeat, и эти четыре — действительно разные факты:

state

heartbeat

значение

alive

growing

тик сдвинулся в пределах одной сессии — единственный ответ живости с ok: true

restarted

restarted

между двумя сэмплами поднялся новый мир: жив, не заморожен, и всё, что было отправлено старой сессии, пропало

frozen

stalled

тот же мир виден дважды, не движется — проблема на стороне скрипта, так что следующий шаг — log_verdict

unknown

unmeasurable

сэмпл не удалось прочитать (или window=0): сравнение не производилось. Не диагноз — вызовите ещё раз

Каждый ответ, прочитавший сэмпл, также несёт session_id — id живого мира, — а ответ restarted несёт и previous_session_id, так что вызывающий может сказать, какой мир исчез.

no_server, stale_command, no_state_file, invalid_state, unreadable_state и outdated_bridge идут перед всем этим: ничего не запущено, команда застряла, мод не загружен, документ состояния — валидный JSON с ошибкой в именованном поле (он сообщает, в каком, и проверяет дважды, прежде чем сказать), файл вообще не разбирается, или разбирается, но предшествует протоколу этого сервера (пересоберите его с помощью bridge_build).

Команды мира

Инструменты мира общаются с мостом через два JSON-файла в каталоге -profiles сервера: почтовый ящик команд (атомарно записываемый с этой стороны, удаляемый модом как заявка) и файл состояния, который мод перезаписывает раз в секунду. У Enforce Script нет rename, так что мод не может писать атомарно — читатель вместо этого терпит рваные записи, и один неудачный запрос никогда не является новостью. Четыре факта, все измеренные на живом стенде, решают, как ими пользоваться:

Готовность сервера — это не готовность моста. Мост начинает заявлять команды через десятки секунд после того, как сервер сообщает о готовности — наблюдаемый разброс на данный момент составляет 18–38 секунд, и он меняется от загрузки к загрузке. Команда, отправленная в это окно, не отклоняется — она заявляется с опозданием и завершается после того, как вызывающий сдался. Поэтому: server_start, дождитесь задания загрузки, затем world_ready(), затем команды. Каждый инструмент мира также отказывает заранее, если тик не движется, называя world_ready средством решения.

Каждое значение аргумента пересекает провод как строка. Парсер мода строг: JSON-число где-либо в args отклоняет весь блок args. Инструменты сами преобразуют числа и булевы значения в строки и отказывают значениям без верной строковой формы (списки, словари, None). Позиции путешествуют одной строкой, "x y z".

Отказ — это результат. Собственное предложение мода возвращается дословно как ошибка: "no player is on the server", "the class does not exist", "the action's own Can() said no". Никого не подключено — нормальное состояние безголового стенда, и каждый глагол, которому нужен игрок, говорит об этом вместо того, чтобы молча ничего не делать.

Id сессии защищает от вчерашней команды. Каждая команда несёт сессию, которую мост опубликовал последней; мод отказывает любой команде, адресованной другой сессии (или никакой), не выполняя её. Команда, записанная, пока стенд был выключен, поэтому никогда не может выстрелить в только что загруженный мир. Инструменты проставляют сессию автоматически — это важно только если вы пишете почтовый ящик вручную.

Действия, и почему нет словаря глаголов

Семантический глагол, как «рука в образце», лжёт: в реальном моде те же слова означают разные вещи в зависимости от того, какое устройство рядом, какая фракция у игрока и что уже разблокировано. Этот контекст неисчислим, поэтому мост не пытается его учитывать. world_action принимает имя класса действия, цель и предмет в руке, и просит движок прогнать его через собственные ворота — те же, через которые проходит нажатие клавиши. Применимость определяется собственным Can() действия, и его отказ — это осмысленный результат теста, а не сбой инструмента. Различимые ответы: менеджер занят, игрок уже действует, игрок бежит, неизвестный класс действия и «собственный Can() действия сказал нет». «Принято» — тоже не успех: команда остаётся активной, пока движок фактически не освободит действие, и каждый путь отказа освобождает менеджер, так что игрок всё равно может действовать после этого.

world_exec — запасной выход

Всё, что мод предоставляет и что не является действием — «сколько очков в фракции» — проходит через world_exec(verb, args): произвольный глагол через тот же транспорт. Каждый ответ помечен как non_standard: этот сервер не знает глагол, не проверяет его и не отвечает за то, что мод с ним делает. Глагол, который сборка моста не знает, возвращается списком глаголов, которые она знает. Проект, которому нужен собственный глагол, правит собственную копию диспетчера моста (комментарий над KnownVerbs() в bridge/scripts/5_Mission указывает точно, где); никакого механизма регистрации намеренно нет — глагол, который этот сервер набрал и проверил, был бы глаголом, за который этот сервер отвечает.

Клиент: три уровня ввода и почему их три

Мост достигает сервера. Чего он не может — смотреть на клиент или действовать через клиент — вести персонажа по земле, открывать меню, заполнять поле, нарисованное модом. Инструменты client_* — это именно то, и они используют три разных пути, потому что ни один из них не может сделать работу двух других. Каждая строка ниже — это измерение на живом клиенте, а не проектное намерение.

Путь

Что он делает

Нужен ли передний план

мост (world_*, client_chat)

мир и текст в чат

нет

виртуальный геймпад, ViGEmBus (client_move / look / press)

движение, камера и некоторый интерфейс

нет

реальные нажатия клавиш, SendInput (client_type)

текст в поле, которое существует только на клиенте

да, и он нужен

Эмуляция клавиатуры не двигает персонажа, а оконные сообщения не делают ничего. SendInput со сканкодами при подтверждённом переднем плане: 25 секунд вперёд, 0 м. PostMessage/SendMessage с WM_KEYDOWN в главное окно и его дочерние: 0 м, и никакой реакции меню тоже. Движок читает ввод из сырых данных и игнорирует эмулируемые клавиши, поэтому ни один инструмент здесь не предлагает оконных сообщений.

Виртуальный геймпад двигает персонажа, даже без фокуса, и он АНАЛОГОВЫЙ — вот причина, по которой он работает даже там, где клавиша не работает. Измерено в одном прогоне со сторонним приложением, удерживающим передний план всё время:

stick fully forward,   10.0 s  ->  38.40 m   (3.84 m/s)
stick at 0.3 forward,   8.0 s  ->  11.34 m   (1.42 m/s)

Тот же путь, та же скорость 2.7× от одного только отклонения стика. «Персонаж идёт, а не бежит» — это нельзя выразить клавишей, которая знает только вкл/выкл. В том же прогоне персонаж прошёл около 141 м сам по себе, и собственный счётчик мода для объектов в радиусе 10 м от него изменился 1 → 0 → 1, когда он отошёл и вернулся — изменение состояния, вызванное присутствием, которое телепорт вызвать не может.

Часть интерфейса отвечает на геймпад, а часть — нет. Измерено, когда окно игры было позади другого приложения всё время: back открывает и закрывает инвентарь, start открывает меню паузы, b закрывает его — всё при стандартном нажатии 0.1 с, так что короткого нажатия достаточно, чтобы движок сработал. Но a ничего не сделал — ни при 0.1 с, ни при 0.5 с, и крестовина тоже не сработала в этих экранах: клиент не переключился в режим навигации контроллером, поэтому не было сфокусированного элемента, на который могла бы нажать кнопка подтверждения. Считайте закрытие меню задачей геймпада, а подтверждение в меню — неподтверждённым.

Глазам фокус не нужен. Захват экрана — это живой кадр, даже когда окно находится в самом низу порядка окон (lit_fraction 0.9997 без фокуса, 0.9997 с фокусом в той же сессии). Единственное состояние, которое их побеждает, — это свёрнутое окно, чья клиентская область схлопывается до 0×0 — отказ с указанием причины, а не сохранение пустой картинки, которая выглядит валидной.

Всё это фоновое поведение зависит от одной настройки клиентаpauseMode (GAME → UPDATE IN BACKGROUND). При значении, измеренном здесь, клиент продолжает отрисовку и симуляцию без фокуса, поэтому кадр живой и стик продолжает двигать персонажа. При «no graphics» оба остановились бы молча — замороженный кадр выглядит точно так же, как живой. Поэтому client_start и client_status ЧИТАЮТ эту настройку и предупреждают; они никогда не пишут её, потому что она принадлежит тому, кто владеет машиной.

client_type — единственный инструмент, которому нужен экран, и он честен об этом: ответ содержит foreground_taken и предложение, объясняющее, что человек за машиной не мог печатать в своём окне, пока шёл ввод. Он проверяет GetForegroundWindow после запроса, потому что SetForegroundWindow возвращает успех, ничего не сделав, когда Windows отказывает, — и слепой ввод отправит нажатия в то окно, которое человек реально использует. Когда передний план получить нельзя, ничего не вводится, и в отказе называется процесс, который его удерживает.

ViGEm — это эмуляция реального устройства, а это испытательный стенд. Драйвер подписан и устанавливается без перезагрузки, а геймпад — это новое устройство, а не фильтр над клавиатурой и мышью машины — фильтрующий драйвер уже пробовали здесь однажды, и он стоил владельцу машины весь ввод с клавиатуры и мыши, пока его не открутили вручную. Ничто из этого не является обещанием насчёт античита на живом сервере, и ничто в этой фазе таких обещаний не даёт.

Индекс знаний

Агент, пишущий мод, снова и снова задаёт игре одни и те же вопросы: есть ли такой API, как он называется, где объявлен, кто его переопределяет. Ответы раньше означали распаковку scripts.pbo и прочёсывание текста — и каждая сессия платила за это заново. knowledge_* превращает эту работу в один вопрос.

Это обычный SQLite-файл в собственной директории .dayz-mcp/ проекта, собираемый этим сервером из игры, модов, объявленных в проекте, и собственных исходников проекта. Никаких эмбеддингов, внешних сервисов и ключей.

Три уровня и почему их ритмы различаются

Уровень

Источник

Когда устаревает

core

игра: dta/scripts.pbo для API, Addons/*.pbo для классов предметов

при обновлении игры

deps

архивы модов, объявленных в профиле, читаются без распаковки

при обновлении зависимости или изменении объявленного набора

project

собственные исходники мода, читаются там, где лежат

при каждой правке

Один индекс, собранный за один проход, оказался бы неверным через минуту после того, как стал верным: игра обновляется несколько раз в год, зависимость — несколько раз в месяц, а проект — между одним ходом агента и следующим. Поэтому каждый уровень строится, стареет и измеряется отдельно, и каждая сборка инкрементальна — неизменённые исходники пропускаются по размеру и времени модификации, а only=[path] пропускает даже обход, который их обнаруживает.

Ответ несёт возраст уровня, из которого он пришёл

Устаревание измеряется, а не угадывается: уровень записывает размер и время модификации каждого исходника, который он прочитал, и это сравнивается с файлами в их текущем состоянии.

  • Каждый ответ называет использованные уровни и возраст каждого. Ответ без результатов называет каждый уровень, который он обыскал — «не найдено» стоит ровно столько же, сколько актуальны уровни за ним.

  • Свежесть уровня проекта измеряется при каждом поиске, участвовал ли он в нём или нет. Это опасный случай: агент добавляет класс, спрашивает о нём, и уровень, собранный минуту назад, говорит «не найдено» — уверенное утверждение о коде, который существует.

  • Поиск по уровню, который никогда не был собран, отклоняется, и в отказе называется вызов, который его собирает. «Не найдено» и «не искали» — разные факты, и только один из них безопасен для действий.

  • Сужение несёт ту же ловушку на уровень ниже, поэтому пустой суженный ответ сообщает, где имя действительно существует: запрос kind='class' по имени, которое игра объявляет только в конфиге, получает честное «нет», которое читается как «в игре нет такого класса».

Конфигурационные классы живут под kind='config', а не kind='class'. По подсчёту в собственном индексе этой машины: 88 102 конфигурационных класса против 43 595 скриптовых объявлений всех видов вместе взятых, так что вперемешку с одним видом они похоронят любой скриптовый ответ. Разделённые, вопрос «есть ли в игре класс предмета X» — это вопрос, который можно задать точно.

Чего индекс не отвечает

Индекс отвечает на вопрос что существует: класс, метод, сигнатура, где объявлен, кто переопределяет. Он не отвечает на вопрос что правильно — что modded class X extends X компилируется и молча не применяется, что _co стоит альфа-канала, что binarize принимает директории, а не файлы. Ничто из этого не выводимо из исходников; это было выучено на горьком опыте и живёт в навыке моддинга и в самом моде. Индекс не пытается заменить ни то, ни другое, и не пытается понять, что означает поле или зачем существует класс.

Семантического поиска здесь намеренно нет

Решение было принято измерением, а не осторожностью: каждый запрос, который формировал ранние фазы этого сервера, был запросом по имени. А индекс эмбеддингов нарушил бы правило, которого держится весь остальной сервер — установил и работает, без внешнего сервиса и без ключа. Проект-предшественник, на котором этот намеренно не строился, документирует свой слой знаний как локальный и бесплатный, пока его код импортирует платный клиент эмбеддингов, падает без ключа и несёт захардкоженные цены. Его два поисковых инструмента также зависают навсегда, потому что клиент за ними был создан без таймаута; отсюда и потолок, под которым работает каждый поиск здесь. Если точного поиска окажется недостаточно, семантический поиск — это отдельная фаза с одним условием: модель поставляется внутри дистрибутива.

Измеренные числа

На этой машине — игра с 2810 скриптовыми файлами, 35 установленными модами, один реальный проект из 41 исходника — через инструменты, а не через их внутренности:

Сборка

Результат

Время

core

2927 исходников, 131 697 объявлений (41 ничего не дал)

70.2 с

deps, четыре объявленных мода

8 архивов, 10 925 объявлений

0.9 с

deps, все моды, установленные здесь

523 архива, 204 768 объявлений, 3 архива нечитаемы и названы

139–147 с

project

41 исходник, 1196 объявлений

0.12 с

Индекс на диске: 74.7 МБ для трёх уровней реального проекта; 110 МБ для 523 архивов зависимостей отдельно. Эти архивы — 92 ГБ, и ни один из них не распакован.

Точки вызова — вот за что платит индекс. В собственных скриптах игры 43 579 объявлений и 113 703 точки вызова, и запись второго множества примерно удваивает индекс: замеры только на игровом слое — 23,8 МБ и 3,7 с на сборку без них, 49,6 МБ и 4,5 с с ними. Такова цена возможности ответить на вопрос «кто это вызывает», и она указана здесь, а не обнаруживается позже на заполненном диске.

Ответ

Время

knowledge_find, точное имя

4,2 мс полного цикла, из которых 3,0 мс — обход проекта

knowledge_find, префикс, лимит 500

3,2 мс на запрос

knowledge_overrides

4,2 мс

knowledge_callers, 23 точки вызова из 113 703

0,38 мс на запрос

mod_lint для мода из 76 файлов

277 мс текстовых проверок, 7 мс проверок индекса

Замерено на живом стенде, три запуска: world_time_set(hour=3, minute=7) перевело часы на 2026-09-20 03:07 и оставило дату на месте; world_weather_set("fog", 0.9, seconds=2) подняло публикуемый туман с 0,085 до 0,900 и удержало его; world_entities(pos="7500 0 7500", radius=150, limit=5) вывело 5 из 171 объектов с truncated: true. Дистанции возвращались как 320 м при радиусе 150 м, пока их не сделали горизонтальными — именно это измеряет собственный тест радиуса движка. | knowledge_show, класс с 400 членами и его предки | 6,8 мс | | knowledge_status, все три слоя измерены | 41 мс (110 мс при первом вызове после сборки) |

Инкрементальность на реальном проекте: полная пересборка — 136 мс; один изменённый файл, найденный обходом, — 8,8 мс (15×); тот же файл, указанный через only=, — 5,8 мс (23×). На дереве из 2810 файлов доминирует обход, и only= даёт гораздо больше — но для проекта такого размера 15× — это то, что обычная пересборка даёт на самом деле.

Потолок действительно ограничивает: запрос, измеренный в 77 мс, запущенный под потолком 19,3 мс, был остановлен на 19,9 мс, и соединение продолжило отвечать.

Конвейер ассетов

Чтобы получить модель из Blender в мод, нужно десять шагов, и до этого этапа все они выполнялись вручную. Ценность не в запуске инструментов. А в том, что каждый инструмент в этой цепочке структурно не способен сообщить об ошибке, и каждое из этих умолчаний уже стоило дней.

Измерено на реальных бинарниках, а не взято с потолка:

Что произошло

Что вернул инструмент

binarize получил файл там, где ожидал каталог

0, пустой выходной каталог, ни строчки текста

binarize с материалом, который не загрузился

0, ODOL размером 46 190 байт там, где правильная сборка — 58 644

binarize получил уже бинаризованную модель

0xC0000005 и файл нулевой длины в выходном каталоге, поверх того, что там было

экспортёр Blender с его же аргументами по умолчанию

FINISHED, код 0, валидный MLOD с 2 из 5 LOD модели и ни слова об этом в 169 строках лога

Поэтому всё это пространство имён построено на правиле: вердикт читается по артефакту, а не по отчёту инструмента. Код возврата фиксируется, но ему не верят ни в ту, ни в другую сторону.

Корень объявляется, а не предполагается

У binarize вообще нет опции корня проекта — полный список ключей был составлен перебором по реальному бинарнику. Корень — это рабочая директория процесса. Одна и та же команда, тот же вход, другая директория — и на выходе валидный ODOL с правдоподобными путями текстур, которые движок рендерит без текстур, с кодом успеха и без единой жалобы. Экспортирующий аддон Blender хранит тот же корень в собственной настройке, запомненной из последнего открытого проекта: на машине, где это разрабатывалось, он указывал на каталог из посторонней сессии, и при неверном корне аддон тоже не падает — он срезает букву диска, оставляет остальное и пишет пути, которые выглядят как пути.

build.project_root — это и есть тот каталог, указанный один раз в переносимой половине профиля. Сервер устанавливает его рабочей директорией бинаризатора и передаёт в аддон на время запуска, так что то, что хранит аддон, ни на что не влияет (об этом сообщается, чтобы вы могли пойти и исправить). Именно поэтому неверный корень становится невозможным, а не обнаруживаемым, и поэтому ключ обязателен до того, как что-либо моделеподобное вообще запустится.

Отказы происходят до создания процесса — замерено 0,0003 с — а отклонённая сборка оставляет модель, которую мод уже поставляет, байт в байт нетронутой.

Двенадцать проверок артефакта, четыре из них — отказ

asset_check выполняет их без сборки и без DayZ Tools, потому что свежий клон должен иметь возможность спросить, здорово ли то, что он поставляет. Четыре отказа: собранная модель существует и является ODOL (C1), ни одна ссылка не выходит за пределы мода (C3), материал действительно встроен (C4), и ничего уже бинаризованного не скармливается обратно binarize (C10). Остальные — предупреждения: висячие ссылки, rvmat, указывающий в другой мод, прозрачность, потерянная в DXT1 (C7), анимация, так и не попавшая в артефакт, model.cfg, не тот, из которого собран артефакт, структурный отпечаток, который больше не совпадает с тем, что развернула последняя сборка. Каждая находка говорит, что делать.

C4 — та самая, о которой стоит знать. Когда binarize разрешает rvmat, он копирует текстурные стадии этого материала в модель — fresnel, #(argb,8,8,3), env_land_co.paa, _nohq, _smdi — строки, которых нет ни в одном MLOD. Шесть артефактов из шести были корректно разделены этим тестом, и он нашёл на этой машине сломанную модель, о которой никто не знал.

Шаг Blender необязателен

asset_export — единственный инструмент здесь, которому нужен Blender, и всё ниже по конвейеру работает с .p3d откуда угодно — ручной экспорт, файл партнёра, модель, закоммиченная годы назад. Машина без Blender собирает и поставляет мод без проблем; отказ сообщает об этом, а не выдаёт это за сломанную установку. Ему нужно, чтобы экспортирующий аддон был включён в том Blender, который он находит, и он никогда не записывает обратно пользовательские настройки Blender (проверено: файл настроек был побайтово идентичен после каждого запуска).

Экспорт и сборка — это два вызова, а не один, потому что у каждой половины свой вердикт, и сборка, отклонённая одним и разрешённая другим, — это не решение.

Побайтовая идентичность не обещается никогда

Ни одна половина этого конвейера не воспроизводима, и дизайн говорит об этом прямо, а не притворяется:

  • Экспорт. Семь экспортов одного неизменного исходного файла — три из одной сессии, три из другой и один, сделанный вручную в GUI месяцами ранее, — дали семь разных SHA-256 при постоянных 334 032 байтах. Разница — в порядке одного внутреннего блока.

  • binarize. Четыре запуска одного неизменного входа дали три разных результата: размер сдвинулся на 5 байт, и два фрагмента по 8 байт вытекли из сжатых областей.

Поэтому модель никогда не кэшируется и не сравнивается по хешу содержимого. Сравнивается структурный отпечаток — тип файла, количество LOD и набор имён. Во всех семи экспортах этот отпечаток был одним значением.

Замеренные цифры

Одна небольшая модель на этой машине, через инструменты:

Шаг

Результат

Время

asset_export

MLOD, 334 032 Б, 5 LOD, чисто

2,1 с (около 8 с на холодном старте)

asset_build

ODOL v55, 58 646 Б, 4 LOD, все пять маркеров C4

43,8 с (75,6–78,7 с замерено за четыре предыдущих запуска)

asset_check

оценено 1 модель и 10 пар текстур

миллисекунды

asset_convert

один PNG в DXT1, 50 764 Б

0,52 с

отказ на неверном корне

до запуска любого процесса

0,0003 с

Оба лога почти полностью состоят из шаблона, и то, что заглушено, подсчитывается, а не отбрасывается: 169 строк Blender сократились до 4, а 91 строка binarize — до 6 — и одна из этих шести была единственной настоящей жалобой модели.

Соединённые в цепочку экспорт и сборка воспроизвели модель, сделанную вручную месяцами ранее: тот же тип, те же 4 LOD, те же 50 строк, а размер — на один байт.

Чего это не отвечает

Правильно ли выглядит модель, правильно ли отмасштабирована, правильно ли намотана, есть ли коллизия. Ничто за пределами игры на это не отвечает. C1–C12 сокращают путь к этому; они его не заменяют.

Известные ограничения

  • Обнаружение устаревшего pbo основано на mtime, а не на содержимом. mod_build отказывается принимать свежесобранный pbo, который старше своих исходников — обычная причина в том, что запущенный сервер всё ещё держит старый файл открытым, поэтому упаковка молча ничего не произвела. Но git checkout меняет время модификации файла, не меняя его содержимого, так что вполне исправный pbo, собранный сразу после переключения веток, тоже может попасть под эту проверку. Если mod_build сообщает «stale pbo» сразу после переключения ветки, вероятная причина именно в этом, а не в реальном сбое упаковки — пересоберите, и проверка пройдёт. Зрелый инструмент в этой области перешёл на хеш содержимого именно по этой причине; это будущая работа здесь, не сделанная на данном этапе (см. packer.py, pack_one).

  • Папка исходников мода упаковывается целиком. mod_build отказывается упаковывать мод, в каталоге исходников которого есть что-либо, соответствующее build.exclude (семь шаблонов по умолчанию перечислены выше), а не молча отправляет это внутрь публикуемого pbo. Он отказывается независимо от build.exclude, когда в исходниках содержатся собственные артефакты этого сервера — ключи подписи, любая половина профиля, хранилище заданий, предыдущая сборка мода — потому что упаковка их публикует приватный ключ подписи, и ни один проект не должен настраивать это вручную. По умолчанию он не создаёт сначала отфильтрованную копию: копия всегда новее исходников, что навсегда отключило бы проверку устаревшего pbo выше, если бы эта проверка измеряла копию. build.stage = true всё же включает копирование — безопасно только потому, что сравнение устаревшего pbo всегда измеряет исходное дерево исходников, а не копию. Это компоновка, необходимая моду, чьи исходники находятся в корне репозитория (там всегда есть хотя бы .git).

  • Вердикт судит весь журнал, а не только строки вашего мода. log_verdict читает журнал стенда, на который он указывает, поэтому стенд, разделяемый с другими модами, засчитывает их предупреждения в ваш бюджет expect.max_warnings, а их ошибки — как причины. Два проекта, разделяющие один machine.stand_root, увидят базовый уровень друг друга. Либо дайте каждому проекту собственный стенд, либо задайте бюджет с учётом того, что ещё загружено. Фильтр в рамках проекта, симметричный expect.error_regex, — очевидное улучшение, и он не реализован.

  • expect.noise не может спасти строку, которая уже считается ошибкой. Классификация упорядочена: forbid → сбой → ошибка → noise → предупреждение, поэтому строка, содержащая ERROR или FATAL (или одну из ваших строк forbid), решается до того, как будет учтён noise. Этот порядок намеренный — если бы noise сопоставлялся первым, безобидная подстрока могла бы поглотить фатальную строку — но это означает, что noise может подавлять только предупреждения и обычные строки и никогда не понижает строку уровня ошибки.

  • client_verdict — это вердикт об ошибках и сбоях, а не о готовности. [expect] описывает журнал сервера: его строка готовности и счётчики печатаются серверной инициализацией мода, а max_warnings — это бюджет, подсчитанный по тому же журналу. Клиентский .RPT не содержит ничего из этого, поэтому эти три ключа намеренно здесь не применяются, и ответ перечисляет их в not_applied. forbid, error_regex и noise касаются текста строки журнала и по-прежнему применяются. Клиентской строки готовности, которую можно было бы объявить, нет; попал ли клиент внутрь, определяется количеством игроков, которого ждёт client_start, а не его журналом.

  • Клиентские инструменты подключаются к стенду, который эта сессия не запускала; client_chat — не может. client_start с радостью подключится к тому, что уже есть на порту, и сообщает, чей это стенд. Но чат доставляется на стороне сервера, по тому же каналу, что и инструменты world_*, а этот канал действует только на сервер, запущенный этой сессией — поэтому на заимствованном стенде работает всё, кроме client_chat. Отказ называет pid'ы, удерживающие порт, а не предлагает server_start, который отказал бы им.

  • Чат недоступен с геймпада, и подтверждение меню — тоже. Игра привязывает свою строку чата к Enter и ни к чему больше, и экранной клавиатуры нет, поэтому текст — это либо сообщение моста (client_chat, бесплатно), либо реальные нажатия клавиш (client_type, стоит фокуса переднего плана). client_type("", submit=True) отправляет один Enter — так открывается строка чата — и, судя по приведённым выше данным, это единственное подтверждение, которое есть у набора инструментов.

  • Каждый поиск по знаниям оплачивает обход дерева проекта. Этот обход — то, как измеряется устаревание проектного слоя при каждом ответе, и это единственное свойство, ради которого существует индекс. Он стоит 3,0 мс на реальном моде из 41 исходника (в его дереве около 1800 записей) и 21 мс на дереве из 2810 файлов. Кэширование его на секунду-другую убрало бы стоимость и восстановило ровно то окно тишины, от которого отказывается дизайн; если это когда-нибудь станет слишком дорого, этот компромисс придётся принять осознанно, а не случайно.

  • Сборка всегда проходит через задание, и задание стоит дороже, чем небольшая сборка. Время оборота измерено в 70–95 мс против 6 мс пересборки проекта: job_wait опрашивает с интервалом 100 мс. Единая форма намеренна — вызывающему не нужно знать, какие блоки сборки — и ничто не заставляет вас ждать, потому что следующий поиск измеряет сам слой.

  • Слой зависимостей измеряется относительно профиля в его текущем виде. Добавьте мод в mods.required — и его архивы появятся как added; удалите — и его архивы будут читаться как missing. Это требование (объявленный набор — часть того, из чего строится слой), но выглядит так, будто индекс устарел, когда на самом деле изменился профиль.

  • core всегда включает конфиги игры, и это большая часть его стоимости. 70 секунд с ними против примерно 4 секунд только для скриптов. Переключателя нет: без конфигов на вопрос «есть ли класс предмета под названием X» нельзя ответить, а вторая ось сделала бы измерение устаревания неоднозначным — обход не знал бы, ждать ли архивы Addons.

  • knowledge_show отвечает сначала ближайшим слоем. Для класса, который зависимость переоткрывает с помощью modded class, объявление мода идёт раньше объявления игры. Это правильный порядок, и он удивителен; передайте layer='core' для собственного объявления игры.

  • Условная компиляция индексируется, а не разрешается. 4,9% строк скриптов игры находятся внутри #ifdef, включая около сотни объявлений классов. Этот сервер управляет серверной, клиентской и диагностической сборками, поэтому единого правильного набора defines не существует: индексируется всё, и guard записывается в объявление. Поэтому может быть сообщено имя, которое конкретная сборка исключает — альтернатива, фильтрация по одной догадке о defines, отрицала бы существование методов, которые есть в работающей сборке.

  • Отпечаток C12 несёт размер файла, а размер binarize нестабилен. Пересборка модели, которую никто не редактировал, дала артефакт на один байт больше, чем поставляемый, с тем же типом, тем же количеством LOD и теми же пятьюдесятью строками — и другим дайджестом, потому что размер входит в него. Поэтому C12 может предупредить о пересборке, которая ничего не изменила. Он предупреждает, а не отказывает, именно по этой причине, и части, из которых он построен, сообщаются рядом, чтобы сравнение можно было провести вручную. Разделение дайджеста на стабильную половину и размер — очевидное улучшение, и оно не сделано.

  • Частичный экспорт предупреждает; он не отказывает. С собственными аргументами экспортёра по умолчанию модель вышла с 2 из 5 LOD и прошла все остальные проверки. Этот сервер не передаёт эти аргументы, поэтому такого не должно происходить — но объект, помеченный как LOD и не связанный со сценой, учитывается на одной стороне сравнения и не учитывается на другой, что является законной причиной для расхождения счётчиков, поэтому отказ дал бы ложные срабатывания. Читайте E3.

  • Правило контейнера не видит каждый неправильный корень. Оно отказывает корню, который не содержит папку префикса мода — это измеренный сбой. Корень, который всё же содержит папку с таким именем — например, репозиторий, чей собственный каталог мода написан как префикс — проходит его, и этот случай ловят C10 или C3/C4 на один слой ниже. Измерено: при указании на такой корень сборка отказала, ничего не развернула и оставила поставляемый артефакт нетронутым, но отказ пришёл от задания, а не от вызова.

  • asset_export требует, чтобы экспортирующий аддон был включён в Blender, и не может его установить. Blender запускается с реальными настройками владельца машины, потому что запуск с --factory-startup полностью убирает аддон. Остальные его аддоны удерживаются вне пути поиска для запуска (два из установленных здесь обращаются к сети при старте и обвиняются в сбоях), что Blender сообщает как «Add-on not loaded» в журнале — эта строка — дело рук самого сервера, а не ошибка.

  • У бинаризованного конфига нет тела для показа. knowledge_show(body=True) читает объявление обратно из файла или архива, из которого оно было проиндексировано, но config.bin хранит бинарную форму, тогда как индекс хранит то, что CfgConvert из неё сделал. Ответ сообщает об этом вместо того, чтобы вернуть ничего.

Установка

python -m pip install -e ".[dev]"
python -m pytest

Зарегистрируйте в вашем MCP-клиенте:

{ "mcpServers": { "dayz": { "command": "dayz-mcp" } } }

Лицензия

GPL-3.0-or-later. См. NOTICE.md.

Install Server
A
license - permissive license
B
quality
B
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Related MCP Servers

View all related MCP servers

Related MCP Connectors

  • An MCP server that gives your AI access to the source code and docs of all public github repos

  • A MCP server built for developers enabling Git based project management with project and personal…

  • Augments MCP Server - A comprehensive framework documentation provider for Claude Code

View all MCP Connectors

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/covalschi/dayz-agentic-modding-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server