Skip to content

Commit d813073

Browse files
committed
Add search_class_paths and get_mass_config_traits MCP tools
Two new tools for working with Mass Entity Config trait properties. Files changed: - EpicUnrealMCPDataAssetCommands.h: Added HandleSearchClassPaths and HandleGetMassConfigTraits declarations - EpicUnrealMCPDataAssetCommands.cpp: Implemented search_class_paths (searches UClass hierarchy by name filter + parent class constraint, returns full /Script/Module.ClassName paths with optional editable properties) and get_mass_config_traits (reads all traits from a MassEntityConfigAsset with expanded property values) - EpicUnrealMCPBridge.cpp: Added both commands to dispatcher routing - data_assets.py: Added Python tool definitions with docstrings and usage examples. Updated set_data_asset_property docstring with Mass Entity Config trait editing instructions and _ClassName format. search_class_paths supports parent_class filter for traits, processors, tags, actors — any UClass hierarchy. get_mass_config_traits expands instanced trait objects showing class_path + current property values.
1 parent d182dca commit d813073

4 files changed

Lines changed: 346 additions & 1 deletion

File tree

Source/UnrealMCPBridge/Private/Commands/EpicUnrealMCPDataAssetCommands.cpp

Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,14 @@ TSharedPtr<FJsonObject> FEpicUnrealMCPDataAssetCommands::HandleCommand(
5050
{
5151
return HandleGetPropertyValidTypes(Params);
5252
}
53+
if (CommandType == TEXT("search_class_paths"))
54+
{
55+
return HandleSearchClassPaths(Params);
56+
}
57+
if (CommandType == TEXT("get_mass_config_traits"))
58+
{
59+
return HandleGetMassConfigTraits(Params);
60+
}
5361

5462
return FEpicUnrealMCPCommonUtils::CreateErrorResponse(
5563
FString::Printf(TEXT("Unknown data asset command: %s"), *CommandType));
@@ -776,3 +784,259 @@ TSharedPtr<FJsonObject> FEpicUnrealMCPDataAssetCommands::HandleListDataAssets(
776784
Result->SetArrayField(TEXT("assets"), AssetArray);
777785
return Result;
778786
}
787+
788+
// ============================================================================
789+
// search_class_paths — Search UClass by name filter + parent class constraint
790+
// ============================================================================
791+
792+
TSharedPtr<FJsonObject> FEpicUnrealMCPDataAssetCommands::HandleSearchClassPaths(
793+
const TSharedPtr<FJsonObject>& Params)
794+
{
795+
FString Filter;
796+
Params->TryGetStringField(TEXT("filter"), Filter);
797+
798+
FString ParentClassName;
799+
Params->TryGetStringField(TEXT("parent_class"), ParentClassName);
800+
801+
int32 MaxResults = 50;
802+
if (Params->HasField(TEXT("max_results")))
803+
{
804+
MaxResults = static_cast<int32>(Params->GetNumberField(TEXT("max_results")));
805+
}
806+
807+
bool bIncludeProperties = false;
808+
if (Params->HasField(TEXT("include_properties")))
809+
{
810+
bIncludeProperties = Params->GetBoolField(TEXT("include_properties"));
811+
}
812+
813+
// Resolve parent class constraint
814+
UClass* ParentClass = UObject::StaticClass(); // Default: search everything
815+
if (!ParentClassName.IsEmpty())
816+
{
817+
// Try direct path first (e.g., "/Script/MassEntity.MassEntityTraitBase")
818+
UClass* Found = FindObject<UClass>(nullptr, *ParentClassName);
819+
820+
// Try common patterns
821+
if (!Found)
822+
{
823+
// Search all classes for name match
824+
for (TObjectIterator<UClass> It; It; ++It)
825+
{
826+
UClass* C = *It;
827+
if (!C)
828+
{
829+
continue;
830+
}
831+
832+
const FString Name = C->GetName();
833+
if (Name == ParentClassName ||
834+
Name == (TEXT("U") + ParentClassName) ||
835+
Name == (TEXT("F") + ParentClassName) ||
836+
Name == ParentClassName.RightChop(1))
837+
{
838+
Found = C;
839+
break;
840+
}
841+
}
842+
}
843+
844+
if (!Found)
845+
{
846+
return FEpicUnrealMCPCommonUtils::CreateErrorResponse(
847+
FString::Printf(TEXT("Parent class not found: %s"), *ParentClassName));
848+
}
849+
ParentClass = Found;
850+
}
851+
852+
// Get all derived classes
853+
TArray<UClass*> Derived;
854+
GetDerivedClasses(ParentClass, Derived, true);
855+
856+
// Also include the parent class itself if it matches
857+
if (!ParentClass->HasAnyClassFlags(CLASS_Abstract))
858+
{
859+
Derived.Insert(ParentClass, 0);
860+
}
861+
862+
// Filter by name keyword
863+
TArray<TSharedPtr<FJsonValue>> ResultArray;
864+
const FString FilterLower = Filter.ToLower();
865+
866+
for (UClass* C : Derived)
867+
{
868+
if (!C || C->HasAnyClassFlags(CLASS_Abstract | CLASS_Deprecated | CLASS_NewerVersionExists))
869+
{
870+
continue;
871+
}
872+
873+
const FString ClassName = C->GetName();
874+
875+
// Apply name filter
876+
if (!Filter.IsEmpty() && !ClassName.ToLower().Contains(FilterLower))
877+
{
878+
continue;
879+
}
880+
881+
TSharedPtr<FJsonObject> Entry = MakeShared<FJsonObject>();
882+
Entry->SetStringField(TEXT("name"), ClassName);
883+
Entry->SetStringField(TEXT("class_path"), C->GetPathName());
884+
885+
// Include editable properties if requested
886+
if (bIncludeProperties)
887+
{
888+
TArray<TSharedPtr<FJsonValue>> PropsArray;
889+
for (TFieldIterator<FProperty> PropIt(C, EFieldIteratorFlags::ExcludeSuper); PropIt; ++PropIt)
890+
{
891+
FProperty* Prop = *PropIt;
892+
if (!Prop || !Prop->HasAnyPropertyFlags(CPF_Edit))
893+
{
894+
continue;
895+
}
896+
897+
TSharedPtr<FJsonObject> PropObj = MakeShared<FJsonObject>();
898+
PropObj->SetStringField(TEXT("name"), Prop->GetName());
899+
PropObj->SetStringField(TEXT("type"), Prop->GetCPPType());
900+
901+
// Get default value
902+
const UObject* CDO = C->GetDefaultObject();
903+
if (CDO)
904+
{
905+
const void* ValuePtr = Prop->ContainerPtrToValuePtr<void>(CDO);
906+
FString DefaultStr;
907+
Prop->ExportTextItem_Direct(DefaultStr, ValuePtr, nullptr, nullptr, PPF_None);
908+
if (!DefaultStr.IsEmpty())
909+
{
910+
PropObj->SetStringField(TEXT("default"), DefaultStr);
911+
}
912+
}
913+
914+
PropsArray.Add(MakeShared<FJsonValueObject>(PropObj));
915+
}
916+
Entry->SetArrayField(TEXT("properties"), PropsArray);
917+
}
918+
919+
ResultArray.Add(MakeShared<FJsonValueObject>(Entry));
920+
921+
if (ResultArray.Num() >= MaxResults)
922+
{
923+
break;
924+
}
925+
}
926+
927+
TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
928+
Result->SetNumberField(TEXT("count"), ResultArray.Num());
929+
Result->SetStringField(TEXT("parent_class"), ParentClass->GetName());
930+
if (!Filter.IsEmpty())
931+
{
932+
Result->SetStringField(TEXT("filter"), Filter);
933+
}
934+
Result->SetArrayField(TEXT("classes"), ResultArray);
935+
return Result;
936+
}
937+
938+
// ============================================================================
939+
// get_mass_config_traits — Read all traits from a Mass Entity Config with
940+
// their properties expanded (not just object paths)
941+
// ============================================================================
942+
943+
TSharedPtr<FJsonObject> FEpicUnrealMCPDataAssetCommands::HandleGetMassConfigTraits(
944+
const TSharedPtr<FJsonObject>& Params)
945+
{
946+
FString AssetPath;
947+
if (!Params->TryGetStringField(TEXT("asset_path"), AssetPath))
948+
{
949+
return FEpicUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Missing 'asset_path'"));
950+
}
951+
952+
UObject* Asset = StaticLoadObject(UObject::StaticClass(), nullptr, *AssetPath);
953+
if (!Asset)
954+
{
955+
return FEpicUnrealMCPCommonUtils::CreateErrorResponse(
956+
FString::Printf(TEXT("Asset not found: %s"), *AssetPath));
957+
}
958+
959+
// Find the Config struct property
960+
FStructProperty* ConfigProp = nullptr;
961+
for (TFieldIterator<FStructProperty> It(Asset->GetClass()); It; ++It)
962+
{
963+
if (It->GetName() == TEXT("Config"))
964+
{
965+
ConfigProp = *It;
966+
break;
967+
}
968+
}
969+
970+
if (!ConfigProp)
971+
{
972+
return FEpicUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Asset has no 'Config' struct property (not a MassEntityConfigAsset?)"));
973+
}
974+
975+
const void* ConfigPtr = ConfigProp->ContainerPtrToValuePtr<void>(Asset);
976+
977+
// Find the Traits array inside the Config struct
978+
FArrayProperty* TraitsArrayProp = nullptr;
979+
for (TFieldIterator<FArrayProperty> It(ConfigProp->Struct); It; ++It)
980+
{
981+
if (It->GetName() == TEXT("Traits"))
982+
{
983+
TraitsArrayProp = *It;
984+
break;
985+
}
986+
}
987+
988+
if (!TraitsArrayProp)
989+
{
990+
return FEpicUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Config struct has no 'Traits' array"));
991+
}
992+
993+
FObjectProperty* InnerObjProp = CastField<FObjectProperty>(TraitsArrayProp->Inner);
994+
if (!InnerObjProp)
995+
{
996+
return FEpicUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Traits array inner type is not an object"));
997+
}
998+
999+
// Read the array
1000+
FScriptArrayHelper ArrayHelper(TraitsArrayProp, TraitsArrayProp->ContainerPtrToValuePtr<void>(ConfigPtr));
1001+
1002+
TArray<TSharedPtr<FJsonValue>> TraitsArray;
1003+
for (int32 i = 0; i < ArrayHelper.Num(); ++i)
1004+
{
1005+
UObject* TraitObj = InnerObjProp->GetObjectPropertyValue(ArrayHelper.GetRawPtr(i));
1006+
if (!TraitObj)
1007+
{
1008+
continue;
1009+
}
1010+
1011+
TSharedPtr<FJsonObject> TraitJson = MakeShared<FJsonObject>();
1012+
TraitJson->SetNumberField(TEXT("index"), i);
1013+
TraitJson->SetStringField(TEXT("name"), TraitObj->GetName());
1014+
TraitJson->SetStringField(TEXT("class"), TraitObj->GetClass()->GetName());
1015+
TraitJson->SetStringField(TEXT("class_path"), TraitObj->GetClass()->GetPathName());
1016+
1017+
// Serialize all editable properties
1018+
TSharedPtr<FJsonObject> PropsJson = MakeShared<FJsonObject>();
1019+
for (TFieldIterator<FProperty> PropIt(TraitObj->GetClass(), EFieldIteratorFlags::ExcludeSuper); PropIt; ++PropIt)
1020+
{
1021+
FProperty* Prop = *PropIt;
1022+
if (!Prop || !Prop->HasAnyPropertyFlags(CPF_Edit))
1023+
{
1024+
continue;
1025+
}
1026+
1027+
const void* ValuePtr = Prop->ContainerPtrToValuePtr<void>(TraitObj);
1028+
FString ValueStr;
1029+
Prop->ExportTextItem_Direct(ValueStr, ValuePtr, nullptr, nullptr, PPF_None);
1030+
PropsJson->SetStringField(Prop->GetName(), ValueStr);
1031+
}
1032+
TraitJson->SetObjectField(TEXT("properties"), PropsJson);
1033+
1034+
TraitsArray.Add(MakeShared<FJsonValueObject>(TraitJson));
1035+
}
1036+
1037+
TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
1038+
Result->SetStringField(TEXT("asset_path"), AssetPath);
1039+
Result->SetNumberField(TEXT("trait_count"), TraitsArray.Num());
1040+
Result->SetArrayField(TEXT("traits"), TraitsArray);
1041+
return Result;
1042+
}

Source/UnrealMCPBridge/Private/EpicUnrealMCPBridge.cpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -655,7 +655,9 @@ FString UEpicUnrealMCPBridge::ExecuteCommand(
655655
CommandType == TEXT("set_data_asset_property") ||
656656
CommandType == TEXT("set_data_asset_properties") ||
657657
CommandType == TEXT("list_data_assets") ||
658-
CommandType == TEXT("get_property_valid_types"))
658+
CommandType == TEXT("get_property_valid_types") ||
659+
CommandType == TEXT("search_class_paths") ||
660+
CommandType == TEXT("get_mass_config_traits"))
659661
{
660662
ResultJson = DataAssetCommands->HandleCommand(CommandType, Params);
661663
}

Source/UnrealMCPBridge/Public/Commands/EpicUnrealMCPDataAssetCommands.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ class UNREALMCPBRIDGE_API FEpicUnrealMCPDataAssetCommands
2525
TSharedPtr<FJsonObject> HandleSetDataAssetProperties(const TSharedPtr<FJsonObject>& Params);
2626
TSharedPtr<FJsonObject> HandleListDataAssets(const TSharedPtr<FJsonObject>& Params);
2727
TSharedPtr<FJsonObject> HandleGetPropertyValidTypes(const TSharedPtr<FJsonObject>& Params);
28+
TSharedPtr<FJsonObject> HandleSearchClassPaths(const TSharedPtr<FJsonObject>& Params);
29+
TSharedPtr<FJsonObject> HandleGetMassConfigTraits(const TSharedPtr<FJsonObject>& Params);
2830

2931
// Limited to UDataAsset subclasses. For any class use PU::ResolveAnyClass.
3032
static UClass* ResolveDataAssetClass(const FString& ClassName);

unrealmcp/tools/data_assets.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,25 @@ def set_data_asset_property(asset_path: str, property_name: str, property_value)
7070
arrays, object refs ("/Game/..."), soft refs, and None (clears reference).
7171
7272
property_name is case-sensitive (exact FProperty name).
73+
74+
**Mass Entity Config — Editing Trait Properties:**
75+
To edit trait properties on a MassEntityConfigAsset (e.g., BeltSpeed, SlotCount),
76+
set property_name="Config" with a JSON value containing the Traits array.
77+
Each trait needs a "_ClassName" with FULL path "/Script/Module.ClassName".
78+
79+
Example — set BeltSpeed on a conveyor config:
80+
set_data_asset_property(
81+
asset_path="/Game/.../DA_ConveyorMassConfig_Mk2",
82+
property_name="Config",
83+
property_value={
84+
"Traits": [{"_ClassName": "/Script/Jiggify.MassConveyorBeltTrait", "BeltSpeed": 200}]
85+
}
86+
)
87+
88+
IMPORTANT: _ClassName MUST be full path ("/Script/Jiggify.MassConveyorBeltTrait"),
89+
NOT short name ("MassConveyorBeltTrait"). Short names create wrong base class.
90+
For configs with multiple traits, include ALL traits in the array (entire array is replaced).
91+
Only set the properties you want to change — others use C++ defaults.
7392
"""
7493
return _call("set_data_asset_property", {
7594
"asset_path": asset_path,
@@ -125,6 +144,64 @@ def get_property_valid_types(
125144
})
126145

127146

147+
@mcp.tool()
148+
def search_class_paths(
149+
filter: str = "",
150+
parent_class: str = "",
151+
max_results: int = 50,
152+
include_properties: bool = False,
153+
) -> str:
154+
"""Search for UClass types by name filter and parent class constraint.
155+
Returns full /Script/Module.ClassName paths needed for _ClassName in trait editing.
156+
157+
Use this to find the correct class_path before calling set_data_asset_property
158+
with Mass Entity Config traits.
159+
160+
Args:
161+
filter: Case-insensitive substring match on class name (e.g. "Conveyor", "Miner")
162+
parent_class: Only return subclasses of this class (e.g. "MassEntityTraitBase"
163+
for traits, "MassFragment" for fragments, "MassTag" for tags,
164+
"MassProcessor" for processors). Empty = search all classes.
165+
max_results: Maximum results to return (default 50)
166+
include_properties: If true, also return each class's editable UPROPERTY names,
167+
types, and default values. Use sparingly — adds tokens per result.
168+
169+
Examples:
170+
search_class_paths(filter="Conveyor", parent_class="MassEntityTraitBase")
171+
→ finds MassConveyorBeltTrait with class_path "/Script/Jiggify.MassConveyorBeltTrait"
172+
173+
search_class_paths(filter="Pipeline", parent_class="MassFragment")
174+
→ finds all pipeline-related fragments
175+
176+
search_class_paths(parent_class="MassEntityTraitBase")
177+
→ lists ALL available traits (no filter)
178+
"""
179+
return _call("search_class_paths", {
180+
"filter": filter,
181+
"parent_class": parent_class,
182+
"max_results": max_results,
183+
"include_properties": include_properties,
184+
})
185+
186+
187+
@mcp.tool()
188+
def get_mass_config_traits(asset_path: str) -> str:
189+
"""Read all traits from a Mass Entity Config asset with their properties expanded.
190+
191+
Unlike get_data_asset_properties (which shows trait object paths), this tool
192+
expands each trait's editable properties with current values.
193+
194+
Args:
195+
asset_path: Path to MassEntityConfigAsset (e.g. "/Game/.../DA_ConveyorMassConfig_Mk1")
196+
197+
Returns per trait: index, name, class, class_path (for _ClassName), and all
198+
editable properties with current values.
199+
"""
200+
return _call("get_mass_config_traits", {
201+
"asset_path": asset_path,
202+
})
203+
204+
128205
@mcp.tool()
129206
def list_data_assets(
130207
path: str = "/Game",

0 commit comments

Comments
 (0)