diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPAssetFinder.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPAssetFinder.cpp index 3d1d6981..651823bf 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPAssetFinder.cpp +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPAssetFinder.cpp @@ -63,21 +63,26 @@ UMCPAssetFinder* UMCPAssetFinder::GetUpdatedAssets() Self->AllMaterialAssets.Empty(); Self->AllMaterialInstanceAssets.Empty(); Self->AllMaterialFunctionAssets.Empty(); + Self->AllStructAssets.Empty(); + Self->AllEnumAssets.Empty(); AR.GetAssetsByClass(UBlueprint::StaticClass()->GetClassPathName(), Self->AllBlueprintAssets, true); AR.GetAssetsByClass(UWorld::StaticClass()->GetClassPathName(), Self->AllMapAssets, false); AR.GetAssetsByClass(UMaterial::StaticClass()->GetClassPathName(), Self->AllMaterialAssets, false); AR.GetAssetsByClass(UMaterialInstanceConstant::StaticClass()->GetClassPathName(), Self->AllMaterialInstanceAssets, false); AR.GetAssetsByClass(UMaterialFunction::StaticClass()->GetClassPathName(), Self->AllMaterialFunctionAssets, false); + AR.GetAssetsByClass(UUserDefinedStruct::StaticClass()->GetClassPathName(), Self->AllStructAssets, false); + AR.GetAssetsByClass(UUserDefinedEnum::StaticClass()->GetClassPathName(), Self->AllEnumAssets, false); Self->AllBlueprintAndMapAssets = Self->AllBlueprintAssets; Self->AllBlueprintAndMapAssets.Append(Self->AllMapAssets); Self->bDirty = false; - UE_LOG(LogTemp, Display, TEXT("MCPAssetFinder: Refreshed — BP %d, Map %d, Mat %d, MI %d, MF %d"), + UE_LOG(LogTemp, Display, TEXT("MCPAssetFinder: Refreshed — BP %d, Map %d, Mat %d, MI %d, MF %d, Struct %d, Enum %d"), Self->AllBlueprintAssets.Num(), Self->AllMapAssets.Num(), Self->AllMaterialAssets.Num(), - Self->AllMaterialInstanceAssets.Num(), Self->AllMaterialFunctionAssets.Num()); + Self->AllMaterialInstanceAssets.Num(), Self->AllMaterialFunctionAssets.Num(), + Self->AllStructAssets.Num(), Self->AllEnumAssets.Num()); return Self; } @@ -122,6 +127,18 @@ const TArray& UMCPAssetFinder::GetMaterialFunctionAssets() return Self ? Self->AllMaterialFunctionAssets : EmptyAssetArray; } +const TArray& UMCPAssetFinder::GetStructAssets() +{ + UMCPAssetFinder* Self = GetUpdatedAssets(); + return Self ? Self->AllStructAssets : EmptyAssetArray; +} + +const TArray& UMCPAssetFinder::GetEnumAssets() +{ + UMCPAssetFinder* Self = GetUpdatedAssets(); + return Self ? Self->AllEnumAssets : EmptyAssetArray; +} + // ============================================================ // Find helpers (search cached lists by name or path) // ============================================================ @@ -204,6 +221,16 @@ FAssetData* UMCPAssetFinder::FindMaterialFunctionAsset(const FString& NameOrPath return FindInList(GetMaterialFunctionAssets(), NameOrPath, OutError); } +FAssetData* UMCPAssetFinder::FindStructAsset(const FString& NameOrPath, FString* OutError) +{ + return FindInList(GetStructAssets(), NameOrPath, OutError); +} + +FAssetData* UMCPAssetFinder::FindEnumAsset(const FString& NameOrPath, FString* OutError) +{ + return FindInList(GetEnumAssets(), NameOrPath, OutError); +} + FAssetData* UMCPAssetFinder::FindAnyAsset(const FString& NameOrPath, FString* OutError) { FAssetData* Asset = FindBlueprintAsset(NameOrPath, OutError); @@ -289,3 +316,29 @@ UMaterialFunction* UMCPAssetFinder::LoadMaterialFunctionByName(const FString& Na OutError = FString::Printf(TEXT("Material Function '%s' not found. Use list_material_functions to see available assets."), *NameOrPath); return nullptr; } + +UUserDefinedStruct* UMCPAssetFinder::LoadStructByName(const FString& NameOrPath, FString& OutError) +{ + FAssetData* Asset = FindStructAsset(NameOrPath, &OutError); + if (Asset) + { + UUserDefinedStruct* Struct = Cast(Asset->GetAsset()); + if (Struct) return Struct; + } + if (OutError.IsEmpty()) + OutError = FString::Printf(TEXT("UserDefinedStruct '%s' not found."), *NameOrPath); + return nullptr; +} + +UUserDefinedEnum* UMCPAssetFinder::LoadEnumByName(const FString& NameOrPath, FString& OutError) +{ + FAssetData* Asset = FindEnumAsset(NameOrPath, &OutError); + if (Asset) + { + UUserDefinedEnum* Enum = Cast(Asset->GetAsset()); + if (Enum) return Enum; + } + if (OutError.IsEmpty()) + OutError = FString::Printf(TEXT("UserDefinedEnum '%s' not found."), *NameOrPath); + return nullptr; +} diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers.cpp index e4f87159..83324dbc 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers.cpp +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers.cpp @@ -4,3 +4,4 @@ #include "MCPHandlers_PinMutation.h" #include "MCPHandlers_AssetMutation.h" #include "MCPHandlers_Validation.h" +#include "MCPHandlers_UserTypes.h" diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_UserTypes.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_UserTypes.cpp deleted file mode 100644 index 96cc6d50..00000000 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_UserTypes.cpp +++ /dev/null @@ -1,386 +0,0 @@ -#include "MCPServer.h" -#include "MCPUtils.h" -#include "Engine/UserDefinedStruct.h" -#include "Engine/UserDefinedEnum.h" -#include "Kismet2/BlueprintEditorUtils.h" -#include "UserDefinedStructure/UserDefinedStructEditorData.h" -#include "Kismet2/EnumEditorUtils.h" -#include "Serialization/JsonReader.h" -#include "Serialization/JsonWriter.h" -#include "Serialization/JsonSerializer.h" -#include "UObject/SavePackage.h" -#include "Misc/PackageName.h" -#include "AssetRegistry/AssetRegistryModule.h" -#include "AssetRegistry/IAssetRegistry.h" -#include "AssetToolsModule.h" -#include "IAssetTools.h" -#include "Factories/StructureFactory.h" -#include "Factories/EnumFactory.h" - -// ============================================================ -// HandleCreateStruct — create a new UserDefinedStruct asset -// ============================================================ - -void FBlueprintMCPServer::HandleCreateStruct(const FJsonObject* Json, FJsonObject* Result) -{ - FString AssetPath = Json->GetStringField(TEXT("assetPath")); - if (AssetPath.IsEmpty()) - { - return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: assetPath (e.g. '/Game/DataTypes/S_MyStruct')")); - } - - // Split path into package path and asset name - FString PackagePath, AssetName; - int32 LastSlash; - if (AssetPath.FindLastChar('/', LastSlash)) - { - PackagePath = AssetPath.Left(LastSlash); - AssetName = AssetPath.Mid(LastSlash + 1); - } - else - { - return MCPUtils::MakeErrorJson(Result, TEXT("assetPath must be a full path (e.g. '/Game/DataTypes/S_MyStruct')")); - } - - if (AssetName.IsEmpty()) - { - return MCPUtils::MakeErrorJson(Result, TEXT("Invalid asset name in assetPath")); - } - - // Check if asset already exists - FAssetRegistryModule& ARM = FModuleManager::LoadModuleChecked("AssetRegistry"); - FAssetData ExistingAsset = ARM.Get().GetAssetByObjectPath(FSoftObjectPath(AssetPath + TEXT(".") + AssetName)); - if (ExistingAsset.IsValid()) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Asset already exists at '%s'"), *AssetPath)); - } - - // Create the struct using the AssetTools factory - FAssetToolsModule& AssetToolsModule = FModuleManager::LoadModuleChecked("AssetTools"); - IAssetTools& AssetTools = AssetToolsModule.Get(); - - UStructureFactory* Factory = NewObject(); - UObject* NewAsset = AssetTools.CreateAsset(AssetName, PackagePath, UUserDefinedStruct::StaticClass(), Factory); - - if (!NewAsset) - { - return MCPUtils::MakeErrorJson(Result, TEXT("Failed to create UserDefinedStruct asset")); - } - - UUserDefinedStruct* NewStruct = Cast(NewAsset); - if (!NewStruct) - { - return MCPUtils::MakeErrorJson(Result, TEXT("Created asset is not a UserDefinedStruct")); - } - - // Add properties if specified - const TArray>* PropsArray = nullptr; - int32 PropsAdded = 0; - if (Json->TryGetArrayField(TEXT("properties"), PropsArray) && PropsArray) - { - for (const TSharedPtr& PropVal : *PropsArray) - { - TSharedPtr PropObj = PropVal->AsObject(); - if (!PropObj) continue; - - FString PropName = PropObj->GetStringField(TEXT("name")); - FString PropType = PropObj->GetStringField(TEXT("type")); - if (PropName.IsEmpty() || PropType.IsEmpty()) continue; - - FEdGraphPinType PinType; - FString TypeError; - if (!MCPUtils::ResolveTypeFromString(PropType, PinType, TypeError)) - { - UE_LOG(LogTemp, Warning, TEXT("BlueprintMCP: Could not resolve type '%s' for property '%s': %s"), *PropType, *PropName, *TypeError); - continue; - } - - // Snapshot existing GUIDs so we can find the newly added one - TSet ExistingGuids; - for (const FStructVariableDescription& Var : FStructureEditorUtils::GetVarDesc(NewStruct)) - { - ExistingGuids.Add(Var.VarGuid); - } - - bool bAdded = FStructureEditorUtils::AddVariable(NewStruct, PinType); - if (bAdded) - { - // Find the new variable by diffing GUID sets - FGuid NewPropGuid; - for (const FStructVariableDescription& Var : FStructureEditorUtils::GetVarDesc(NewStruct)) - { - if (!ExistingGuids.Contains(Var.VarGuid)) - { - NewPropGuid = Var.VarGuid; - break; - } - } - if (NewPropGuid.IsValid()) - { - FStructureEditorUtils::RenameVariable(NewStruct, NewPropGuid, PropName); - } - PropsAdded++; - } - } - } - - // Save - UPackage* Package = NewStruct->GetPackage(); - FString PackageFilename = FPackageName::LongPackageNameToFilename(Package->GetName(), FPackageName::GetAssetPackageExtension()); - FSavePackageArgs SaveArgs; - SaveArgs.TopLevelFlags = RF_Standalone; - bool bSaved = UPackage::SavePackage(Package, NewStruct, *PackageFilename, SaveArgs); - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Created UserDefinedStruct '%s' with %d properties, save %s"), - *AssetPath, PropsAdded, bSaved ? TEXT("succeeded") : TEXT("failed")); - - Result->SetBoolField(TEXT("success"), true); - Result->SetStringField(TEXT("assetPath"), AssetPath); - Result->SetStringField(TEXT("assetName"), AssetName); - Result->SetNumberField(TEXT("propertiesAdded"), PropsAdded); - Result->SetBoolField(TEXT("saved"), bSaved); -} - -// ============================================================ -// HandleCreateEnum — create a new UserDefinedEnum asset -// ============================================================ - -void FBlueprintMCPServer::HandleCreateEnum(const FJsonObject* Json, FJsonObject* Result) -{ - FString AssetPath = Json->GetStringField(TEXT("assetPath")); - if (AssetPath.IsEmpty()) - { - return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: assetPath (e.g. '/Game/DataTypes/E_MyEnum')")); - } - - // Split path - FString PackagePath, AssetName; - int32 LastSlash; - if (AssetPath.FindLastChar('/', LastSlash)) - { - PackagePath = AssetPath.Left(LastSlash); - AssetName = AssetPath.Mid(LastSlash + 1); - } - else - { - return MCPUtils::MakeErrorJson(Result, TEXT("assetPath must be a full path (e.g. '/Game/DataTypes/E_MyEnum')")); - } - - if (AssetName.IsEmpty()) - { - return MCPUtils::MakeErrorJson(Result, TEXT("Invalid asset name in assetPath")); - } - - // Get values - const TArray>* ValuesArray = nullptr; - if (!Json->TryGetArrayField(TEXT("values"), ValuesArray) || !ValuesArray || ValuesArray->Num() == 0) - { - return MCPUtils::MakeErrorJson(Result, TEXT("Missing or empty required field: values (array of strings)")); - } - - TArray EnumValues; - for (const TSharedPtr& Val : *ValuesArray) - { - FString Str = Val->AsString(); - if (!Str.IsEmpty()) EnumValues.Add(Str); - } - if (EnumValues.Num() == 0) - { - return MCPUtils::MakeErrorJson(Result, TEXT("No valid enum values provided")); - } - - // Create the enum using AssetTools - FAssetToolsModule& AssetToolsModule = FModuleManager::LoadModuleChecked("AssetTools"); - IAssetTools& AssetTools = AssetToolsModule.Get(); - - UEnumFactory* Factory = NewObject(); - UObject* NewAsset = AssetTools.CreateAsset(AssetName, PackagePath, UUserDefinedEnum::StaticClass(), Factory); - - if (!NewAsset) - { - return MCPUtils::MakeErrorJson(Result, TEXT("Failed to create UserDefinedEnum asset")); - } - - UUserDefinedEnum* NewEnum = Cast(NewAsset); - if (!NewEnum) - { - return MCPUtils::MakeErrorJson(Result, TEXT("Created asset is not a UserDefinedEnum")); - } - - // Add enum values — UUserDefinedEnum starts with a MAX value. - // We need to add entries before MAX. - for (int32 i = 0; i < EnumValues.Num(); ++i) - { - // AddNewEnumeratorForUserDefinedEnum adds before the _MAX entry (returns void) - FEnumEditorUtils::AddNewEnumeratorForUserDefinedEnum(NewEnum); - // The new entry is at index (NumEnums - 2) because _MAX is last - int32 NewIndex = NewEnum->NumEnums() - 2; - FEnumEditorUtils::SetEnumeratorDisplayName(NewEnum, NewIndex, FText::FromString(EnumValues[i])); - } - - // Save - UPackage* Package = NewEnum->GetPackage(); - FString PackageFilename = FPackageName::LongPackageNameToFilename(Package->GetName(), FPackageName::GetAssetPackageExtension()); - FSavePackageArgs SaveArgs; - SaveArgs.TopLevelFlags = RF_Standalone; - bool bSaved = UPackage::SavePackage(Package, NewEnum, *PackageFilename, SaveArgs); - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Created UserDefinedEnum '%s' with %d values, save %s"), - *AssetPath, EnumValues.Num(), bSaved ? TEXT("succeeded") : TEXT("failed")); - - Result->SetBoolField(TEXT("success"), true); - Result->SetStringField(TEXT("assetPath"), AssetPath); - Result->SetStringField(TEXT("assetName"), AssetName); - Result->SetNumberField(TEXT("valueCount"), EnumValues.Num()); - Result->SetBoolField(TEXT("saved"), bSaved); -} - -// ============================================================ -// HandleAddStructProperty — add a property to UserDefinedStruct -// ============================================================ - -void FBlueprintMCPServer::HandleAddStructProperty(const FJsonObject* Json, FJsonObject* Result) -{ - FString AssetPath = Json->GetStringField(TEXT("assetPath")); - FString PropName = Json->GetStringField(TEXT("name")); - FString PropType = Json->GetStringField(TEXT("type")); - - if (AssetPath.IsEmpty() || PropName.IsEmpty() || PropType.IsEmpty()) - { - return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: assetPath, name, type")); - } - - // Find the struct - UUserDefinedStruct* Struct = LoadObject(nullptr, *AssetPath); - if (!Struct) - { - // Try with asset name appended - FString FullPath = AssetPath + TEXT(".") + FPackageName::GetShortName(AssetPath); - Struct = LoadObject(nullptr, *FullPath); - } - if (!Struct) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("UserDefinedStruct not found at '%s'"), *AssetPath)); - } - - // Resolve type - FEdGraphPinType PinType; - FString TypeError; - if (!MCPUtils::ResolveTypeFromString(PropType, PinType, TypeError)) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Cannot resolve type '%s': %s"), *PropType, *TypeError)); - } - - // Snapshot existing GUIDs so we can find the newly added one - TSet ExistingGuids; - for (const FStructVariableDescription& Var : FStructureEditorUtils::GetVarDesc(Struct)) - { - ExistingGuids.Add(Var.VarGuid); - } - - bool bAdded = FStructureEditorUtils::AddVariable(Struct, PinType); - if (!bAdded) - { - return MCPUtils::MakeErrorJson(Result, TEXT("Failed to add property to struct")); - } - - // Find the new variable by diffing GUID sets and rename it - for (const FStructVariableDescription& Var : FStructureEditorUtils::GetVarDesc(Struct)) - { - if (!ExistingGuids.Contains(Var.VarGuid)) - { - FStructureEditorUtils::RenameVariable(Struct, Var.VarGuid, PropName); - break; - } - } - - // Save - UPackage* Package = Struct->GetPackage(); - FString PackageFilename = FPackageName::LongPackageNameToFilename(Package->GetName(), FPackageName::GetAssetPackageExtension()); - FSavePackageArgs SaveArgs; - SaveArgs.TopLevelFlags = RF_Standalone; - bool bSaved = UPackage::SavePackage(Package, Struct, *PackageFilename, SaveArgs); - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Added property '%s' (%s) to struct '%s', save %s"), - *PropName, *PropType, *AssetPath, bSaved ? TEXT("succeeded") : TEXT("failed")); - - Result->SetBoolField(TEXT("success"), true); - Result->SetStringField(TEXT("assetPath"), AssetPath); - Result->SetStringField(TEXT("propertyName"), PropName); - Result->SetStringField(TEXT("propertyType"), PropType); - Result->SetBoolField(TEXT("saved"), bSaved); -} - -// ============================================================ -// HandleRemoveStructProperty — remove a property from UserDefinedStruct -// ============================================================ - -void FBlueprintMCPServer::HandleRemoveStructProperty(const FJsonObject* Json, FJsonObject* Result) -{ - FString AssetPath = Json->GetStringField(TEXT("assetPath")); - FString PropName = Json->GetStringField(TEXT("name")); - - if (AssetPath.IsEmpty() || PropName.IsEmpty()) - { - return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: assetPath, name")); - } - - // Find the struct - UUserDefinedStruct* Struct = LoadObject(nullptr, *AssetPath); - if (!Struct) - { - FString FullPath = AssetPath + TEXT(".") + FPackageName::GetShortName(AssetPath); - Struct = LoadObject(nullptr, *FullPath); - } - if (!Struct) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("UserDefinedStruct not found at '%s'"), *AssetPath)); - } - - // Find the property GUID by name - FGuid TargetGuid; - bool bFound = false; - for (const FStructVariableDescription& Var : FStructureEditorUtils::GetVarDesc(Struct)) - { - if (Var.FriendlyName == PropName || Var.VarName.ToString() == PropName) - { - TargetGuid = Var.VarGuid; - bFound = true; - break; - } - } - - if (!bFound) - { - // List available properties - TArray> AvailProps; - for (const FStructVariableDescription& Var : FStructureEditorUtils::GetVarDesc(Struct)) - { - AvailProps.Add(MakeShared(Var.FriendlyName)); - } - MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Property '%s' not found in struct '%s'"), *PropName, *AssetPath)); - Result->SetArrayField(TEXT("availableProperties"), AvailProps); - return; - } - - bool bRemoved = FStructureEditorUtils::RemoveVariable(Struct, TargetGuid); - if (!bRemoved) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Failed to remove property '%s'"), *PropName)); - } - - // Save - UPackage* Package = Struct->GetPackage(); - FString PackageFilename = FPackageName::LongPackageNameToFilename(Package->GetName(), FPackageName::GetAssetPackageExtension()); - FSavePackageArgs SaveArgs; - SaveArgs.TopLevelFlags = RF_Standalone; - bool bSaved = UPackage::SavePackage(Package, Struct, *PackageFilename, SaveArgs); - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Removed property '%s' from struct '%s', save %s"), - *PropName, *AssetPath, bSaved ? TEXT("succeeded") : TEXT("failed")); - - Result->SetBoolField(TEXT("success"), true); - Result->SetStringField(TEXT("assetPath"), AssetPath); - Result->SetStringField(TEXT("removedProperty"), PropName); - Result->SetBoolField(TEXT("saved"), bSaved); -} diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_UserTypes.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_UserTypes.h new file mode 100644 index 00000000..ed3f8aae --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_UserTypes.h @@ -0,0 +1,361 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPAssetFinder.h" +#include "MCPUtils.h" +#include "StructUtils/UserDefinedStruct.h" +#include "Engine/UserDefinedEnum.h" +#include "Kismet2/BlueprintEditorUtils.h" +#include "UserDefinedStructure/UserDefinedStructEditorData.h" +#include "Kismet2/EnumEditorUtils.h" +#include "AssetRegistry/AssetRegistryModule.h" +#include "AssetRegistry/IAssetRegistry.h" +#include "AssetToolsModule.h" +#include "IAssetTools.h" +#include "Factories/StructureFactory.h" +#include "Factories/EnumFactory.h" +#include "MCPHandlers_UserTypes.generated.h" + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +UCLASS(meta=(ToolName="create_struct_asset")) +class UMCPHandler_CreateStruct : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Full package path for the new struct (e.g. '/Game/DataTypes/S_MyStruct')")) + FString AssetPath; + + UPROPERTY(meta=(Optional, Description="Array of initial properties, each with 'name' and 'type' fields")) + FMCPJsonArray Properties; + + virtual FString GetDescription() const override + { + return TEXT("Create a new UserDefinedStruct asset. " + "Optionally add initial properties via the 'properties' array (each element needs 'name' and 'type')."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override + { + FString PackagePath, AssetName; + if (!MCPUtils::SplitAssetPath(AssetPath, PackagePath, AssetName)) + { + return MCPUtils::MakeErrorJson(Result, TEXT("assetPath must be a full path (e.g. '/Game/DataTypes/S_MyStruct')")); + } + + // Check if asset already exists + FAssetRegistryModule& ARM = FModuleManager::LoadModuleChecked("AssetRegistry"); + FAssetData ExistingAsset = ARM.Get().GetAssetByObjectPath(FSoftObjectPath(AssetPath + TEXT(".") + AssetName)); + if (ExistingAsset.IsValid()) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Asset already exists at '%s'"), *AssetPath)); + } + + // Create the struct using the AssetTools factory + FAssetToolsModule& AssetToolsModule = FModuleManager::LoadModuleChecked("AssetTools"); + IAssetTools& AssetTools = AssetToolsModule.Get(); + + UStructureFactory* Factory = NewObject(); + UObject* NewAsset = AssetTools.CreateAsset(AssetName, PackagePath, UUserDefinedStruct::StaticClass(), Factory); + + if (!NewAsset) + { + return MCPUtils::MakeErrorJson(Result, TEXT("Failed to create UserDefinedStruct asset")); + } + + UUserDefinedStruct* NewStruct = Cast(NewAsset); + if (!NewStruct) + { + return MCPUtils::MakeErrorJson(Result, TEXT("Created asset is not a UserDefinedStruct")); + } + + // Add properties if specified + int32 PropsAdded = 0; + for (const TSharedPtr& PropVal : Properties.Array) + { + TSharedPtr PropObj = PropVal->AsObject(); + if (!PropObj) continue; + + FString PropName = PropObj->GetStringField(TEXT("name")); + FString PropType = PropObj->GetStringField(TEXT("type")); + if (PropName.IsEmpty() || PropType.IsEmpty()) continue; + + FEdGraphPinType PinType; + FString TypeError; + if (!MCPUtils::ResolveTypeFromString(PropType, PinType, TypeError)) + { + UE_LOG(LogTemp, Warning, TEXT("BlueprintMCP: Could not resolve type '%s' for property '%s': %s"), *PropType, *PropName, *TypeError); + continue; + } + + // Snapshot existing GUIDs so we can find the newly added one + TSet ExistingGuids; + for (const FStructVariableDescription& Var : FStructureEditorUtils::GetVarDesc(NewStruct)) + { + ExistingGuids.Add(Var.VarGuid); + } + + bool bAdded = FStructureEditorUtils::AddVariable(NewStruct, PinType); + if (bAdded) + { + // Find the new variable by diffing GUID sets + FGuid NewPropGuid; + for (const FStructVariableDescription& Var : FStructureEditorUtils::GetVarDesc(NewStruct)) + { + if (!ExistingGuids.Contains(Var.VarGuid)) + { + NewPropGuid = Var.VarGuid; + break; + } + } + if (NewPropGuid.IsValid()) + { + FStructureEditorUtils::RenameVariable(NewStruct, NewPropGuid, PropName); + } + PropsAdded++; + } + } + + // Save + bool bSaved = MCPUtils::SaveGenericPackage(NewStruct); + + Result->SetBoolField(TEXT("success"), true); + Result->SetStringField(TEXT("assetPath"), AssetPath); + Result->SetStringField(TEXT("assetName"), AssetName); + Result->SetNumberField(TEXT("propertiesAdded"), PropsAdded); + Result->SetBoolField(TEXT("saved"), bSaved); + } +}; + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +UCLASS(meta=(ToolName="create_enum_asset")) +class UMCPHandler_CreateEnum : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Full package path for the new enum (e.g. '/Game/DataTypes/E_MyEnum')")) + FString AssetPath; + + UPROPERTY(meta=(Description="Array of enum value names")) + FMCPJsonArray Values; + + virtual FString GetDescription() const override + { + return TEXT("Create a new UserDefinedEnum asset with the specified values."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override + { + FString PackagePath, AssetName; + if (!MCPUtils::SplitAssetPath(AssetPath, PackagePath, AssetName)) + { + return MCPUtils::MakeErrorJson(Result, TEXT("assetPath must be a full path (e.g. '/Game/DataTypes/E_MyEnum')")); + } + + TArray EnumValues; + for (const TSharedPtr& Val : Values.Array) + { + FString Str = Val->AsString(); + if (!Str.IsEmpty()) EnumValues.Add(Str); + } + if (EnumValues.Num() == 0) + { + return MCPUtils::MakeErrorJson(Result, TEXT("Missing or empty required field: values (array of strings)")); + } + + // Create the enum using AssetTools + FAssetToolsModule& AssetToolsModule = FModuleManager::LoadModuleChecked("AssetTools"); + IAssetTools& AssetTools = AssetToolsModule.Get(); + + UEnumFactory* Factory = NewObject(); + UObject* NewAsset = AssetTools.CreateAsset(AssetName, PackagePath, UUserDefinedEnum::StaticClass(), Factory); + + if (!NewAsset) + { + return MCPUtils::MakeErrorJson(Result, TEXT("Failed to create UserDefinedEnum asset")); + } + + UUserDefinedEnum* NewEnum = Cast(NewAsset); + if (!NewEnum) + { + return MCPUtils::MakeErrorJson(Result, TEXT("Created asset is not a UserDefinedEnum")); + } + + // Add enum values — UUserDefinedEnum starts with a MAX value. + // We need to add entries before MAX. + for (int32 i = 0; i < EnumValues.Num(); ++i) + { + // AddNewEnumeratorForUserDefinedEnum adds before the _MAX entry (returns void) + FEnumEditorUtils::AddNewEnumeratorForUserDefinedEnum(NewEnum); + // The new entry is at index (NumEnums - 2) because _MAX is last + int32 NewIndex = NewEnum->NumEnums() - 2; + FEnumEditorUtils::SetEnumeratorDisplayName(NewEnum, NewIndex, FText::FromString(EnumValues[i])); + } + + // Save + bool bSaved = MCPUtils::SaveGenericPackage(NewEnum); + + Result->SetBoolField(TEXT("success"), true); + Result->SetStringField(TEXT("assetPath"), AssetPath); + Result->SetStringField(TEXT("assetName"), AssetName); + Result->SetNumberField(TEXT("valueCount"), EnumValues.Num()); + Result->SetBoolField(TEXT("saved"), bSaved); + } +}; + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +UCLASS(meta=(ToolName="add_struct_field")) +class UMCPHandler_AddStructField : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Name or package path of the struct asset")) + FString AssetPath; + + UPROPERTY(meta=(Description="Name for the new field")) + FString Name; + + UPROPERTY(meta=(Description="Type for the new field (e.g. 'int32', 'FString', 'FVector')")) + FString Type; + + virtual FString GetDescription() const override + { + return TEXT("Add a new field to a UserDefinedStruct asset."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override + { + // Find the struct + FString StructError; + UUserDefinedStruct* Struct = UMCPAssetFinder::LoadStructByName(AssetPath, StructError); + if (!Struct) + { + return MCPUtils::MakeErrorJson(Result, StructError); + } + + // Resolve type + FEdGraphPinType PinType; + FString TypeError; + if (!MCPUtils::ResolveTypeFromString(Type, PinType, TypeError)) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Cannot resolve type '%s': %s"), *Type, *TypeError)); + } + + // Snapshot existing GUIDs so we can find the newly added one + TSet ExistingGuids; + for (const FStructVariableDescription& Var : FStructureEditorUtils::GetVarDesc(Struct)) + { + ExistingGuids.Add(Var.VarGuid); + } + + bool bAdded = FStructureEditorUtils::AddVariable(Struct, PinType); + if (!bAdded) + { + return MCPUtils::MakeErrorJson(Result, TEXT("Failed to add property to struct")); + } + + // Find the new variable by diffing GUID sets and rename it + for (const FStructVariableDescription& Var : FStructureEditorUtils::GetVarDesc(Struct)) + { + if (!ExistingGuids.Contains(Var.VarGuid)) + { + FStructureEditorUtils::RenameVariable(Struct, Var.VarGuid, Name); + break; + } + } + + // Save + bool bSaved = MCPUtils::SaveGenericPackage(Struct); + + Result->SetBoolField(TEXT("success"), true); + Result->SetStringField(TEXT("assetPath"), AssetPath); + Result->SetStringField(TEXT("propertyName"), Name); + Result->SetStringField(TEXT("propertyType"), Type); + Result->SetBoolField(TEXT("saved"), bSaved); + } +}; + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +UCLASS(meta=(ToolName="remove_struct_field")) +class UMCPHandler_RemoveStructField : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Name or package path of the struct asset")) + FString AssetPath; + + UPROPERTY(meta=(Description="Name of the field to remove")) + FString Name; + + virtual FString GetDescription() const override + { + return TEXT("Remove a field from a UserDefinedStruct asset."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override + { + // Find the struct + FString StructError; + UUserDefinedStruct* Struct = UMCPAssetFinder::LoadStructByName(AssetPath, StructError); + if (!Struct) + { + return MCPUtils::MakeErrorJson(Result, StructError); + } + + // Find the property GUID by name + FGuid TargetGuid; + bool bFound = false; + for (const FStructVariableDescription& Var : FStructureEditorUtils::GetVarDesc(Struct)) + { + if (Var.FriendlyName == Name || Var.VarName.ToString() == Name) + { + TargetGuid = Var.VarGuid; + bFound = true; + break; + } + } + + if (!bFound) + { + // List available properties + TArray> AvailProps; + for (const FStructVariableDescription& Var : FStructureEditorUtils::GetVarDesc(Struct)) + { + AvailProps.Add(MakeShared(Var.FriendlyName)); + } + MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Property '%s' not found in struct '%s'"), *Name, *AssetPath)); + Result->SetArrayField(TEXT("availableProperties"), AvailProps); + return; + } + + bool bRemoved = FStructureEditorUtils::RemoveVariable(Struct, TargetGuid); + if (!bRemoved) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Failed to remove property '%s'"), *Name)); + } + + // Save + bool bSaved = MCPUtils::SaveGenericPackage(Struct); + + Result->SetBoolField(TEXT("success"), true); + Result->SetStringField(TEXT("assetPath"), AssetPath); + Result->SetStringField(TEXT("removedProperty"), Name); + Result->SetBoolField(TEXT("saved"), bSaved); + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPServer.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPServer.cpp index 7d300f2c..45aae93f 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPServer.cpp +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPServer.cpp @@ -862,10 +862,6 @@ void FMCPServer::RegisterHandlers() TEXT("add_blueprint_component"), TEXT("remove_blueprint_component"), TEXT("restore_blueprint_graph_from_snapshot"), - TEXT("create_struct_asset"), - TEXT("create_enum_asset"), - TEXT("add_struct_field"), - TEXT("remove_struct_field"), TEXT("create_material_asset"), TEXT("set_material_property"), TEXT("add_material_expression"), @@ -929,10 +925,6 @@ void FMCPServer::RegisterHandlers() H(TEXT("restore_blueprint_graph_from_snapshot"), &FMCPServer::HandleRestoreGraph); H(TEXT("find_pins_disconnected_since_snapshot"), &FMCPServer::HandleFindDisconnectedPins); H(TEXT("analyze_cpp_rebuild_impact"), &FMCPServer::HandleAnalyzeRebuildImpact); - H(TEXT("create_struct_asset"), &FMCPServer::HandleCreateStruct); - H(TEXT("create_enum_asset"), &FMCPServer::HandleCreateEnum); - H(TEXT("add_struct_field"), &FMCPServer::HandleAddStructProperty); - H(TEXT("remove_struct_field"), &FMCPServer::HandleRemoveStructProperty); H(TEXT("list_material_assets"), &FMCPServer::HandleListMaterials); H(TEXT("dump_material"), &FMCPServer::HandleGetMaterial); H(TEXT("dump_material_expression_graph"), &FMCPServer::HandleGetMaterialGraph); diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPAssetFinder.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPAssetFinder.h index 2c384864..93c68614 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPAssetFinder.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPAssetFinder.h @@ -3,6 +3,8 @@ #include "CoreMinimal.h" #include "Subsystems/EngineSubsystem.h" #include "AssetRegistry/AssetData.h" +#include "StructUtils/UserDefinedStruct.h" +#include "Engine/UserDefinedEnum.h" #include "MCPAssetFinder.generated.h" class UBlueprint; @@ -30,6 +32,8 @@ public: static const TArray& GetMaterialAssets(); static const TArray& GetMaterialInstanceAssets(); static const TArray& GetMaterialFunctionAssets(); + static const TArray& GetStructAssets(); + static const TArray& GetEnumAssets(); // --- Static API: find/load helpers --- // Find functions return nullptr if not found or if the short name is ambiguous. @@ -46,6 +50,10 @@ public: static UMaterialInstanceConstant* LoadMaterialInstanceByName(const FString& NameOrPath, FString& OutError); static FAssetData* FindMaterialFunctionAsset(const FString& NameOrPath, FString* OutError = nullptr); static UMaterialFunction* LoadMaterialFunctionByName(const FString& NameOrPath, FString& OutError); + static FAssetData* FindStructAsset(const FString& NameOrPath, FString* OutError = nullptr); + static UUserDefinedStruct* LoadStructByName(const FString& NameOrPath, FString& OutError); + static FAssetData* FindEnumAsset(const FString& NameOrPath, FString* OutError = nullptr); + static UUserDefinedEnum* LoadEnumByName(const FString& NameOrPath, FString& OutError); private: /** Get the subsystem, refreshing asset caches if stale. Returns nullptr if engine is not initialized. */ @@ -58,6 +66,8 @@ private: TArray AllMaterialAssets; TArray AllMaterialInstanceAssets; TArray AllMaterialFunctionAssets; + TArray AllStructAssets; + TArray AllEnumAssets; // Change detection — set true by asset registry delegates bool bDirty = true; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPServer.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPServer.h index 977e3b44..4dee4699 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPServer.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPServer.h @@ -135,12 +135,6 @@ private: void HandleCreateBlueprint(const FJsonObject* Json, FJsonObject* Result); void HandleCreateGraph(const FJsonObject* Json, FJsonObject* Result); - // ----- User-defined types ----- - void HandleCreateStruct(const FJsonObject* Json, FJsonObject* Result); - void HandleCreateEnum(const FJsonObject* Json, FJsonObject* Result); - void HandleAddStructProperty(const FJsonObject* Json, FJsonObject* Result); - void HandleRemoveStructProperty(const FJsonObject* Json, FJsonObject* Result); - // ----- Graph manipulation ----- void HandleDeleteGraph(const FJsonObject* Json, FJsonObject* Result); void HandleRenameGraph(const FJsonObject* Json, FJsonObject* Result); diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPUtils.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPUtils.h index 764724a6..713a8952 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPUtils.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPUtils.h @@ -105,6 +105,21 @@ public: class MCPUtils { public: + // ----- Asset path helpers ----- + // Splits "/Game/Foo/Bar" into PackagePath="/Game/Foo" and AssetName="Bar". + // Returns false if the path has no slash or the asset name is empty. + static bool SplitAssetPath(const FString& AssetPath, FString& OutPackagePath, FString& OutAssetName) + { + int32 LastSlash; + if (!AssetPath.FindLastChar('/', LastSlash)) + { + return false; + } + OutPackagePath = AssetPath.Left(LastSlash); + OutAssetName = AssetPath.Mid(LastSlash + 1); + return !OutAssetName.IsEmpty(); + } + // ----- JSON helpers ----- static FString JsonToString(TSharedRef JsonObj); static TSharedPtr ParseBodyJson(const FString& Body);