13 KiB
Advanced workflows (live-verified on UE 5.8)
Everything here was executed against a running 5.8 editor unless explicitly marked schema-verified. Where behavior diverged from Epic's docs, this file records what the server actually did.
ProgrammaticToolset — batching without breaking the serial rule
editor_toolset.toolsets.programmatic.ProgrammaticToolset is the sanctioned
way to do N operations in one MCP round-trip. It is ONE tool call on the
game thread, so the serial-call rule stays intact; you're just letting a
script make the sub-calls server-side.
Contract (verified):
- Call
get_execution_environmentONCE per session before the first script. It returnsinstructions(read them — they are authoritative),supported_modules, andlanguage. execute_tool_scripttakes{"script": "<python>"}. The script must definerun() -> Dict[str, Any].- Inside the script,
execute_tool(tool_name, json_input)calls any registered tool.tool_nameis FULLY QUALIFIED INCLUDING the tool segment ("editor_toolset.toolsets.primitive.PrimitiveTools.add_cube") — unlike top-levelcall_tool, there is no separate toolset/tool split.json_inputis a JSON string (usejson.dumps). execute_toolreturns a dict-like object; unwrap results with["returnValue"]. It raisesRuntimeErroron failure — no manual error checking.- Allowed imports (5.8):
json,math,datetime,copy,re,time. Nothing else — nounreal, noos, no file I/O. - The whole script's return value comes back as a JSON string in
returnValue.
Worked example (verified — 12-column colonnade, 36 components, ONE round-trip that would otherwise be 37 serial calls):
import json, math
def add_cylinder(actor_ref, name, radius, height, x, y, z):
return execute_tool(
"editor_toolset.toolsets.primitive.PrimitiveTools.add_cylinder",
json.dumps({"actor": actor_ref, "name": name, "radius": radius,
"height": height,
"local_transform": {"location": {"x": x, "y": y, "z": z}}}))
def run():
spawn = execute_tool(
"editor_toolset.toolsets.scene.SceneTools.add_to_scene_from_class",
json.dumps({"actor_type": {"refPath": "/Script/Engine.Actor"},
"name": "Colonnade",
"xform": {"location": {"x": 75800, "y": 84900, "z": 44300}}}))
host = spawn["returnValue"]
n, ring_r = 12, 900.0
for i in range(n):
a = 2.0 * math.pi * i / n
add_cylinder(host, "Shaft_%02d" % i, 40, 360,
ring_r * math.cos(a), ring_r * math.sin(a), 210)
return {"colonnade": host["refPath"], "columns": n}
When to reach for it: any loop over 5+ homogeneous operations (placement rings, grid scatter, bulk renames, bulk property sweeps). When NOT to: operations where you need to see intermediate results to decide the next step — the script can't ask you questions mid-run.
Failure surface: print() goes to the UE log, not the MCP return — return
diagnostics in the result dict instead. A script exception returns the
traceback as the tool error text.
Blueprint authoring — the DSL loop
editor_toolset.toolsets.blueprint.BlueprintTools (53 tools) authors real
Blueprints. The graph surface is an s-expression DSL, and the workflow that
survives contact with the live server is:
create—{"folder_path": "/Game/Blueprints", "asset_name": "BP_Spinner", "asset_type": {"refPath": "/Script/Engine.Actor"}}→ returns the Blueprint's refPath (/Game/Blueprints/BP_Spinner.BP_Spinner).list_graphs— returns graph refPaths in colon form:...BP_Spinner.BP_Spinner:EventGraph,...BP_Spinner.BP_Spinner:UserConstructionScript.get_graph_dsl_docs— pulls ~9k chars of grammar documentation off the live server. Read it before writing DSL; it coversevent/fn,bind,if/for/while/switch, multi-exec continuation blocks ((:then ...),(:CastFailed ...)), auto-generated underscore variables for data output pins, and quoted pin names.- Resolve every node ID with
find_node_typesBEFORE writing DSL —{"graph": {"refPath": "<graph>"}, "type_id_filter": "MakeRotator", "context_pins": []}→ exact IDs. Node IDs are pipe-delimited category paths and must match the live registry exactly. Verified gotchas:- Engine events use K2 display names:
EventTick(withDeltaSecondsparam),EventBeginPlay—(event Tick ...)fails with "AddEvent|Tick does not exist". Math|Rotator|MakeRotator, not bareMakeRotator.Utilities|Operators|Multiply(wildcard operator), notMultiply_FloatFloat.Transformation|AddActorLocalRotation, notUtilities|Transformation|AddActorLocalRotation— category prefixes in doc examples don't always match the live registry. The registry wins.- There is no
(self)node; the target is implicit — omit:selfentirely for calls on the owning actor.
- Engine events use K2 display names:
write_graph_dsl—{"graph": {"refPath": "<EventGraph>"}, "code": "<dsl>"}. Returnsnullon success. On failure the error is an AssertionError naming the exact failing node and its enclosing form — fix ONE node at a time and rerun; the error moves to the next problem.compile_blueprint—{"blueprint": {"refPath": ...}, "warnings_as_errors": false}. Returnsnullon success.- Spawn an instance —
SceneTools.add_to_scene_from_assetwith{"asset_path": "/Game/Blueprints/BP_Spinner.BP_Spinner", ...}. NOTE: this tool takesasset_pathas a plain STRING, not anassetrefPath object — the error schema is the tiebreaker (see below). The spawned actor's class isBP_Spinner_C(the_Cgenerated-class suffix, visible in the returned refPath).
Verified end-to-end: created BP_Spinner, wrote a Tick handler that yaws
the actor 90°/s ((event EventTick (DeltaSeconds) (Transformation|AddActorLocalRotation :DeltaRotation (Math|Rotator|MakeRotator :Roll 0.0 :Pitch 0.0 :Yaw (Utilities|Operators|Multiply DeltaSeconds 90.0))))), compiled clean,
spawned it, and attached a visible mesh via PrimitiveTools.add_cube on
the instance.
Variables, functions, dispatchers: add_variable (type_name strings),
add_object_variable/add_struct_variable, add_function_graph +
add_function_param, add_event_dispatcher, set_variable_replication —
same refPath discipline. read_graph_dsl round-trips existing graphs back
to DSL for inspection/editing.
Schema-in-error is a first-class discovery mechanism
When a call is missing/mistyping a required param, the server returns the
COMPLETE input schema of the tool in the error text. This is faster than
re-running describe_toolset and is authoritative for the exact function
you called. Two verified cases where it corrected the surface:
add_to_scene_from_asset— advertised conceptually as taking an asset reference; live schema requiresasset_path(string).StartPIE—{}fails, and the error hands you the fullPIESessionOptionsschema.
Rule: on a param error, READ the schema in the error before anything else.
PIE sessions (schema-verified)
EditorAppToolset.StartPIE requires an options object
(FPIESessionOptions):
bSimulate(required):true= Simulate-In-Editor — world ticks, physics/AI run, no player pawn possessed.false= standard PIE with possession.playMode(required):PlayMode_InViewPort,PlayMode_InEditorFloating,PlayMode_Simulate, etc. Out-of-process modes (NewProcess, MobilePreview, VR, QuickLaunch) are silently downgraded to in-viewport — the tool needs in-process PIE for delegate-based completion tracking.warmupSeconds(required): extra settle time after the engine fires PostPIEStarted (BeginPlay has run) before the call returns.0= return as soon as PIE is up.startTransform(optional): spawn the pawn/reference at a specific transform instead of PlayerStart.
IsPIERunning returns a bare boolean. Runtime-state inspection during PIE
(actor transforms ticking, LogsToolset reads) plus StopPIE complete the
loop: start simulate → read state / logs → stop → judge.
The test loop this enables: compile Blueprint → StartPIE (simulate) → sample an actor transform twice a few seconds apart → confirm your Tick logic actually runs → StopPIE. Remember pitfall 15: PIE mutates world state; take editor-world measurements before or after, not across, a PIE session.
Sequencer — orientation for a 140-tool surface
animation_toolset.toolsets.sequencer.SequencerTools is the largest
toolset (140 tools) and follows an open-sequence-implicit-target model:
create_level_sequence / open_sequence / get_focused_sequence, then
most calls operate on the focused sequence.
Capability map (names verified via describe; group by prefix):
- Structure:
add_actors(possessables),add_spawnable_from_class/add_spawnable_from_instance,create_camera(returns a camera-cut ready binding), bindings CRUD (get_bindings,find_binding_by_name,remove_binding,rebind_component,fix_actor_references). - Tracks/sections:
add_track_to_binding/add_track_to_sequence,add_section,set_section_range/set_section_blend_type/ease in-out,set_camera_cut_binding. - Timing:
set_playback_range,set_display_rate,set_tick_resolution,set_work_range, marked frames. - Transport:
play,pause,play_to,set_playhead_frame,force_evaluate,set_playback_speed. - Keyframing lives in the sibling
animation_toolset.toolsets.keyframing.SequencerKeyframingTools(22 tools):get_channel_names→add_key_float/add_key_bool/... →get_keys,set_default_value,bake_channel_keys, curve-editor control. - Baking/IO:
bake_transform,import_exportsibling toolset (FBX etc.),copy_tracks/paste_tracks. - Runtime conditions / custom bindings / ControlRig: dedicated sibling
toolsets (
SequencerConditionTools,SequencerCustomBindingTools,SequencerControlRigTools,ControlRigTools).
Minimal cinematic recipe skeleton: create_level_sequence →
create_camera → add_actors for subjects → keyframe camera transform
channels at frame A and B → set_playback_range → play → capture/judge.
Editor self-debugging with LogsToolset
EditorToolset.LogsToolset: GetLogCategories, Get/SetVerbosity,
GetLogEntries. After any failed operation or suspicious silence, pull
recent log entries filtered to the relevant category (LogBlueprint,
LogNiagara, LogModelContextProtocol, ...) instead of guessing. This is
also how you see print() output from ProgrammaticToolset scripts and
Python toolset internals.
Automation testing
AutomationTestToolset.AutomationTestToolset: DiscoverTests /
ListTests → RunTests or RunTestsByFilter → GetTestStatus (poll —
test runs are async on the editor) → GetTestResults → StopTests if
needed. This is the CI-shaped loop for "make a change, prove nothing
broke" inside a live editor session.
Asset intelligence
SemanticSearchToolset:Search(hybrid vector + BM25 over project assets) andFindSimilar— use for "find me a rusty metal material" style requests before falling back toAssetTools.find_assetsname matching.StaticMeshTools:import_file(bring in external meshes),set_nanite_enabled,generate_lods/set_lod_thresholds,generate_convex_collisions,get_triangle_count/get_bounds— the optimization pass after any import.ConfigSettingsToolset:ListContainers/ListCategories/ListSections→GetSectionSchema→SetSectionProperties(saves to config). The remote path to Project Settings and Editor Preferences — rendering defaults, exposure defaults, auto-start flags — without touching ini files by hand.ToolsetRegistry.AgentSkillToolset:ListSkills/GetSkills/CreateSkill/UpdateSkill— project-embedded agent skills that ship with the .uproject. If a project has them, list them FIRST; they encode project-specific conventions that outrank this file's generic guidance.
Choosing a strategy (decision table)
| Situation | Reach for |
|---|---|
| 5+ homogeneous ops (scatter, bulk edit) | ProgrammaticToolset script |
| Gameplay behavior, event logic | BlueprintTools DSL loop |
| Camera moves / animation over time | SequencerTools + KeyframingTools |
| "Does it actually behave at runtime?" | StartPIE (simulate) + transform/log sampling |
| "Find an asset like X" | SemanticSearch, then AssetTools |
| Imported mesh is heavy | StaticMeshTools nanite/LOD/collision pass |
| Change editor/project settings | ConfigSettingsToolset |
| Anything failed silently | LogsToolset GetLogEntries |
| Project has its own agent skills | AgentSkillToolset first |