Skip to content

MCP scripts

An MCP script is a JSON file with a list of MCP tool calls, and run_script replays them in order. It keeps how a map was built as readable steps, so you can rebuild the map from scratch and review it as text in a pull request, and the tests can replay it too.

The showcase maps in examples/mcp are built this way, and they double as examples for every tool.

Script Builds
mountain_house.json A cabin hanging off a cliff, with a cliff lift and forests
church_school.json A church and a school where three buttons open the courtyard gate
lighthouse_forest.json A lighthouse with a rotating beacon, a boat on a func_train and spawning wisps
withered_city.json A ruined neon city at night with lightning, glow and ssr
night_district.json An industrial street at night with a roller door and a viaduct train
sea_island.json The sea island tutorial
scripted_scene.json The cutscene tutorial
logic_playground.json A hall for the math and logic nodes: coins, a score, a gate on the vault door and a light lottery

Running them

Start the editor with the demo project and its HTTP server, see Starting it, then replay a script with tools/mcp_script.py. If the editor asks for a token, pass it with --token or TRENCHDOT_MCP_TOKEN. Its shot command saves a screenshot from the camera position you give it, and --shade picks the shading:

trenchdot --mcp-http --project godot                        # the editor with the demo project
python tools/mcp_script.py run examples/mcp/sea_island.json   # replay a script
python tools/mcp_script.py shot view.png --pos 4300,900,3300 --look 2200,700,1200 --shade lit

The scripts save into godot/demo/maps/showcase, except scripted_scene.json and logic_playground.json, which save into godot/demo/maps.

A run stops at the first failed step unless you pass --continue-on-error, and --url points the runner at an editor on another port. Each run is one undo step in every map it changes, named after the script's label or its file.

Script format

{
  "format": "trenchdot-mcp-script",
  "label": "Hollow room",
  "vars": { "save_dir": "${project}/maps" },
  "steps": [
    { "note": "A hollow room." },
    { "tool": "map_file", "args": { "op": "new" } },
    { "tool": "create_brush", "args": { "min": [0, 0, 0], "max": [256, 128, 256] }, "save": "room" },
    { "tool": "select", "args": { "ids": "$room.ids" } },
    { "tool": "run_action", "args": { "action": "csg_hollow" } },
    { "tool": "map_file", "args": { "op": "save", "path": "${save_dir}/room.tdm" } }
  ]
}

vars sets defaults for your own variables. A vars object passed to run_script overrides them, so one script can save to different places, and the folders you pass this way are ones the script may read from and write into.

A step without tool is a comment. A step with save stores its result under that name, so later steps can use the ids it created.

Scripts get shared like maps, so their steps only edit and save the map. A step can't open another project or one of its maps, start Godot, build in it, turn on live mode, turn MCP off, copy or cut to the clipboard, throw away unsaved changes or close other tabs. Undo and redo only reach what the script did itself, so the work you haven't saved stays in every tab. screenshot, simulate_input, walkability and run_script can't run inside a script either, and neither can project_content installs other than gameplay.

Every file a step reads or writes has to be in the open project, the script's own folder or a folder you pass in vars, also when a link inside them points somewhere else. That covers the maps it opens, checks, compares, saves and exports, the maps, models, prefabs and textures it imports with the files they name, and everything that conversions, imports, exports and the prefab library write. Prefabs, model files, textures and Hammer instances that a map or model names elsewhere are left out, network paths are refused before the editor looks at them, and a save without a path writes back only to the map that was open when the script started.

A refused step shows up in the summary's errors with the reason. The summary counts tool calls as steps and comments as notes.

Strings in args can refer to saved results and a few built-in variables:

Write Means
"$name.path" The saved JSON value. Numbers index arrays, as in $room.ids.0
"${name.path}" Its text inside a longer string
"$a.ids + $b.ids" Both joined into one array. ["$a.ids", "$b.ids"] works too
$last The previous step's result
$project The open Godot project's folder, an error when none is open
$script_dir The script file's folder
$$ A literal $, so a node path is "$$Player"

An unknown name is an error. A $ that can't start a name, like "$5", stays as it is. Ids given as text, such as "42", work wherever a node id is expected.

$name/rest without braces doesn't expand, so an unbraced $project/maps is an error rather than a path nobody meant to write. Use "${project}/maps".

Ids after CSG

CSG and clip steps replace the brushes they cut with new ones that have new ids. The runner rewrites ids saved by earlier steps to match, so "$wall.ids" still means the whole wall after you cut a window into it, and a removed brush drops out.

Only values under keys that carry node ids are rewritten: id, ids, selection, selected, nodes, letters, copies, entity, group, parent, insert_parent, current_layer, open_groups, targets and keys ending in _id or _ids. Counts such as moved from hierarchy reparent stay as they are, use its ids instead.

Tip

Spell out every brush setting in scatter and blend calls. Anything left out comes from the editor's current Scatter panel or Blend tool settings.