Skip to content

Releases: TomasLucasUTN/godot-mcp-bridge

1.2.1 — 12% off what every request pays

Choose a tag to compare

@TomasLucasUTN TomasLucasUTN released this 04 Sep 23:13

v1.2.1 — 12% off what every request pays

Tool definitions are sent on every request, so the default surface is a standing
cost on every message. This release cuts it from 10,835 tokens to 9,533
without removing a capability, and fixes the measurement that had been
understating it.

The number was wrong

Seven of the tools in tools/list are answered by the server itself — status,
the guides, find_tools, the toolset controls. They are not Godot tools and are
not in TOOLSETS, but they ride in every request. They were defined inline in
the ListTools handler, which put them out of reach of the measurement script,
because importing index.ts starts a server.

So the published figure was 8,764 for a surface that cost 10,067. The
definitions live in meta-tools.ts now; the handler and the script share one
copy, and the README quotes what a request actually pays.

What came out

Prose that was already somewhere else. modify_node_property.value listed
the whole Godot variant table, which the scene-editing guide lists word for
word — 213 tokens down to 91, keeping inline the part that prevents a mistake
(Resource-typed properties need a different tool). run_scene enumerated its
own response fields and repeated the testing loop that get_guide exists for.
add_node explained the two child key styles, then children explained them
again. delete_file's confirm and force restated the tool description.

dry_run had eight different wordings across sixteen core tools, most of
them describing what the caller sees in the response anyway. They share the
short form now, except on create_script and edit_script, where the preview
reports something the response cannot: whether the snippet would have matched.

Lists that belonged in the schema. The 22 toolset names appeared four times
in prose across enable_toolset, disable_toolset and list_toolsets, and the
guide slugs twice. Both are enum now — shorter than the sentence that listed
them, and a field a client can validate against.

One behaviour change

modify_node_property is no longer in the default surface. It is
set_node_properties with exactly one entry in the map — same target, same
parse, same save — and carrying both cost 354 tokens per request while giving a
model two near-identical tools to choose between.

It is not gone. It falls back to the scene_editing toolset, so it is
advertised whenever that toolset is on, and it stays callable by name either
way (execution has never been gated on toolsets). set_node_properties takes
one property as happily as many, and now carries the warning
modify_node_property held: a "script" property there rewrites the file and
leaves the editor's in-memory node scriptless, which makes connect_signal
fail.

If your setup calls it by name, nothing changes. If it relies on seeing it in
tools/list, add scene_editing to GODOT_MCP_TOOLSETS.

Numbers

before after
Default surface, as a client sees it 10,835 9,533
Tools in that surface 45 44
list_toolsets 2,006 855
Everything on 50,563 50,934

Tests: 243 Node, 44 live, 786 GDScript. set_node_properties with a single
property, modify_node_property itself, create_scene, a dry-run add_node,
delete_file's confirm gate, and all seven server-side tools were checked
against a live editor after the cuts.

1.2.0 — the answers that never arrived

Choose a tag to compare

@TomasLucasUTN TomasLucasUTN released this 04 Sep 22:31

v1.2.0 — the answers that never arrived

1.1.7 was written but never tagged, so this release carries it and everything
found afterwards: a tool-by-tool pass over all 231 tools, driven by hand against
a real editor rather than through the test suite. That is where most of what
follows came from, and the pattern it kept finding was not tools that fail —
it was tools that answer ok while the thing you asked for did not happen.

The one that hid the longest

A control character in captured output killed the call. Godot's
JSON.stringify leaves C0 control characters raw. One of them makes the whole
message unparseable at the server, which logs Bad control character in string literal and drops it — the call then hangs to its timeout with the answer
already computed and the editor perfectly healthy.

That is what made the GUT runner look broken. GUT prints its summary in colour,
the escape byte rode along in the job's log, and every get_gut_status for a
finished job died on the way back. The symptom pointed everywhere but the cause:
it looked like OS.execute never returning, so the subprocess, the threading
and the project path all got blamed and "fixed" first. A probe writing to a file
showed the handler returning in 43 ms and the plugin reporting the result sent;
the parse error had been sitting in the server log the whole time. Godot's own
JSON parser accepts these bytes, so nothing on that side ever complained — only
the strict parser at the far end refuses them.

Both send paths now clean the payload, so no tool can reintroduce it through
whatever it captures. Tabs and newlines stay; JSON escapes those. GUT now runs
through the tool in ~2 s, the same as from a shell, on both the blocking and the
async path.

Fixed

enable_toolset did nothing for any client but the first. The toolset tools
change what this process advertises, but a proxy forwarded them to the primary:
the primary flipped its own set, the proxy kept serving tools/list from the set
it started with, and the call answered enabled: true while the tools never
appeared. Every client after the first is a proxy. Measured: the list stayed at
45 tools through enable_toolset and the list_changed notification that
followed it. They are answered locally now — 45 tools, enable tilemap, 53.
enable_toolset("all") works too, which GODOT_MCP_TOOLSETS has always
documented and this refused as an unknown name.

Two renames in a row corrupted the source. The language server was told about
a document once, at didOpen, and stayed on version 1 for the rest of the
session — so every later request answered about text that no longer existed on
disk, and gd_rename computes its edit ranges from that text. Renaming a
symbol twice turned keys_counted into keys_seented, in a file that still
parsed, so validate_scripts called it clean.

A shader parameter that silently did not land. set_shader_param parsed its
value without a type, so "#ff0000" stayed a String, set_shader_parameter
dropped it without a word, and reading the uniform back gave the shader's
default. It now parses against the type the shader itself declares, reports the
value that landed, and warns when the uniform does not exist.

A detached game outlived restart_editor and could not be stopped. The
orphan held the runtime connection, so every runtime tool answered from a game
you thought you had stopped, run_scene started a second one beside it, and
stop_scene reached neither. The pid is written to user:// and adopted on the
next call — which needs more than OS.is_process_running, since on Windows that
only knows handles this process opened.

mp_set_authority left a script that no longer compiled, answering ok. It
now compiles what it wrote and restores the original when that fails.

The project map opened on an invisible canvas. fitToView divided the
viewport by the graph's span with a ceiling and no floor, so it could hand the
camera a zoom that renders nothing — including exactly 0 when the viewport had
not been measured yet. 60 unconnected scripts lay out across ~53,000 px, which
fits at 1%.

Said plainly instead of not at all

A long tail of refusals that were technically true and practically useless:

  • A missing path argument came back as "Path escapes the project sandbox"
    a security refusal, sending you after a traversal bug in a call whose only
    problem was the argument you never passed.
  • An unknown tool name reached the client as "HTTP 500 / the primary may have
    shut down, restart your AI client"
    , advice to restart a healthy process,
    with the primary's own "did you mean …" list thrown away on a non-200.
  • gd_diagnostics handed a .tscn parsed it as GDScript and answered with one
    Unexpected "[" in class body per scene section, all severity error, which
    reads as a broken scene.
  • get_navigation_info pointed at the scene root — its default — answered with
    the root's class and nothing else.
  • validate_meshes dropped a path that was not a mesh without saying so.
  • configure_input_map created an action bound to nothing and called it ok, so
    the game read that action as never pressed.
  • get_input_map returned the editor's shortcuts mixed in with the project's.
  • An enum-shaped refusal that did not name the values it would accept
    (set_mesh, set_sprite_texture, and friends).

Takes the form you already write

[x, y] and [x, y, z] work wherever a vector is asked for; "#ff0000"
wherever a colour is. A node path rooted at the root's own name (Root,
Root/Child) resolves, which is how every scene dump prints it. set_mesh
takes "sphere", the same word add_mesh_instance takes to build the node.
configure_input_map accepts the event shape get_input_map hands back.
setup_collision builds a capsule. The argument suggester weights shared name
parts by rarity, so track_path points at track_index rather than at
node_path, and folds singular and plural together.

Tokens

list_toolsets was spending 2,006 tokens listing all 238 tool names on
every call, on a question that is usually just "which toolset do I turn on".
Now 855, with the names one argument away (include_tools: true).
search_project stopped returning hits from res://addons/ by default —
third-party code you did not write, crowding your own matches out from under
max_results — and from the .bak files delete_file leaves behind, which
handed back the contents of a file you had just deleted.

The export log stopped carrying its own file list: a successful export spent 147
of its lines naming each packed file, pushing the real output past the 4,000-char
tail. The same 4 KB now ends on the export's actual errors.

Numbers

Default tool surface 38 tools, 9,516 tokens (everything on: 231 / 50,563)
list_toolsets 2,006 → 855 tokens
GUT through the tool never returned → ~2 s
Tools exercised by hand against a live editor 231 of 231
Tests 243 Node, 44 live, 786 GDScript

Upgrading

Reinstall the addon so the editor side matches the server — the transport fix
lives in addons/godot_mcp/, and an older addon will still drop results that
contain control characters. diagnose_connection reports both versions.

1.1.6 — close the gaps found by building a game with it

Choose a tag to compare

@TomasLucasUTN TomasLucasUTN released this 28 Jul 17:19
bedf492

godot-mcp-bridge 1.1.6 — everything here came from building a game with it

The previous release was a backlog being closed. This one is different: every
item below was found by using the server to build a real Metroidvania vertical
slice — a player controller with a state machine, a component-based combat
system, a boss with phases, a level — and writing down every point where the
tools got in the way.

That produced a list of seven problems and four missing tools. Three of the
seven were not bugs at all
, which is the part worth reading.


The validator was calling working code broken

validate_scripts reported 4 of 4 scripts invalid in a real project. All four
compiled and ran.

It built a GDScript in memory and called reload() on the source, which
compiles the file in isolation — and isolation is exactly what a project
script does not have. Any file referencing an autoload came back as a parse
error, because singletons do not exist to a standalone compile. Any file
extending a class_name from another file came back as "could not find base
class". A comment above the function asserted that autoloads were visible
there. That comment was wrong, and nobody had checked it.

It now loads the file the way the editor does and judges it by whether the
parser resolved a base type. Two seemingly-reasonable alternatives are wrong and
are documented in the code so they do not get "simplified" back in:

  • can_instantiate() is false for a perfectly valid script that references a
    singleton.
  • CACHE_MODE_IGNORE builds a separate copy per load, so sweeping scripts that
    extend each other produces two live copies of one global class and hard-crashes
    the engine
    (exit 5, reproduced on a 3-file sweep of this addon).

The sweep is also bounded now. Validation costs ~34ms per script on the editor's
main thread, so a whole-project sweep on a large codebase could approach the
bridge's 20-second ping watchdog and drop the connection it was answering
through — the failure mode run_scene and find_unused_resources have both
shipped before. addons/ is skipped by default and the response carries
elapsed_ms.

A scene was built with 400 HP and shipped with 100

create_scene was called with a node carrying both a script and
{"max_health": 400}. It answered ok. The value was 100 — the default — and
that was only noticed hours later, at runtime.

Properties were applied before set_script. A node's exported properties do
not exist until its script is attached, so every exported value passed in the
same call was silently dropped. Deterministic, in four separate code paths, for
as long as the tools have existed.

Scripts attach first now. Anything that still cannot be applied comes back under
warnings rather than nowhere.

Writes are read back

A property can exist, accept an assignment without complaint, and still hold
something else. A TextureRect asked for size.y = 6.667 keeps 16, because a
Control's minimum size is its texture's. In the game, that produced grass
strips three times taller than intended and nothing anywhere said so.

Scene tools now compare what landed against what was asked for and report the
difference. Values that land exactly stay silent.

Three tools that were missing

set_node_reference points an exported node slot (@export var target: Area2D, @export var health: HealthComponent) at another node. There was no way
to do this at all — every property tool takes a JSON value and this needs a live
object reference. The workaround, during the game build, was to redesign the
game's components
so they discovered each other at runtime instead of being
wired in the scene. That is the tool dictating the architecture, which is
backwards.

render_scene_preview renders a 2D scene to a PNG without launching the
game, auto-framed on its content. Looking at a scene previously meant a launch, a
runtime connection, and remembering to stop it — so in practice nobody looked,
and every visual mistake was found late or not at all.

restart_editor saves and restarts. A new autoload or class_name is
invisible to a running editor: measured, the setting registers and the script
still fails to compile. Combined with the silent property drop above, that cost
about fifteen minutes of a fifty-minute session to diagnose, and the only way out
was killing the process from a shell.

Two things that were not bugs

send_input works. It was reported broken because a jump never fired. The
real cause was floor snapping cancelling the velocity — something confirmed
later in the same session without connecting the two facts. All three delivery
routes update is_action_pressed and is_action_just_pressed, now covered by a
test that runs as a scene, because Input only advances its action state on real
frames.

create_sprite_animation already sets UPDATE_DISCRETE on the frame track.
That was a hypothesis recorded without checking.

Both are written down because a misdiagnosis nobody records is one somebody
repeats — and this project has now had five of them.

Smaller, but paid for on every call

The piggybacked editor digest no longer carries filesystem_changed. It fires on
every file the agent writes but arrives deferred, so it was attributed to the
developer and rode along on nearly every response — roughly 90% of all digest
content, telling the agent it had written a file it had just written.

game_eval rejects a node_path that does not resolve instead of handing null
to the snippet. The null access that follows cannot be caught from GDScript,
breaks the attached debugger, and costs the entire runtime connection.

The About on GitHub said 213 tools

It said 213. The server shipped 228. Fifteen releases of drift on the first
sentence anyone reads about the project, because the number was maintained by
hand.

scripts/sync-about.mjs derives that sentence from the built tool registry, and
CI checks it on every run using the default token (read-only, so no secret is
needed). "Out of date" is no longer a state the description can be in.


Upgrading

Nothing to do. npx godot-mcp-bridge install stages the updated addon; existing
tool calls are unchanged, with two additions to what responses may contain:

  • Scene-building tools may now include a warnings array. It is advisory — the
    call still succeeded — but it is the only place a dropped or clamped property
    is reported.
  • validate_scripts entries carry message instead of a bare error_code, and
    the sweep skips addons/ unless include_addons is set.

1.1.5

Choose a tag to compare

@TomasLucasUTN TomasLucasUTN released this 27 Jul 20:33

godot-mcp-bridge 1.1.5 — the edits you make can now be saved, and three "known bugs" were never bugs

Twelve tools added and a long backlog closed, but the part worth reading is the
pattern: five times a recorded cause turned out to be wrong, and each real
one came from a test that failed rather than from re-reading the code. Two of
this project's loudest known limitations — including one documented at length in
the 1.1.4 notes — were not defects at all.


save_scene: the loop was broken and nobody noticed

Every mutating tool edits the live tree when its target scene is open in the
editor. That is deliberate: it is what stops a tool call clobbering unsaved work.
But nothing could persist those edits. So the sequence any agent runs — edit a
scene, launch the game, look at the result — tested the previous version of the
file, silently, every time.

save_scene closes it. Every live-path response now also carries unsaved: true
and says to call it before run_scene.

Every write to an integer property was reported as a failure

_values_match compared values of different types as text. JSON has one
number type, so every integer an MCP client sends arrives as a float; setting an
int property with it works, Godot coerces it, and the read-back returns an int.
The check then compared "1" against "1.0" and called a correct write a
failure — and a single-property call, seeing a failure, saved nothing. Two
separate backlog entries ("a no-op reported as failure", "nothing was written to
the file") were this one bug.

Breakpoints work. They always did.

1.1.4 shipped a section explaining that breakpoints never pause the game, with a
Godot source reading to back it. That reading led to a drafted engine bug report.
Do not believe it. The blocker was the editor's "Skip Breakpoints" toggle,
which was on: it suppresses every breakpoint from every source, persists between
sessions, and cannot be read from GDScript.

With it off, on Godot 4.7, all eleven debug_* tools do exactly what their names
say — debug_launch stopped at a real line, debug_scopes returned real frames,
debug_evaluate read velocity = (0.0, 20.0) out of a paused frame, a
breakpoint added to an already-running game hit, and debug_step advanced.

Three independent investigations had each concluded the code was broken, each
one confirming the last, because every test ran with the same flag suppressing
the thing being measured. Nothing in the code was ever wrong.

game_eval does not kill the game either

Recorded as: takes ~24s, the game dies inside GDScript.reload(), the error
branch never runs. Measured, both ways, with the same broken snippet:

game launched from result elapsed
the editor Godot runtime disconnected ~25 s
a plain CLI command Compile error in eval snippet (err=43) 0 s

The error branch is fine. A parse error goes through the engine's global error
handler, and with the editor's debugger attached that handler breaks — the
game pauses, it does not die. Same family as the breakpoint finding.

Snippets are now compiled in the editor first, where nothing is debugging, and
rejected there, so a typo stays a typo. The throwaway compile is given a path
under res://addons/ on purpose: exclude_addons mutes warnings there, so a
project that promotes warnings to errors cannot make the pre-check reject a
snippet that would have run. With no editor connected there is no debugger
either, and the snippet goes straight through.

New tools

  • validate_references — checks that the names scripts use exist: groups,
    input actions, and signals emitted but never declared. validate_scripts only
    answers "does this parse"; these fail silently at runtime instead. Run against
    a real project it found injected faults with the right lines and the right
    suggestions (playrplayer, move_lefmove_left).
  • create_sprite_animation / create_sprite_frames — one call instead of
    ~10 per animation, with the layout tracks emitted before the frame track by
    construction. SpriteFrames could not be built at all before, so the
    AnimatedSprite2D route — what most Godot tutorials teach — was simply
    unavailable.
  • get_skeleton_info / add_bone / set_bone_pose — 2D and 3D stay
    distinct on purpose: Skeleton3D owns bones as internal indices, Skeleton2D
    owns Bone2D nodes.
  • mp_set_authority — generates the code that claims authority.
    multiplayer_authority is runtime state with no scene-file representation, so
    a tool that wrote it into a .tscn would write a value Godot never reads.
  • read_scene takes a properties list — "where is everything" in one call
    instead of an all-or-nothing dump of a fixed twelve. A misspelled name comes
    back under missing_properties rather than silently absent.

Context cost

This server's argument is that a large tool surface is expensive, so two paths
that quietly weren't cheap are fixed:

  • The activity digest rides on every tool response, and nothing capped the
    size of a single event. Godot hands resources_reimported an array of every
    path it just imported, so dropping an asset pack into a project made the next
    unrelated tool call return 1.5 MB — about 390,000 tokens.
  • The unknown-tool error listed all ~180 registered names, and batch_execute
    repeats it per failed operation: one typo in a two-operation batch cost roughly
    2,000 tokens.

The README now publishes the measurement instead of asserting it: core is 7,540
tokens of 45,132
across the full surface, and mcp-server/scripts/measure-tools.mjs
lets you re-run it.

Also fixed

  • validate_script was leaving its throwaway scripts on disk. A real project
    had __mcp_validate_1.gd and its .uid sitting next to the files they were
    copies of, while the code's own comment claimed nothing was written. Both
    validators clean up after themselves now.
  • setup_collision fills an empty CollisionShape instead of adding a useless
    sibling next to it.
  • A res:// path assigned to an Object-typed property is loaded instead of
    silently rejected — set() refuses a String there, so the call reported
    success and nothing happened.
  • add_animation_track warns when a frame track would land before its
    hframes track: Godot exposes no way to reorder tracks, and the result is
    "Index p_frame is out of bounds" logged every frame, blamed on the wrong
    animation.
  • Four handlers were wired with no schema, so no client could call them. The
    registry test runs both ways now: a wired handler must be advertised, or
    declared internal with a reason.

Testing

  • GDScript suite: 375 → 526 checks.
  • Node suite: 124 → 148.
  • Live editor harness: 34 → 44, now covering save_scene, live-tree reads,
    and that an instanced child keeps its script through a full edit-and-save
    cycle.

Upgrade notes

225 tools, up from 213.

Most of this release is in the addon, so reinstall it — updating only the npm
package leaves the fixes in place:

npx godot-mcp-bridge install

New tools additionally need the editor restarted, because the executor's dispatch
table is built once at startup.

1.1.4

Choose a tag to compare

@TomasLucasUTN TomasLucasUTN released this 27 Jul 04:56

godot-mcp-bridge 1.1.4 — run_scene stops freezing the editor, and the game gets a voice

Everything here came from one decision: make the test harness actually launch a
game. Nothing in this project ever had. The moment it did, it found a bug in a
core tool that had been there the whole time and was hiding behind a comment
that described its own symptom as normal.


run_scene froze the editor for its entire timeout, on every call

run_scene waits for the game to come up. Both of its wait loops did that with
OS.delay_msec, which blocks Godot's main thread. With the main thread blocked
the addon's _process never runs, so it never pumps its WebSocket and never
answers a ping — and after two missed ping intervals the server force-closes the
connection. The agent got Godot disconnected back from a call that had launched
the game perfectly well.

It was worse than a freeze, because the conditions those loops were waiting on —
is_playing_scene(), and the runtime-connected flag the server sets — are
themselves updated by the main loop. Blocking it meant they could never flip, so
every call ran the full startup_timeout_ms, whatever actually happened.

That is what this comment, sitting above the timeout default, was really
measuring:

MCPRuntime connects anywhere from ~11s to ~20s after run_scene is called
(engine boot + autoload init + WS handshake — timing varies with system load)

It does not take 11–20 seconds. The default had been raised from 10s to 20s to
accommodate a measurement of the bug, which pushed it past the 20s watchdog and
guaranteed the disconnect.

run_scene is now a coroutine and yields to the SceneTree, the way the wait
tool already did — wait even carries a comment warning about this exact hazard.
It just never reached run_scene.

A test now enforces it. Any tool handler containing an await must be
registered in the executor's coroutine set with a dispatch case, or GDScript
hands the caller a GDScriptFunctionState instead of a Dictionary and the agent
sees "Tool returned no status". The check is verified against a deliberate
regression rather than assumed to work.

The running game now reports what only it can see

1.1.3 gave the developer's editor activity a push channel. The game had none.

Split by what each side can actually observe, because they are not
interchangeable:

  • The runtime autoload reports its own scene swaps (game_started,
    game_scene_changed). The editor cannot see a change_scene_to_file(), and
    it is the event that matters most: every runtime node path the agent is
    holding points at a freed node afterwards. Compared by object identity, so
    reloading the same scene still counts.
  • The editor's debugger reports the game's life and death (game_running,
    game_paused, game_crashed, game_stopped, game_resumed), through a new
    passive EditorDebuggerPlugin. This has to live on the editor side: an engine
    error inside a running game is not observable from GDScript in that game.

The activity summary gained a sentence for the game, which reports state rather
than counting transitions — three scene changes mean the game is in the third
scene — and always calls out a crash.

Also corrected: a comment in mcp_runtime.gd claimed engine errors were captured
into the runtime log ring. Nothing captured them. It now says so, and points at
where they are actually visible.

The activity feed could go silent for an hour

begin_agent_call() opened the agent-attribution window for a full hour, closed
by end_agent_call() at the end of the tool call. A handler that dies mid
coroutine never reaches that second call — and 1.1.3 shipped fixes for three
handlers that did exactly that. Until the editor restarted, every action the
developer took was then filed as the agent's own work and the feed reported
nothing.

The window is now bounded whether or not it is closed properly, and counted
rather than flagged, so two overlapping tool calls no longer un-tag each other.

Testing

  • The e2e harness launches a real game for the first time: 31 → 34
    assertions, covering the runtime feed end to end rather than by source
    inspection. CI runs it under Xvfb, because the game is a separate process that
    does not inherit the editor's --headless.
  • GDScript suite: 358 → 375 assertions.
  • Node suite: 118 → 124.

Also

  • take_screenshot writes into addons/godot_mcp/cache. With the addon
    junctioned into a project, those images could be picked up by the build and
    shipped inside the npm package. Excluded now.

Known limitation: breakpoints still do not pause the game

Unchanged from 1.1.3 in effect, but the investigation moved a long way.

A real bug was found in Godot itself, from source: the debug adapter's
update_breakpoints calls ScriptEditorDebugger::_set_breakpoint, which only
draws the editor's gutter marker, instead of the public set_breakpoint, which
is the one that actually messages the running game. That is exactly why the
adapter answers verified: true and nothing ever stops. Identical in master
and 4.5-stable.

It is not the whole story, and this is stated rather than glossed:
EditorDebuggerSession.set_breakpoint reaches the correct function, and testing
it against a live game — armed twice, on a line running every physics frame —
still did not pause anything. So the remaining fault is downstream of the
message. No fix is shipped on the strength of the source reading alone, because
shipping an unverified debugger fix is the precise failure 1.1.3 documented.

Upgrade notes

213 tools, unchanged. No schema changes.

The run_scene and activity fixes are all in the addon, so reinstall it
(npx godot-mcp-bridge install) — updating only the npm package leaves both in
place. run_scene additionally needs the editor restarted, because the
executor's dispatch table is built once at startup.

v1.1.3 — what the tests found, and what hand-testing found after that

Choose a tag to compare

@TomasLucasUTN TomasLucasUTN released this 27 Jul 00:08

godot-mcp-bridge 1.1.3 — what the tests found, and what hand-testing found after that

Two exercises produced almost everything here. The first was finishing automated
coverage of every tool that writes — 52 of 99 at the start of the cycle, 99 of 99
now. The second was hand-testing the 28 tools added since the original 185-tool
pass, which had never had one.

Both found bugs the other would have missed. That is the point of doing both.


Every analysis tool was reading the wrong scene

analysis_tools was the one tool node the executor never called
set_editor_plugin on. Without the plugin, _edited_root_if_open can never find
the live tree — so every analysis tool silently read the last saved version of
a scene. scene_diff would report "no changes" for an edit sitting unsaved in
the editor in front of you.

This is the same stale-read class that eleven read tools were fixed for in 1.1.1,
reintroduced by adding a new file and forgetting one line. No test caught it,
because no test asserted the wiring. One does now: every _*_tools node the
executor declares must appear in a set_editor_plugin call.

Found by hand-testing scene_diff against a real editor. It would not have shown
up in the headless suite, which runs without an EditorPlugin by design.

close_scene_tab was broken on the minimum version we advertise

EditorInterface.get_unsaved_scenes() is Godot 4.6+. On 4.5 — which the README
promises — calling it aborted the handler mid-way and the caller got "Tool
returned no status". The tool simply did not work on the floor of our own
support range.

Caught by the two-version CI matrix added in 1.1.2, on its first real run: green
on 4.7, red on 4.5. It paid for itself immediately.

Where that API is missing there is no way to ask which scenes are dirty, so the
safe reading is to assume one might be: force is now required explicitly, with
a message that says why, rather than closing a tab that could be holding unsaved
work.

Three shapes of silent failure

Writing the last of the coverage turned up the same shape three times — a tool
reporting success for something that did not happen:

  • remove_state_machine_transition never worked. It called
    find_transition(), a method AnimationNodeStateMachine does not have. The
    handler aborted and returned an empty dictionary — no ok, no error, no
    message, undo action left open.
  • [x, y] was rejected wherever a vector was expected. {x, y} is
    canonical, but an agent that just wrote position: [100, 100] somewhere else
    reasonably tries an array here. set_node_properties handed it straight to
    node.set(), which no-ops on a type mismatch, so you got "set had no effect
    (type mismatch?)" for a perfectly natural value. Tilemap coordinates were
    worse: they fell through to (0, 0) and painted the wrong cell, reporting
    success. Fixed centrally in the shared codec, plus the two places that parsed
    on their own.
  • A res:// path for a Resource property did nothing. Assigning a resource
    by path is exactly what attach_script, set_sprite_texture and
    assign_shader_material take. set_node_properties now loads it.

Also: painting tilemap cells on a layer with no TileSet reported plain success,
when nothing can ever render. It warns now — and the warning disappears once a
TileSet is assigned, because a warning that never clears is one nobody reads.

scene_diff — stop re-reading the whole tree

The question an agent asks constantly is "what changed since I last looked",
and the only way to answer it was to call read_scene again and re-read
everything. On a scene of any size that is the largest single token cost in a
session, and nearly all of it is nodes that did not change.

scene_diff takes a snapshot and returns an id — no tree at all. Call it again
with that id and you get only the added, removed and modified nodes, with
per-property before/after for the ones that changed. It compares the actual
tree rather than tracking tool calls, so the developer's edits show up exactly
like the agent's own.

mp_diagnose — the multiplayer bugs that never error

Six independent Godot Forum threads over about a year describe the same failure
shapes: an RPC arriving before the node it targets has replicated, state
diverging per-peer in ways invisible from one machine. What they have in common
is that nothing errors when you write the code — the game looks fine until a
second peer joins.

mp_diagnose checks for the ones that are statically detectable: a method called
with .rpc() that has no @rpc annotation (the remote call is simply dropped),
a MultiplayerSynchronizer whose replication config is empty, a
MultiplayerSpawner with no spawnable scenes or a spawn_path that does not
resolve. The scaffolding tools build a correct setup; this one checks the setup
you already have.

Editor activity is pushed now, and reads as a sentence

The activity feed used to poll the editor every 1.5 seconds while a client was
subscribed. The editor knows the instant something happens, so it now says so:
each human action goes over the existing socket as it occurs, and the resource
notifies immediately. Polling stays as a fallback for older addons and drops to
a 30-second heartbeat once the first push arrives.

Reading the resource now leads with intent rather than records — "The developer
saved levels/one.tscn and changed the selection 12 times"
— because twelve
selection entries are what the summary exists to replace. Saves, script edits,
reimports and undo are named individually; selection churn, which a single drag
generates continuously, collapses to a count.


Known limitation: breakpoints do not pause the game

Stated plainly because the toolset previously implied otherwise.

debug_launch starts a session. debug_status reports the adapter connected.
Godot accepts and verifies each breakpoint — it responds with
{id, line, verified: true} and source checksums, so the file and the path
mapping are right. And then execution never stops. Reproduced against a line
inside _physics_process that runs every frame, against _ready, and with
stop_on_entry: true. Every downstream tool correctly refuses, because nothing
is paused.

The cause is not yet identified, so this ships as a documented limitation rather
than a guess at a fix. Two things did change: the debug toolset's descriptions
now say this instead of promising breakpoint debugging, and the
.catch(() => undefined) that swallowed breakpoint-application errors during the
handshake is gone — that swallow is why this never surfaced as an error in the
first place.

Until it is fixed, inspect a running game with get_runtime_log, game_eval
and query_runtime_node. The debug toolset is opt-in and not part of core,
so nothing enables it by accident.

C#: the spike is closed, and the answer is no

Four CI runs. The toolchain was never the blocker — Godot Mono, OmniSharp and
netcoredbg all install and start on a plain Linux runner in about a minute. The
blocker is that there is no non-interactive way to produce the .csproj:
--import does not emit it, and --build-solutions never returns (20 minutes
under --headless, exit 124 at a 300s timeout, and exit 124 again under
xvfb-run, so a missing display was the wrong diagnosis — it opens an editor
session and stays open).

cs_* tools built on that would only work for someone who had already opened
the project in a real editor. That is a precondition, and shipping tools that
quietly misbehave without it is worse than not shipping them. C# stays as
create_csharp_script plus an honest csharp_status.

Testing

  • GDScript suite: 285 → 358 assertions.
  • Live-editor harness: 28 → 31, and now covers the editor-only tools
    (select_nodes, close_scene_tab, selection, export presets) that a headless
    SceneTree cannot reach.
  • Node suite: 109 → 118.
  • 99 of 99 mutating tools have automated coverage. The ones that cannot do
    their real work in CI assert their refusal contract instead of being skipped.

Upgrade notes

213 tools (+scene_diff, +mp_diagnose). Nothing removed, no schemas changed
in a breaking way; 1.1.x usage keeps working.

The analysis_tools and close_scene_tab fixes are both in the addon, so
reinstall it (npx godot-mcp-bridge install) — updating only the npm package
leaves both bugs in place.

v1.1.2 — a WebSocket that trusted the wrong thing

Choose a tag to compare

@TomasLucasUTN TomasLucasUTN released this 26 Jul 18:37

godot-mcp-bridge 1.1.2 — a WebSocket that trusted the wrong thing

The bridge binds 127.0.0.1. That stops a remote host. It does not stop the
browser on the same machine, and it was never supposed to be relied on for that.

Any page you visited could drive your editor

A WebSocket handshake is not subject to the same-origin policy. Binding to
loopback keeps a remote attacker out, but a page open in your browser — any
page, while the editor happened to be running — could connect to
ws://127.0.0.1:6505 exactly as the addon does, and from there call any tool.
That includes writing files into the project and game_eval.

Browsers always send an Origin header on a WebSocket handshake and cannot be
told not to. Godot's WebSocketPeer never sends one. So the fix is one check:
refuse any handshake carrying Origin. Always on, no configuration, and it does
not touch the addon's own connection — Godot was never sending the header that
now gets it rejected.

This does not need active exploitation in the wild to be worth a same-day patch.
It needed one page with a script tag.

A shared secret, if you want a second lock

Origin rejection closes the browser vector completely, but not every local
process: something else running as you on the same machine can still open a
plain WebSocket and claim to be Godot. GODOT_MCP_SECRET closes that too — set
it on the server, set the same value in the editor (same env var, or the
godot_mcp/network/secret project setting), and the handshake is checked in
constant time. Unset on either side, and nothing changes: this is additive, not
a new requirement.

SECURITY.md lists what actually guards the bridge now, in order, instead of
just saying "runs on localhost."

Along the way: coverage found three real bugs

Extending the GDScript suite to 29 more mutating tools (52 of 99 covered → 81)
was meant to be mechanical. It wasn't:

  • remove_state_machine_transition never worked. It called find_transition(),
    which does not exist on AnimationNodeStateMachine. The call aborted the
    handler mid-edit and returned an empty dictionary — no ok, no error, no
    message, undo action left open. Fixed by locating the transition index
    directly instead of a method that was never there.
  • Painting a tilemap cell with no TileSet assigned reported plain success.
    The cell is stored; nothing can render it, because the source ID resolves
    against a TileSet that doesn't exist yet. Refusing would break the legitimate
    case of assigning the TileSet afterwards, so both tilemap_set_cell and
    tilemap_fill_rect now succeed and say so in a warning field.
  • Tilemap coordinates silently dropped [x, y] arrays and painted at (0, 0).
    Every other coordinate-taking tool here — add_node's position,
    setup_collision's size — accepts an array, so trying one here was a
    reasonable guess that used to fail quietly. Arrays are now accepted alongside
    {x, y}.

None of these three would have been caught by hand. That's the whole argument
for writing the coverage in the first place.

A live activity feed instead of a poll

get_editor_activity only tells you something if you ask, and the digest
riding along on tool responses only arrives when a tool happens to be called —
between calls is exactly when the developer is doing something worth knowing
about. godot-mcp://editor/activity is now a subscribable MCP resource:
subscribe once and the server pushes notifications/resources/updated when the
developer touches something (the agent's own edits are filtered out, and a
burst — dragging a node — coalesces into one notification instead of a dozen).
Nothing is polled while nobody is subscribed.

Also

  • CI now runs the GDScript and live-editor suites against two Godot
    versions
    — the advertised minimum (4.5) and the latest stable (4.7) — instead
    of one. Both engine breaks this project has had were found by hand, late; a
    version matrix turns the next one into a red build instead.
  • The MCP registry entry now publishes itself on every GitHub release, via
    GitHub OIDC (no secret to leak). It had been sitting at 1.0.0 through two
    releases because nobody re-ran the manual step.

Upgrade notes

Same tool surface as 1.1.1 — nothing removed, no schemas changed. The security
fix lives in the npm package (the bridge), not the addon, so update the npm
package
(npm install -g godot-mcp-bridge@latest, or just let npx pick up
the new version next run) — a reinstalled addon alone does not carry this fix.

If 1.1.1 or earlier is what's actually running today, updating is the same
priority as any other same-day security patch: do it before the next session
where the editor is open in the background while you're also browsing.

v1.1.1 — the release that came from building a test

Choose a tag to compare

@TomasLucasUTN TomasLucasUTN released this 26 Jul 17:09

godot-mcp-bridge 1.1.1 — the release that came from building a test

1.1.0 shipped a debugger and a language server. This one shipped almost nothing
you can point at, and is probably the most important release so far.

It started as a chore: write an automated test for the live-scene path — the case
where the scene is open in the editor and tools mutate the real node tree instead
of a file on disk. That half of the project carries its hardest invariants and had
no automated coverage at all. The GDScript suite runs without an EditorPlugin, so
it only ever exercised the disk path, and the end-to-end suite skipped in CI for
want of a Godot binary. Green by omission.

The test found four things that had been true for months.


Fifteen tools destroyed unsaved work

They loaded the scene from disk, mutated the copy, and saved it. On an open scene
that does two destructive things at once: the save throws away whatever is unsaved
in the editor, and the reload that follows wipes the live tree.

Confirmed by hand before the test existed — an attach_script call deleted an
unsaved node from a real project outright. Not "failed to save it". Deleted it.

attach_script, detach_script, set_node_groups, set_collision_shape,
set_sprite_texture, set_mesh, set_material, set_anchor_preset,
snap_node_to_grid, set_scene_node_property, set_resource_property,
instance_scene, connect_signal, disconnect_signal, and the batch editor.

Not clobbering your work is the first thing this project claims about itself. It
was false for fifteen tools, and nothing would have caught it.

Eleven read tools had the mirror problem: they reported last-saved state, so an
agent that edited and then read back got stale values with no way to tell — which
reads as "the edit silently failed". Also fixed.

Undo was mostly an illusion

Only seven structural actions ever reached Godot's undo history. Every other live
edit — properties, materials, animation tracks, tilemap cells, physics layers —
had no undo entry at all, so Ctrl+Z did nothing and the only way back was closing
the scene without saving.

Worse, the seven that did register were filing into the wrong history.
EditorUndoRedoManager keeps one per scene and infers it from the first object an
action registers; add_node registered the tool node itself, which lives under
the EditorPlugin rather than in the scene. Ctrl+Z on the scene never reached it.

Every live edit now registers, with the history pinned explicitly. Tilemaps record
one snapshot of the layer's whole cell payload rather than an entry per
coordinate, so a hundred-thousand-cell fill costs one undo step. A batch_scene_edit
is a single entry, so Ctrl+Z reverts the whole batch.

undo_last / redo_last are new: the agent can take back its own last change
without asking you to reach for the keyboard. The history is shared with you, so
if you acted after it did, it undoes your action — the response says what it
actually undid rather than assuming.

"Batch discarded, nothing saved" was a lie

batch_scene_edit said that on failure while leaving its earlier operations
applied, because the discard path is a no-op on a live scene. The batch is now
committed as one undo entry and immediately undone on failure, so the message is
finally true.

The advertised Godot version was wrong

The badge said 4.3+. It isn't. The editor-mode scene path — the one every scene
tool depends on — fails outright on 4.3 and 4.4.

This was not a guess or a reading of release notes: the live harness was run
against each release. 4.3 fails, 4.4 fails, 4.5 passes all 28 assertions. The
minimum is now stated as 4.5, and CI pins that version, so the badge is
something you can check rather than something we hope is true.

CI had been pinned at 4.3 the whole time and stayed green, because the old parse
check called load() — too weak to notice. Three constructs needing even newer
engines were replaced with portable equivalents along the way.

validate_scripts flagged a file that was fine

It compiled a throwaway copy built from source alone, which has no path — and
several GDScript warning settings are path-sensitive, notably exclude_addons.
Without a path that exemption is lost, and any warning the project promotes to an
error fails the compile with an empty error list. The copy now gets a path next to
the original.


Also

  • The port is configurable. GODOT_MCP_PORT, or a godot_mcp/network/port
    project setting. More useful: GODOT_MCP_PROJECT makes the bridge refuse any
    editor that doesn't have that project open. The addon dials a fixed port and
    cannot tell which server answered, so without this the first editor to connect
    is trusted with every tool call — which is how a test suite once created a
    scene inside an unrelated game project.
  • query_runtime_node distinguishes "this property is null" from "this
    property doesn't exist". A typo used to read as legitimate state.
  • Stringified arguments no longer coerce "true"/"false" on free-form text
    keys. A node named true used to become a boolean.
  • PathGuard now covers five script-writing functions in the visualizer that
    had been skipping it.

Testing

  • The live harness: 28 assertions against a real headless editor, now running in
    CI where the e2e suite previously skipped.
  • GDScript suite: 150 → 207 assertions.
  • parse_all.gd compiles every addon script instead of calling load(), whose
    resource cache let a genuinely broken file through three times.

Upgrade notes

211 tools (209 + undo_last, redo_last). Nothing removed, no schemas changed;
1.1.0 usage keeps working. Versions bumped across mcp-server/package.json,
mcp-server/server.json and addons/godot_mcp/plugin.cfg.

The addon is where nearly all of this lives, so reinstall it (npx godot-mcp-bridge install) rather than only updating the npm package.

v1.1.0 — a debugger, a language server, and honest C#

Choose a tag to compare

@TomasLucasUTN TomasLucasUTN released this 26 Jul 05:03

godot-mcp-bridge 1.1.0 — A debugger, a language server, and honest C#

Until now, when an agent hit a bug in your game it did what it could: read
get_errors, add a print(), re-run, and infer what the value must have been.
That works until the interesting state isn't printed — a wrong number deep in a
physics step, a null that only appears on the third frame, a branch nobody
expected to be taken.

1.1.0 adds the other option: stop the program at the failure and look.


The debugger

Godot already runs a Debug Adapter (the same protocol VS Code uses) on port
6006, alongside its GDScript language server on 6005. Both are on by default —
the editor prints "Debug adapter server started on port 6006" at startup. This
release adds a DAP client to the MCP server that talks to it directly.

That's an architectural first for this project: the debug_* tools do not
go through the WebSocket bridge to the editor addon. They are answered inside
the Node server, which is also why they keep working when get_godot_status
reports the addon as disconnected — the adapter is a separate listener owned by
the editor.

A session looks like this:

debug_set_breakpoints({path: "res://scenes/player.gd", lines: [17]})
debug_launch({scene: "res://scenes/level.tscn"})
  → { state: "stopped", stopped_reason: "breakpoint" }
debug_stack_trace()
  → _physics_process at player.gd:17
debug_scopes() → debug_variables({variables_reference: 1})
  → delta = 0.01666666666667, direction = <null>
debug_evaluate({expression: "velocity"})
  → (0.0, 0.0)
debug_step({mode: "over"})
  → now at line 18

Eleven tools: debug_launch, debug_attach, debug_set_breakpoints,
debug_continue, debug_step, debug_stack_trace, debug_scopes,
debug_variables, debug_evaluate, debug_status, debug_disconnect.

They are not in core. A debugger is a deliberate move, not something an
agent should wander into while doing routine scene edits — run
enable_toolset({name: "debug"}) first. There's a debugging guide in
get_guide (and in docs/TOOLS.md) covering the loop and the engine quirks.

Two things learned by running it, not by reading the spec

Godot fills frame variables asynchronously. scopes returns variable
references immediately, but the values arrive from the debuggee a moment later.
A variables request that lands first is rejected with a bare "unknown"
which reads exactly like a bug in your own code. The client now retries briefly,
so this is invisible to callers.

Conditional breakpoints may not work. Godot's adapter does not advertise
supportsConditionalBreakpoints in its capabilities. The conditions argument
is forwarded, but the engine may break unconditionally. Documented rather than
quietly promised.


The language server: renames that are actually correct

Godot runs a GDScript language server on port 6005 alongside the debug adapter,
and it understands the real symbol table. That matters because of a correctness
bug this project has been shipping: rename_symbol_project_wide does
word-boundary text substitution. Rename a local speed and it will also rename
an unrelated class's speed, silently.

gd_rename goes through the language server, so it doesn't. gd_references
finds the real references rather than string matches. gd_diagnostics reports
type errors without running the game — the old validate_script only told you
whether a file parsed.

Eight tools in the new code_intel toolset. Only what Godot actually advertises
is exposed: it reports no support for workspace symbols, code actions,
formatting or folding ranges, so there are no tools pretending otherwise.
gd_lsp_status shows what the connected server really supports.

The transport was free — Godot serves LSP with the same Content-Length framing
as DAP, so the client written for the debugger was factored into framing.ts and
reused.


Multiplayer scaffolding

The runtime side could already test netcode (spawn_headless_peers,
call_rpc_runtime). Now it can build it:

  • mp_add_spawner / mp_add_synchronizer — Godot 4's replication nodes. The
    synchronizer is the fiddly one: its replicated property list lives in a
    SceneReplicationConfig sub-resource, so the test asserts the config survives
    a round-trip to disk rather than just checking the call returned ok.
  • mp_wire_rpc — writes a correctly-annotated @rpc method. A wrong annotation
    is the classic Godot 4 multiplayer bug: the remote call silently does nothing,
    with no error.
  • mp_scaffold_lobby — the ENet host/join plumbing every project rewrites.

C#, stated honestly

The standard Godot build has no C# support at all — no CSharpScript class.
A .cs file written there saves fine, attaches to nothing, and fails silently.

Rather than ship C# tools that couldn't be verified, this release adds
csharp_status: it reports whether C# can work in this editor and project before
any is written, and create_csharp_script now carries the same warning in its
result. Full C# language-server and debugger support (OmniSharp, netcoredbg) is
deliberately not in this release — it could not be tested against a standard
build, and shipping unverified tools would undercut the one claim this project
actually cares about.


An opt-in safety gate

GODOT_MCP_REQUIRE_CONFIRM=true makes operations with no undo path require an
explicit confirm: true: file deletes and renames, script rewrites, mass
renames, project settings, autoloads.

Scene edits are deliberately exempt. When the scene is open they go through
Godot's undo history — Ctrl+Z already covers them, and gating them would be
noise. Off by default so nothing built against 1.0 breaks.


Bug fixes: the same shape, three more times

1.0.0 shipped a fix for set_particle_material, which applied several
properties before validating the last one — and on a scene open in the editor,
_discard_scene is a no-op (the target is the editor's live node, not a
copy), so a late validation failure left the earlier writes applied while the
tool reported total failure.

That's a shape, not an incident. Sweeping for it found three more:

  • set_physics_layers — applied collision_layer, then validated
    collision_mask. An invalid mask name left the layer applied.
  • set_material_3d — applied metallic/roughness before validating
    emission_color.
  • setup_environment — worst of the three: it creates a
    WorldEnvironment node and adds it to the live tree, and only then validated
    the sky colours. A bad colour left a stray node in your scene while reporting
    failure.

All three now resolve and validate every argument before writing anything.


Also in this release

  • CI runs the GDScript handlers against a real headless Godot, not just the
    Node-side suite. The half of the codebase that actually mutates scenes was
    previously only covered by tests run by hand.
  • llms.txt at the repo root — a machine-readable project summary, including
    an explicit limitations section.
  • The README now leads with what actually makes this different: it's
    bidirectional (get_editor_activity reports what you did, not just what the
    agent did), and every claim in it maps to a tool that was run by hand.

Upgrade notes

  • Versions bumped to 1.1.0 across mcp-server/package.json,
    mcp-server/server.json, and addons/godot_mcp/plugin.cfg.
  • 209 tools total (185 + 11 debugger + 8 language server + 4 multiplayer + csharp_status). The default core set is unchanged at 35.
  • Nothing removed, no schemas changed. 1.0.0 usage keeps working as-is.

v1.0.0

Choose a tag to compare

@TomasLucasUTN TomasLucasUTN released this 25 Jul 18:29

godot-mcp-bridge 1.0.0 — Runtime loop, generic resource editing, and a serious crash purge

This is the first public release under this name. It introduces a dedicated in-game runtime channel, a much richer set of scene/resource editing tools, an MCP resources layer for discoverable long-form docs, and fixes a half-dozen long-standing footguns — including two reproducible Godot editor crashes.


Headline features

1. Agents can now drive and observe the running game

Until 0.5.0, MCP tools could edit .tscn files and start run_scene, but once the game was running the agent went blind. This release ships a tiny MCPRuntime autoload that the plugin auto-registers when enabled. When you hit Play (or call run_scene), the autoload boots inside the running game and opens its own WebSocket connection to the server as role: "runtime". The server routes runtime tool calls to it, while editor tool calls still flow to the plugin.

With that in place, the agent can:

  • take_screenshot — capture the live viewport as a PNG (not an editor screenshot). Files land in res://addons/godot_mcp/cache/screenshots/ so they survive project renames.
  • send_input — synthesize a mouse button, motion, key, or named InputMap action directly into the running game. This is what makes the "AI plays the game" loop work.
  • query_runtime_node — read any property off a live node by path (/root/Main/Player, visible_global_position, global_transform, etc.).
  • get_runtime_log — pull from a ring buffer maintained inside the game, separate from the editor's get_console_log / get_errors.
  • list_signal_connections (source: "runtime") — see what's actually connected at runtime vs. what the .tscn says should be connected.

2. A reliable testing loop: run_scene that actually waits

run_scene now has:

  • block_until_started (default true) — returns only once Godot reports the scene is playing.
  • wait_for_runtime — additionally wait for the runtime autoload to connect back to the server before returning.
  • startup_timeout_ms (default 10000, up from 6000).
  • Returns scene_path and runtime_root (e.g. /root/Main) computed from the actual root node name — this is the correct prefix for query_runtime_node arguments and fixes a very common "node not found" footgun where the agent assumed the runtime root was named after the scene file.

Combined with the new get_runtime_status (unified playing / scene / uptime / runtime-connected report) and a non-blocking wait tool (see below), this gives the agent the primitives it needs to run a scripted testing loop: start the scene, wait for runtime, send input, screenshot, assert, repeat.

3. Generic resource editing

Previously get_resource_info only understood PNGs saved to disk. Now:

  • get_resource_info introspects any Resource on disk (textures, meshes, audio streams, packed scenes, materials, animations, shapes) AND accepts {scene_path, node_path, resource_property} to read a resource that lives on a node without having to save it to a .tres first.
  • set_resource_property edits any property of any embedded Resource (CircleShape2D.radius, StandardMaterial3D.albedo_color, AudioStreamPlayer.stream.loop, …).
  • save_resource_to_file persists an inline resource as a reusable .tres.

This pattern replaces a long tail of domain-specific tools we would have otherwise needed to add.

4. Scene-editing productivity

  • add_node now accepts script, groups, and children — build an entire subtree with scripts attached and groups assigned in one call.
  • set_node_properties — apply many property changes to one node in a single tool call, with per-property success/failure reporting. Cuts round-trips during scene setup.
  • Node groups: set_node_groups, get_node_groups, find_nodes_in_group — Godot group memberships are now first-class MCP operations.
  • Signal wiring in both directions: list_signal_connections, connect_signal, and disconnect_signal. list_signal_connections accepts source: "scene_file" | "runtime" so you can compare what the .tscn says with what is actually wired at runtime.

5. MCP resources + get_guide tool

Tool descriptions have been trimmed. The long-form guidance moved to proper MCP resources under godot-mcp://guide/...:

  • testing-loop
  • scene-editing
  • asset-generation
  • troubleshooting
  • tool-index

For clients that do not implement resources/list / resources/read (Claude Desktop, Cursor chat, etc.), the same guides are exposed through the new get_guide tool: call with no args to list, or {slug: "testing-loop"} to read.


Tool changes worth flagging

  • generate_2d_asset was rewritten to decode SVG via Image.load_svg_from_buffer instead of writing a temp file in user://. Also accepts optional width, height, and scale. No more temp-file races, no more silent failure after project rename, and the single-quote attribute parser works now.
  • update_project_settings detects application/config/name changes, pre-creates the new user:// directory, and returns a structured warning explaining why previously generated files look like they "disappeared."
  • set_sprite_texture now reports texture_class, texture_path, width, and height in its response. The canonical texture_type is FromPath; ImageTexture is kept as a deprecated alias, and NewImageTexture forces an in-memory ImageTexture when you actually want one.
  • modify_node_property refuses property_name == "script" (it would only update the .tscn on disk, not the editor's live node, which silently broke subsequent connect_signal calls). Use attach_script instead.
  • delete_file description clarifies that confirm: true is required and that a .bak backup is written next to the deleted file.
  • wait accepts either ms or seconds. Any value over 20 000 ms is clamped; the response includes requested_ms and clamped: true when that happens.

Bug fixes

Crashes

  • wait froze the editor and crashed the session on large values. The old implementation used OS.delay_msec, which blocks the editor's main thread — which also pumps the WebSocket. Any wait of ~30 s or more caused the MCP server's 30 s per-request timeout to fire on a still-running tool call; the server would reject the pending Promise and then Godot would try to write the eventual result to a disconnected socket. Net result: crash / broken session. The tool is now non-blocking (SceneTree.create_timer(...).timeout), the hard cap is 20 000 ms to leave buffer under the transport timeout, and the dispatch path (tool_executor.gd, plugin.gd) was made coroutine-aware so the async tool is awaited correctly.
  • delete_file crashed Godot when deleting a scene open in the editor. The previous guard only covered the simplest case. The tool now refuses to delete any file that is open anywhere in the editor (scene tab or script-editor tab), names which tab holds it, and reports whether it is the active one. Opt-in force: true bypasses the guard for known-safe cases.
  • Stale-primary replacement could SIGTERM Godot. On macOS/Linux killProcessOnPort used lsof -ti :PORT, which returns any process with a socket on that port — including WebSocket clients. Godot, as the client on port 6505, was on that list. When a newer MCP server replaced an older primary (e.g. Claude app starting while Cursor's older server was running), the port was killed twice and the second kill landed on Godot. lsof is now filtered with -sTCP:LISTEN so only listeners are killed. (Windows already filtered for LISTENING.)

Correctness

  • Project rename silently broke asset generation and other user:// writes. Renaming the project re-resolves user://, and Godot does not always create the new folder. A new MCPPaths.ensure_user_dir() helper is called from every writer that still touches user://, and the MCP plugin's own scratch space moved under res://addons/godot_mcp/cache/ (project-relative, survives renames).
  • connect_signal did not persist to .tscn. Connections were made with runtime-only flags, so PackedScene.pack() dropped them on save. We now force Object.CONNECT_PERSIST on every connection AND re-read the saved scene to verify the [connection] entry landed. Returns a clear error instead of falsely reporting success.
  • add_node silently accepted wrong keys inside children. Passing {node_name, node_type} (the same keys used at the top level) used to fall back to a generic Node with the default name. Child specs now accept BOTH {name, type} and {node_name, node_type} and reject any unknown keys with a clear error.
  • Structured error details were dropped on the wire. Tools that returned {ok: false, error, …extra fields} (e.g. delete_file's open_in_editor / where / is_active, wait's clamped / requested_ms) had those extra fields stripped before the agent ever saw them. mcp_client.gd now ships the full result dict on failure too, the bridge attaches it to the rejected error as details, and the server merges those details into the visible response.
  • get_godot_status reported a stale hardcoded version. The server now reads its version from package.json at startup, so mode, MCP serverInfo, and the status payload always agree with the actually-installed package.

Upgrade notes

  • Versions bumped to 1.0.0 across mcp-server/package.json, mcp-server/server.json, and addons/godot_mcp/plugin.cfg.
  • If you wrote a client against wait({ms: 60000}), note that values above 20 000 are now clamped and the response includes clamped: true and requested_ms. Existing usage under 20 s is unaffected.
  • If you were setting scripts via modify_node_property with property_name: "script", switch to `a...
Read more