Commit 98589b4
committed
Add reflection-driven actor instance control to the editor MCP
Before this change, manipulating actors in a level was tightly limited.
spawn_actor only accepted a hardcoded handful of types — StaticMeshActor,
PointLight, a few others — and there was no way to write arbitrary
properties to a placed instance. set_blueprint_class_defaults hits the
CDO, not the placed actor, so per-instance edits were unreachable.
PostProcessVolume settings, SkyAtmosphere parameters, and any
project-specific actor's fields all sat behind that wall.
This change opens it up properly. Five tools land here, all driven by
Unreal's reflection system so they work with engine classes, plugin
classes, and any USTRUCT or UPROPERTY a project defines without further
code on our side.
set_actor_property writes to the live placed actor or one of its
components. It walks dotted paths like "Settings.BloomIntensity" or
"Settings.LensFlareTints[3]", handling structs, dynamic arrays, sets,
and C-style static arrays through Field[N] syntax. PostEditChangeProperty
fires on the right top-level FProperty so the renderer reacts. Modify()
registers with the transaction system so Ctrl+Z works. JSON objects, UE
text-format strings, asset paths for object refs, enum names — all
accepted and all routed through one universal dispatcher
(SetPropertyValueAtAddr) that replaced about 140 lines of duplicated
type handling. SetProperty, SetPropertyAtPath, and the struct-field
walker now share that single path.
spawn_actor_by_class takes any UClass path, Blueprint asset path, or
short class name, validates that it's a placeable AActor subclass, and
spawns through UEditorActorSubsystem::SpawnActorFromClass so undo works
out of the box.
get_actor_property_metadata is the type-discovery companion. It returns
the cpp_type, ue_type, category, display_name, tooltip, current value,
clamp_min/max, ui_min/max, valid_values for enums, inner/element/key/
value types for containers, and the inheritance chain. Callers — AI
agents in particular — can now know exactly what they're allowed to
write before they try.
To keep responses sane on heavy classes (a UPointLightComponent has
~200 inherited FProperties), paths landing on FObjectProperty no
longer auto-descend into the target's class. They return a single ref
entry plus a hint pointing at the existing component_name parameter.
When descent is genuinely wanted, descend_into_objects opts in.
Every metadata response carries a _summary header with total_available,
total_returned, truncated, next_cursor, own_count, inherited_count, the
full class_chain, and a UPROPERTY(Category) breakdown. A single
max_entries=0 call gives a complete map of what's available without
paying for any of the body. max_entries and cursor handle pagination
the same way find_actors already does.
find_actors searches the level by name pattern, outliner label, class
filter (resolves to a UClass for IsA, falls back to substring on the
class name and path), tag, exact_class, or any combination. Reports
total_scanned and total_matched independently of result truncation, so
you know whether to widen max_results.
delete_actor now matches both the outliner label and the internal
UObject name, calls Modify() on actor and level before destroy, and
prefers UEditorActorSubsystem::DestroyActor so deletions land in the
undo stack like any other editor action.
get_actor_properties picks up a flat dotted-path mode with the same
filter, depth, category, and pagination story as the metadata tool. A
query like "everything containing 'bloom' on this PostProcessVolume"
actually finds Settings.BloomIntensity, not just top-level FProperty
names. The original nested mode is still the default, unchanged.
A second non-obvious fix tucked into this change:
SetStructFieldsFromJson used to silently ignore unknown JSON keys.
A typo like {"R","Q","B","A"} on an FLinearColor returned success but
wrote nothing. Now those errors propagate as
"[field 'Q' not found on struct 'LinearColor']" and the whole write
fails — which is the right answer for a tool that other tools build on.
Engine APIs used throughout, all matching the patterns Epic uses
internally: UEditorActorSubsystem for spawn and destroy with transaction
support; Actor->Modify() and Level->Modify() before mutation; FProperty's
ContainerPtrToValuePtr with the index parameter for static C array
addressing; TFieldIterator with IncludeSuper/ExcludeSuper for
inheritance control; FProperty::GetMetaData and GetOwnerClass for
category and ownership tracking; UEnum::NumEnums()-1 paired with
HasMetaData("Hidden", i) to list valid enum entries the same way the
Details panel does.
Python tools (unrealmcp/tools/level.py) and the Go CLI
(cli/cmd/editor.go) both pick up matching wrappers with documented
parameters and worked examples.
Files touched:
Source/UnrealMCPBridge/Public/Commands/EpicUnrealMCPPropertyUtils.h
Source/UnrealMCPBridge/Private/Commands/EpicUnrealMCPPropertyUtils.cpp
Source/UnrealMCPBridge/Public/Commands/EpicUnrealMCPEditorCommands.h
Source/UnrealMCPBridge/Private/Commands/EpicUnrealMCPEditorCommands.cpp
Source/UnrealMCPBridge/Private/EpicUnrealMCPBridge.cpp
unrealmcp/tools/level.py
cli/cmd/editor.go1 parent 325f270 commit 98589b4
7 files changed
Lines changed: 1766 additions & 173 deletions
File tree
- Source/UnrealMCPBridge
- Private
- Commands
- Public/Commands
- cli/cmd
- unrealmcp/tools
0 commit comments