# 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): 1. Call `get_execution_environment` ONCE per session before the first script. It returns `instructions` (read them — they are authoritative), `supported_modules`, and `language`. 2. `execute_tool_script` takes `{"script": ""}`. The script must define `run() -> Dict[str, Any]`. 3. Inside the script, `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`). 4. `execute_tool` returns a dict-like object; unwrap results with `["returnValue"]`. It raises `RuntimeError` on failure — no manual error checking. 5. Allowed imports (5.8): `json`, `math`, `datetime`, `copy`, `re`, `time`. Nothing else — no `unreal`, no `os`, no file I/O. 6. 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): ```python 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: 1. **`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`). 2. **`list_graphs`** — returns graph refPaths in colon form: `...BP_Spinner.BP_Spinner:EventGraph`, `...BP_Spinner.BP_Spinner:UserConstructionScript`. 3. **`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. 4. **Resolve every node ID with `find_node_types` BEFORE writing DSL** — `{"graph": {"refPath": ""}, "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` (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. - There is no `(self)` node; the target is implicit — omit `:self` entirely for calls on the owning actor. 5. **`write_graph_dsl`** — `{"graph": {"refPath": ""}, "code": ""}`. 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. 6. **`compile_blueprint`** — `{"blueprint": {"refPath": ...}, "warnings_as_errors": false}`. Returns `null` on success. 7. **Spawn an instance** — `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. ## 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 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. ## 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_export` sibling 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) 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. ## 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 |