Skip to main content

ue5-mcp

Field 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.

Source facts

Repository
ibrews/ue5-mcp
Last source activity
October 29, 2026 at 08:06
Detected SKILL.md language
English
Stars
46
Forks
7

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
ue5-mcp
description
Field 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.
version
3.3.0
date
2026-08-08T00:00:00.000Z
license
MIT
# ue5-mcp — Field manual for driving Unreal Engine 5 via MCP 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. 1. **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. 2. **Discover before assuming.** Call `tools/list` once 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. 3. **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. 4. **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++: 1. Allocate a scratch buffer aligned to `Property->GetMinAlignment()` and call `Property->InitializeValue(Scratch)`. 2. `Property->ImportText_Direct(RequestedValue, Scratch, Owner, PPF_None)` to canonicalize what was requested. 3. `Property->ExportTextItem_Direct(ExpectedText, Scratch, ...)` for the canonical form of "what we asked for." 4. Apply the write, then `Property->ExportTextItem_Direct(ActualText, ...)` for "what we got." 5. Compare `ExpectedText` to `ActualText`. 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*Complete` predicate before continuing. - Subscribe to the completion delegate if the subsystem exposes one (`FAssetCompilingManager::Get().GetPostCompilationDelegate()`, etc.). - For MetaHuman: poll `IsTextureSourceRequestComplete(Character)` after `RequestTextureSources`. - For asset save: don't immediately re-read the package file; let `UPackage::SavePackage` complete 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: ```cpp 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`: 1. Construct the node (`NewObject<UEdGraphNode>(Graph)`). 2. `Graph->Nodes.Add(NewNode)`. 3. `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: ```cpp 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: ```cpp 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: ```cpp 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. ```cpp // 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: ```cpp 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: 1. Reading `is_active: true` off the Niagara actor after spawn. 2. Entering PIE and screenshotting the running game viewport. 3. 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
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub