Help us improve
Share bugs, ideas, or general feedback.
From ue5-mcp
Drives Unreal Engine 5.7/5.8 via MCP with engine-level gotchas, silent-fail edges, crash patterns, and proven call sequences. Activates on UE5/MCP tool detection or UE terms.
npx claudepluginhub ibrews/ue5-mcpHow this skill is triggered — by the user, by Claude, or both
Slash command
/ue5-mcp:ue5-mcpThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Engine-level wisdom an LLM needs to drive UE5 through an MCP server without
Expert guidelines for Unreal Engine 5.x C++ development covering UObject lifecycle, reflection system, performance patterns, and Epic's naming conventions.
Provides behavioral guidelines to reduce common LLM coding mistakes, focusing on simplicity, surgical changes, assumption surfacing, and verifiable success criteria.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Share bugs, ideas, or general feedback.
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.
UE5 is a structured-asset editor. The agent that wins is the one that reads state before mutating it.
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.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.SaveDirtyAssets) before re-reading.These bite agents regardless of which MCP server sits in front of them. Every one has cost real debugging time.
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++:
Property->GetMinAlignment() and
call Property->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."Property->ExportTextItem_Direct(ActualText, ...)
for "what we got."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.
_C suffixLoadObject<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.
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:
Is*Complete predicate before continuing.FAssetCompilingManager::Get().GetPostCompilationDelegate(), etc.).IsTextureSourceRequestComplete(Character) after
RequestTextureSources.UPackage::SavePackage complete first.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.
PostEditChangeProperty is required after direct property writesProperty->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.
To safely add a node to a UEdGraph:
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.
These are crashes that hit any agent driving the editor, regardless of MCP server. Worth knowing before you do them.
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.
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.
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.
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.
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:
is_active: true off the Niagara actor after spawn.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.
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.
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 hierarchyA "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.
Engine-level facts about specific subsystems. These apply regardless of MCP server — the underlying UE5 behavior is the same.
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.
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.
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.
script_usage is part of the module identityThe 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 particleA 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.
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.
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.
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.
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.
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 = UnlitA lit translucent particle material renders as a black/featureless sprite because the lighting calc has nothing to work with.
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).
CreateWidget needs an owning player contextA widget created without specifying an owning player context renders blank
at runtime. The widget Blueprint compiles, the instance spawns, and
AddToViewport accepts it — but nothing draws because the widget has no
World context to anchor in.
Fix: pass GetPlayerController(0) as the Owner argument to
CreateWidget (the Owner pin in the Blueprint node, or the OwningPlayer
parameter in the Python unreal.CreateWidget call).
WidgetClassAdding a WidgetComponent to a Blueprint doesn't automatically associate
it with a Widget Blueprint. Set the WidgetClass property explicitly —
it's a TSubclassOf<UUserWidget> (an FClassProperty), so the value is the
generated class path (/Game/UI/WBP_HUD.WBP_HUD_C) or the asset path
(/Game/UI/WBP_HUD, which auto-expands).
Default draw mode is screen-space; for a 3D billboard, set the component's
Space to World.
When adding an audio component to a Blueprint via reflection, the type name
is Audio, not AudioComponent. Some servers' helpers handle either; raw
reflection lookups don't. Sound asset assignment goes through the Sound
UPROPERTY (a FObjectProperty); pass the sound asset's path as the value
and the property handler resolves the load.
The UltraDynamicSky / UltraDynamicWeather plugin uses a "manual override"
pattern. Each weather parameter (rain, snow, wind, etc.) has a companion
bool flag named <Param> - Manual Override. The manual override bool
must be set to true before setting the parameter value — otherwise the
plugin's auto-weather logic overrides whatever you set on the next tick.
Other UltraDynamicSky notes:
Ultra_Dynamic_Sky and Ultra_Dynamic_Weather at origin together.Servers implementing MCP 2025-03-26 Streamable HTTP generally require both content types in the Accept header:
Accept: application/json, text/event-stream
Omit it and the server returns 406 Not Acceptable. Most MCP clients
(Claude Desktop, Cursor, VS Code's MCP UI, Epic's EDA panel) set this
automatically. Raw curl does not — pass
-H 'Accept: application/json, text/event-stream' when testing manually.
MCP supports an image content block alongside text:
{
"content": [{
"type": "image",
"data": "<base64-encoded PNG>",
"mimeType": "image/png"
}]
}
The decoded bytes start with \x89PNG\r\n\x1a\n. Most MCP clients render
images inline in the chat UI. UE5 servers commonly use this for editor
viewport / PIE / depth captures.
MCP 2025-03-26 defines notifications/cancelled with a requestId
parameter. Whether it actually aborts an in-flight tool call is
server-dependent — synchronous servers can't interrupt themselves
mid-execution. Don't depend on cancellation working unless the server
documents support.
Servers that implement the Mcp-Session-Id header expect clients to echo
it back on subsequent requests in the same session. The server mints a
fresh ID if the request arrives without one. Sessions usually expire on
idle timeout — re-send initialize if the server returns "unknown
session."
UE's Python interpreter is reachable from most MCP servers via a console
command (py <code>) or a dedicated execute_script tool. Python is the
right hammer for:
Critical limitation: Python's print() and stdout go to the UE log,
not back to the MCP client. The agent that ran the script can't see what
it produced.
Workaround — Actor Tags as a data channel:
import unreal
result = "...whatever the script produced..."
note = unreal.EditorLevelLibrary.spawn_actor_from_class(
unreal.Note, unreal.Vector(0, 0, 9999)
)
# Keep tags short — UE truncates very long tag strings on save.
note.tags = [result[:200]]
Read back via MCP: query the Note actor's properties and inspect tags.
Delete the Note actor after. Spawn a fresh actor for each result — stale
actors return stale tags.
For larger payloads, write to a known file under Saved/Logs/ or
Saved/AgentScratch/ and read it back via the MCP server's file-read tool
(if available).
Server-side Python execution alternative: some MCP servers expose
execute_script (or similar) that captures the return value of a run()
function directly into the response. If your server has this, prefer it —
the data-channel workaround becomes unnecessary.
If you're building an MCP server against the UE5 editor, the following patterns have proven valuable in practice. None are required by the MCP spec — they're hard-won pragmatic recommendations.
When a tool call fails input validation, return the tool's full input JSON Schema inline in the error text. The LLM caller reads the schema, fixes the argument, and retries. Saves 3–5 round-trips per error compared to a bare "Invalid argument" message.
{
"isError": true,
"content": [{
"type": "text",
"text": "Validation failed: 'body_type' must be one of [Skinny, Athletic, Heavyset]. Full schema:\n\n{...inputSchema JSON...}"
}]
}
Ship an outputSchema for every introspection / read-only tool. Fields in
the schema are guaranteed to appear; additional metadata fields may also
appear but aren't promised. Clients can parse responses against the schema
instead of guessing field names.
UE5 asset dumps can run hundreds of KB. MCP clients vary on how they
handle large text content. Pattern: cap response text at N bytes (64KB
is a reasonable default), stash the remainder under an opaque token,
return token + first chunk + _remaining_chars so the client knows
there's more. Expose a continue_response(token, max_bytes) tool to fetch
the next chunk.
FScopedTransaction for mutating toolsWrap every UPROPERTY write, asset edit, level mutation, or Blueprint graph
change in FScopedTransaction. Ctrl+Z in the editor then reverts the
agent's change — critical for user trust. Losing the undo path is the
fastest way to make agents feel scary to work with.
If your server exposes 200+ tools, the default tools/list response can
overwhelm small-context-window clients. Pattern: split the surface into
categories, default tools/list to a small set of meta-tools
(list_categories, describe_category, load_category) plus a few
always-on essentials, and load full categories on demand. Use
notifications/tools/list_changed to signal subscribers when the visible
surface changes.
Slate operations, viewport rendering, asset operations, and Python
execution must all run on the game thread. MCP requests typically arrive
on a worker thread; dispatch the actual work back to the game thread via
AsyncTask(ENamedThreads::GameThread, ...) and signal completion via a
TPromise or similar. check(IsInGameThread()) defensively before any
Slate / viewport call.
Asset structures can contain cycles (Blueprint references, Material Functions, Sound Cues with self-referential branches). Always cap recursion depth (1024 is a safe default for graph walks). Always cap output size before returning. Always opportunistically GC any per-tool caches.
tools/list once per sessionTool surfaces are stable within a session. Cache the response on connect. Polling mid-session wastes round-trips and can confuse cancellation logic.
outputSchema when presentIf the server publishes output schemas, validate against them instead of guessing field names. Saves debugging cycles when an agent assumes a field exists that doesn't, or doesn't notice a new field appearing in a later version.
UE5 has too many silent-fail edges to trust a successful response. The full pattern:
Verify-after-mutate is the difference between an agent that works 80% of the time and one that works 99%.
isError: true as load-bearingThe MCP spec specifies tool errors return isError: true with the error
text in content[0].text. Servers may also return JSON-RPC transport-level
errors with the error field at the top level. Both need handling; both
contain debugging info worth reading.
If a tool dumps an asset from disk, the dump reflects the on-disk state. If you just mutated the asset in memory and haven't saved, the dump shows the pre-mutation state. Save explicitly between mutate and dump if you need the current state.
These affect any agent driving UE5 across engine versions, regardless of MCP layer.
UMovieGraphConfig, UMovieGraphPipeline, and graph-based rendering
composition land in 5.8. UE 5.7 has MovieRenderQueue
(UMoviePipelineQueueEngineSubsystem) but no graph composition layer.
FJsonObject::Values key type changeFJsonObject::Values is TMap<FString, ...> on 5.7 and
TMap<UE::FSharedString, ...> on 5.8. Affects native C++ iteration;
doesn't affect Python or JSON-over-the-wire usage.
Several PathTracer settings migrated from CVar-only to UPROPERTY on
UPostProcessVolume in 5.8. An agent that sets them via console commands
works on both engines; one that sets them via reflection on the
post-process volume needs 5.8.
ModelContextProtocol plugin is 5.8-onlyEpic's MCP plugin ships in UE 5.8 (experimental). UE 5.7 users running an agent against the editor need a third-party MCP server.
UE 5.8 reorganised some Niagara editor APIs into a NiagaraStackEditor
module. Tools that introspect Niagara stack issues need to depend on the
right module on each engine version.
What gets serialized when common UE5 assets are dumped to JSON. Useful for parsing responses regardless of which server produced them. This isn't an exhaustive schema — see Epic's UE docs for the authoritative structure.
UbergraphPages — event graphs plus auto-generated wrappersFunctionGraphs — user-defined functionsDelegateSignatureGraphs — dispatcher signaturesMacroGraphs — user macros (if any)Variables — name, type, default value, CategoryName, RepNotifyFuncComponents — SimpleConstructionScript root + AddedComponentsParentClass, implemented interfaces, compile stateSystemSpawnScript / SystemUpdateScript — system-level VM scriptsEmitterHandles — per-emitter wrapper (Enabled, LocalSpace, EmitterMode)UserParameters — parameters exposed to the component for runtime tuningSpawnScript / UpdateScript / RenderScript stacks,
modules per stack, renderer settingsExpressions — UMaterialExpression* nodes (one per node in the graph)ScalarParameterValues, VectorParameterValues, TextureParameterValuesShadingModel, BlendMode, MaterialDomainActors — array of actor entries with name, class, transform, componentsWorldSettings — game mode, level scripts, navmesh settingsStreaming — sublevels and their state (loaded / visible / unloaded)ULevelSequence)MovieScene — root sceneWidgetTree — hierarchy of widgets (root, panels, leaf widgets)Slot properties (depend on parent panel type),
variable-or-not flag, accessibility textBindingClass — MVVM viewmodel class (if any)Two actors can share a label. The label is user-editable. Use the full path.
tools/list mid-sessionTool surfaces are stable per session. Cache once on connect.
Two MCP servers against UE5 will have different tool catalogues, different
parameter names for similar concepts, and different output shapes. Always
read tools/list to discover what's actually exposed before assuming a
tool exists.
isError: trueThe error message is usually load-bearing. Schema-in-error servers put the input schema right in there.
Most introspection tools serialize from disk. Mutation without save is invisible to those tools.
Texture compile, Niagara compile, shader compile, asset save, package compile — all async. Poll completion or wait on the relevant delegate before continuing.
Check get_asset_references (or equivalent) first. Deleting referenced
meshes/materials crashes the editor with RegisteredElementType.
Runtime crash. Use audio buffer sources (Oscillators, Noise) into Audio pins. Scalar math (Multiply, Add) on the audio path goes through Audio variants of those nodes, not the float variants.
unreal.* module surfaceFor server-specific tool catalogues, query the server with tools/list.
| Version | Date | Notes |
|---|---|---|
| 3.0.0 | 2026-05-21 | Server-agnostic rewrite. No plugin dependency; works against any MCP server exposing UE5. Engine-level wisdom only. |
| 2.x | 2026-05-19 and earlier | Tied to a specific MCP plugin; deprecated. |