Ported UserTypes handlers over to new MCP handlers

This commit is contained in:
2026-03-06 22:23:10 -05:00
parent 7c66aee47a
commit 7f6438e423
8 changed files with 442 additions and 402 deletions

View File

@@ -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<FAssetData>& UMCPAssetFinder::GetMaterialFunctionAssets()
return Self ? Self->AllMaterialFunctionAssets : EmptyAssetArray;
}
const TArray<FAssetData>& UMCPAssetFinder::GetStructAssets()
{
UMCPAssetFinder* Self = GetUpdatedAssets();
return Self ? Self->AllStructAssets : EmptyAssetArray;
}
const TArray<FAssetData>& 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<UUserDefinedStruct>(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<UUserDefinedEnum>(Asset->GetAsset());
if (Enum) return Enum;
}
if (OutError.IsEmpty())
OutError = FString::Printf(TEXT("UserDefinedEnum '%s' not found."), *NameOrPath);
return nullptr;
}

View File

@@ -4,3 +4,4 @@
#include "MCPHandlers_PinMutation.h"
#include "MCPHandlers_AssetMutation.h"
#include "MCPHandlers_Validation.h"
#include "MCPHandlers_UserTypes.h"

View File

@@ -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<FAssetRegistryModule>("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<FAssetToolsModule>("AssetTools");
IAssetTools& AssetTools = AssetToolsModule.Get();
UStructureFactory* Factory = NewObject<UStructureFactory>();
UObject* NewAsset = AssetTools.CreateAsset(AssetName, PackagePath, UUserDefinedStruct::StaticClass(), Factory);
if (!NewAsset)
{
return MCPUtils::MakeErrorJson(Result, TEXT("Failed to create UserDefinedStruct asset"));
}
UUserDefinedStruct* NewStruct = Cast<UUserDefinedStruct>(NewAsset);
if (!NewStruct)
{
return MCPUtils::MakeErrorJson(Result, TEXT("Created asset is not a UserDefinedStruct"));
}
// Add properties if specified
const TArray<TSharedPtr<FJsonValue>>* PropsArray = nullptr;
int32 PropsAdded = 0;
if (Json->TryGetArrayField(TEXT("properties"), PropsArray) && PropsArray)
{
for (const TSharedPtr<FJsonValue>& PropVal : *PropsArray)
{
TSharedPtr<FJsonObject> 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<FGuid> 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<TSharedPtr<FJsonValue>>* 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<FString> EnumValues;
for (const TSharedPtr<FJsonValue>& 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<FAssetToolsModule>("AssetTools");
IAssetTools& AssetTools = AssetToolsModule.Get();
UEnumFactory* Factory = NewObject<UEnumFactory>();
UObject* NewAsset = AssetTools.CreateAsset(AssetName, PackagePath, UUserDefinedEnum::StaticClass(), Factory);
if (!NewAsset)
{
return MCPUtils::MakeErrorJson(Result, TEXT("Failed to create UserDefinedEnum asset"));
}
UUserDefinedEnum* NewEnum = Cast<UUserDefinedEnum>(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<UUserDefinedStruct>(nullptr, *AssetPath);
if (!Struct)
{
// Try with asset name appended
FString FullPath = AssetPath + TEXT(".") + FPackageName::GetShortName(AssetPath);
Struct = LoadObject<UUserDefinedStruct>(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<FGuid> 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<UUserDefinedStruct>(nullptr, *AssetPath);
if (!Struct)
{
FString FullPath = AssetPath + TEXT(".") + FPackageName::GetShortName(AssetPath);
Struct = LoadObject<UUserDefinedStruct>(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<TSharedPtr<FJsonValue>> AvailProps;
for (const FStructVariableDescription& Var : FStructureEditorUtils::GetVarDesc(Struct))
{
AvailProps.Add(MakeShared<FJsonValueString>(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);
}

View File

@@ -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<FAssetRegistryModule>("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<FAssetToolsModule>("AssetTools");
IAssetTools& AssetTools = AssetToolsModule.Get();
UStructureFactory* Factory = NewObject<UStructureFactory>();
UObject* NewAsset = AssetTools.CreateAsset(AssetName, PackagePath, UUserDefinedStruct::StaticClass(), Factory);
if (!NewAsset)
{
return MCPUtils::MakeErrorJson(Result, TEXT("Failed to create UserDefinedStruct asset"));
}
UUserDefinedStruct* NewStruct = Cast<UUserDefinedStruct>(NewAsset);
if (!NewStruct)
{
return MCPUtils::MakeErrorJson(Result, TEXT("Created asset is not a UserDefinedStruct"));
}
// Add properties if specified
int32 PropsAdded = 0;
for (const TSharedPtr<FJsonValue>& PropVal : Properties.Array)
{
TSharedPtr<FJsonObject> 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<FGuid> 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<FString> EnumValues;
for (const TSharedPtr<FJsonValue>& 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<FAssetToolsModule>("AssetTools");
IAssetTools& AssetTools = AssetToolsModule.Get();
UEnumFactory* Factory = NewObject<UEnumFactory>();
UObject* NewAsset = AssetTools.CreateAsset(AssetName, PackagePath, UUserDefinedEnum::StaticClass(), Factory);
if (!NewAsset)
{
return MCPUtils::MakeErrorJson(Result, TEXT("Failed to create UserDefinedEnum asset"));
}
UUserDefinedEnum* NewEnum = Cast<UUserDefinedEnum>(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<FGuid> 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<TSharedPtr<FJsonValue>> AvailProps;
for (const FStructVariableDescription& Var : FStructureEditorUtils::GetVarDesc(Struct))
{
AvailProps.Add(MakeShared<FJsonValueString>(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);
}
};

View File

@@ -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);

View File

@@ -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<FAssetData>& GetMaterialAssets();
static const TArray<FAssetData>& GetMaterialInstanceAssets();
static const TArray<FAssetData>& GetMaterialFunctionAssets();
static const TArray<FAssetData>& GetStructAssets();
static const TArray<FAssetData>& 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<FAssetData> AllMaterialAssets;
TArray<FAssetData> AllMaterialInstanceAssets;
TArray<FAssetData> AllMaterialFunctionAssets;
TArray<FAssetData> AllStructAssets;
TArray<FAssetData> AllEnumAssets;
// Change detection — set true by asset registry delegates
bool bDirty = true;

View File

@@ -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);

View File

@@ -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<FJsonObject> JsonObj);
static TSharedPtr<FJsonObject> ParseBodyJson(const FString& Body);