1.2.0 — the answers that never arrived
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_diagnosticshanded a.tscnparsed it as GDScript and answered with one
Unexpected "[" in class bodyper scene section, all severity error, which
reads as a broken scene.get_navigation_infopointed at the scene root — its default — answered with
the root's class and nothing else.validate_meshesdropped a path that was not a mesh without saying so.configure_input_mapcreated an action bound to nothing and called it ok, so
the game read that action as never pressed.get_input_mapreturned 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.