Skip to main content
Glama

mcp-defold

MCP server for driving Defold games through an in-game debug bridge. An MCP client can build the game, launch it, start a run, press inputs, and read the screen, state, and logs. Enough to playtest.

I built it because tests and simulators kept passing on builds of my own games that were broken or unplayable. Making the agent play the real build caught what they missed.

Tools

Tool

What it does

defold_run

Build with the bridge enabled and launch, detached. Returns when the bridge answers a ping from this server, not just when the run script says it should. The one exception is a ping that never leaves the per-port queue, which means another call is already connected there. Before launching it pings the port and refuses if anything accepts the dial: a bridged game already answers, the ping times out waiting for the slot (another call is connected), or the port is held by something that never answers, replies with something that is not JSON, replies with JSON that is not an object, or hangs up or resets before replying. One test each. Only a refused dial (BridgeUnreachable) reads as an empty port. On a refusal, defold_stop first, except after a queued ping: then let the other call finish and call defold_run again. When the holder gave no bridge reply, the refusal says defold_stop cannot reach a wedged engine and gives the commands to find the process on the port. It says to end that process only if it is the game; if another program holds the port, end it or give the project another port in projects.json. skip_build relaunches fast.

defold_stop

Quit the running game cleanly, then wait for the port to be free and say if it is still held. force kills the Defold engine on the port by PID (Windows) when the bridge no longer answers; anything that is not a Defold engine is refused.

defold_start

Skip the front-end (title/chargen) and begin a run. options passes game-defined start options through.

defold_input

Press input triggers in order, then return the screen. stop_on (down, mode_change, player_turn, scene, combat_end) ends the batch at the first key that meets one, read from the game's per-key reply; mode changes and unhandled keys are reported above the frame.

defold_screen

The current screen as a composed text frame.

defold_screenshot

A PNG of the game's real window, captured without bringing it to the front (Windows, PrintWindow). For what the text frame cannot show: fonts, colours, layout, sprites.

defold_state

Compact game-state JSON (fields are game-defined).

defold_log

Tail of the game's mirrored debug log.

defold_bridge

Raw passthrough for game-specific bridge commands.

defold_test

Run the project's headless test suite, if configured. A non-zero exit is a tool error, not a result. tail widens the output window (default 12 lines).

Related MCP server: godot-test-mcp

Install

git clone https://github.com/haksanlulz/mcp-defold
cd mcp-defold
npm install
cp projects.example.json projects.json   # register your game

Runs directly with tsx; no build step. PowerShell 7 (pwsh, cross-platform) runs the run and test scripts, and the unit suite spawns it too. Skip the projects.json step and the server still starts: the first tool call is what tells you to copy the example.

Two Node floors, and they are different numbers.

  • Runtime: package.json declares engines: >=18 and the single runtime dependency agrees, but that bound is declared and hasn't been measured. CI runs 20 and 22; nothing here has been run on 18.

  • Development: higher, and measured. npm install pulls vitest (4.1.11 as locked, engines: ^20.0.0 || ^22.0.0 || >=24.0.0), which pulls vite (8.2.2, ^20.19.0 || >=22.12.0). Without engine-strict that's a warning, and this repo ships no .npmrc, so an install on 18 is untested, which isn't the same as known broken.

Node 20 or newer to work on it. Below that, treat the runtime bound as unverified.

Use it from an MCP client

{
  "mcpServers": {
    "defold": {
      "command": "npx",
      "args": ["tsx", "/absolute/path/to/mcp-defold/index.ts"]
    }
  }
}

Env: MCP_DEFOLD_PROJECTS overrides the registry path; MCP_DEFOLD_DEFAULT picks the default project.

A session

Four calls take an agent from nothing to a readable game. The frame below is real output from a private project, with its proper nouns replaced.

defold_run builds with the bridge compiled in, launches the engine detached, and returns once the bridge answers a ping from this server (with the queue exception noted in the table above):

== bob build (variant debug, bridge port 17890) ==
  bob build OK
== launching dmengine (detached, log: .defold-tools/last-bridged-run.out) ==
  pid 30032
== bridge LIVE on 127.0.0.1:17890 ==

defold_start begins a run. It exists because injected input can't reach a GUI front-end: a title screen or character creator has to be skipped through the bridge, since it can't be driven. options is whatever your game's bridge accepts, here {"mode": "casual"}:

{"ok":true,"started":true}

defold_input presses triggers in order and returns the screen. This is {"triggers": ["key_w", "key_w"]}, two steps north into a fountain:

== village | Day 1, Hour 08 ==
HP 17 / 17 | Stamina 25 / 25
...................>....................
.,,,,,,.,,,,,,,,,,.:.,,,,,.,,,,,,,,,,,,.
.,,,,,.,,,,,,,,,,,.:.,,,,,,,,,,,,,,,,,,.
.,,,,,,,,,,,,,,,,,.:..,,,,,,,,,,,,,,,,,.
.,,,,,,,,,,,,,,,::::.,,,,,,,,,,,,,,,,,,.
..........T^^^^^:######^^^^^..........T.
T..........^^^T^:#__p_#^^^^^..........T.
...........^^^^^:##+###^^^^^............
T.........T.....::::...........T........
...................:....................
...............v.v.:....................
...................:....................
...................:...::::::::.........
.........::::::%v%%%%%v%.#####:.........
.........:#####%%%%%%%%%.#___#:.........
.........:#_S_#%%%%%%%%%.+_H_#:.........
>:::::::::#___+%%%%O%%%%.#___#:::::::::>
..........#_N_#%%%%@%%%%v#___#..........
..........#####%%%%v%%%%.#####..........
...............%%%%%%%%%................
....................:%..................
.................%K%:%%M%%..............
.................%%%:%%%%%..............
.................#+#:#+#.#+#............
.................#_#:#_#.#_#............
.................###:###.###............
....................:...................
~~~~~~~~~~~~~~~~*=========~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~*=========~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~~~~~>~~~~~~~~~~~~~~~~~~
Contract: clear the west field — 0/3 — 14s
On the stone road | Silver 0 | Talents: none
The fountain bars the way.

The last line is the game's answer to the second press. This server renders nothing. The frame is text the game's own bridge composes, so an agent reads whatever a player would. defold_screen returns the same frame without pressing anything, defold_state returns the fields the game chooses to expose as JSON, and defold_stop quits the game cleanly.

What your game has to provide

Two things, both on the game side (this server is just the client):

  1. An in-game bridge: newline-delimited JSON over localhost TCP answering ping / screen / state / input / start / log / quit. Protocol in PROTOCOL.md; working skeleton in example/bridge.lua. Gate it behind a debug flag so it never ships in release builds.

  2. A run script: builds with the bridge compiled in, launches the engine detached, prints bridge LIVE on <port>.

The screen is text on purpose: compose whatever view carries your game's state (an ASCII map + HUD for a roguelike, a score readout for an arcade game).

Limitations

  • Screens are text frames composed by the game's bridge; there are no pixel screenshots.

  • defold_input presses and releases each trigger; nothing can be held down.

  • Bridge input drives the in-game controller and can't reach the GUI front-end; defold_start exists because injected input can't get past menus.

  • Run and test scripts are invoked through pwsh; there is no other runner.

  • Bridge calls are serialized per port, because the bridge answers one client per frame. Parallel tool calls queue rather than overlap, and each one's timeout covers its wait as well as its socket time.

  • That serialization is per call, not per tool. defold_input makes one call per trigger plus one for the screen, so a defold_screen fired in parallel can return a frame from the middle of the sequence. Drive one tool at a time when the order matters.

Testing

npm test          # vitest: tool handlers against a fake in-process bridge (no Defold needed)
npm run smoke     # spawn the real server over stdio and exercise its error paths (offline)
npm run typecheck

Two tiers, both offline. npm test is the unit tier: 80 tests in 2 files, run in about twenty-five seconds. Thirteen of them spawn a real pwsh to exercise the exit-code, marker and skip_build paths, so PowerShell 7 is a requirement of the unit tier as well as of the run scripts. One of the thirteen spends eight of those seconds waiting out a real queue timeout, which is the behaviour it is there to pin. npm run smoke spawns the real server as a child process over stdio and checks its error paths and the run-script marker contract; it needs no Defold install and no network. npm run typecheck reads index.ts, server.ts, smoke.ts and the tests, so the smoke harness is typechecked by the step that runs before it.

Counts, measured 2026-10-13:

  • app: 979 lines (find . -maxdepth 1 -name '*.ts' -not -name '*.test.ts' -not -name 'smoke.ts' | xargs wc -l; index.ts + server.ts). smoke.ts is another 298 lines of test harness.

  • tests: 1430 lines (find test -name '*.test.ts' | xargs wc -l), 74 it() blocks.

What they cover, by layer. The TCP bridge layer: timeout, unreachable port, non-JSON reply, a reply that parses into something other than an object (null, a number, a string, an array), a 70 KB reply split across chunks, a bridge that hangs up before replying (what a game quitting mid-request produces), a bridge that resets after accepting (its message must not send the caller to defold_run, which would refuse), a frame field that is not a string, five concurrent calls against a bridge that serves one at a time, a queued call's deadline, that a call which gives up while queued still frees the slot behind it, and which cause and which figure a timeout message carries in each of the two cases (queued, and dialed after a wait; the second is also measured as elapsed time, so the remaining-budget arithmetic can't be dropped without a test noticing). The registry loader: missing file, missing fields, non-string testScript, duplicate ports. The marker scan: each of its three verdicts, the precedence between them, and both of the paths that reach defold_run (failure marker, and an exit with no marker at all). The tool handlers: every tool driven through a real MCP client against a fake bridge that speaks the wire protocol, including input ordering, the failing-trigger message, argument validation on every argument the tools take (triggers, options, args and cmd, the wrong-typed tail and skip_build that are rejected rather than coerced, and project, where both a non-string and an empty string are errors rather than a silent fall-back to the default), a project name that is an Object.prototype key, the refuse-to-double-launch preflight in seven cases (the ping answered, a ping that never leaves the queue, and a port that accepts the dial and then never answers, sends non-JSON, sends JSON that is not an object, hangs up, or resets), the post-marker confirmation in all three of its outcomes (a bridge that answers, a port with nothing on it, and a ping still queued behind another call), the missing-root check, defold_log's tail default, defold_run's skip_build, defold_bridge's frame-or-JSON branch, and MCP_DEFOLD_DEFAULT project selection. defold_test's exit-code handling runs real pwsh scripts. A source-level guard pins the import surface to the stdio transport and the runtime dependency list to one entry.

Mutation probes, both re-run against the 80-test suite on 2026-10-13. Swapping the spread order in defold_start, so a caller's options.cmd overrides the fixed start command, turned exactly one test red: defold_start options cannot override the start command, with the other 79 green. Inverting the precedence in markerVerdict, so a failure marker outranks a success marker, turned exactly one test red: lets success outrank a failure printed earlier in the run, with the other 79 green. The source was restored after each run. Every fix in this suite's history carries its own probe in the commit that added it. These two are re-measured here because a green count is only true of the suite it was counted on.

Assertion policy: assert behavior and payloads, never that a function was called. A grep for toHaveBeenCalled finds zero sites in this suite; nothing was pruned.

AI assistance

This project was built with AI assistance (Claude). Correctness was established by the test suite (tool handlers driven end-to-end through a real MCP client against a fake in-process bridge that speaks the actual wire protocol, plus failure-path tests for the TCP layer), an offline smoke run that spawns the real server over stdio and checks its error paths (npm run smoke), and playtesting my own games with it. I review the code and am responsible for it.

License

MIT © Abishai James. Defold is a trademark of the Defold Foundation; this project is unofficial.

Available Tools

9 tools
defold_bridgeA

Raw bridge command passthrough for anything the named tools don't cover. cmd = the bridge command string; args = an object merged into the request. Returns the frame (if any) or the JSON response.

Inputs: cmd (required), args, project.

defold_inputA

Press one or more input triggers in the running game, in order, then return the screen. Trigger ids are the game's action ids from its input bindings (e.g. key_w, key_e, key_space). NOTE: bridge input drives the in-game controller, not the GUI front-end; use defold_start to get past menus first.

Inputs: triggers (required), project.

defold_logB

Tail of the running game's mirrored debug log.

Inputs: tail, project.

defold_runA

Build the target project with the debug bridge enabled and launch it (windowed, detached). Returns when the bridge answers (~30-90s with a build). Pass skip_build=true to relaunch the last build fast. If a bridged game is already running, stop it first with defold_stop.

Inputs: project, skip_build.

defold_screenA

The running game's current screen as a composed text frame: whatever view the game's bridge composes (an ASCII map + HUD for a glyph game, a score readout for an arcade game).

Inputs: project.

defold_startA

Begin a run, skipping the game's front-end (title menu / character creation) via the bridge start command, which GUI input can't reach. options is passed through to the game's bridge; what it accepts is per-game (a new-game seed, a character build, a scenario id).

Inputs: options, project.

defold_stateB

Compact state of the running game as JSON. Fields are game-defined (position, health, score, flags, whatever the bridge reports).

Inputs: project.

defold_stopB

Quit the running bridged game cleanly (sends the bridge quit command).

Inputs: project.

defold_testA

Run the project's headless test suite (if a testScript is registered). Returns the tail of the output when the suite passes; a non-zero exit is a tool ERROR carrying the exit code and the same tail. Widen tail when the failing assertion is further up than the default window. Can take minutes.

Inputs: tail, project.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    A3.9/5.0

    Scored across 9 tools

    Disambiguation4/5

    Each tool targets a distinct action or output channel, and the descriptions clarify the differences between launching, starting past menus, input, and observation. The only mild ambiguity is between defold_run and defold_start, since both relate to beginning a session, but their roles are clearly separated by the front-end skip distinction.

    Naming Consistency4/5

    All tools share the defold_ prefix and use lowercase snake_case, which makes the set feel uniform. However, the second part mixes imperative verbs (run, stop, start, test) with nouns (input, screen, state, log, bridge), so the pattern is not perfectly consistent.

    Tool Count5/5

    Nine tools is well-scoped for a game debugging bridge: build/launch, control, screen/state/log introspection, raw passthrough, and test execution are all covered without unnecessary duplication or bloat.

    Completeness5/5

    The tool surface covers the full lifecycle of a bridged Defold session: launching, stopping, advancing past menus, sending input, reading frames/state/logs, issuing arbitrary bridge commands, and running headless tests. The raw bridge passthrough fills any long-tail gaps, so there are no obvious dead ends.

    Maintenance

    ActivityNo data
    ResponsivenessOutdated data

    Related MCP Connectors

    Related MCP Servers