ue5-mcp — Field manual for driving Unreal Engine 5 via MCP
SkillDev toolsField manual for driving Unreal Engine 5.7 / 5.8 through MCP. Engine-level gotchas, silent-fail edges, crash patterns, and the call sequences that actually work — applicable regardless of which MCP server you're using (Epic's official ModelContextProtocol plugin, custom servers, or anything else). Auto-trigger when Unreal Engine MCP tools are detected in a session, or when the user mentions Unreal Engine, UE5, Blueprints, Niagara, MetaSound, materials, or any UE editor automation workflow. This skill contains hard-won knowledge from real debugging sessions — most entries trace back to an actual editor crash or hours-long faceplant. Ignoring it when UE5 MCP tools are present will lead to wasted time hitting known dead ends.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the ue5-mcp — Field manual for driving Unreal Engine 5 via MCP skill
What this skill tells your AI
The instructions your AI receives, as published by ibrews/ue5-mcp in skills/ue5-mcp/SKILL.md and read by ahel’s review.
Engine-level wisdom an LLM needs to drive UE5 through an MCP server without
faceplanting on UE's silent-fail edges. This skill is server-agnostic:
the gotchas, patterns, and identifiers documented here apply whether you're
connected to Epic's official ModelContextProtocol plugin (UE 5.8+) or any
other MCP server that exposes UE5 functionality.
What this skill isn't: a list of commands for any particular MCP server.
Each server publishes its own tool catalogue — ask the server with
tools/list for what it actually exposes. This skill covers what bites you
after you know the tool names.
1. Session checklist — read before you write
UE5 is a structured-asset editor. The agent that wins is the one that reads state before mutating it.
- Always dump the asset before editing it. Blueprint, material, Niagara
system, widget, level — every MCP server worth its salt exposes a
dump_*/inspect_*/read_*family. Use it. Editing a graph without first knowing what's there creates broken connections, duplicate nodes, and unrecoverable corruption faster than anything else. - Discover before assuming. Call
tools/listonce on connect, cache it, and use it to figure out which tools your server actually exposes. Different servers wrap UE5 differently; recipes from this manual that reference a generic capability (e.g., "dump the Blueprint graph") will map to different concrete tool names on different servers. - Verify after mutating. UE5 has too many silent-fail edges to trust a successful response. Read the property back. Compare to what you asked for. If they differ, re-examine.
- Save explicitly. Most introspection tools serialize from disk. If
your last mutation is in-memory only, the dump returns the pre-mutation
state. Save the asset (or
SaveDirtyAssets) before re-reading.
2. UE5 reflection gotchas
These bite agents regardless of which MCP server sits in front of them. Every one has cost real debugging time.
2.1 PascalCase, not snake_case, for UPROPERTY writes
Setting a UPROPERTY via the Python binding's set_editor_property("auto_possess_ai", ...)
silently no-ops on many builds. UPROPERTY names are PascalCase at the
reflection layer: AutoPossessAI. Python's unreal module accepts
snake_case at the call site, but the underlying lookup is case-sensitive
against the PascalCase name. There is no error returned — the property
simply doesn't change.
Detection pattern: round-trip verify. After any UPROPERTY write, read the
property back and compare. Naive string compare misses normalization
(EFoo::Bar vs Foo::Bar, (X=1,Y=2) vs (X=1.000000,Y=2.000000)). The
robust pattern in native C++:
- Allocate a scratch buffer aligned to
Property->GetMinAlignment()and callProperty->InitializeValue(Scratch). Property->ImportText_Direct(RequestedValue, Scratch, Owner, PPF_None)to canonicalize what was requested.Property->ExportTextItem_Direct(ExpectedText, Scratch, ...)for the canonical form of "what we asked for."- Apply the write, then
Property->ExportTextItem_Direct(ActualText, ...)for "what we got." - Compare
ExpectedTexttoActualText.
If they differ, the write didn't take — usually due to snake_case mismatch, an enum-class qualifier issue, or a struct-text format the property doesn't recognize.
2.2 Blueprint class path needs the _C suffix
LoadObject<UClass>(nullptr, "/Game/Path/BP_Foo") returns nullptr. The
Blueprint's generated class lives under a different name:
/Game/Path/BP_Foo.BP_Foo_C. StaticLoadClass expands this internally;
LoadObject<UClass> does not.
If an MCP tool returns "Class not found: /Game/Path/BP_Foo," that's almost
always the missing suffix. Retry with <path>.<asset>_C.
2.3 Async asset operations don't block
MetaHuman texture downloads, asset compilation, shader compilation, derived-data builds, Niagara compile, package save — all async. An agent that requests "download MetaHuman textures" and immediately reads the character sees the previous texture state, not the new one.
Patterns:
- Poll the relevant
Is*Completepredicate before continuing. - Subscribe to the completion delegate if the subsystem exposes one
(
FAssetCompilingManager::Get().GetPostCompilationDelegate(), etc.). - For MetaHuman: poll
IsTextureSourceRequestComplete(Character)afterRequestTextureSources. - For asset save: don't immediately re-read the package file; let
UPackage::SavePackagecomplete first.
2.4 Save before reading from disk
Many "dump" / "serialize" operations read the asset from its .uasset
package on disk. If the most recent edits are in-memory only, the dump
returns the pre-edit state. Save explicitly between mutate and read, or use
an in-memory-aware introspection path if the server provides one.
2.5 PostEditChangeProperty is required after direct property writes
Property->CopyCompleteValue(Dest, Src) writes the value but doesn't fire
PostEditChangeProperty. Any derived state set up by the object's
PostEditChangeProperty handler — preview meshes, generated thumbnails,
recompiles, dependent properties — won't update. Notify it manually:
FPropertyChangedEvent ChangeEvent(Property, EPropertyChangeType::ValueSet);
Object->PostEditChangeProperty(ChangeEvent);
The Details panel will show the new value either way, but the object's behaviour won't reflect it until the event fires.
2.6 Blueprint graph mutations need three steps, not one
To safely add a node to a UEdGraph:
- Construct the node (
NewObject<UEdGraphNode>(Graph)). Graph->Nodes.Add(NewNode).Graph->NotifyGraphChanged().
Single-call helpers in some bindings do step 1 only and leave the graph in an inconsistent state — the node exists but the editor's pin-resolution + compile pipeline doesn't see it. Symptoms: phantom "missing node" errors at compile, broken connect operations, or nodes that vanish after editor reload.
2.7 Enum-string resolution has three accepted forms
UENUM-defined enums store their entries as fully-qualified FName forms
like EAutoExposureMethod::AEM_Manual. UEnum::GetValueByNameString
matches the fully-qualified form, but bare short names (AEM_Manual) and
the Python-binding casing the unreal module exposes (AEM_MANUAL from
unreal.EAutoExposureMethod.AEM_MANUAL) silently miss — those are the
forms agents most naturally reach for, especially when copying values
out of dump_post_process_settings output or Python docs.
A 3-step resolver covers the common cases:
int64 ResolveEnumValue(UEnum* Enum, const FString& Name)
{
if (!Enum) return INDEX_NONE;
int64 Val = Enum->GetValueByNameString(Name); // EEnumType::ShortName
if (Val != INDEX_NONE) return Val;
Val = Enum->GetValueByName(FName(*Name)); // FName lookup
if (Val != INDEX_NONE) return Val;
const int32 N = Enum->NumEnums(); // case-insensitive
for (int32 i = 0; i < N; ++i) // suffix-after-::
{
FString EntryName = Enum->GetNameStringByIndex(i);
int32 ColonPos = INDEX_NONE;
if (EntryName.FindLastChar(TEXT(':'), ColonPos))
EntryName = EntryName.RightChop(ColonPos + 1);
if (EntryName.Equals(Name, ESearchCase::IgnoreCase))
return Enum->GetValueByIndex(i);
}
return INDEX_NONE;
}
Affects every reflection-driven property setter that accepts enum-typed
JSON strings (FEnumProperty, FByteProperty whose Enum field is
populated, properties resolved via StaticEnum<...>()). The 1-step form
Enum->GetValueByNameString(Name, EGetByNameFlags::CaseSensitive) is
the most fragile — it rejects everything except the fully-qualified
form. The fallback chain trades a tiny scan cost (enums rarely have more
than a few dozen entries) for actually accepting the strings callers
pass in.
The same pattern applies on the agent side: when calling an MCP tool
that takes an enum-named string, prefer the C++ short form
(AEM_Manual) — it's accepted by every resolver that follows even
minimal best practice; the Python-binding uppercase form may not be.
When resolution misses, list the valid values in the error. Returning
"unsupported type or value coercion failed" and nothing else forces the
caller to grep engine source for the enum's entries. The same
NumEnums() / GetNameStringByIndex() iteration that backs the
case-insensitive fallback also gives you the discovery surface — trim
each entry to its short name (after the last ::), skip the
auto-generated _MAX terminator, and join the rest into the error
string:
TArray<FString> ValidNames;
const int32 N = Enum->NumEnums();
for (int32 i = 0; i < N; ++i)
{
FString EntryName = Enum->GetNameStringByIndex(i);
int32 ColonPos = INDEX_NONE;
if (EntryName.FindLastChar(TEXT(':'), ColonPos))
EntryName = EntryName.RightChop(ColonPos + 1);
if (EntryName.EndsWith(TEXT("_MAX")))
continue;
ValidNames.Add(EntryName);
}
// "Could not apply 'X' (enum EAutoExposureMethod). Valid values:
// AEM_Histogram, AEM_Basic, AEM_Manual. (Case-insensitive; C++ short
// name, not Python display name.)"
The error message becomes self-documenting: any agent that calls the
tool with an invalid string immediately sees the valid set in the
response. No round-trip through engine source. This pairs naturally
with the resolver above — same iteration, same _MAX filter, used for
discovery instead of resolution.
2.8 Actor "properties" may live on the RootComponent, not the AActor
A reflection-driven property setter that only walks Actor->GetClass()
silently misses the properties that look actor-level in the editor but
are actually stored on the RootComponent (a SceneComponent). The
member-of-component set includes Mobility, bHidden, bVisible,
RelativeLocation, RelativeRotation, RelativeScale3D, AreaClass,
and the other SceneComponent transform/visibility fields.
The failure mode is hostile: the setter returns success-shaped (the
property name is real, and the JSON value coerced cleanly), the call
log shows "property_name": "Mobility", "applied": true, but a
follow-up read returns the old value. There's no error, no
deprecation warning, no typo suggestion — just a write that went into
the void because the writer aimed at the wrong UObject.
Fix: when the property isn't found on Actor->GetClass() and the
caller didn't pin a specific component, fall back to the RootComponent's
class:
UClass* TargetClass = Actor->GetClass();
void* TargetPtr = Actor;
FProperty* Prop = TargetClass->FindPropertyByName(*PropertyName);
if (!Prop && Actor->GetRootComponent())
{
USceneComponent* Root = Actor->GetRootComponent();
if (FProperty* RootProp = Root->GetClass()->FindPropertyByName(*PropertyName))
{
Prop = RootProp;
TargetClass = Root->GetClass();
TargetPtr = Root;
}
}
Surface which container actually received the write in the response
(target_object: "Actor" | "<ComponentName>"). Without that hint, a
caller debugging "why didn't the mobility change?" has no clue whether
the fallback fired or whether the original Actor-level write succeeded
on a same-named property. The silent-magic failure mode is worse than
the original wart — the call now appears to work but you can't tell
where the change landed.
The same pattern applies to other SceneComponent-resident sets — light intensity / color on light components, mesh on StaticMeshComponent, etc. Those usually have explicit component-targeting parameters in MCP surfaces, so the silent-miss mode there is rarer, but the fallback is the right default for any property setter accepting an actor name without an explicit component scope.
2.9 A mutation without Modify() isn't "undoable but with one field missing" — it's not in the undo history at all
FScopedTransaction wraps a block of editor code as one undo step, but the
transaction only actually contains an object if that object's Modify()
was called before it changed. An empty transaction — every object in scope
mutated via a raw setter or direct FProperty write that skipped Modify()
— is discarded as transient rather than pushed onto the undo stack
(verified against UE 5.8's EditorTransaction.cpp). The failure mode isn't
"Ctrl+Z reverts everything except this one field" — it's "Ctrl+Z does
nothing at all for this entire edit," with no error and no indication
anything was skipped. A human editing the same property through the
Details panel gets a working undo step for free (the panel's property
handle calls Modify() for you); a tool that reaches past the UI and
writes the property directly does not, unless it calls Modify() itself.
The rule: call Modify() on the object before the mutation, not
after — it snapshots pre-mutation state, so calling it post-hoc records
nothing useful.
// Wrong: SetMobility doesn't call Modify() itself, so this mutation
// never enters the transaction — Ctrl+Z after this silently does nothing.
RootComponent->SetMobility(EComponentMobility::Movable);
// Right:
RootComponent->Modify();
RootComponent->SetMobility(EComponentMobility::Movable);
For a transform-style write that touches both the actor and its root
component, Modify() both — matching whichever object's state the engine
actually reads back on undo:
Actor->Modify();
Actor->GetRootComponent()->Modify();
Actor->SetActorTransform(NewTransform);
Not every mutator needs this. High-level engine entry points that are
themselves undo-aware self-record when a transaction is active — UWorld:: SpawnActor and AActor::Destroy() both record into GUndo automatically
(verified in engine source), so wrapping a spawn/destroy in an outer
FScopedTransaction needs no extra Modify() call. The gap is specifically
low-level setters (SetMobility, SetIntensity, a generic reflected
FProperty write via ImportText_Direct / CopyCompleteValue) that
mutate state directly without going through an undo-aware wrapper.
If you're auditing an existing surface for this bug, look for "it built, it ran, the response said success" as the tell — this is not a crash or an error-returning bug, so it never shows up in normal QA. The only way to catch it is to explicitly test Ctrl+Z (or the transaction system's equivalent) after every mutating call and confirm the specific property you changed actually reverts — not just that undo doesn't crash.
3. UE5 stability — actions that crash the editor
These are crashes that hit any agent driving the editor, regardless of MCP server. Worth knowing before you do them.
3.1 Don't delete or modify assets that other actors reference
Deleting (or transforming) a mesh asset while level actors reference it
triggers a RegisteredElementType assertion crash. The editor goes down
and any unsaved work in other windows is lost.
Safe pattern: before deleting, walk the asset dependency graph. Most
MCP servers expose this (get_asset_references or similar). If anything
depends on the asset, create a new replacement asset, swap actor references
to it, then delete the original.
3.2 Don't spawn-then-immediately-delete actors in quick succession
Same RegisteredElementType assertion. Spawn → delete → focus in rapid
succession (sub-frame timing) corrupts the actor registry. Add a small
delay or interleave with other operations.
3.3 Niagara assertions and MetaSound crashes wipe unsaved changes
When Niagara or MetaSound asserts during PIE, the editor reverts to the last on-disk save. Custom nodes, in-memory tweaks, and uncompiled edits are gone. Save before every PIE test for these subsystems. Pattern after a crash: restart editor, dump the asset, recreate the lost nodes from the dump.
3.4 MetaSound: scalar literal on an Audio-type pin
Setting a float (or other scalar) literal directly on a pin typed as
Audio crashes the editor at runtime, not at edit time. The edit
succeeds silently; the crash fires when PIE starts and the graph
evaluates. The stack signature is:
bExpectsNone [MetasoundDataFactory.h:395]
Rule: Audio-type pins expect audio buffer connections, not scalar values. Don't pipe a Multiply (Audio) directly from a Constant; route it through an Oscillator or noise source that produces an audio-rate buffer.
After this crash, all custom MetaSound nodes are wiped on next editor
launch — only OnPlay, OnFinished, and Output survive.
3.5 Editor sprite icons are not particles
Editor viewport screenshots include sprite icons for each component (NiagaraComponent, AudioComponent, etc.). They look like particles but are the editor's UI overlay, not the actual VFX. Editor screenshots are not reliable verification for live Niagara behavior. Verify by:
- Reading
is_active: trueoff the Niagara actor after spawn. - Entering PIE and screenshotting the running game viewport.
- Or using pixel streaming for real-time visual confirmation.
4. Identifier and path conventions
4.1 Actor labels are not stable identifiers
a.get_actor_label() returns the display string shown in the Outliner. Two
actors can share a label. The label is user-editable.
Use the actor's full path as the stable identifier:
/Game/Maps/Level.Level:PersistentLevel.BP_Character_C_0
Most MCP servers accept either, but the path is the only form that survives renames and disambiguates duplicates.
4.2 Asset path forms
UE5 accepts three forms for an asset, and they mean different things:
| Form | Example | What it loads |
|---|---|---|
| Package name | /Game/Foo/Bar | The package (used by the asset registry) |
| Package.Asset | /Game/Foo/Bar.Bar | The primary asset within the package (LoadObject<UObject>) |
| Package.Asset_C | /Game/Foo/Bar.Bar_C | The generated class of a Blueprint (LoadObject<UClass>) |
If a tool returns a path-not-found error, check that the form matches what the tool expects.
4.3 Widget paths vs widget Blueprint paths
UMG-related tools usually take one of two parameters with similar names:
widget_blueprint_path— path to the WidgetBlueprint asset on disk (/Game/UI/WBP_HUD)widget_path— the identifier of a widget within a tree, addressing a node inside the WidgetBlueprint's hierarchy
A "compile this widget" tool wants the Blueprint path. A "remove this widget from its parent" tool wants the tree-internal path. Servers vary on which they expose where — check input schemas before assuming.
5. UE5 subsystem gotchas
Engine-level facts about specific subsystems. These apply regardless of MCP server — the underlying UE5 behavior is the same.
5.1 Lumen lighting — Movable mobility is mandatory
Lumen Global Illumination only considers lights with Movable mobility.
Static and Stationary lights contribute nothing to Lumen GI. Agents that
spawn a DirectionalLight default to Stationary and then complain that GI
isn't working — the fix is to set Mobility to Movable explicitly.
5.2 Blueprint instance override staleness
Level-placed Blueprint instances retain editor-modified component property
overrides even after the parent Blueprint changes. If you edit
BP_Character to change Speed from 600 to 800, instances of
BP_Character placed in the level keep their old override (whatever the
designer or a prior agent set on that specific instance).
Pattern after any parent BP property change: walk affected level instances and either revert overrides to defaults or re-apply the new value explicitly per instance. The "Reset to Defaults" right-click in the Details panel does this for humans; for agents, set the property directly on each instance.
5.3 Niagara: created-from-empty systems don't emit
Programmatically constructing a UNiagaraSystem from scratch produces a
system that compiles clean but never emits. The empty-system default state
isn't valid for emission (missing system spawn script wiring, missing
emitter mode, etc.).
Working pattern: start from a working template. UE ships
/Niagara/DefaultAssets/DefaultSystem which has a valid sprite emitter.
Most MCP servers expose an "asset duplicate" tool; use it to duplicate a
working system and then mutate the copy. Don't try to build emitters from
nothing.
5.4 Niagara: script_usage is part of the module identity
The same module name can appear in multiple script-usage stages:
system_spawn,system_update— once per system frameemitter_spawn,emitter_update— once per emitter per frameparticle_spawn,particle_update— once per particle
A module named SpawnRate typically lives in emitter_update. A module
named Initialize Particle lives in particle_spawn. When setting module
inputs, always specify script_usage — the same module name in
different stages is a different module instance, and the wrong stage
silently no-ops.
5.5 Niagara: user-facing inputs vs script pins
The Niagara stack panel shows "user-facing inputs" — the named tweakable parameters per module. These are NOT the same as the underlying script's function-call pins. An agent that reads script pins and assumes they're the inputs will fail to set values.
Discovery pattern: ask for the module's input list before setting
anything. Servers usually expose this as list_module_inputs or
equivalent. The returned names are what set_*_module_input expects.
5.6 Niagara: dynamic input setting is broken in many versions
set_niagara_dynamic_input (or equivalent) typically fails with:
Failed to load random range script
This is a UE5 Niagara API gap, not an MCP-server bug. Workarounds: bake the dynamic value to a constant before assignment, or compute the value in Python and set the static input.
5.7 MetaSound: exact pin names matter
MetaSound pin names are case-sensitive and exact. SuperOscillator uses
Frequency, Voices, Detune — not Base Frequency or Freq. Always
dump the MetaSound's nodes (dump_metasound_graph or equivalent) before
setting pins to learn the exact names.
5.8 Materials: emissive bloom threshold
Emissive intensity must exceed 1.0 to trigger bloom in the Post Process pipeline. Values of 3–10 produce visibly bloomed emissive surfaces. An emissive material with intensity 0.8 looks dim and self-lit but won't bloom.
Additional requirement: Post Process Volume must have Bloom enabled in its Effects settings. Default volumes have it on, but an agent that explicitly disabled bloom for performance won't get emissive bloom either.
5.9 Materials: translucent particle materials need Unlit shading
Lit translucent materials require normal vectors. Niagara sprite particles don't reliably provide normals (the orientation comes from the renderer, not the geometry). Pattern for particle materials:
blend_mode = Translucentshading_model = Unlit- Emissive output, no base color routing
A lit translucent particle material renders as a black/featureless sprite because the lighting calc has nothing to work with.
5.10 Materials: compilation lag
Creating or modifying a material kicks off shader compilation. Depending on how many permutations the material has (number of materials in the project using it, light counts, etc.), compilation takes seconds to minutes. Visual output doesn't update until compilation completes.
Pattern: after a material edit, poll for compile completion before judging visuals. Most servers expose a "get material errors" or "is asset compiled" predicate; if not, screenshot after a fixed delay (10–30s for moderately complex materials).
5.11 UMG widgets: CreateWidget needs an owning player context
Shortened here. Read the whole file on GitHub.
Signals
- GitHub stars
- 38
- Forks
- 5
- Last commit
- Aug 2026
Advanced
- Catalog kind
- skill
- Gateway key
ue5-mcp- Source
- github.com/ibrews/ue5-mcp