🧠 Brains complement: IF an
unreal-engine-skills-managertool (external MCP) exists in this session, call it with{action: "load", skill: "asset-management"}for UE domain knowledge on this topic — correct APIs, architecture, best practices — and treat it as the rubric for any review / "best practices" question. If no such tool is available (e.g. running under Claude Code or Codex without that MCP), skip this line entirely and proceed with this skill alone — do NOT attempt the call.
Asset Discovery & Management Skill
For static-mesh LOD reimport, lost textures or gray/default surfaces despite a
correct component material, read mesh reimport material mappings.
The bundled scripts/mesh_material_slots.py exposes explicit section inspection
and remapping through the existing engine Python API; no native service rebuild.
🔀 Engine owns general asset ops now. In the Unreal 5.8 consolidation, searching, loading, saving, moving, renaming, duplicating, and deleting assets moved to Unreal's native
AssetToolstoolset (reach it viacall_tool; rundescribe_toolsetfor its actions) — or you can driveunreal.EditorAssetLibrary/unreal.AssetRegistryHelpersdirectly fromexecute_python_code. The oldmanage_assetMCP tool is GONE. VibeUE'sAssetDiscoveryServicewas trimmed to only the crash-safe delta the engine doesn't provide:
Kept on AssetDiscoveryServicePurpose import_asset(src, dest_folder, name)→(path, err)Import an image, mesh or any importable file (folder + name) import_texture(src, dest_asset_path)Same importer, full asset path export_texture(asset_path, file_path)Export a texture to disk get_primary_content_browser_selection()→AssetData or NonePrimary Content Browser selection is_asset_open(asset_path)→boolWhether an asset is open in an editor Everything else below (
search_assets,list_assets_in_path,move_asset,duplicate_asset,delete_asset,save_asset,get_open_assets,get_asset_referencers,find_asset_by_path,asset_exists, …) was removed fromAssetDiscoveryService— use the engineAssetToolstoolset orEditorAssetLibrary/ the Asset Registry.
Data Assets & Data Tables — no dedicated VibeUE service (issues #451, #452)
There is no DataAssetService / DataTableService. Drive them natively:
pythonimport unreal # --- Data Assets (UPrimaryDataAsset / UDataAsset subclasses) --- # Create: engine DataAssetTools toolset via call_tool (describe_toolset for the action), OR a factory. da = unreal.load_asset("/Game/Data/DA_Thing") val = da.get_editor_property("MyField") # read da.set_editor_property("MyField", 42) # write — works for any UPROPERTY unreal.EditorAssetLibrary.save_loaded_asset(da) # --- Data Tables --- dt = unreal.load_asset("/Game/Data/DT_Items") names = unreal.DataTableFunctionLibrary.get_data_table_row_names(dt) # row keys # Read/write rows with the editor DataTable API: # unreal.DataTableFunctionLibrary.evaluate_curve_table_row(...) / get_data_table_column_as_string(dt, "Col") # Engine DataTableTools (call_tool) covers add/remove/get rows + import/export CSV/JSON.
⚠️ Data Table gotchas (verified, #452): the row key ("Name") is reported as a column by some schema readers — it is the row id, not a data field. Writing a row with the wrong value type can be silently coerced — read the row back to confirm. There is no
clear_rows; remove rows individually (engineDataTableTools) or re-import an empty CSV. An emptyGameplayTagfield serializes as"None".
Critical Rules
⚠️ delete_asset on a REFERENCED asset opens a modal dialog that wedges an unattended editor
EditorAssetLibrary.delete_asset asks "this asset is referenced, force delete?" through a modal
window when anything points at the asset (a data asset holding the montage you are replacing, a
Blueprint default, a level). Nobody answers it from MCP: the game thread stalls (308 s observed),
every later call hangs, and the process has to be force-killed with the files removed from disk.
Use the plugin's dialog-free delete instead:
python# Refuses when referenced and tells you who: result = unreal.AssetDiscoveryService.delete_asset_unattended("/Game/Anim/AM_Old", False) # Delete anyway and null every reference (what the dialog's Force Delete does): result = unreal.AssetDiscoveryService.delete_asset_unattended("/Game/Anim/AM_Old", True) if not result.b_success: print(result.error_message) # why it did not happen print(result.referencers) # what pointed at the asset
The call returns an FUnattendedDeleteResult struct — result.b_success, result.referencers,
result.error_message — so the reason ALWAYS survives, even on a refusal. (It used to return a
bool with out-params, and Python maps a False return to None, which silently dropped the reason;
that is fixed.) result.referencers is filled on a refusal AND on a forced delete, so you always see
what pointed at the asset. Prefer creating the replacement under a NEW name and repointing references
over force-deleting.
Even delete_asset_unattended(path, True) refuses a natively rooted in-memory object — it never
prompts. Before deleting, the call does a conservative, non-mutating check: it looks at who holds the
asset and refuses ONLY when a native GC root holds it directly — a UGCObjectReferencer, which in an
agent session is a Python module-level global that created or loaded the asset earlier this session.
That is the one case the engine's own force delete cannot clear, so it would stall on the modal "is in
use" dialog (6+ minutes observed). When it refuses, the call returns b_success == False with those
roots in result.referencers and an result.error_message explaining it. The fix is what the error
says: release the Python globals holding it — del my_var, then unreal.SystemLibrary.collect_garbage()
— and retry. Every OTHER in-memory referencer is left to the engine's real force delete, which clears it
without prompting: on-disk asset references, the Blueprint-palette node spawners, and transient editor
helpers like an anim data controller all delete fine. Read-only package files are also refused before
the engine delete path, because Unreal can show a read-only-package prompt even when confirmation is
disabled. Clear the filesystem/source-control read-only state explicitly, then retry.
The check never mutates state (it does not null any references before deciding), so a refusal leaves the asset and everything around it exactly as they were — safe to retry after releasing the global.
⚠️ Never delete_asset a Blueprint you loaded or compiled this session
EditorAssetLibrary.delete_asset on a Blueprint (Anim Blueprints especially) that is still loaded —
which it is, if you created, compiled or read it earlier in the same script — fails inside
ForceDeleteObjects and leaves the package half-deleted and corrupt:
Ensure condition failed: false [ObjectTools.cpp:4045] Failed to unload all packages during ForceDeleteObjects - these packages are likely corrupt. Consider restarting the editor, noting which assets remain and then deleting them from the file system manually: /Game/Path/ABP_Thing
Every later call against that path then times out, and the editor typically has to be killed. The
recovery is exactly what the message says — close the editor, delete the .uasset from disk,
relaunch (watch for a ZenServer stall on the way back up) — so it costs several minutes.
The rebuild-an-asset pattern, instead of delete-then-create:
- Write to a new name and swap references, or
- Delete the file on disk while the editor is closed, then create it fresh, or
- Edit the existing asset in place (clear the graph, re-add nodes) rather than recreating it.
The same caution applies to any asset currently open in an editor tab or referenced by a loaded level. Non-Blueprint assets you never loaded (textures, meshes written by a factory) delete fine.
⚠️ Out-Params Become Return Values in Python — Never Pass an AssetData Argument
get_primary_content_browser_selection is shaped like bool Func(FAssetData& Out) in C++ and is
exposed to Python as () -> AssetData or None. Passing an AssetData argument raises TypeError.
python# WRONG - TypeError: takes no arguments (1 given) asset = unreal.AssetData() unreal.AssetDiscoveryService.get_primary_content_browser_selection(asset) # CORRECT - call with no out-arg, check the return for None asset = unreal.AssetDiscoveryService.get_primary_content_browser_selection() if asset: print(asset.asset_name)
🔀 Where each operation lives now
| Operation | Use |
|---|---|
| Search / find / list assets | engine AssetTools toolset via call_tool, or unreal.AssetRegistryHelpers.get_asset_registry() |
| Load / save / save-all | unreal.EditorAssetLibrary.load_asset / save_asset / save_directory, or AssetTools |
| Move / rename / duplicate / delete | unreal.EditorAssetLibrary.rename_asset (move), duplicate_asset, delete_asset (unreferenced only — see the modal warning above), or AssetTools; unreal.AssetDiscoveryService.delete_asset_unattended for anything that may be referenced |
| Existence check | unreal.EditorAssetLibrary.does_asset_exist(path) |
| Referencers / dependencies | unreal.AssetRegistryHelpers.get_asset_registry().get_referencers(...) |
| Open an asset / list ALL open editors | Epic EditorAppToolset via call_tool (see below) |
| Import a file from disk (image, mesh, …) | unreal.AssetDiscoveryService.import_asset / import_texture (see below) |
| Export a texture to disk | unreal.AssetDiscoveryService.export_texture |
| Primary Content Browser selection | unreal.AssetDiscoveryService.get_primary_content_browser_selection() |
| Is an asset open in an editor | unreal.AssetDiscoveryService.is_asset_open(path) |
🔀 ALL open assets / ALL selections / open an asset — use Epic's EditorAppToolset
VibeUE covers the single/primary queries above; the list-all and open operations live on
Epic's native EditorToolset.EditorAppToolset (call via call_tool; returns {"returnValue": [...]}
of package-path strings). These are NOT Python-bound — they only work through call_tool:
| Need | Call |
|---|---|
| All assets open in editors | call_tool("GetOpenAssets", "EditorToolset.EditorAppToolset") |
| All Content Browser selections | call_tool("GetSelectedAssets", "EditorToolset.EditorAppToolset") |
| Open an asset in its editor | call_tool("OpenEditorForAsset", "EditorToolset.EditorAppToolset", {"assetPath": "/Game/.../BP_X"}) |
python# "Show me all open Blueprints" = Epic GetOpenAssets + a type filter opens = call_tool("GetOpenAssets", "EditorToolset.EditorAppToolset")["returnValue"] bps = [p for p in opens if isinstance(unreal.load_asset(p), unreal.Blueprint)] # "Open the selected asset" = VibeUE primary-selection + Epic OpenEditorForAsset sel = unreal.AssetDiscoveryService.get_primary_content_browser_selection() if sel: call_tool("OpenEditorForAsset", "EditorToolset.EditorAppToolset", {"assetPath": str(sel.package_name)})
🚨 Never list broad paths — a /Game listing can return 30,000+ assets
Listing every asset under /Game returns tens of thousands of entries and floods the conversation.
Whether you go through the engine AssetTools toolset or EditorAssetLibrary.list_assets, always
narrow first:
- Filter by a specific subfolder (e.g.
/Game/Blueprints/HUD), and by class where supported. - If you only need a count or sample, slice in Python before printing — never print the full array.
⚠️ EditorAssetLibrary Save Methods
There is no save_dirty_assets (AttributeError). The real options:
unreal.EditorAssetLibrary.save_asset(path), save_directory(dir), save_loaded_asset(obj),
save_loaded_assets([objs]). To save everything dirty, iterate or use the engine AssetTools
toolset's save action.
⚠️ EditorAssetLibrary.list_assets Has No Class Filter
unreal.EditorAssetLibrary.list_assets(directory_path, recursive=True, include_folder=False)
returns path strings only — there is no asset_class_names or any other class-filter kwarg
(TypeError: invalid keyword argument). To list assets of one type, either filter the returned
paths in Python (load each and check its class), or query the Asset Registry with a class filter
via unreal.AssetRegistryHelpers.get_asset_registry().get_assets(...).
⚠️ UE 5.8 AssetData Property Names
get_primary_content_browser_selection() returns an AssetData. Read it with:
| WRONG (old) | CORRECT |
|---|---|
asset.name | asset.asset_name |
asset.path | asset.package_path |
asset.asset_class | asset.asset_class_path (a TopLevelAssetPath; compare .asset_name) |
asset.object_path | f"{asset.package_name}.{asset.asset_name}" |
AssetData has no object_path attribute (AttributeError). Build it from package_name +
asset_name, or just use str(asset.package_name).
⚠️ Importing Files From Disk — Use AssetDiscoveryService.import_asset
To bring a file from disk into the Content Browser, use AssetDiscoveryService.import_asset
(or import_texture, which takes a full asset path and runs the same importer). It takes any
format the editor imports, and saves what it imports:
- Images (png, jpg, jpeg, bmp, tga, dds, exr, hdr, tiff, tif, psd, pcx) go through the
texture factory's direct binary path and become a
Texture2D. - Everything else (FBX, OBJ, glTF/GLB, audio, …) goes through an automated, synchronous
AssetImportTask— no option dialogs.
pythonimport unreal # Disk → Content Browser. Returns (asset_path, error); path is "" on failure. path, err = unreal.AssetDiscoveryService.import_asset( "C:/Images/rocks.jpg", "/Game/UI/Textures", "T_Rocks") print(path or err) # A mesh takes the same call. Give a file that brings materials/textures a folder of its own. path, err = unreal.AssetDiscoveryService.import_asset( "C:/Art/crate.fbx", "/Game/Props/Crate", "SM_Crate") print(path or err)
What to expect:
- Same-name assets are replaced, without a prompt. That includes the side assets a multi-asset file brings along: an FBX's materials and textures (and a skeletal mesh's Skeleton and PhysicsAsset) overwrite any asset of the same name already in the destination folder. Their names are only known once the importer has run, so there is no pre-check — import such files into an empty or dedicated folder.
- The returned path is the main asset. For a file that brings several assets it is the mesh, not a material or texture; when the importer did not use the name you passed, the path carries the importer's name. List the folder (Asset Registry) to find the side assets.
- The one context where an import would crash, and is refused instead. A synchronous AssetTools
import waits by pumping the game thread's task queue. If the caller is itself running inside a
game-thread task (the queue is already being processed), TaskGraph's
RecursionGuardasserts and the editor goes down.import_assetchecks for exactly that and returns an error namingRecursionGuardinstead.execute_python_codeand MCP tool calls do not run inside such a task, so imports from them go ahead. Callingunreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks(...)yourself skips that check (and the save, and the main-asset pick) — preferimport_asset.
Need a source image to import? Editor screenshots live under the project's
Saved/Screenshots (and Saved/VibeUE/Screenshots):
pythonimport os, unreal shots = os.path.join(unreal.Paths.project_saved_dir(), "Screenshots") print(os.listdir(shots) if os.path.isdir(shots) else "no screenshots yet")
⚠️ Asset Paths Must Be Content Browser Paths
Use /Game/... paths, not file system paths, everywhere except the disk side of import/export.
python# WRONG - file system path unreal.EditorAssetLibrary.does_asset_exist("C:/Projects/Content/BP_Player.uasset") # CORRECT - content browser path unreal.EditorAssetLibrary.does_asset_exist("/Game/Blueprints/BP_Player")
⚠️ Never Emulate Move/Rename with Duplicate + Delete
Duplicating creates a new asset identity. References stay pointed at the original, so deleting the original after a duplicate can break those references. Use a real move/rename (which fixes up references) instead:
pythonimport unreal # EditorAssetLibrary.rename_asset performs a move + reference fixup unreal.EditorAssetLibrary.rename_asset( "/Game/StateTree/STT_Rotate", "/Game/StateTree/Tasks/STT_Rotate")
(The engine AssetTools toolset exposes an equivalent move/rename action via call_tool.)
Creating Widget Blueprints
Use the engine's WidgetBlueprintFactory — a plain BlueprintFactory with a UserWidget
parent creates a regular Blueprint, not a WidgetBlueprint (the removed
BlueprintService.create_blueprint used to pick the UMG factory automatically):
pythonfactory = unreal.WidgetBlueprintFactory() factory.set_editor_property("ParentClass", unreal.UserWidget) wbp = unreal.AssetToolsHelpers.get_asset_tools().create_asset("WBP_Menu", "/Game/UI", None, factory)
For designing the widget afterwards (hierarchy, slots, bindings), load the umg-widgets skill.
Workflows
Search Pattern
Searching is an engine AssetTools job (via call_tool) — or query the Asset Registry directly:
pythonimport unreal ar = unreal.AssetRegistryHelpers.get_asset_registry() # All Blueprints under /Game (recursive). Use a class filter and a narrow path to avoid huge results. f = unreal.ARFilter( package_paths=["/Game/Blueprints"], class_paths=[unreal.TopLevelAssetPath("/Script/Engine", "Blueprint")], recursive_paths=True) for a in ar.get_assets(f)[:25]: print(f"{a.asset_name}: {a.package_path}")
Check Exists Pattern
pythonimport unreal if unreal.EditorAssetLibrary.does_asset_exist("/Game/BP_Player"): print("Found") else: print("Not found - create it")
Duplicate Pattern
pythonimport unreal dup = unreal.EditorAssetLibrary.duplicate_asset("/Game/BP_Enemy", "/Game/BP_EnemyBoss") if dup: print("Duplicated")
Move / Rename Pattern
pythonimport unreal ok = unreal.EditorAssetLibrary.rename_asset( "/Game/StateTree/STT_Rotate", "/Game/StateTree/Tasks/STT_Rotate") print("Moved (references fixed up)" if ok else "Move failed")
Save Pattern
pythonimport unreal # Save a specific asset unreal.EditorAssetLibrary.save_asset("/Game/BP_Player") # Save a whole directory of dirty assets unreal.EditorAssetLibrary.save_directory("/Game/Blueprints")
Check References Before Delete
Asset Registry referencer queries return package-name strings, not AssetData.
pythonimport unreal ar = unreal.AssetRegistryHelpers.get_asset_registry() refs = ar.get_referencers( "/Game/SM_Weapon", unreal.AssetRegistryDependencyOptions(include_hard_package_references=True)) if not refs: unreal.EditorAssetLibrary.delete_asset("/Game/SM_Weapon") else: for ref in refs: # ref is a package name, e.g. "/Game/Blueprints/BP_Player" print(f"In use by: {ref}")
⚠️ Deleting levels:
delete_asseton a recently-loaded.umapcan returnTrue(asset gone from the registry) yet leave the package file on disk, sodelete_directoryon its folder then fails. After deleting level assets, verify theContent/...folder on disk and remove leftover.umapfiles manually.
⚠️
delete_assetfalse success is not limited to levels (issue #557). Blueprints with live references have also returnedTruewhile their files stayed on disk — and later touching such half-deleted assets ("files gone, editor memory ghosts") has crashed the editor. After ANY scripted delete, verify with a filesystem check (os.path.existson the.uasset) ordoes_asset_exist, and never read properties (e.g. mesh bounds) off an asset you just deleted.
ℹ️
CaptureAssetImagecannot render Blueprints or Niagara systems ("Asset type does not support image capture"). To preview those, spawn them in the level on a clear spot, aim the viewport camera, and use thecapture_imageMCP tool; delete the preview actors after.
Import / Export Textures (VibeUE)
pythonimport unreal # Import (disk → Content Browser). Returns (asset_path, error); asset_path is "" on failure. # Pass a destination FOLDER + optional asset name. path, err = unreal.AssetDiscoveryService.import_asset( "C:/Textures/logo.png", "/Game/Textures", "T_Logo") print(path or err) # import_texture(src, dest_asset_path) takes a full asset path and uses the same importer: unreal.AssetDiscoveryService.import_texture("C:/Textures/logo.png", "/Game/Textures/T_Logo") # Export (project → file system) unreal.AssetDiscoveryService.export_texture("/Game/Textures/T_Logo", "C:/Exports/logo.png")
Editor State & Content Browser (VibeUE)
pythonimport unreal # Is a specific asset open in an editor? if unreal.AssetDiscoveryService.is_asset_open("/Game/BP_Player"): print("BP_Player is open") # Primary Content Browser selection — NO out-arg, returns AssetData or None asset = unreal.AssetDiscoveryService.get_primary_content_browser_selection() if asset: print(f"Selected: {asset.asset_name} at {asset.package_path}") # Open it via the AssetEditorSubsystem (engine), loading the object first: obj = unreal.EditorAssetLibrary.load_asset(str(asset.package_name)) unreal.get_editor_subsystem(unreal.AssetEditorSubsystem).open_editor_for_assets([obj]) else: print("Nothing selected")
For the full multi-selection set, open-editor lists, and search, use the engine
AssetTools/AssetEditorSubsystemtoolsets viacall_tool.
Inspect the Selection's Class
asset.asset_class_path is a TopLevelAssetPath struct, not a string — compare its asset_name:
pythonimport unreal asset = unreal.AssetDiscoveryService.get_primary_content_browser_selection() if asset and str(asset.asset_class_path.asset_name) == "Blueprint": print(f"{asset.asset_name} is a Blueprint")
Create Non-Standard Asset Types (Factory Pattern)
Assets not covered by a VibeUE service (e.g., LandscapeGrassType) require AssetToolsHelpers + a
factory. (create_asset imports nothing, so the import caveats above do not apply to it.)
pythonimport unreal asset_tools = unreal.AssetToolsHelpers.get_asset_tools() factory = unreal.LandscapeGrassTypeFactory() lgt = asset_tools.create_asset("LGT_MyGrass", "/Game/Landscape", unreal.LandscapeGrassType, factory) # Set properties via set_editor_property, then save unreal.EditorAssetLibrary.save_asset("/Game/Landscape/LGT_MyGrass")
Tip: Use
discover_python_module("unreal", name_filter="Factory")to find available factories.
Common Asset Class Names for Filtering
Blueprint,WidgetBlueprintTexture2D,Material,MaterialInstanceConstantStaticMesh,SkeletalMeshDataTable,PrimaryDataAssetLandscapeGrassType,LandscapeLayerInfoObject
Class names must match UE class names exactly (e.g., LandscapeGrassType, not GrassType). When a
typed Asset Registry query unexpectedly returns 0, retry without the class filter and inspect each
result's asset_class_path.asset_name to learn the real class name.
Sample scripts (run via execute_python_code)
scripts/find_and_save.txt— find an asset (Asset Registry /EditorAssetLibrary) and duplicate + save it.
Additional gotchas
unreal.Rotator(...)positional order is(roll, pitch, yaw)— pass by keyword.unreal.AssetTools.create_asset(...)is a descriptor and throws; the working call isAssetToolsHelpers.get_asset_tools().create_asset(...).EditDefaultsOnlyproperties cannot be written on instances from Python; a server-onlyUPROPERTY()with no Blueprint flag reads as "protected"; and when a boolbIsDeadand aUFUNCTION IsDead()both map tois_dead, the property wins.TInstancedStructauthoring:inst = unreal.InstancedStruct(); inst.import_text('/Script/<Module>.<Struct>(Field=...)'); aTSoftClassPtrUPROPERTY wants the loaded class, not aSoftClassPath.LevelEditorPlaySettingsis not in the Python stub — reach it viaload_class(None, "/Script/UnrealEd.LevelEditorPlaySettings")and exact CamelCase property names.- Editor-world traces need a real world:
line_trace_single(actor.get_world(), ...)(LevelEditorSubsystem.get_world()is a null context);HitResult.component/actorare read-protected, so readexport_text(). - World subsystems have no Python accessor — reach one via
ObjectIterator(unreal.YourSubsystem)filtered onget_outer().get_path_name(). - Never
save_dirty_packages(True, True)— save by explicit path; a dirty package you did not touch is the user's work.


