optional-skills/creative/unreal-mcp/references/advanced-workflows.md
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.
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):
get_execution_environment ONCE per session before the first
script. It returns instructions (read them — they are authoritative),
supported_modules, and language.execute_tool_script takes {"script": "<python>"}. The script must
define run() -> Dict[str, Any].execute_tool(tool_name, json_input) calls any
registered tool. tool_name is FULLY QUALIFIED INCLUDING the tool
segment ("editor_toolset.toolsets.primitive.PrimitiveTools.add_cube")
— unlike top-level call_tool, there is no separate toolset/tool split.
json_input is a JSON string (use json.dumps).execute_tool returns a dict-like object; unwrap results with
["returnValue"]. It raises RuntimeError on failure — no manual error
checking.json, math, datetime, copy, re, time.
Nothing else — no unreal, no os, no file I/O.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.
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 covers event/fn,
bind, if/for/while/switch, multi-exec continuation blocks
((:then ...), (:CastFailed ...)), auto-generated underscore
variables for data output pins, and quoted pin names.find_node_types BEFORE 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:
EventTick (with DeltaSeconds
param), EventBeginPlay — (event Tick ...) fails with
"AddEvent|Tick does not exist".Math|Rotator|MakeRotator, not bare MakeRotator.Utilities|Operators|Multiply (wildcard operator), not
Multiply_FloatFloat.Transformation|AddActorLocalRotation, not
Utilities|Transformation|AddActorLocalRotation — category prefixes
in doc examples don't always match the live registry. The registry
wins.(self) node; the target is implicit — omit :self
entirely for calls on the owning actor.write_graph_dsl — {"graph": {"refPath": "<EventGraph>"}, "code": "<dsl>"}. Returns null on 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}. Returns null on success.SceneTools.add_to_scene_from_asset with
{"asset_path": "/Game/Blueprints/BP_Spinner.BP_Spinner", ...}.
NOTE: this tool takes asset_path as a plain STRING, not an asset
refPath object — the error schema is the tiebreaker (see below). The
spawned actor's class is BP_Spinner_C (the _C generated-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.
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 requires asset_path (string).StartPIE — {} fails, and the error hands you the full
PIESessionOptions schema.Rule: on a param error, READ the schema in the error before anything else.
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.
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):
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).add_track_to_binding / add_track_to_sequence,
add_section, set_section_range/set_section_blend_type/ease in-out,
set_camera_cut_binding.set_playback_range, set_display_rate,
set_tick_resolution, set_work_range, marked frames.play, pause, play_to, set_playhead_frame,
force_evaluate, set_playback_speed.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.bake_transform, import_export sibling toolset (FBX
etc.), copy_tracks/paste_tracks.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.
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.
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.
SemanticSearchToolset: Search (hybrid vector + BM25 over project
assets) and FindSimilar — use for "find me a rusty metal material"
style requests before falling back to AssetTools.find_assets name
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.| 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 |