Unreal MCP + Python Automation Guide¶
Summary: Practical playbook for driving the Unreal editor from Claude via MCP and the
pyconsole command. Covers UE 5.8 + Epic's native ModelContextProtocol plugin. The in-repoMcpAutomationBridgeplugin and its Node TS server were removed 2026-07-14 — sections below the legacy banner still describe bridge-era tool names and need re-validation against the native toolsets before you lean on them.
Table of Contents¶
- Setup (Native MCP)
- Calling Convention & Toolsets
- The Python Console Pattern
- Always Read the Editor Log
- Inspecting & Driving GAS in PIE
- Failure-Mode Taxonomy
- Asset Playbooks
- Editor Lifecycle & Builds
- Crash / Hang Recovery
- Reliability Cheat Sheet
- Related
- Recent Changes
Setup (Native MCP)¶
UE 5.8 ships Epic's own MCP server: the ModelContextProtocol engine plugin (Experimental) plus a
toolset ecosystem (Engine/Plugins/Experimental/Toolsets/*). No third-party plugin, no Node process,
no version-locking — the server lives inside the editor and speaks streamable HTTP.
The project wiring (all committed, nothing to install per-machine):
| Piece | Where | What it does |
|---|---|---|
ModelContextProtocol + AllToolsets plugins |
ProjectEternal.uproject (Editor targets only) |
MCP server + all ~21 Epic toolsets (GAS, UMG, Sequencer, PCG, SlateInspector, AutomationTest, …) |
bAutoStartServer=True |
Config/DefaultEditorPerProjectUserSettings.ini |
HTTP server starts with the editor — no manual start, no reconnect dance |
unreal-engine server entry |
.mcp.json (project scope) |
Claude Code connects to http://127.0.0.1:8000/mcp whenever the editor is running |
Endpoint: http://127.0.0.1:8000/mcp (port/path in Editor Preferences → Model Context Protocol,
backed by the same ini). Since transport is plain HTTP, Claude reconnects per request — restarting the
editor does not require restarting Claude Code, and there is no heartbeat/reconnect-storm failure mode
like the old bridge had. Editor closed = tool calls fail cleanly until it's back.
Settings live on UModelContextProtocolSettings (config=EditorPerProjectUserSettings): ServerUrlPath
(/mcp), ServerPortNumber (8000), bAutoStartServer, bEnableToolSearch (on: tools/list stays tiny and
toolset tools are discovered on demand).
Calling Convention & Toolsets¶
With tool search on, the server exposes exactly three MCP tools (verified live 2026-07-14):
list_toolsets— one-line description per registered toolset.describe_toolset{toolset_name}— full tool list + input schemas for one toolset.call_tool{toolset_name, tool_name, arguments}— executes a toolset tool.tool_nameis the SHORT name (get_current_level), not the fully-qualifiededitor_toolset.toolsets.scene.SceneTools.get_current_level— the long form returns "Unknown tool". The args field isarguments(an object), nottool_args— a wrong field name is silently dropped and the server errors with "input params Json is empty" (verified live 2026-07-17).
Toolset highlights (from the live registry): editor_toolset.toolsets.* (actor/asset/blueprint/material/
data-table/object/scene/skeletal-mesh/texture + a sandboxed Python ProgrammaticToolset for batching),
GASToolsets.* (AttributeSet discovery, ASC runtime inspection, gameplay cues), AutomationTestToolset
(discover/run/monitor the same tests as Session Frontend), SlateInspectorToolset (Playwright-style editor
UI automation with refs + screenshots), UMGToolSet (widget-tree authoring — follow its list_properties →
get/set_properties workflow), NiagaraToolsets.*, PCGToolset, Sequencer suites, GameplayTagsToolset,
ConfigSettingsToolset, SemanticSearchToolset.
Raw HTTP debugging (when MCP client plumbing is in doubt): POST JSON-RPC to the endpoint with
Accept: application/json, text/event-stream; capture the Mcp-Session-Id response header from
initialize and send it on every later call; tools/call responses arrive as SSE (data: lines).
Project toolset — ProjectEternalEditor.EternalCheatToolset¶
Beyond Epic's toolsets, the project registers its own toolset so an agent can fire the dev-console
cheats over MCP — the native plugin has no console-exec tool, so this is the only agent path to them.
Source: Source/ProjectEternalEditor/Private/MCP/EternalCheatToolset.{h,cpp}, registered via
UToolsetRegistry. It wraps UEternalCheatManager (resolving the first PIE world that has a local player
controller) and returns each cheat's own LogTemp lines in Log — read Log, don't grep the editor log.
The tools, by group (count the AICallable UFUNCTIONs in EternalCheatToolset.h before quoting a
number anywhere — this listing goes stale first):
- Combat:
SpawnEnemy,ClearEnemies,KillTarget,ActivateAbility,SetGodMode(idempotent bool, not a toggle),AddAttribute,ApplyStatus,TriggerCombatEffect. - Itemization:
GiveItem,EquipItem,CraftModifier,ClearItemModifiers,ListItemModifiers. - Remnants / Greed:
GiveSealedRemnant,GiveAwakenedRemnant,DumpRemnant,ListRemnantPoolIDs,ConsumeGreed. - Structured loadout reads (return
FEternalItemInfo[]as DATA, not log — assert on these):GetInventory(bag) andGetEquipment(equipped, tagged with slot name); each item carriesitemId,itemLevel,slotName, andmodifiers[](channel Implicit/Prefix/Suffix, modifierId, tier, value, description). The itemization analogue of the GAS inspector.
Not every cheat is exposed over MCP. TeleportToRoom <query> and ListDungeonRooms (dungeon navigation —
see Itemization Tooling) exist only as console execs on
UEternalCheatManager; reaching them from an agent means a console line, not a toolset call.
Notes: arg names are the C++ param names, PascalCase (SlotName, ModifierID, ItemID, bEnabled). No PIE
/ no reachable player raises a script error (isError:true) — "start PIE first" is explicit, not a silent
no-op. The DLL loads on editor launch, so a fresh build needs an editor relaunch before the tools appear.
Any C++ toolset registration must defer past GEditor creation (the registry subsystem does not exist at
module StartupModule) — this toolset registers on FCoreDelegates::GetOnPostEngineInit(). The
ToolsetRegistry plugin is listed explicitly in ProjectEternal.uproject (Editor only); ProjectEternalEditor
hard-depends its module. Full PIE workflow and the reusable raw-HTTP caller live in the pie-test skill.
⚠️ Legacy content below — bridge-era (pre-2026-07-14)¶
Everything below this banner was written against the removed
McpAutomationBridgetool surface (system_control,manage_*,control_*,inspect). The tool names no longer exist; the lessons (log-reading discipline, deferred FBX import, crash taxonomy, editor lifecycle) largely transfer. Re-validate each pattern against the native toolsets on first use and migrate the section up above the banner once verified.
The Python Console Pattern¶
Bridge ≥ v0.5.30 — prefer system_control execute_python. It runs Python directly (no console hop),
takes either inline code or a file path, and returns captured stdout/stderr + exec time in the
MCP result — so you no longer have to round-trip through the log just to read output. Write longer scripts
to ArtSource/ (never Content/) and pass the path:
system_control → execute_python → { "file": "C:/Projects/ProjectEternal/ArtSource/<feature>/script.py" }
system_control → execute_python → { "code": "import unreal; unreal.log('hi')" } # inline, ≤ 1 MB
- Constraints (new in 0.5.30): inline
codecapped at 1 MB; thefilepath is validated to resolve inside the project dir (symlink-escape blocked) — ourArtSource/is in-project, so fine.codeandfileare mutually exclusive. - Still editor-world context — cheat-manager Execs are not reachable this way (see GAS section); that limitation is unchanged.
Legacy fallback — console_command "py ..." (works on any version, use if execute_python is absent):
control_editor / system_control → console_command:
py exec(open(r'C:\Projects\ProjectEternal\ArtSource\<feature>\script.py').read())
- One-liners work too (
;-separated), but multi-statement logic (loops, try/except) needs a file +exec. - The console call returns when the statement finishes and reports almost nothing — confirm via the log
marker (below).
execute_pythonis strictly better here since it returns stdout directly. - Long ops (recompile, import) can exceed the 30 s MCP timeout — a timeout is not failure; the Python may still be running. Verify via the log. (Screenshots are now genuinely async — see Asset Playbooks.)
- Globals persist across
pycalls in one editor session (e.g. a_handlefrom a prior exec is still bound). unreal.log("MARKER ...")/log_error(...)at the end of every script — that string confirms success from the log. Even withexecute_python's stdout capture, keep tailing the log for engine-level signals ([SM6]shader errors,Handled ensure, crashes) that never reach Python stdout.
Probe Instead of Guessing¶
When inspect returns only a Package wrapper for an asset path (no parent material, no params, no
displacement/other internals) — the sign the MCP surface can't reach what you need — don't assume; write a
one-shot probe. A small script that EditorAssetLibrary.load_assetes the asset and dumps it via reflection
(get_editor_property, MaterialEditingLibrary param listings, etc.) produces ground truth that inspect
couldn't. Two thin inspect calls on the same path is the trigger to switch to a probe.
- Probe scripts live in
Tools/Python(or a temp path), NEVER underContent/.Content/Pythonis reserved for editor-managed Python (init_unreal.pyauto-runs there); scratch dumped there silently accumulates — a real cleanup once had to weed ~19 stray one-shot scripts out ofContent/Python. Delete a probe as soon as you've read its output; only promote it toTools/Pythonif it's a genuinely reusable tool.
Always Read the Editor Log¶
After every py/MCP mutation, tail Saved/Logs/ProjectEternal.log (Grep). The MCP response says
"success" even when the engine logged errors — the bridge only warns. Look for:
| Signal | Meaning |
|---|---|
your MARKER_DONE string |
actually succeeded |
LogPython: Error: / Traceback |
script threw (your try/except should log *_FAILED) |
Handled ensure: |
non-fatal engine assert — often a half-applied op |
=== Critical error: === / Assertion failed |
crash; process is dying |
ForceDeleteObject failed ... potentially corrupt |
delete_asset on a referenced asset failed |
The log rotates per launch (fresh ProjectEternal.log each start; previous goes to a backup), so grepping
the live file is current-session only — good for "did my last op work".
Inspecting & Driving GAS in PIE¶
Reading/poking the player's attributes while Play-In-Editor runs. MCP inspect / find_by_class see the
editor world, not the PIE world — they return 0 actors during PIE. Go through Python's PIE world instead.
Where things live (this project): the ASC + UEternalAttributeSet are on the PlayerState
(AEternalPlayerState), not the pawn (EternalPlayerCharacter) — see System Ownership Matrix. The pawn's
IAbilitySystemInterface just forwards to it.
Reusable read probe (write to Saved/, exec, then grep the log for the marker):
import unreal
w = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem).get_game_world() # PIE world (None if not playing)
ps = unreal.GameplayStatics.get_player_state(w, 0)
asc = next(c for c in ps.get_components_by_class(unreal.ActorComponent)
if "AbilitySystemComponent" in c.get_class().get_name())
attrs = {str(a.get_editor_property("attribute_name")): a for a in asc.get_all_attributes()}
def val(n): # get_gameplay_attribute_value returns (float, bFound)
v = asc.get_gameplay_attribute_value(attrs[n]); return v[0] if isinstance(v, tuple) else v
unreal.log("PROBE Ferocity=%s MaxHealth=%s" % (val("Ferocity"), val("MaxHealth")))
| Need | How |
|---|---|
| PIE world | UnrealEditorSubsystem.get_game_world() (NOT EditorLevelLibrary/editor world) |
| Player pawn / state / controller | GameplayStatics.get_player_pawn/get_player_state/get_player_controller(world, 0) |
| The ASC | scan PlayerState components for class-name "AbilitySystemComponent" (engine AbilitySystemBlueprintLibrary is not Python-bound; pawn.get_player_state() is also absent — use GameplayStatics) |
| Current attribute value | asc.get_gameplay_attribute_value(attr) → tuple (value, bFound), take [0]; handles from asc.get_all_attributes() keyed by get_editor_property("attribute_name") |
| Base vs current | attrset = ps.get_editor_property("attribute_set"); attrset.get_editor_property("MaxHealth") → FGameplayAttributeData with .base_value / .current_value |
You cannot build/apply a UGameplayEffect from Python. Its duration_policy, and a modifier's attribute,
are EditDefaultsOnly → set_editor_property throws "cannot be edited on instances". To change an attribute
through the real GAS pipeline (aggregator → live re-aggregation of dependents), use a C++ dev cheat.
Cheat-manager Exec functions are NOT reachable via MCP console_command. That dispatches at the
engine/GEngine level — it runs stat, ShowDebug, py, CVars fine, but a UCheatManager Exec only logs a
bare Cmd: echo and never executes (the PIE player's cheat manager isn't the exec target). Invoke it on the
cheat-manager object via Python instead:
pc = unreal.GameplayStatics.get_player_controller(w, 0)
pc.get_editor_property("cheat_manager").call_method("AddAttribute", ("Stats.Attributes.Hard.Ferocity", 100.0))
AddAttribute <Tag> <Amount>(UEternalCheatManager, dev-only, requires authority) flat-adds to any attribute by tag through an infinite Additive GE → the canonical way to test derived stats / live re-aggregation in PIE. Note runtime GE changes do not persist across PIE restarts (the GE is on the runtime ASC) — re-apply each session.ShowDebug AbilitySystemoften renders nothing in this top-down custom-HUD game — don't rely on a screenshot; use the Python readout above.
On a dedicated PIE session¶
Under bLaunchSeparateServer the reachable player controller is the client's, so
cheat_manager above is the client's cheat manager — not the server's. Authority-requiring cheats still
work, because AEternalPlayer self-forwards them to the server over Server_ExecuteCheat. The guards on
that relay are worth knowing, because they define what is and is not reachable from a client:
| Guard | Effect |
|---|---|
Requires a server-side CheatManager |
Cheats must be enabled on the server (always in PIE; -EnableCheats otherwise). That existence check is the authorization gate |
Dispatches via ProcessConsoleExec on the cheat manager |
Only declared cheat functions are reachable — a client on a cheats-enabled server cannot run arbitrary engine console commands |
| 256-character command cap | Bounds reliable-channel abuse |
| Compiled out of Shipping | The relay does not exist in a shipping build |
GiveSimulatedItem remains the exception: it takes a rolled manifest struct, which is not
console-parseable, so it still requires a server console (a listen host).
Failure-Mode Taxonomy¶
Distinguish these — they need different responses:
| Symptom (MCP result) | Likely cause | Response |
|---|---|---|
timed out after 30000ms |
long op still running (recompile/import) | wait + poll the log for the marker; don't re-run |
Connection lost mid-op |
editor crashed during the op | check process; relaunch; avoid the op that crashed |
ENGINE_ERROR ... Handled ensure |
op partially applied (e.g. bad widget add) | inspect actual state; clean up the half-result |
| process alive but all MCP calls time out | game thread blocked — usually a modal dialog | force-kill + relaunch (can't dismiss a modal headless) |
| editor exits, launch task "exit 3" | crash earlier in the session | relaunch clean |
Asset Playbooks¶
Newly-created assets may not show up in queries immediately (bridge ≥ v0.5.30). 0.5.30 dropped the synchronous asset-registry scan from query/workflow handlers (it was blocking the GameThread), so a just-created asset can be absent from
does_asset_exist/ asset queries until the editor rescans — even though it landed. Verify on disk instead:Globthe.uasset(filesystem is authoritative and unaffected). Treat a freshdoes_asset_exist=falseas "registry not rescanned yet", not "create failed".Screenshots are async (bridge ≥ v0.5.30). The screenshot handler returns
async: truewith anexpectedDelay— the image isn't ready when the call returns. Poll/wait per the returned timing instead of assuming the file exists immediately. (Still ASK before any camera move for a screenshot — don't reposition the viewport; let the user F-focus.)Don't touch the user's editor state mid-task. The user is actively navigating the editor, so scripts must never mutate the viewport camera (
set_level_viewport_camera_infoand similar), change the actor selection of arbitrary actors, open/close tabs, switch levels, or change the ViewMode. To help the user find a spawned test actor,set_selected_level_actorsit so theirF(focus selected) works — but do not move the camera yourself. If a script genuinely needs a camera move (e.g. an automated screenshot), ASK first.
Textures¶
- Generate sources with PIL/numpy →
ArtSource/Textures/<feature>/(PIL + numpy are available viapython). - Import via a Slate post-tick callback, not directly — a direct Interchange import from a
pyconsole call hits a taskgraph re-entrancy crash. Registerunreal.register_slate_post_tick_callback(fn), do theAssetImportTaskinside, then unregister. - CRITICAL: unregister the callback as the FIRST line inside it, before any work — not at the end. The
tick can queue the callback more than once; unregistering only after the import lets a queued second call
re-enter →
RecursionErrorspam. Even brief spam poisons the session (stale shared-ptrs) and the next heavy op crashes withAssertion failed: IsValid()(SharedPointer.h:1082). Usedef _do(d): unregister(_h); <import>. - After an import session (especially any that spammed), restart the editor before the next heavy op (material build, etc.). A clean session runs asset/material ops fine; a post-import/post-spam session tends to crash on the next big operation. Observed repeatedly.
texture.set_editor_property("lod_group", unreal.TextureGroup.TEXTUREGROUP_UI)raises a type-conversion error in 5.6 — set the texture group via MCPmanage_texture set_texture_groupinstead (or skip; default group is fine for UI).
Materials (base)¶
- To ITERATE a material, build at a NEW unique name — never
delete_asset+ recreate. Once a material has been opened in the editor or referenced by anything (e.g. a widget image), it's loaded;delete_assetthen failsForceDeleteObjects("package is now potentially corrupt") and the recreate returnsNone. This bit the chant flourish repeatedly. Bump the asset name (..._Altar,..._v2) and re-point references, or restart the editor to unload before deleting. Either way: don't delete a loaded/referenced asset headless. - ALWAYS grep
[SM6](andMaterialEditorStats: Error) afterrecompile_material— a*_DONElog marker only means the script ran, not that the shader compiled. Common errors: aComponentMaskwith no input connected; aTextureSamplewhose sampler type doesn't match the texture (sRGB-off texture needs the Linear Color sampler — setsampler_type = SAMPLERTYPE_LINEAR_COLOR; data/mask textures should be sRGB-off + Masks/LinearColor). - Reliable path:
unreal.MaterialEditingLibraryin one synchronous script. Create the asset withAssetTools.create_asset(name, pkg, unreal.Material, unreal.MaterialFactoryNew()), setmaterial_domain/blend_mode, add expressions,connect_material_expressions,connect_material_propertyfor outputs (MP_EMISSIVE_COLOR,MP_OPACITY, …), thenrecompile_material+save_asset. This does not crash (unlike asset import) and is far more reliable than MCP node-by-node. - Custom HLSL node: set
code+output_type; addunreal.CustomInput()with onlyinput_name(do not setinput_type— black material in 5.6). Texture inputs auto-name a<InputName>Sampler. Custom nodes have been implicated in editor hangs during graph build — prefer standard math nodes (Subtract/Divide/Clamp/Multiply) when feasible. - Custom HLSL forbids nested function definitions —
coderuns as a function body;float foo() {...}errors withfunction definition is not allowed here. Inline all helpers (loops/conditionals/locals fine). - NEVER probe pin names by looping
connect_material_expressionsover candidates on a live graph. Each successful connection silently overwrites the target node's existing input wire — no warning. Probing pin names against a realRuntimeVirtualTextureOutputonce overwrote the liveLandscapeLayerBlendwires with a zero-constant, so RVT pages stored zeros and rocks went dark (~30 min to trace, because the symptom looked like a shader bug, not a self-inflicted rewire). Probe on a throwaway material (or a disposable target instance), keep a known-good pin-name cheat-sheet (reference_ue56_python_material_quirks), and if you must probe live, callconnectonce with the best guess and check the return rather than iterating candidates. - Graph inspection:
mat.get_editor_property("expressions")throws "protected and cannot be read" in 5.6. Use MCPget_material_info— returns full node list, parameters, and connections (sourceNodeId/sourceOutputIndex/targetNodeId/targetInput); the only reliable way to see wiring. - MI parent ref breaks on parent delete+recreate. If a build script deletes and recreates a parent material,
every existing MI's parent reference goes invalid (MI loads but shows no params / wrong defaults). Re-link at
the end of the script: load each known MI,
set_editor_property("parent", parent_mat), save. (Better: don't delete+recreate — see the iterate-at-a-new-name rule above.) - UI materials:
MaterialDomain.MD_UI,BLEND_ADDITIVEfor glows. Note the materialTimenode in a UMG material does not match worldGetGameTimeInSeconds— do not build one-shot envelopes that compare an externally-setStartTimeagainst materialTime; they never line up. Drive UI one-shots by animating a material scalar param (or the Image's RenderOpacity) from a widget animation instead, and keep the material "always on" (gated only by that param).
Material Instances¶
- Create with MCP
create_material_instance(PythonMaterialInstanceConstantFactoryNewsilently fails in 5.6). Set params with PythonMaterialEditingLibrary.set_material_instance_*_parameter_value.
Blueprints — class vs graph¶
- Reparent works via Python:
BlueprintEditorLibrary.reparent_blueprint(bp, unreal.NewParentClass)+compile_blueprint+save_asset. (bp.get_editor_property('parent_class')is not a valid property — don't read it back that way; checkbp.generated_class()instead, thoughget_super_class()is also absent.) - Set CDO defaults: MCP
manage_blueprint set_defaultworks (e.g. assign an object property). From Python, reach the CDO with the module formcdo = unreal.get_default_object(bp.generated_class())— that returns the Actor/object CDO, socdo.set_editor_property(...)+EditorAssetLibrary.save_loaded_asset(bp, False)works. The method formbp.generated_class().get_default_object()returns the class, andset_editor_propertyon it fails ("not found on BlueprintGeneratedClass"). - Never edit or
compile_blueprinta Blueprint the user is currently playing in PIE. PIE holds live instances of the class; recompiling it underneath them stalls the editor (the user has to interrupt, and PIE gets stuck). Callcontrol_editor stop_piefirst — or ask the user to exit PIE — before any py that touches a played BP (component defaults, SCS edits,compile_blueprint). Asset-only edits (materials, MICs) are safer but still prefer PIE stopped. - Graph editing (events, delegate binds, node wiring) via MCP is unreliable:
add_eventreturns GUID node names, and binding a multicast delegate (OnX) to a custom event headless is error-prone. Do graph work in-editor by hand, or move the logic to C++. BlueprintEditorLibrary.reparent_blueprint+compile_blueprintdoes NOT save. Follow with an explicitEditorAssetLibrary.save_asset(..., only_if_is_dirty=False)or the reparent silently reverts on editor close (cost a session a template's parent class before the test suite caught it).
Gameplay tags & struct values from Python¶
request_gameplay_tag/make_literal_gameplay_tagare unbound in 5.6, but every exposed struct hasimport_text— construct tags in pure Python:t = unreal.GameplayTag(); t.import_text('(TagName="Desire.Break.Poise")')(thenGameplayTagLibrary.make_gameplay_tag_container_from_tag(t)for containers). Works for any ExportText-able struct value on data assets (set +save_assetdirectly). For Blueprint CDOs still prefer theEternal.BP.SetCDOPropertyconsole seam — it handles the modify/compile/save dance.- Reading a tag back:
str(tag)prints an opaque struct — useGameplayTagLibrary.get_tag_name(tag). - The stamp console command (
Eternal.Ability.Stamp) tokenizes on whitespace and does not honor quotes —AbilityName="Echo Strike"aborts. Stamp with no spaced values, then set display fields viaEternal.BP.SetCDOProperty(which joins trailing args, so spaces are fine there).
UMG Widgets¶
- Widget tree is not editable from Python —
WidgetBlueprint.widget_treeis not an exposed property in 5.6. - MCP
add_image/set_*are flaky: names may not stick (an added image came back namedImage), and setting a material as an image brush threw a handled ensure. - If you do add slots via
manage_widget_authoring, name them withslotName— NOTname. Theadd_*handlers readslotName; anamefield is ignored, so the widget silently takes the default class name (TextBlock/VerticalBox). This is the #1 trap, and it's unrecoverable:remove_widget/rename_widgetexist in the bridge source but are not reachable via the live tool (UNKNOWN_ACTION), so a mis-named add is permanent — onlyset_visibility Collapsedcan neutralize a stray. Get the name right on the first try. add_*returnsENGINE_ERROR: Handled ensurebut still creates the widget (the ensure is the compiler's "added but did not get a GUID" — non-fatal). Don't trust the error response; confirm withget_widget_infothat the slot actually appeared. C++BindWidget/BindWidgetOptionalresolves by tree name at runtime regardless of the ensure, so a correctly-named slot binds fine — preferBindWidgetOptionalso a missing slot never blocks compile.set_visibilityand otherset_*also target viaslotNameand work cleanly; nest children withparentSlot=<container name>.- There is no add-user-widget-instance action — you cannot instance an existing
UserWidget(e.g. aW_StatEntryrow) as a child via MCP; those must be authored in-editor by hand. - Conclusion: build/lay out widgets and author widget animations in-editor by hand (or add plain BindWidget
slots via
manage_widget_authoringwith the caveats above). Generate the content they consume (materials, textures, sounds) via automation; wire the widget yourself.
Level actors & Sequencer¶
control_actor spawn(byclassPath, e.g./Script/Engine.CameraActoror a BP path) +add_tagare reliable; find at runtime by tag.manage_sequence createmakes a Level Sequence; deep track/binding authoring is best done in-editor.
Editor Lifecycle & Builds¶
| Change type | How to apply | Editor open? |
|---|---|---|
.cpp only (no header/UPROPERTY/module change) |
Live Coding: console LiveCoding.Compile |
yes — no restart |
Header change, new class, new module dependency (Build.cs) |
Full UBT build | no — close first |
- Build (Home env):
dotnet "Q:\Unreal\UE_5.6\...\UnrealBuildTool.dll" ProjectEternalEditor Win64 Development "-Project=...\ProjectEternal.uproject" -WaitMutex -FromMSBuild. Always confirm Home vs Work env first. - Full build requires the editor closed (DLL lock). Guarded pattern: wait for the process to exit, then build, in one backgrounded command.
- Launch + wait-for-ready: relaunch the editor in the background, then poll the log until
(Engine Initialization) Totalappears and the process is up (avoids racing the MCP bridge). - Close cleanly with console
QUIT_EDITOR(saves + exits). The bridge reconnects automatically on relaunch.
Crash / Hang Recovery¶
- Confirm state: is
UnrealEditor.exerunning? Tail the log forCritical error/Assertion. - Hung (modal) editor: force-kill —
taskkill //F //IM UnrealEditor.exe(git-bash) — then relaunch. You cannot dismiss a modal dialog headless. delete_assetcorruption: deleting an asset that is referenced (e.g. a material used by a widget image) failsForceDeleteObjectsand flags the package corrupt. Don'tdelete_assetreferenced assets headless. Instead build the replacement at a fresh path and re-point the reference, or edit in place.- Saved assets persist across crashes — re-verify what landed (
does_asset_exist) before redoing work. - Bulk move/delete modal-hangs when Source Control is on. MCP
fixup_redirectorsandEditorAssetLibrary.delete_directoryhang the game thread when the SC provider isGit LFS 2— the checkout / mark-for-delete prompt is a modal that can't be dismissed headless, so every MCP call then times out (log frozen, CPU idle). Fix: editSaved/Config/WindowsEditor/SourceControlSettings.ini→Provider=None, relaunch, run the destructive ops (they now run instantly, no modal), then restoreProvider=Git LFS 2(effective next launch).
Reliability Cheat Sheet¶
| ✅ Reliable headless | ⚠️ Flaky / avoid headless |
|---|---|
Material graphs via MaterialEditingLibrary |
UMG widget tree / widget animations |
MI creation via MCP create_material_instance |
BP graph node wiring / delegate binds |
Texture import via Slate-tick-deferred AssetImportTask |
Texture import direct from console py |
reparent_blueprint, set_default, compile_blueprint |
delete_asset on referenced assets |
control_actor spawn/tag/transform; manage_level save |
Custom HLSL nodes during graph build (hang risk) |
Live Coding for .cpp-only changes |
Reading bp.parent_class / class.get_super_class() |
Rule of thumb: automation generates assets; humans (or C++) wire logic and UI. When an op crashes or hangs twice, stop iterating headless and hand that step to the editor.
Related¶
- Skeletal Mesh Rig Transfer — Blender→UE pipeline, also uses MCP/Python import.
- Gameplay Cue Visuals — data-driven materials/auras.
Source/ProjectEternal/Public/AbilitySystem/EternalAttributeSet.h— attribute names used by the PIE probe.Source/ProjectEternal/Public/Debug/EternalCheatManager.h— the runtime dev cheats (combat, itemization, remnants, greed) usable from the in-game console.Source/ProjectEternalEditor/Private/MCP/EternalCheatToolset.{h,cpp}— the MCP toolset wrapping those cheats (the agent path; see "Project toolset" above), incl. the structuredGetInventory/GetEquipmentreaders.
Recent Changes¶
| Date | Change | Impact |
|---|---|---|
| 2026-05-30 | Initial guide from the glyph-flourish session | Captures import-via-slate-tick, material-via-Python, UMG/BP-graph headless limits, delete-on-referenced corruption, UMG material-time vs game-time mismatch |
| 2026-06-17 | Added "Inspecting & Driving GAS in PIE" | PIE-world (not editor-world) access, ASC-on-PlayerState discovery, attribute read pattern (tuple return; base vs current), GE-not-constructible-in-Python, cheat-manager execs need call_method not MCP console, AddAttribute cheat for re-aggregation tests |
| 2026-07-03 | Merged orphaned .claude/notes/UnrealMCP_Guide.md |
Custom-HLSL no-nested-functions rule, get_material_info for protected expressions list, MI-parent-invalidation on parent delete+recreate |
| 2026-06-18 | MCP bridge 0.5.21 → v0.5.30 | Prefer system_control execute_python (file/inline, stdout/stderr captured; 1 MB + in-project-path limits) over console_command "py exec()"; fresh does_asset_exist may lag the registry rescan (verify on disk); screenshots now async (expectedDelay) |
| 2026-07-03 | Promoted 7 session-memory notes | New Setup & Versioning section (version-locked server+plugin, 4-step upgrade); probe-when-inspect-thin rule + probes in Tools/Python not Content/; destructive pin-name probe warning; widget slotName-not-name authoring rules; CDO write via get_default_object(bp.generated_class()); no BP edit/compile during PIE; Git-LFS-2 modal-hang on fixup_redirectors/delete_directory (flip Provider=None); broadened viewport rule (no selection/tab/ViewMode changes mid-task) |
| 2026-07-15 | Documented ProjectEternalEditor.EternalCheatToolset (project's own MCP toolset) |
18 tools (combat + itemization + remnants/greed + structured GetInventory/GetEquipment loadout reads); the only agent path to the project's console cheats; C++ toolset registration must defer to GetOnPostEngineInit() (registry subsystem absent at module StartupModule); ToolsetRegistry now an explicit uproject plugin; live-verified end-to-end over MCP |
| 2026-08-06 | Corrected the cheat-toolset listing and documented dedicated-session cheat reach | The toolset had grown past the stated count (ApplyStatus, TriggerCombatEffect were missing) — count the AICallable UFUNCTIONs rather than trusting a number. Player resolution is now "first PIE world with a local PC", so on a dedicated session the reachable cheat manager is the client's; authority cheats self-forward over Server_ExecuteCheat, gated on a server-side cheat manager, ProcessConsoleExec dispatch and a 256-char cap. TeleportToRoom / ListDungeonRooms are console-only, not toolset tools |