MCP tool to enable complete AI control over your dayz client/server. Used to develop many mods. Can be used to close the loop and let AI test ingame developed mods, or even use AI as a game...
Overview
Two things fall out of that, and both are new for this game: - Editing Enforce Script, config.cpp and models is file work; packing a PBO is a command. What could not be automated was the part that decides whether the change actually works — getting the game up with the mod loaded, putting a player, a vehicle or an object into the exact situation the change is about, and reading the result. That step was a human at the client. DayZ-MCP makes it tool calls: an agent can now change, build, run, measure and fix a mod on its own. - The same verbs that set up a test scene are the ones an event director or an admin needs: see every player, teleport one, spawn or remove something, change time and weather, message everyone, watch the log, wait for a condition — all through MissionServer, all serialized behind one daemon with leases and an audit trail. Point an agent at a server and it can operate it, not just query it.
README
DayZ-MCP
An MCP server that puts an agent’s hands on a running DayZ: build a mod, launch the game, put the world into a state, act, and read back what the engine did — 107 typed tools, server-authoritative, no keyboard, no OCR.
Two things fall out of that, and both are new for this game:
- The autonomous mod-development loop closes. Editing Enforce Script, config.cpp and models is file work; packing a PBO is a command. What could not be automated was the part that decides whether the change actually works — getting the game up with the mod loaded, putting a player, a vehicle or an object into the exact situation the change is about, and reading the result. That step was a human at the client. DayZ-MCP makes it tool calls: an agent can now change, build, run, measure and fix a mod on its own.
- An agent can run a server. The same verbs that set up a test scene are the ones
an event director or an admin needs: see every player, teleport one, spawn or remove
something, change time and weather, message everyone, watch the log, wait for a
condition — all through
MissionServer, all serialized behind one daemon with leases and an audit trail. Point an agent at a server and it can operate it, not just query it.
The verbs are also useful one at a time — spawn a car and read its telemetry, grab a
frame, raycast a placement, record a 20 Hz drive trace as a regression fixture
(tools/dayz_mcp/vehicle_trace.py:17).
The development loop, as tools
| Step | Tool(s) | What it does |
|---|---|---|
| Build + launch | dayz_test_run(project, mode, build=True, …) |
Packs the mod with AddonBuilder, starts a diag server and/or client with it loaded, waits for readiness. Managed run, returns a run_id. |
| Set up the scene | world_spawn, world_spawn_batch, player_teleport, player_heal, player_godmode, object_set_health, vehicle_enter, inventory_attach, inventory_give, loadout_apply, hands_attach, hands_take, world_time_set, world_weather_set, engine_set |
Put the world into the state the test needs — deterministically, from script. Heal a player at once, seated or not, or switch its godmode. Set one object’s Health and read it back. Spawn up to 200 objects in one call, sent in groups of per_tick that the bridge each creates in one server tick, every one registered under a scene when given a scene_id. |
| Act | vehicle_control, object_anim, vehicle_door, camera_set, camera_path_play, hud_set, player_view_set, notify_players, weapon_raise, weapon_aim, weapon_fire, weapon_sights, input_trigger, player_move, actor_emote, survivor_drive |
Drive, animate, open or close a car door, frame the shot (focus included), move the camera along a keyed path, hide the HUD, set the local player’s first or third person view, raise, aim and fire the local weapon, click, hold or press a key through the script key handlers, walk the on-foot player in a direction or to a point, play an emote on it, and walk a spawned survivor on the server. |
| Iterate the UI | ui_reload_layout, ui_tree, ui_dialog, capture_screenshot |
Reload a .layout written into the client’s profile directory and read back the rectangles the engine computed for it. The file is re-read on every call, so a panel can be edited and re-measured in seconds instead of one repack-and-boot per change. ui_dialog is the client modal (acknowledge/confirm/form) for the local player. |
| Observe | wait_for, logs_since, query_player_state, weapon_state, object_inspect, object_doors, loadout_get, input_describe, world_time_get, scene_entities, vehicle_telemetry, vehicle_trace, player_trace, anim_timeline, scene_raycast, surface_query, server_perf, nav_path_query, players_visibility, capture_screenshot |
Structured state from the server, log tails since a cursor, 20 Hz vehicle and on-foot player traces, client action/animation-phase timelines, raycasts, frames. Building door flags, registered input bindings, the in-game clock, what a scene holds, the server frame rate with its entity counts, a navmesh path and which players could see a point are reads. Data an agent can assert on, not pixels to squint at. |
| Reset + repeat | scene_clear, restore_gameplay, dayz_test_stop, session_acquire_wait, session_release, session_status |
Delete the objects a scene spawned, as object_delete deletes one (or nothing, when one shares a hierarchy with another scene’s), return the world to normal, stop the managed run, hand the game to the next session. |
An agent that can call these can iterate on a mod the way it iterates on code: change, build, run, measure, fix — the way this repo itself was developed and gated.
Godmode is off by default (SUPUESTO D1, option a, not confirmed). Players take
vanilla damage in every run that loads DayZ_MCP, unless the run is a harness test run
whose MCP bridge config says profile=test: there each player is immune to damage
when it connects or respawns. Call player_godmode(on=true) to switch godmode on
explicitly for one player and player_godmode(on=false) to give vanilla damage back;
the choice stays for that player identity across player_respawn until the mission
ends. query_all_players and query_player_state report godmode for each player,
and player_heal heals a player at once without moving it.
For a custom terrain, dayz_test_run(project, mode="server", navmesh_data_server=True, …)
starts the managed server with -startNavmeshDataServer for DayZ Tools NavMeshGenerator.
The option defaults to false and rejects other modes and pack_only; preflight
only validates. Connect the generator and save its .nm separately: launcher readiness
does not certify a generator connection or a usable navmesh. Stop this run with
dayz_test_stop(run_id) as usual.
The server, as tools
| Need | Tool(s) |
|---|---|
| Who is online, where, in what state | query_all_players, query_player_state, object_inspect |
| Move, equip, stage | player_teleport, player_heal, player_godmode, inventory_give, inventory_attach, world_spawn, object_delete, object_anim, vehicle_prepare_fixture |
| Set the stage | world_time_set, world_weather_set, engine_set |
| Talk to players | notify_players |
| Watch | logs_since (tail from a cursor), wait_for (block on a condition or a log substring), telemetry_read, vehicle_telemetry, world_time_get (the in-game clock) |
| Undo, hand over | restore_gameplay, session_acquire_wait / session_release / session_status |
Everything server-side works against a headless server — it returns data, not frames.
Visual capture (capture_screenshot, camera_get, camera_set) needs a rendered
client on the same machine. Several agents can share one running game: the daemon
owns the port, hands out one lease at a time and audits what each holder did.
How it works
Control and data are engine-native and server-authoritative: CreateObjectEx,
StartCommand_Vehicle, the Car setters, RaycastRVProxy, SetTimeMultiplier,
read back in MissionServer. No synthesised keystrokes, no OCR. The one exception is
visual capture — MakeScreenshot is broken in the diag build (T165276), so frames
come from an external window grab of the rendered client, which only reads pixels.
107 tools (+ exec_enforce when an allowlist is configured) across world, player,
vehicle, camera, telemetry, lifecycle, knowledge and session coordination:
action_cursor, action_hold, action_use, actor_emote, anim_timeline, bridge_status, camera_get, camera_path_play, camera_path_release, camera_set, camera_set_focus, capture_screenshot, gameplay_state,
dayz_knowledge_find, dayz_knowledge_prepare, dayz_knowledge_show, dayz_knowledge_status, dayz_test_close, dayz_test_foreground, dayz_test_run, dayz_test_stop, engine_set, entities_query, hands_attach, hands_take, hud_set, infected_drive, input_describe, input_trigger, inventory_attach, inventory_give, inventory_move, key_press,
lease_acquire, list_projects, loadout_apply, loadout_get, logs_since, nav_path_query, notify_players, object_anim,
object_delete, object_doors, object_inspect, object_resolve, object_set_health, pipeline_feedback, pipeline_inbox,
pipeline_resolve, playbook_reload, playbook_run, player_find, player_godmode, player_heal, player_kill, player_look_at, player_move, player_respawn, player_teleport, player_trace, player_view_set, players_visibility, bot_start, bot_stop, query_all_players, query_get_in_condition,
query_player_state, restore_gameplay, scene_clear, scene_entities, scene_raycast, server_perf, session_acquire,
session_acquire_wait, session_cancel, session_heartbeat, session_release,
session_status, session_wait, surface_query, survivor_drive, telemetry_read, ui_click, ui_dialog, ui_focus, ui_reload_layout,
ui_set_text, ui_tree, vehicle_control, vehicle_door, vehicle_enter, vehicle_get_in_client,
vehicle_prepare_fixture, vehicle_release, vehicle_telemetry, vehicle_trace,
wait_for, weapon_aim, weapon_fire, weapon_raise, weapon_sights, weapon_state, world_spawn, world_spawn_batch, world_time_get, world_time_set, world_weather_set.
Several agent sessions can share one running game through a single daemon that owns
the port and hands out leases. The full surface, the transport and the security
model are in dayz-mcp-architecture.md;
the acceptance contract is in product-spec.md. The rules that
hold the design together, and why each one exists, are in
ARCHITECTURE-DECISIONS.md.
In-game numbers
A1. query_player_state against an independent mission marker (target=marker): 0.0313 m, pass line < 0.5 m (product-spec.md:42).
A2. Python held /poll for 600 ms; ticks_in_flight = 4741 (run_045213). Pass line ≥ 5 (product-spec.md:43).
Camera paths (#263). A natural end returns to the player through the same
camera handoff and gameplay restoration used by restore_gameplay, and clears
the playing command id. A successful play replaces a running play and reports
its id as camera_path.replaced_command_id; busy still rejects an exclusive
job. The daemon settles lease tracking on the client’s terminal finished
report. Explicit mode=stop keeps its existing last-pose behavior.
camera_path_release(command_id) is public and requires the same lease as play;
the daemon also uses it internally for lease cleanup. A matching command_id
aborts the path and releases camera/gameplay (matched=true); a stale id cannot
stop its successor (matched=false). The LIVE tool pack still excludes it.
camera_set_focus exposes the existing camera setter with required focus
distance and blur, sharing camera_set’s validation and lease rules.
gameplay_state() reads controls (bridge flag and vanilla engine view), the
HUD widget’s Show flag, HUD_HIDE/QUICKBAR_HIDE and the bridge’s simulation flag
without a lease or state changes. Use it after restore_gameplay alongside
camera_get. PBO rebuild and in-game retest remain required.
Infected heading. infected_drive at 90°: authoritative heading 92.1° (error 2.1°). Run 1a1cb6e1-7230-43c4-9164-41ab3b0be936 (tools/tests/test_task9_spawn_phase_markers.py:101-105). Same run: 76.6 m away from a player at 0.95 m; 270° measured 270.1°, 41.7 m in 46 s, 0.02 m lateral; mode="release" returns vanilla AI (171.7°, 0.25 m/s).
B3. Fixture ready, throttle 1.0, PHYSICS: engine_on_server=1, speedo≈0, pos_delta≈0 (product-spec.md:56). ActionStartEngine returns on INSTANCETYPE_SERVER when PHYSICS (actionstartengine.c:51-58). Cars are not driven from MissionServer (product-spec.md:325). Cars move from the owning client (vehicle_control).
What this cannot do, and why
What this is NOT
DayZ-MCP is a local control and observation surface, not a hosted knowledge service.
- It does not require embeddings, a vector store, or a paid service.
- It does not use OCR to interpret the game.
- It does not call
SendInputor synthesize operating-system keystrokes. - It sends no usage analytics or other telemetry; runtime game telemetry is read locally.
- It includes no integrated knowledge database.
Visual capture is not engine-native. MakeScreenshot exists as a proto (proto.c:142) and is a no-op on DayZDiag as well as retail (T165276). Probe 2026-06-06: MakeScreenshot("$profile:mcpshot.dds") twice, zero .dds anywhere, ScreenShots folder never created (dayz-mcp-architecture.md:17-24). RenderTargetWidget is display-only — no readback to file or bytes (dayz-mcp-architecture.md:28-32). Frames come from an external window-grab of the rendered client (Graphics.CopyFromScreen; first probe meanB 65, nbRatio 0.999 — dayz-mcp-architecture.md:41-42). That grab is the only non-native piece in the stack. A headless server returns data, not pixels (dayz-mcp-architecture.md:62-64; product-spec.md:162-163).
The API key cannot go in an HTTP header. RestContext.SetHeader(string) sets Content-Type only (restapi.c:135-141; tools/README-mcp.md:7). The key travels as ?key= (addon/scripts/5_Mission/MCPBridge.c:226). The listener binds 127.0.0.1; there is no 0.0.0.0 mode and no remote mode (product-spec.md:168).
SetTimeMultiplier(0) freezes the entire simulation, animations included (world.c:14; dayz-mcp-architecture.md:185-187). Condition lighting and weather after seating and animations have finished, not before a pending get-in.
infected_drive speed is not metres per second. speed=3 measured 0.91 m/s (tools/tests/test_task9_spawn_phase_markers.py:105). The scale is uncalibrated.
PHYSICS cars do not move from the server. See the B3 probe above. vehicle_control is the client-owner path.
exec_enforce does not execute on a headless diag server. ExecuteEnforceScript is marked Developer-only (game.c:776) and returned false under NO_GUI, including with the vanilla script-console wrapper (product-spec.md:171-177). Allowlist gating and JSONL audit are verified in-game; script effect is not a contract. The tool is opt-in breakglass, not a general interpreter (product-spec.md:167).
wait_for and logs_since read script logs, .RPT and a profile .ADM only when the launch time is known. Player chat is not in the script log or the RPT. With dayz_test_run(server_adminlog=true) the server starts with -adminlog. Those tools tail a .ADM of any name whose mtime is at or after the run’s start. Without that timestamp no .ADM is read, so a leftover from another run on the same profile stays out. .RPT and script selection is unchanged. (tools/README-mcp.md). wait_for on timeout still returns ok: true with satisfied: false — gate on satisfied (tools/README-mcp.md:124). Its pattern is a plain substring, never a regex: [DayZ-MCP], not \[DayZ-MCP\] (tools/README-mcp.md:126). For a line printed at mission start, pass lookback_from="launch"; lookback_lines cannot reach that far back.
inventory_move authority=client issues the player’s predictive call. That a juncture is held while the item moves is confirmed in game, not by the call. authority=server does not promise there was no juncture already. split_half is the right-click split and can drop the new stack on the ground when nothing has room. split_to_hands does not split a stack that already fits in the hand (ShouldSplitQuantity, itembase.c). to_inventory uses FindInventoryLocationType.ANY, which includes hands, and a landing in that player’s hands counts as confirmed. A client refusal after the job id is read comes back with accepted=false and that code. Only a failure to read the id ends as timeout.
Synchronous RestApi calls block the sim. POST_now is documented as a thread-blocking operation (restapi.c:125-128). The bridge uses callback GET/POST only (dayz-mcp-architecture.md §9, “Bloqueo del loop”).
No OS keystrokes, no OCR, no second game process. Control is CreateObjectEx / StartCommand_Vehicle (dayz-mcp-architecture.md:79, dayz-mcp-architecture.md:84) / MissionServer reads. No OS input (product-spec.md:160-161). Several agent sessions share one running instance through one daemon (product-spec.md:164-166).
Not tested:
- Navmesh follow:
AIWorld.FindPath,RaycastNavMesh,SampleNavmeshPosition,PGFilter.SetCost(all inaiworld.c; the four protos are adjacent but none has been driven).AIWorldhas a private constructor (aiworld.c,private void AIWorld());new PGFilter()has not been instantiated in-game. - Survivor locomotion overrides:
HumanInputController.OverrideMovementSpeed/OverrideMovementAngle/OverrideAimChangeX/OverrideAimChangeY(human.c:253-262) and the vanilla bot FSM under4_world/systems/bot/. Whether a synthetic survivor population is viable is open. - Calibrating
infected_drivespeedto m/s.
What you need
- Windows, with DayZ and DayZ Tools installed (the server talks to
DayZDiag_x64) - DayZ 1.30: v1.4 is verified on 1.30.164.078, the stable build since 2026-10-15
- Python 3.11 or newer
- A DayZ server you are allowed to run mods on. The daemon binds
127.0.0.1only and sits on the same machine as the game it drives — there is no remote mode and no multi-user mode, by design. The reference deployment is a local DayZDiag server plus a client for visual capture; the server-side verbs need only the bridge mod loaded.
Choose the setup that matches the agent:
- Experienced DayZ modder: install DayZ-MCP by itself. The MCP exposes the live engine without adding a knowledge layer.
- New to DayZ modding: use the paired installer. It installs DayZ-MCP with the
DayZ Modding Knowledge Pack
by default; pass
-SkipKnowledgePackto opt out. - Agent with its own DayZ knowledge: DayZ-MCP also works by itself. Its runtime
responses remain actionable:
bad_argsnames the rejected field and expected unit or scale;wait_forreportssatisfied,timed_out,observed, andscannedso the next step follows evidence;bridge_status.reasonnames the readiness cause.
Two halves
addon/ |
The in-game bridge, in Enforce Script. A modded class MissionServer that dispatches commands on OnUpdate and answers over HTTP. Build it into a PBO, or run it with file patching. Tracked in git; the development tree sparse-excludes it, so it exists in a full clone but not in every checkout. |
tools/ |
The Python MCP server, its installer, and the offline gates. |
The mod pulls commands and pushes results; the Python side is a passive
endpoint. Positions and state are read in MissionServer, the authority. The one
exception is driving: a PHYSICS car only moves from its owning client, so
vehicle_control runs there (the B3 probe above).
Install
The usual installer does not need a CLI pin. It registers by calling
claude mcp add / codex.cmd mcp add directly, so an npm .cmd shim is fine:
cd tools
.\install-mcp.ps1 -Register
It creates tools\.venv-mcp, installs the pinned dependencies plus this package,
generates an API key, and writes the client configuration. -Register also
registers the server with your MCP client when dayz-mcp is not already
registered.
The hardened Python path is different: python install_mcp.py --register talks
only to native x64 claude.exe / codex.exe recorded on this machine. Pin those
first (writes under %LOCALAPPDATA%\DayZ_MCP\security\):
cd tools
python install_mcp.py --pin-clis
python install_mcp.py --register
If claude / codex on PATH are shims (.cmd / .ps1), pass the native x64
executables with --claude-exe and --codex-exe. Re-run --pin-clis after those
binaries change. .\install-mcp.ps1 -Register does not read that pin.
In client mode the first tool list is compact: the session and lifecycle tools plus
the reads that need no lease. A lease reveals the rest; releasing it, or a call that
finds it expired or lost, hides them again, and the server sends
tools/list_changed each time. Claude Code does not re-list tools after
tools/list_changed (#93), so a client registered with --client-platform claude
lists every tool from the start, as --no-progressive-disclosure does: that is now
the default for Claude. .\install-mcp.ps1 -Register -ClaudeNoProgressiveDisclosure
and python install_mcp.py --register --claude-no-progressive-disclosure, which add
the flag, are still accepted. Codex keeps the compact list. The list does not gate
calls: a tool it leaves out still runs when called by name, and lease-gated tools
refuse to run without a lease whatever the list shows.
Both installers register the client with --supervised: it serves the host from a
worker the supervisor can replace, so server_reload picks up edited sources without
the host reconnecting, and the lease crosses each replacement. With a positive
--idle-timeout, 1800 seconds by default and never less than 125, the supervisor
also retires its worker once that long has passed with no host line, no answer, no
call in flight and no request of the worker’s that the host has not answered, and
starts a new one on the next host line, which waits for it to start: a host that
leaves a finished thread’s stdin open keeps only the supervisor. The lease has run
out by then, so the next call that needs it reads lease_required. --no-supervised /
-NoSupervised opts out. python install_mcp.py --register refuses to drop an
option the current registrations carry (registration_would_drop_options names each
one) unless you add --allow-option-removal. .\install-mcp.ps1 -Register only
adds dayz-mcp when Claude and Codex do not already have it, and that path
never removes a name. If one of them does, the check cannot be read, or the
name appears before the add, the script stops. Re-register with
python tools/install_mcp.py --register from the repository root, or pass
-ReplaceExistingRegistration when you really want this script to remove and
replace the current registration (that drops options it already carries).
Three run modes (python -m dayz_mcp; tools/dayz_mcp/server_cli.py:96-118):
--client— what the installer registers. Does not bind; proxies to the daemon and starts it lazily. Lets several agent sessions share one running game. It is also the official stdio fallback (plan B): spawn the same process the installer registers, speak MCP JSON-RPC on stdin/stdout, and close it at session end. See tools/README-mcp.md#host-vsock-failure-official-stdio-plan-b.--daemon— the single owner of the port; the only process that talks to the game.- no flag — embedded single-session mode.
Before the first test run
Each of these fails closed with a named error instead of guessing (#93):
-
Server config and profiles. Each project’s
dev_root\_serveranddev_root\_clientroots must already exist, and so mustdev_root\_server\serverDZ.cfg. The default instance also requires the legacy leavesdev_root\_server\profilesanddev_root\_client\profiles(offline uses the client leaf); this batch does not create those. A named instance (--instance) creates only the missing leafprofiles-under the existing role root during launch preparation, and seedsdayz_mcp.jsonthere. An existing file at that leaf, a missing role root, or adayz_mcp.jsonwhose endpoint or key belongs to another daemon is rejected and nothing above the leaf is created. Named VPP SuperAdmins and credentials are not copied; put them in the named server leaf when that instance should have them. Without the server config the server exits with “Could not find server config”. A profile the launcher cannot seed isinstance_config_missing. -
Mission storage that does not classify. A launch refuses with
storage_recovery_requiredand does not delete world bytes. The precise reason and the repair steps are in docs/STORAGE_RECOVERY.md. Stop retries, inventory the mission, and restore only artifacts you can authenticate. -
One
modname per project. Sharedmodnames arebad_project. The PBO file name is not the$PBOPREFIX$namespace.build_namespace_source_mismatch: “Binarize requires the source folder name to match the PBO namespace, ignoring ASCII case. Use pack_only=true if binarization is unnecessary; for a single-segment namespace, rename the source folder compatibly with the project’s registered source policy; otherwise use the project’s own build.”pack_onlyis exempt. -
No retail client. A running
DayZ_x64.exeorDayZ_BE.exeblocks every test run withretail_quarantine. Close it first. -
Which DayZDiag. The daemon starts only the
DayZDiag_x64.exeunderDAYZ_GAME_PATH(default:C:\Program Files (x86)\Steam\steamapps\common\DayZ). Any other executable isexecutable_not_allowed(the retail executables of that install giveretail_manual_lifecycle_required). For a second install such asDayZ Exp, start the daemon withDAYZ_GAME_PATHset to it. -
DayZ Tools on another drive. The bundle builder and the bundle verifier find DayZ Tools the same way:
DAYZ_TOOLS_PATHif it points at them, then the registry, then the defaultC:path.dayz_test_run(build=true)runs the AddonBuilder sealed into the native launcher when it was built, so the daemon must find the same DayZ Tools. If DayZ Tools move, build and register the launcher again (steps 2 and 3 under “What is deliberately not in this repo”).
A DayZ started by hand cannot find Steam
Claude Desktop and Codex keep a private copy of the registry for every process
they start. Once a Steam started from one of their shells has written its keys
there, that copy keeps naming that Steam after it exits. A DayZ you start later
from the app’s shells reads it and stops at “Unable to locate a running instance
of Steam”, while dayz_test_run still launches as long as its daemon runs
outside the app (dayz-mcp starts it through WMI). session_status,
session_acquire_wait and session_acquire name it with the warning
steam_app_view_stale.
- Start that DayZ from outside the app: from Steam, from Explorer, or through
WMI (
Invoke-CimMethod -ClassName Win32_Process -MethodName Create -Arguments @{CommandLine='"" '; CurrentDirectory=''}). - Never start Steam, or write or delete its registry keys, from the app’s shells: that is what freezes the copy.
- To remove the copy, close the app fully and run
tools\clear-app-steam-shadow.ps1from an elevated PowerShell. It backs up the app’s hive and lists the Valve keys in it;-Applydeletes them.
Files created from the app’s shells
Claude Desktop also virtualizes %LOCALAPPDATA% for every process it starts
(Codex was not checked). A new file or folder that such a process creates under
it lands in the app’s own copy,
%LOCALAPPDATA%\Packages\Claude_pzs8sxrjxfjjc\LocalCache\Local\, and the
app’s processes see it in its usual place. Processes outside the app see only
the real folder. So does the daemon when dayz-mcp starts it through WMI, which
it tries first; with DAYZ_MCP_DAEMON_SPAWN_WMI=0, or when WMI fails, the
client starts it directly, inside the app, and that daemon sees the copy as
well. The client’s stderr names the way it took, on a line that starts with
SPAWN:: created through WMI for WMI; for a direct start,
created with CREATE_BREAKAWAY_FROM_JOB or launched job-bound, after a line
that ends in launching directly if WMI was tried and failed.
A change to a file that only the real folder has goes to the real file:
runs.json and coordination.json were updated in place on 2026-10-08. A
file that the copy already has opens in the copy for the app’s processes, so
their changes stay there. For one, an installer run again from the app updates
the knowledge pack in the copy and leaves the real folder as it was.
So what the installers or the doctor create from the app’s shells can be
missing for the daemon. On 2026-10-08 the knowledge pack installed by
install_mcp.py --register --instance 130 from Claude’s Bash was in the app’s
copy, and the doctor run there reported PACK_SKILLS_UNREGISTERED with a
manifest path inside it. Lock files under DayZ_MCP_shared\build-locks had
been created there as well: a lock file made inside the app and the one the
daemon makes are two files, and they do not exclude each other.
- Run
install-mcp.ps1,install_mcp.py,python -m dayz_mcp.doctorand anypython -m dayz_mcpyou start by hand from a PowerShell opened outside the app, for example from the Start menu. - A path with
\Packages\and\LocalCache\in their output is a sign that the command ran inside the app, unless a setting such asDAYZ_MCP_PACK_DIRorLOCALAPPDATApoints there: check those, then run it again from outside. Get-ChildItem "$env:LOCALAPPDATA\Packages\*\LocalCache\Local\*" -Directory -Filter 'DayZ_MCP*'lists the folders left in an app’s copy. Do not delete them: they can also hold files that the app’s own processes wrote and the real folder lacks, such as aninbox, alauncher-policy.jsonor acapture-frame-state.json. To bring what you need back into the real folder, from a PowerShell outside the app:- Stop what writes to either folder. Close every Claude and Codex session
that uses dayz-mcp, then wait for the daemon, which can outlive the
session that started it. With a positive
--idle-timeout, 1800 seconds by default, it exits by itself once that long has passed with no game polls and no client requests; with--idle-timeout 0it never does, and only stopping its process outside MCP ends it. Go on whenGet-CimInstance Win32_Process -Filter "Name = 'python.exe'" | Where-Object CommandLine -Match '-m dayz_mcp --daemon' | Format-List ProcessId, CommandLineprints nothing. It matches text in the command line, which it prints with the--idle-timeout: read the line of a process it lists before you stop that process. - Compare each folder with its real one:
robocopy "" "$env:LOCALAPPDATA\" /E /L /XX /NJH /NJS /NDL /FPcopies nothing (/L) and lists every file of the copy that the real folder lacks or holds with another size or time, older or newer. - Copy the real folder to a folder of your own before you change it, so that a file you replace can be put back.
- Copy into the real folder by hand what you keep. For a file that both hold, compare the two first and keep the content you need: the real one may be the newer.
- Move the copy to a folder of your own, from where it can be moved back. If the move fails, or the listing shows the folder again, something is still using it.
- Stop what writes to either folder. Close every Claude and Codex session
that uses dayz-mcp, then wait for the daemon, which can outlive the
session that started it. With a positive
Security model
Fail-closed from the first line: the listener binds 127.0.0.1 only, every request
carries an API key, and commands outside the whitelist are rejected. The key
travels as a query parameter because the engine’s RestContext.SetHeader() only
sets Content-Type — with a loopback bind the exposure stays local, and the key
is generated per install and never committed.
What is deliberately not in this repo — and how to generate it
Three pieces are machine-local by design. Each one is generated on your machine, in this order:
-
The launcher policy. The builder reads a host file outside the repo and never loads the published example. Copy
tools/launcher-policy.example.jsonto%LOCALAPPDATA%\DayZ_MCP\launcher-policy.jsonand replaceExampleModand every path with trees that exist on your machine — tools/README-mcp.md (“Native launcher host policy”) walks each field. -
The built native launcher. Its source is under
tools/native-launchers/dayz-test-v1/src/; the build output is 40+ MB of compiled launcher and embedded CPython, sealed against the machine that built it (.gitignore:15-16). The shipped dependency lock pins the author’s MSVC and Windows SDK, so re-pin it to yours, then build:cd tools .\.venv-mcp\Scripts\python.exe relock_toolchain.py .\.venv-mcp\Scripts\python.exe build_native_launcher.py --verify-reproducibleThe relock rewrites only the toolchain description; every shipped supply-chain pin (vendored psutil wheel, embedded CPython URL and hash) stays byte-identical. The build downloads the pinned CPython once, verifies it against the lock, and builds twice to prove the output reproducible.
-
The launcher registry. Only the empty baseline ships. Seed the live registry from it, then install the bundle you just built:
.\.venv-mcp\Scripts\python.exe -m dayz_mcp.launcher_registry_update bootstrap .\.venv-mcp\Scripts\python.exe -m dayz_mcp.launcher_registry_update install-dayz-test-v1 --expected-sha256
Tests
The MCP server and its test suite are Windows-only.
cd tools
.\.venv-mcp\Scripts\python.exe -m unittest discover -s tests -t .
Tests that need a built launcher, an installed registry or development-only evidence skip with the reason named. Everything else passes on a fresh clone, so anything red is worth reporting.
For the edit-run loop there is a fast tier. With DAYZ_MCP_FAST_TESTS=1 the
tests marked @slow_test in tools/tests/_tiers.py skip with their reason: a
few hundred that start real processes or wait on real time, and most of the
suite’s run time. The rest runs in about a minute. It is a subset, not a
verdict: CI runs the whole suite on every pull request, and that run is the
one that has to pass.
$env:DAYZ_MCP_FAST_TESTS = "1"
.\.venv-mcp\Scripts\python.exe -m unittest discover -s tests -t .
Remove-Item Env:DAYZ_MCP_FAST_TESTS # back to the whole suite
The virtualenv has to be the one the installer creates, at tools/.venv-mcp:
the daemon resolves that path when it checks its own identity, and an environment
somewhere else fails at startup rather than falling back. The daemon then prints
one line naming the interpreter it expects and exits with code 78.
Licence
MIT — see LICENSE.
DayZ is a trademark of Bohemia Interactive. This project is not affiliated with or endorsed by Bohemia Interactive, and ships none of their game data.
Install
This server does not publish a one-line install command.
Open the repository installation guide New to MCP? Start with the mcpskill.org guide →