Why batch operations matter for Godot MCP
by Fireal Software · ~7 min read
Ask Claude to build a grid of 10×10 tiles. Without batch tools, that’s 100 individual godot_create_node calls, each one a full MCP round trip — tool call, TS server, WebSocket, GDScript handler, response back up the stack. At 25ms per call on localhost, that’s 2.5 seconds of latency for a task that takes Godot maybe 50ms to actually do.
Multiply by a whole session — an agent doing scene generation, property tweaks, signal wiring — and the call overhead dominates. Batch operations exist because “one call per intent” doesn’t scale.
The batch tools
Five tools in src/tools/batch-tools.ts:
godot_batch_operations— run any array of{method, params}calls in parallelgodot_batch_get_properties— pull properties from many nodes in one gogodot_batch_set_properties— set properties on many nodes in one gogodot_batch_create_nodes— create many nodes in one call (sequential, to respect parent-before-child)godot_batch_delete_nodes— delete many nodes in one call (sequential, deepest first)
The first is a generic escape hatch. The other four are common-case wrappers.
Chatty vs bulk: the numbers
A back-of-envelope for creating 20 nodes:
Chatty (20 separate calls):
- 20 × (stdio round trip + JSON-RPC request/response + WebSocket round trip + GDScript handler + response)
- ~20 × 25ms = 500ms wall time
- Plus: 20 × tool schema overhead in Claude’s context on every call
Batch (one call):
- 1 × stdio round trip + 1 × WebSocket round trip
- 20 × GDScript handler inside one WebSocket message
- ~80ms total
- One tool schema in Claude’s context
6× faster end-to-end, and 20× less context overhead. Both numbers matter, but the context one matters more for long sessions where tool schemas compound.
The N>3 heuristic
AGENT_PROMPT.md in the Godot Catalyst repo tells Claude:
Batch tools exist for bulk property changes and bulk node creation. Use them instead of N individual calls when N > 3.
Three is the cutoff where the batch overhead (writing the array arg) starts saving over three individual calls. For 1-3 calls, individual is about as fast and has clearer error semantics. For 4+, batching wins.
Claude mostly follows this without prompting after a few sessions, but reinforce it in your project’s CLAUDE.md if you see the model regressing to chatty mode:
Use batch tools when operating on more than 3 nodes:
- godot_batch_create_nodes for creating multiple nodes
- godot_batch_set_properties for updating multiple nodes
- godot_batch_delete_nodes for removing multiple nodes
- godot_batch_operations for mixed operations
How batch_create_nodes works
Input:
{
"nodes": [
{
"parent_path": "/root/Main",
"node_type": "CharacterBody2D",
"node_name": "Player",
"properties": {"position": "Vector2(100, 100)"}
},
{
"parent_path": "/root/Main/Player",
"node_type": "Sprite2D",
"node_name": "Sprite"
},
{
"parent_path": "/root/Main/Player",
"node_type": "CollisionShape2D",
"node_name": "Collision"
}
]
}
Creates sequentially in array order. Parent-before-child is the caller’s responsibility — if you ask to create a child before its parent exists, you get an error for that entry and the rest continue.
Return is an array of {name, status, result|error} entries matching the input. Partial success is the expected behavior: if three nodes create and one fails, you get three successes and one error. The caller decides whether to roll back.
How batch_set_properties works
Input:
{
"updates": [
{"node_path": "Player", "properties": {"speed": 300, "jump_height": 500}},
{"node_path": "Player/Sprite", "properties": {"modulate": "Color(1,0,0)"}},
{"node_path": "Enemy1", "properties": {"active": false}}
]
}
Under the hood this runs Promise.allSettled — every update goes out in parallel. The editor handles them on its main thread in the order the WebSocket delivered them, but from the MCP server’s perspective they’re concurrent.
Return shape is again an array of successes and errors. Partial success is normal.
When parallelism bites you
godot_batch_operations runs operations in Promise.allSettled, which means concurrent fire. For read-only queries this is safe. For mutations this is mostly safe because Godot’s editor main thread serializes them, but there are corner cases.
Creating a parent and a child in the same batch, in parallel? Nondeterministic. The child creation might land before the parent exists. This is why godot_batch_create_nodes specifically goes sequential — the sequential order is load-bearing for parent-child creation patterns.
If you’re using godot_batch_operations for mutations that have ordering dependencies, split them into separate batches and await each. Or just use the specialized batch tools where the ordering is sorted out for you.
Batch delete needs reverse order
godot_batch_delete_nodes sorts the input by path depth, deepest first:
const sorted = [...node_paths].sort(
(a, b) => b.split("/").length - a.split("/").length
);
Deleting Player/Sprite before Player is safe. Deleting Player before Player/Sprite would leave a dangling reference (or more realistically, the second delete would fail because the node is already gone).
The sort is one line but it’s the kind of thing an agent would miss if you asked it to write the tool from scratch. Wrapping this in godot_batch_delete_nodes means Claude doesn’t have to think about it.
When not to batch
Batch tools are for “I have a list of things to do”. They’re not for “I need to decide what to do based on the result of the first operation”.
- Chain: create a node, read its auto-assigned name, set a property using that name → individual calls, because step 2 depends on step 1’s result.
- Batch: create five nodes with predetermined names → one
batch_create_nodescall.
Claude usually picks correctly. If you see it doing chatty calls where a batch would work, prompt it: “batch the node creations into one call”.
The runtime tool bridge
There’s also godot_execute_snippet which isn’t technically a batch tool but serves a similar purpose — run arbitrary GDScript in the editor process. For very complex batch operations where even batch_operations is awkward, writing a GDScript block and calling it as a snippet can be the cleanest path.
This is the “escape hatch above the escape hatch”. Use sparingly — arbitrary code execution has obvious risks, and a well-designed tool call is almost always cleaner than a GDScript string.
The design principle
Every tool call has cost. Optimize for calls that do real work per call. If you find yourself making 50 small calls, there should be a batch tool for that pattern. If there isn’t, the tool set is incomplete.
The batch category exists because “make a grid” is a common ask, “update all these nodes” is a common ask, “clean up this subtree” is a common ask. Each of those should be one tool call, not twenty.
Turn Claude into a Godot co-developer
Godot Catalyst is an MCP server with 240+ tools for Godot 4.x. GDScript LSP, DAP debugging, offline parsing, asset pipelines. 7-day free trial, $25 one-time.
Try Godot Catalyst