From 54fa926ab3277d3a6a6cbf9fb3e83653b549fa02 Mon Sep 17 00:00:00 2001 From: jyelon Date: Sun, 8 Mar 2026 22:17:14 -0400 Subject: [PATCH] Split MCP handlers --- .../Handlers/MCPHandlers_AnimMutation.h | 466 ------ .../Handlers/MCPHandlers_AssetMutation.h | 338 ---- .../Private/Handlers/MCPHandlers_Components.h | 361 ---- .../Private/Handlers/MCPHandlers_Discovery.h | 645 ------- .../Private/Handlers/MCPHandlers_Graphs.h | 554 ------ .../Private/Handlers/MCPHandlers_Interfaces.h | 303 ---- .../Handlers/MCPHandlers_MaterialInstance.h | 722 -------- .../Handlers/MCPHandlers_MaterialMutation.h | 1481 ----------------- .../Handlers/MCPHandlers_MaterialRead.h | 962 ----------- .../Private/Handlers/MCPHandlers_Mutation.h | 1417 ---------------- .../Private/Handlers/MCPHandlers_Params.h | 490 ------ .../Handlers/MCPHandlers_PinMutation.h | 382 ----- .../Private/Handlers/MCPHandlers_Read.h | 669 -------- .../Handlers/MCPHandlers_StateMachine.h | 665 -------- .../Private/Handlers/MCPHandlers_UserTypes.h | 350 ---- .../Private/Handlers/MCPHandlers_Variables.h | 576 ------- .../UMCPHandler_AddAnimStateToMachine.h | 117 ++ .../UMCPHandler_AddAnimStateTransition.h | 110 ++ .../UMCPHandler_AddBlueprintComponent.h | 179 ++ .../UMCPHandler_AddBlueprintInterface.h | 152 ++ .../UMCPHandler_AddBlueprintVariable.h | 102 ++ ...ers.h => UMCPHandler_AddEventDispatcher.h} | 75 +- .../UMCPHandler_AddFunctionParameter.h | 161 ++ .../UMCPHandler_AddMaterialExpression.h | 239 +++ .../Handlers/UMCPHandler_AddStructField.h | 85 + .../Handlers/UMCPHandler_BackupAsset.h | 56 + .../UMCPHandler_ChangeBlueprintVariableType.h | 198 +++ .../UMCPHandler_ChangeFunctionParameterType.h | 206 +++ .../UMCPHandler_ChangeStructNodeType.h | 279 ++++ ...PHandler_CheckPinConnectionCompatibility.h | 136 ++ ...ation.h => UMCPHandler_CompileBlueprint.h} | 3 +- .../Handlers/UMCPHandler_CompileMaterial.h | 116 ++ .../UMCPHandler_ConnectBlueprintPins.h | 134 ++ ...MCPHandler_ConnectMaterialExpressionPins.h | 215 +++ .../UMCPHandler_CreateAnimBlueprintAsset.h | 133 ++ .../UMCPHandler_CreateBlendSpaceAsset.h | 100 ++ .../UMCPHandler_CreateBlueprintAsset.h | 146 ++ .../UMCPHandler_CreateBlueprintGraph.h | 136 ++ .../Handlers/UMCPHandler_CreateEnumAsset.h | 97 ++ .../UMCPHandler_CreateMaterialAsset.h | 175 ++ .../UMCPHandler_CreateMaterialFunctionAsset.h | 118 ++ .../UMCPHandler_CreateMaterialInstanceAsset.h | 125 ++ .../Handlers/UMCPHandler_CreateStructAsset.h | 140 ++ .../Handlers/UMCPHandler_DeleteAsset.h | 167 ++ .../UMCPHandler_DeleteBlueprintGraph.h | 105 ++ .../UMCPHandler_DeleteMaterialExpression.h | 180 ++ .../UMCPHandler_DeleteNodeFromGraph.h | 138 ++ .../UMCPHandler_DescribeMaterialInEnglish.h | 245 +++ ...ints.h => UMCPHandler_DiffTwoBlueprints.h} | 3 +- .../UMCPHandler_DisconnectBlueprintPins.h | 144 ++ ...PHandler_DisconnectMaterialExpressionPin.h | 177 ++ .../Handlers/UMCPHandler_DumpBlueprint.h | 52 + .../Handlers/UMCPHandler_DumpBlueprintGraph.h | 80 + .../Handlers/UMCPHandler_DumpMaterial.h | 293 ++++ .../UMCPHandler_DumpMaterialExpressionGraph.h | 95 ++ .../UMCPHandler_DumpMaterialFunction.h | 128 ++ ...CPHandler_DumpMaterialInstanceParameters.h | 264 +++ .../UMCPHandler_DuplicateNodesInGraph.h | 200 +++ .../UMCPHandler_FindAssetReferences.h | 85 + .../UMCPHandler_FindMaterialReferences.h | 85 + .../Handlers/UMCPHandler_GetNodeComment.h | 91 + .../Handlers/UMCPHandler_GetPinDetails.h | 127 ++ .../Handlers/UMCPHandler_ListAnimSlotNames.h | 77 + .../Handlers/UMCPHandler_ListAnimSyncGroups.h | 77 + .../UMCPHandler_ListBlueprintAssets.h | 91 + .../UMCPHandler_ListBlueprintComponents.h | 105 ++ .../UMCPHandler_ListBlueprintInterfaces.h | 69 + .../Handlers/UMCPHandler_ListClassFunctions.h | 131 ++ .../UMCPHandler_ListClassProperties.h | 106 ++ .../UMCPHandler_ListEventDispatchers.h | 86 + .../Handlers/UMCPHandler_ListMaterialAssets.h | 85 + .../UMCPHandler_ListMaterialFunctionAssets.h | 74 + .../UMCPHandler_RefreshAllNodesInGraph.h | 151 ++ .../UMCPHandler_RemoveAnimStateFromMachine.h | 89 + .../UMCPHandler_RemoveBlueprintComponent.h | 106 ++ .../UMCPHandler_RemoveBlueprintInterface.h | 109 ++ .../UMCPHandler_RemoveBlueprintVariable.h | 85 + .../UMCPHandler_RemoveFunctionParameter.h | 154 ++ .../Handlers/UMCPHandler_RemoveStructField.h | 86 + .../Handlers/UMCPHandler_RenameAsset.h | 95 ++ .../UMCPHandler_RenameBlueprintGraph.h | 115 ++ .../Handlers/UMCPHandler_ReparentBlueprint.h | 117 ++ .../UMCPHandler_ReparentMaterialInstance.h | 133 ++ ...PHandler_ReplaceFunctionCallsInBlueprint.h | 303 ++++ .../Handlers/UMCPHandler_RestoreAsset.h | 72 + .../UMCPHandler_SearchSpawnableNodeTypes.h | 118 ++ .../UMCPHandler_SearchTypeUsageInBlueprints.h | 283 ++++ .../UMCPHandler_SearchUnrealClasses.h | 137 ++ .../UMCPHandler_SearchWithinBlueprints.h | 156 ++ .../UMCPHandler_SearchWithinMaterials.h | 138 ++ .../UMCPHandler_SetAnimStateAnimation.h | 102 ++ .../UMCPHandler_SetAnimStateBlendSpace.h | 229 +++ .../UMCPHandler_SetAnimTransitionRule.h | 124 ++ .../UMCPHandler_SetBlendSpaceSamplePoints.h | 155 ++ ...UMCPHandler_SetBlueprintVariableMetadata.h | 234 +++ .../UMCPHandler_SetClassDefaultValue.h | 219 +++ ...MCPHandler_SetMaterialExpressionPosition.h | 167 ++ ...MCPHandler_SetMaterialExpressionProperty.h | 330 ++++ ...UMCPHandler_SetMaterialInstanceParameter.h | 258 +++ .../UMCPHandler_SetMaterialProperty.h | 257 +++ .../Handlers/UMCPHandler_SetNodeComment.h | 108 ++ .../Handlers/UMCPHandler_SetNodePositions.h | 136 ++ .../UMCPHandler_SetPinDefaultValues.h | 133 ++ .../Handlers/UMCPHandler_ShowCommands.h | 79 + .../Handlers/UMCPHandler_SpawnNodesInGraph.h | 195 +++ .../UMCPHandler_TestSaveBlueprintPackage.h | 67 + .../BlueprintMCP/Private/MCPHandlers.cpp | 109 +- tools/split-handlers.py | 92 + 108 files changed, 12640 insertions(+), 10475 deletions(-) delete mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/MCPHandlers_AnimMutation.h delete mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/MCPHandlers_AssetMutation.h delete mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/MCPHandlers_Components.h delete mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/MCPHandlers_Discovery.h delete mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/MCPHandlers_Graphs.h delete mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/MCPHandlers_Interfaces.h delete mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/MCPHandlers_MaterialInstance.h delete mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/MCPHandlers_MaterialMutation.h delete mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/MCPHandlers_MaterialRead.h delete mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/MCPHandlers_Mutation.h delete mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/MCPHandlers_Params.h delete mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/MCPHandlers_PinMutation.h delete mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/MCPHandlers_Read.h delete mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/MCPHandlers_StateMachine.h delete mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/MCPHandlers_UserTypes.h delete mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/MCPHandlers_Variables.h create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_AddAnimStateToMachine.h create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_AddAnimStateTransition.h create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_AddBlueprintComponent.h create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_AddBlueprintInterface.h create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_AddBlueprintVariable.h rename Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/{MCPHandlers_Dispatchers.h => UMCPHandler_AddEventDispatcher.h} (68%) create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_AddFunctionParameter.h create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_AddMaterialExpression.h create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_AddStructField.h create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_BackupAsset.h create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_ChangeBlueprintVariableType.h create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_ChangeFunctionParameterType.h create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_ChangeStructNodeType.h create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_CheckPinConnectionCompatibility.h rename Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/{MCPHandlers_Validation.h => UMCPHandler_CompileBlueprint.h} (99%) create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_CompileMaterial.h create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_ConnectBlueprintPins.h create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_ConnectMaterialExpressionPins.h create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_CreateAnimBlueprintAsset.h create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_CreateBlendSpaceAsset.h create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_CreateBlueprintAsset.h create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_CreateBlueprintGraph.h create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_CreateEnumAsset.h create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_CreateMaterialAsset.h create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_CreateMaterialFunctionAsset.h create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_CreateMaterialInstanceAsset.h create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_CreateStructAsset.h create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_DeleteAsset.h create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_DeleteBlueprintGraph.h create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_DeleteMaterialExpression.h create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_DeleteNodeFromGraph.h create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_DescribeMaterialInEnglish.h rename Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/{MCPHandlers_DiffBlueprints.h => UMCPHandler_DiffTwoBlueprints.h} (99%) create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_DisconnectBlueprintPins.h create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_DisconnectMaterialExpressionPin.h create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_DumpBlueprint.h create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_DumpBlueprintGraph.h create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_DumpMaterial.h create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_DumpMaterialExpressionGraph.h create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_DumpMaterialFunction.h create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_DumpMaterialInstanceParameters.h create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_DuplicateNodesInGraph.h create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_FindAssetReferences.h create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_FindMaterialReferences.h create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_GetNodeComment.h create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_GetPinDetails.h create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_ListAnimSlotNames.h create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_ListAnimSyncGroups.h create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_ListBlueprintAssets.h create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_ListBlueprintComponents.h create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_ListBlueprintInterfaces.h create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_ListClassFunctions.h create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_ListClassProperties.h create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_ListEventDispatchers.h create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_ListMaterialAssets.h create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_ListMaterialFunctionAssets.h create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_RefreshAllNodesInGraph.h create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_RemoveAnimStateFromMachine.h create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_RemoveBlueprintComponent.h create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_RemoveBlueprintInterface.h create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_RemoveBlueprintVariable.h create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_RemoveFunctionParameter.h create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_RemoveStructField.h create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_RenameAsset.h create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_RenameBlueprintGraph.h create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_ReparentBlueprint.h create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_ReparentMaterialInstance.h create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_ReplaceFunctionCallsInBlueprint.h create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_RestoreAsset.h create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_SearchSpawnableNodeTypes.h create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_SearchTypeUsageInBlueprints.h create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_SearchUnrealClasses.h create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_SearchWithinBlueprints.h create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_SearchWithinMaterials.h create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_SetAnimStateAnimation.h create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_SetAnimStateBlendSpace.h create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_SetAnimTransitionRule.h create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_SetBlendSpaceSamplePoints.h create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_SetBlueprintVariableMetadata.h create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_SetClassDefaultValue.h create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_SetMaterialExpressionPosition.h create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_SetMaterialExpressionProperty.h create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_SetMaterialInstanceParameter.h create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_SetMaterialProperty.h create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_SetNodeComment.h create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_SetNodePositions.h create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_SetPinDefaultValues.h create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_ShowCommands.h create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_SpawnNodesInGraph.h create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_TestSaveBlueprintPackage.h create mode 100644 tools/split-handlers.py diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/MCPHandlers_AnimMutation.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/MCPHandlers_AnimMutation.h deleted file mode 100644 index 97bab034..00000000 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/MCPHandlers_AnimMutation.h +++ /dev/null @@ -1,466 +0,0 @@ -#pragma once - -#include "CoreMinimal.h" -#include "MCPHandler.h" -#include "MCPAssetFinder.h" -#include "MCPUtils.h" -#include "Engine/Blueprint.h" -#include "EdGraph/EdGraph.h" -#include "EdGraph/EdGraphNode.h" -#include "Kismet2/KismetEditorUtilities.h" -#include "Dom/JsonValue.h" -#include "Animation/AnimBlueprint.h" -#include "Animation/AnimBlueprintGeneratedClass.h" -#include "Animation/Skeleton.h" -#include "AnimGraphNode_Base.h" -#include "Animation/AnimSequence.h" -#include "Animation/BlendSpace.h" -#include "MCPHandlers_AnimMutation.generated.h" - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -UCLASS() -class UMCPHandler_CreateAnimBlueprintAsset : public UObject, public IMCPHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Description="Name for the new Animation Blueprint asset")) - FString Name; - - UPROPERTY(meta=(Description="Package path where the asset will be created (must start with /Game)")) - FString PackagePath; - - UPROPERTY(meta=(Description="Name or path of the skeleton asset to use")) - FString Skeleton; - - UPROPERTY(meta=(Optional, Description="Parent class name (default: AnimInstance)")) - FString ParentClass; - - virtual FString GetDescription() const override - { - return TEXT("Create a new Animation Blueprint asset with a specified skeleton."); - } - - virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override - { - - if (Name.IsEmpty() || PackagePath.IsEmpty() || Skeleton.IsEmpty()) - { - return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: name, packagePath, skeleton")); - } - - if (!PackagePath.StartsWith(TEXT("/Game"))) - { - return MCPUtils::MakeErrorJson(Result, TEXT("packagePath must start with '/Game'")); - } - - // Check if asset already exists - FString FullAssetPath = PackagePath / Name; - MCPAssets ExistCheck; - if (!ExistCheck.Exact(Name).Errors(Result).EAny().Info()) return; - - // Resolve skeleton - MCPAssets SkeletonAssets; - if (!SkeletonAssets.Exact(Skeleton).Errors(Result).ENone().ETwo().Load()) return; - USkeleton* SkeletonObj = SkeletonAssets.Object(); - - // Resolve parent class (default: UAnimInstance) - UClass* ParentClassObj = UAnimInstance::StaticClass(); - if (!ParentClass.IsEmpty() && ParentClass != TEXT("AnimInstance")) - { - for (TObjectIterator It; It; ++It) - { - if (It->GetName() == ParentClass && It->IsChildOf(UAnimInstance::StaticClass())) - { - ParentClassObj = *It; - break; - } - } - } - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Creating AnimBlueprint '%s' in '%s' with skeleton '%s'"), - *Name, *PackagePath, *SkeletonObj->GetName()); - - // Create the package - FString FullPackagePath = PackagePath / Name; - UPackage* Package = CreatePackage(*FullPackagePath); - if (!Package) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Failed to create package at '%s'"), *FullPackagePath)); - } - - // Create the Animation Blueprint - UAnimBlueprint* NewAnimBP = CastChecked( - FKismetEditorUtilities::CreateBlueprint( - ParentClassObj, - Package, - FName(*Name), - BPTYPE_Normal, - UAnimBlueprint::StaticClass(), - UAnimBlueprintGeneratedClass::StaticClass() - )); - - if (!NewAnimBP) - { - return MCPUtils::MakeErrorJson(Result, TEXT("FKismetEditorUtilities::CreateBlueprint returned null for AnimBlueprint")); - } - - // Set target skeleton - NewAnimBP->TargetSkeleton = SkeletonObj; - - // Compile - FKismetEditorUtilities::CompileBlueprint(NewAnimBP); - - // Save - bool bSaved = MCPUtils::SaveBlueprintPackage(NewAnimBP); - - - TArray> GraphNames = MCPUtils::AllGraphNamesJson(NewAnimBP); - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Created AnimBlueprint '%s' with %d graphs (saved: %s)"), - *Name, GraphNames.Num(), bSaved ? TEXT("true") : TEXT("false")); - - Result->SetStringField(TEXT("assetPath"), FullAssetPath); - Result->SetStringField(TEXT("targetSkeleton"), SkeletonObj->GetName()); - Result->SetStringField(TEXT("parentClass"), ParentClassObj->GetName()); - Result->SetBoolField(TEXT("saved"), bSaved); - Result->SetArrayField(TEXT("graphs"), GraphNames); - } -}; - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -UCLASS() -class UMCPHandler_ListAnimSlotNames : public UObject, public IMCPHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Description="Animation Blueprint name or package path")) - FString Blueprint; - - virtual FString GetDescription() const override - { - return TEXT("List all animation slot names used in an Animation Blueprint."); - } - - virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override - { - if (Blueprint.IsEmpty()) - { - return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: blueprint")); - } - - MCPAssets Assets; - if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; - UAnimBlueprint* AnimBP = Assets.Object(); - - // Walk all anim nodes to collect slot names - TSet SlotNames; - for (UAnimGraphNode_Base* AnimNode : MCPUtils::AllNodes(AnimBP)) - { - // Check for SlotName property via reflection - for (TFieldIterator PropIt(AnimNode->GetClass()); PropIt; ++PropIt) - { - if (PropIt->GetName().Contains(TEXT("SlotName")) || PropIt->GetName().Contains(TEXT("Slot"))) - { - FName SlotValue = PropIt->GetPropertyValue_InContainer(AnimNode); - if (!SlotValue.IsNone()) - { - SlotNames.Add(SlotValue.ToString()); - } - } - } - } - - TArray> SlotsArr; - for (const FString& Slot : SlotNames) - { - SlotsArr.Add(MakeShared(Slot)); - } - - Result->SetArrayField(TEXT("slots"), SlotsArr); - Result->SetNumberField(TEXT("count"), SlotsArr.Num()); - } -}; - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -UCLASS() -class UMCPHandler_ListAnimSyncGroups : public UObject, public IMCPHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Description="Animation Blueprint name or package path")) - FString Blueprint; - - virtual FString GetDescription() const override - { - return TEXT("List all sync group names used in an Animation Blueprint."); - } - - virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override - { - if (Blueprint.IsEmpty()) - { - return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: blueprint")); - } - - MCPAssets Assets; - if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; - UAnimBlueprint* AnimBP = Assets.Object(); - - // Walk all anim nodes to collect sync group names - TSet SyncGroupNames; - for (UAnimGraphNode_Base* AnimNode : MCPUtils::AllNodes(AnimBP)) - { - // Check for SyncGroup/GroupName property via reflection - for (TFieldIterator PropIt(AnimNode->GetClass()); PropIt; ++PropIt) - { - if (PropIt->GetName().Contains(TEXT("SyncGroup")) || PropIt->GetName().Contains(TEXT("GroupName"))) - { - FName GroupValue = PropIt->GetPropertyValue_InContainer(AnimNode); - if (!GroupValue.IsNone()) - { - SyncGroupNames.Add(GroupValue.ToString()); - } - } - } - } - - TArray> GroupsArr; - for (const FString& Group : SyncGroupNames) - { - GroupsArr.Add(MakeShared(Group)); - } - - Result->SetArrayField(TEXT("syncGroups"), GroupsArr); - Result->SetNumberField(TEXT("count"), GroupsArr.Num()); - } -}; - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -UCLASS() -class UMCPHandler_CreateBlendSpaceAsset : public UObject, public IMCPHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Description="Name for the new Blend Space asset")) - FString Name; - - UPROPERTY(meta=(Description="Package path where the asset will be created (must start with /Game)")) - FString PackagePath; - - UPROPERTY(meta=(Description="Name or path of the skeleton asset to use")) - FString Skeleton; - - virtual FString GetDescription() const override - { - return TEXT("Create a new 2D Blend Space asset with a specified skeleton."); - } - - virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override - { - - if (Name.IsEmpty() || PackagePath.IsEmpty() || Skeleton.IsEmpty()) - { - return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: name, packagePath, skeleton")); - } - - if (!PackagePath.StartsWith(TEXT("/Game"))) - { - return MCPUtils::MakeErrorJson(Result, TEXT("packagePath must start with '/Game'")); - } - - // Check if asset already exists - FString FullAssetPath = PackagePath / Name; - MCPAssets ExistCheck; - if (!ExistCheck.Exact(Name).Errors(Result).EAny().Info()) return; - - // Resolve skeleton - MCPAssets SkeletonAssets; - if (!SkeletonAssets.Exact(Skeleton).Errors(Result).ENone().ETwo().Load()) return; - USkeleton* SkeletonObj = SkeletonAssets.Object(); - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Creating Blend Space '%s' in '%s' with skeleton '%s'"), - *Name, *PackagePath, *SkeletonObj->GetName()); - - // Create the package - FString FullPackagePath = PackagePath / Name; - UPackage* Package = CreatePackage(*FullPackagePath); - if (!Package) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Failed to create package at '%s'"), *FullPackagePath)); - } - - // Create the Blend Space - UBlendSpace* NewBS = NewObject(Package, FName(*Name), RF_Public | RF_Standalone); - if (!NewBS) - { - return MCPUtils::MakeErrorJson(Result, TEXT("Failed to create Blend Space object")); - } - - // Set skeleton - NewBS->SetSkeleton(SkeletonObj); - - // Mark dirty and save - NewBS->MarkPackageDirty(); - bool bSaved = MCPUtils::SaveGenericPackage(NewBS); - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Created Blend Space '%s' (saved: %s)"), - *Name, bSaved ? TEXT("true") : TEXT("false")); - - Result->SetStringField(TEXT("assetPath"), FullAssetPath); - Result->SetStringField(TEXT("skeleton"), SkeletonObj->GetName()); - Result->SetBoolField(TEXT("saved"), bSaved); - } -}; - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -USTRUCT() -struct FBlendSpaceSampleEntry -{ - GENERATED_BODY() - - UPROPERTY() - FString AnimationAsset; - - UPROPERTY() - float X = 0.0f; - - UPROPERTY() - float Y = 0.0f; -}; - -UCLASS() -class UMCPHandler_SetBlendSpaceSamplePoints : public UObject, public IMCPHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Description="Blend Space asset name or package path")) - FString BlendSpace; - - UPROPERTY(meta=(Optional, Description="Display name for the X axis")) - FString AxisXName; - - UPROPERTY(meta=(Optional, Description="Display name for the Y axis")) - FString AxisYName; - - UPROPERTY(meta=(Optional, Description="Minimum value for X axis")) - float AxisXMin = 0.0f; - - UPROPERTY(meta=(Optional, Description="Maximum value for X axis")) - float AxisXMax = 0.0f; - - UPROPERTY(meta=(Optional, Description="Minimum value for Y axis")) - float AxisYMin = 0.0f; - - UPROPERTY(meta=(Optional, Description="Maximum value for Y axis")) - float AxisYMax = 0.0f; - - UPROPERTY(meta=(Optional, Description="Array of sample points, each with animationAsset, x, y")) - FMCPJsonArray Samples; - - virtual FString GetDescription() const override - { - return TEXT("Set axis parameters and animation sample points on a Blend Space. " - "Replaces all existing samples."); - } - - virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override - { - if (BlendSpace.IsEmpty()) - { - return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: blendSpace")); - } - - // Load the blend space - MCPAssets Assets; - if (!Assets.Exact(BlendSpace).Errors(Result).ENone().ETwo().Load()) return; - UBlendSpace* BS = Assets.Object(); - - // Set axis parameters - BS->PreEditChange(nullptr); - - const FBlendParameter& ParamX = BS->GetBlendParameter(0); - const FBlendParameter& ParamY = BS->GetBlendParameter(1); - - // We need to modify BlendParameters directly — use const_cast since there's no setter API - FBlendParameter& MutableParamX = const_cast(ParamX); - FBlendParameter& MutableParamY = const_cast(ParamY); - - if (!AxisXName.IsEmpty()) MutableParamX.DisplayName = AxisXName; - if (Json->HasField(TEXT("axisXMin"))) MutableParamX.Min = AxisXMin; - if (Json->HasField(TEXT("axisXMax"))) MutableParamX.Max = AxisXMax; - - if (!AxisYName.IsEmpty()) MutableParamY.DisplayName = AxisYName; - if (Json->HasField(TEXT("axisYMin"))) MutableParamY.Min = AxisYMin; - if (Json->HasField(TEXT("axisYMax"))) MutableParamY.Max = AxisYMax; - - // Clear existing samples (delete from end to start) - int32 NumExisting = BS->GetNumberOfBlendSamples(); - for (int32 i = NumExisting - 1; i >= 0; --i) - { - BS->DeleteSample(i); - } - - // Add new samples - int32 SamplesSet = 0; - - for (const TSharedPtr& SampleVal : Samples.Array) - { - FBlendSpaceSampleEntry Entry; - if (!MCPUtils::PopulateFromJson(FBlendSpaceSampleEntry::StaticStruct(), &Entry, SampleVal, Result)) return; - - UAnimSequence* AnimSeq = nullptr; - if (!Entry.AnimationAsset.IsEmpty()) - { - MCPAssets AnimAssets; - if (AnimAssets.Exact(Entry.AnimationAsset).Load()) - AnimSeq = AnimAssets.Object(); - } - - FVector SampleValue(Entry.X, Entry.Y, 0.0f); - if (AnimSeq) - { - BS->AddSample(AnimSeq, SampleValue); - } - else - { - BS->AddSample(SampleValue); - } - SamplesSet++; - } - - BS->ValidateSampleData(); - BS->PostEditChange(); - - // Save - BS->MarkPackageDirty(); - bool bSaved = MCPUtils::SaveGenericPackage(BS); - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Set %d samples on Blend Space '%s' (saved: %s)"), - SamplesSet, *BS->GetName(), bSaved ? TEXT("true") : TEXT("false")); - - Result->SetStringField(TEXT("blendSpace"), BS->GetPathName()); - Result->SetNumberField(TEXT("samplesSet"), SamplesSet); - Result->SetBoolField(TEXT("saved"), bSaved); - } -}; - diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/MCPHandlers_AssetMutation.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/MCPHandlers_AssetMutation.h deleted file mode 100644 index 87209516..00000000 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/MCPHandlers_AssetMutation.h +++ /dev/null @@ -1,338 +0,0 @@ -#pragma once - -#include "CoreMinimal.h" -#include "MCPHandler.h" -#include "MCPAssetFinder.h" -#include "MCPUtils.h" -#include "Engine/Blueprint.h" -#include "Kismet2/BlueprintEditorUtils.h" -#include "UObject/SavePackage.h" -#include "Misc/PackageName.h" -#include "AssetRegistry/AssetRegistryModule.h" -#include "AssetRegistry/IAssetRegistry.h" -#include "AssetToolsModule.h" -#include "IAssetTools.h" -#include "FileHelpers.h" -#include "MCPHandlers_AssetMutation.generated.h" - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -UCLASS() -class UMCPHandler_DeleteAsset : public UObject, public IMCPHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Description="Package path of the asset to delete")) - FString AssetPath; - - UPROPERTY(meta=(Optional, Description="If true, skip reference check and force delete")) - bool Force = false; - - virtual FString GetDescription() const override - { - return TEXT("Delete a .uasset after verifying no references. " - "Use force=true to skip the reference check."); - } - - virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override - { - - // Check if asset file exists on disk - FString PackageFilename = FPackageName::LongPackageNameToFilename( - AssetPath, FPackageName::GetAssetPackageExtension()); - PackageFilename = FPaths::ConvertRelativePathToFull(PackageFilename); - - if (!IFileManager::Get().FileExists(*PackageFilename)) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Asset file not found on disk: %s"), *PackageFilename)); - } - - // Check references - IAssetRegistry& Registry = *IAssetRegistry::Get(); - TArray Referencers; - Registry.GetReferencers(FName(*AssetPath), Referencers); - - // Filter out self-references - Referencers.RemoveAll([this](const FName& Ref) { - return Ref.ToString() == AssetPath; - }); - - if ((Referencers.Num() > 0) && !Force) - { - // Classify references as "live" (loaded in memory) vs "stale" (only on disk) - TArray> LiveRefs; - TArray> StaleRefs; - for (const FName& Ref : Referencers) - { - FString RefStr = Ref.ToString(); - UPackage* RefPackage = FindPackage(nullptr, *RefStr); - if (RefPackage) - { - LiveRefs.Add(MakeShared(RefStr)); - } - else - { - StaleRefs.Add(MakeShared(RefStr)); - } - } - - MCPUtils::MakeErrorJson(Result, TEXT("Asset is still referenced. Remove all references first.")); - Result->SetStringField(TEXT("assetPath"), AssetPath); - Result->SetNumberField(TEXT("referencerCount"), Referencers.Num()); - Result->SetNumberField(TEXT("liveReferencerCount"), LiveRefs.Num()); - Result->SetArrayField(TEXT("liveReferencers"), LiveRefs); - Result->SetNumberField(TEXT("staleReferencerCount"), StaleRefs.Num()); - Result->SetArrayField(TEXT("staleReferencers"), StaleRefs); - Result->SetStringField(TEXT("suggestion"), - StaleRefs.Num() > 0 - ? TEXT("Some references may be stale. Consider force=true to skip the reference check, or use change_variable_type to migrate references first.") - : TEXT("All references are live. Migrate with change_variable_type or replace_function_calls before deleting.")); - return; - } - - // Force delete: unload the package from memory first - TArray> RefWarnings; - if (Force) - { - // Collect reference warnings when force-deleting with existing references - for (const FName& Ref : Referencers) - { - RefWarnings.Add(MakeShared( - FString::Printf(TEXT("Warning: '%s' still references this asset"), *Ref.ToString()))); - } - - UPackage* Package = FindPackage(nullptr, *AssetPath); - if (Package) - { - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Force-unloading package '%s' from memory"), *AssetPath); - - // Collect all objects in this package - TArray ObjectsInPackage; - GetObjectsWithPackage(Package, ObjectsInPackage); - - // Clear flags and remove from root to allow GC - for (UObject* Obj : ObjectsInPackage) - { - if (Obj) - { - Obj->ClearFlags(RF_Standalone | RF_Public); - Obj->RemoveFromRoot(); - } - } - Package->ClearFlags(RF_Standalone | RF_Public); - Package->RemoveFromRoot(); - - // Reset loaders to release file handles - ResetLoaders(Package); - // Force garbage collection to free the objects - CollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS); - } - } - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Deleting asset '%s' (%s)%s"), - *AssetPath, *PackageFilename, Force ? TEXT(" [FORCE]") : TEXT("")); - - // Delete the file on disk - bool bDeleted = IFileManager::Get().Delete(*PackageFilename, false, true); - - if (bDeleted) - { - // Trigger an asset registry rescan so it notices the deletion - TArray PathsToScan; - int32 LastSlash; - if (AssetPath.FindLastChar(TEXT('/'), LastSlash)) - { - PathsToScan.Add(AssetPath.Left(LastSlash)); - } - if (PathsToScan.Num() > 0) - { - Registry.ScanPathsSynchronous(PathsToScan, true); - } - } - - Result->SetStringField(TEXT("filename"), PackageFilename); - if (!bDeleted) - { - MCPUtils::MakeErrorJson(Result, TEXT("Failed to delete file from disk")); - } - if (RefWarnings.Num() > 0) - { - Result->SetArrayField(TEXT("warnings"), RefWarnings); - } - } -}; - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -UCLASS() -class UMCPHandler_RenameAsset : public UObject, public IMCPHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Description="Current package path of the asset")) - FString AssetPath; - - UPROPERTY(meta=(Description="New package path or new asset name")) - FString NewPath; - - virtual FString GetDescription() const override - { - return TEXT("Rename or move an asset with reference fixup."); - } - - virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override - { - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Renaming asset '%s' -> '%s'"), *AssetPath, *NewPath); - - // Use FAssetToolsModule to perform the rename with reference fixup - FAssetToolsModule& AssetToolsModule = FModuleManager::LoadModuleChecked("AssetTools"); - IAssetTools& AssetTools = AssetToolsModule.Get(); - - // Build the source/dest arrays - TArray RenameData; - - // We need to load the asset to get the object - MCPAssets Assets; - if (!Assets.Exact(AssetPath).Errors(Result).ENone().ETwo().Load()) return; - UObject* AssetObj = Assets.Object(); - - // Parse new path into package path and asset name - FString NewPackagePath, NewAssetName; - int32 LastSlash; - if (NewPath.FindLastChar(TEXT('/'), LastSlash)) - { - NewPackagePath = NewPath.Left(LastSlash); - NewAssetName = NewPath.Mid(LastSlash + 1); - } - else - { - // If no slash, assume same directory with new name - FString OldPackagePath; - if (AssetPath.FindLastChar(TEXT('/'), LastSlash)) - { - OldPackagePath = AssetPath.Left(LastSlash); - } - NewPackagePath = OldPackagePath; - NewAssetName = NewPath; - } - - FAssetRenameData RenameEntry(AssetObj, NewPackagePath, NewAssetName); - RenameData.Add(RenameEntry); - - bool bSuccess = AssetTools.RenameAssets(RenameData); - - if (bSuccess) - { - } - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Rename %s"), bSuccess ? TEXT("succeeded") : TEXT("failed")); - - Result->SetStringField(TEXT("newPackagePath"), NewPackagePath); - Result->SetStringField(TEXT("newAssetName"), NewAssetName); - if (!bSuccess) - { - MCPUtils::MakeErrorJson(Result, TEXT("Asset rename failed. The target path may be invalid or a conflicting asset may exist.")); - } - } -}; - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -UCLASS() -class UMCPHandler_BackupAsset : public UObject, public IMCPHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Description="Full package path of the asset (e.g. /Game/Widgets/WB_Hotkeys)")) - FString AssetPath; - - virtual FString GetDescription() const override - { - return TEXT("Copy an asset's .uasset file to a .uasset.bak backup."); - } - - virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override - { - FString Filename = FPaths::ConvertRelativePathToFull( - FPackageName::LongPackageNameToFilename(AssetPath, FPackageName::GetAssetPackageExtension())); - - if (!IFileManager::Get().FileExists(*Filename)) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Asset file not found: %s"), *Filename)); - } - - FString BackupFilename = Filename + TEXT(".bak"); - uint32 CopyResult = IFileManager::Get().Copy(*BackupFilename, *Filename, true); - if (CopyResult != COPY_OK) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Failed to back up %s"), *Filename)); - } - - Result->SetStringField(TEXT("backupFile"), BackupFilename); - } -}; - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -UCLASS() -class UMCPHandler_RestoreAsset : public UObject, public IMCPHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Description="Full package path of the asset (e.g. /Game/Widgets/WB_Hotkeys)")) - FString AssetPath; - - virtual FString GetDescription() const override - { - return TEXT("Restore a .uasset file from its .uasset.bak backup, reloading it in the editor."); - } - - virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override - { - FString Filename = FPaths::ConvertRelativePathToFull( - FPackageName::LongPackageNameToFilename(AssetPath, FPackageName::GetAssetPackageExtension())); - FString BackupFilename = Filename + TEXT(".bak"); - - if (!IFileManager::Get().FileExists(*BackupFilename)) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Backup file not found: %s"), *BackupFilename)); - } - - // Release file handles if the package is loaded - UPackage* Package = FindPackage(nullptr, *AssetPath); - if (Package) - { - ResetLoaders(Package); - } - - // Copy backup over the original - uint32 CopyResult = IFileManager::Get().Copy(*Filename, *BackupFilename, true); - if (CopyResult != COPY_OK) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Failed to restore %s"), *Filename)); - } - - // Reload the package if it was loaded - if (Package) - { - bool bReloaded = false; - FText ErrorMessage; - UEditorLoadingAndSavingUtils::ReloadPackages({Package}, bReloaded, ErrorMessage, EReloadPackagesInteractionMode::AssumePositive); - } - - Result->SetStringField(TEXT("restoredFrom"), BackupFilename); - } -}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/MCPHandlers_Components.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/MCPHandlers_Components.h deleted file mode 100644 index c459928d..00000000 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/MCPHandlers_Components.h +++ /dev/null @@ -1,361 +0,0 @@ -#pragma once - -#include "CoreMinimal.h" -#include "MCPHandler.h" -#include "MCPAssetFinder.h" -#include "MCPUtils.h" -#include "Engine/Blueprint.h" -#include "Engine/SimpleConstructionScript.h" -#include "Engine/SCS_Node.h" -#include "Components/ActorComponent.h" -#include "Kismet2/BlueprintEditorUtils.h" -#include "UObject/UObjectIterator.h" -#include "MCPHandlers_Components.generated.h" - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -UCLASS() -class UMCPHandler_ListBlueprintComponents : public UObject, public IMCPHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Description="Blueprint name or package path")) - FString Blueprint; - - virtual FString GetDescription() const override - { - return TEXT("List all components in a Blueprint's SimpleConstructionScript, " - "including hierarchy and scene root information."); - } - - virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override - { - MCPAssets Assets; - if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; - UBlueprint* BP = Assets.Object(); - - USimpleConstructionScript* SCS = BP->SimpleConstructionScript; - if (!SCS) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf( - TEXT("Blueprint '%s' does not have a SimpleConstructionScript (not an Actor Blueprint)"), - *Blueprint)); - } - - const TArray& AllNodes = SCS->GetAllNodes(); - - TArray> ComponentsArr; - for (USCS_Node* Node : AllNodes) - { - if (!Node) - { - continue; - } - - TSharedRef CompObj = MakeShared(); - CompObj->SetStringField(TEXT("name"), Node->GetVariableName().ToString()); - - if (Node->ComponentClass) - { - CompObj->SetStringField(TEXT("componentClass"), Node->ComponentClass->GetName()); - } - else - { - CompObj->SetStringField(TEXT("componentClass"), TEXT("None")); - } - - // Parent component info - USCS_Node* ParentNode = nullptr; - for (USCS_Node* Candidate : AllNodes) - { - if (Candidate && Candidate->GetChildNodes().Contains(Node)) - { - ParentNode = Candidate; - break; - } - } - - if (ParentNode) - { - CompObj->SetStringField(TEXT("parentComponent"), ParentNode->GetVariableName().ToString()); - } - - // Check if this is a default scene root (first root node with SceneComponent class) - bool bIsSceneRoot = false; - const TArray& RootNodes = SCS->GetRootNodes(); - if (RootNodes.Num() > 0 && RootNodes[0] == Node) - { - bIsSceneRoot = true; - } - CompObj->SetBoolField(TEXT("isSceneRoot"), bIsSceneRoot); - - // List child count for informational purposes - CompObj->SetNumberField(TEXT("childCount"), Node->GetChildNodes().Num()); - - ComponentsArr.Add(MakeShared(CompObj)); - } - - Result->SetNumberField(TEXT("count"), ComponentsArr.Num()); - Result->SetArrayField(TEXT("components"), ComponentsArr); - } -}; - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -UCLASS() -class UMCPHandler_AddBlueprintComponent : public UObject, public IMCPHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Description="Blueprint name or package path")) - FString Blueprint; - - UPROPERTY(meta=(Description="Component class name (e.g. StaticMeshComponent, SceneComponent)")) - FString ComponentClass; - - UPROPERTY(meta=(Description="Component name for the new component")) - FString Component; - - UPROPERTY(meta=(Optional, Description="Name of the parent component to attach to")) - FString ParentComponent; - - virtual FString GetDescription() const override - { - return TEXT("Add a component to a Blueprint's SimpleConstructionScript. " - "Optionally attach it to an existing parent component."); - } - - virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override - { - MCPAssets Assets; - if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; - UBlueprint* BP = Assets.Object(); - - USimpleConstructionScript* SCS = BP->SimpleConstructionScript; - if (!SCS) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf( - TEXT("Blueprint '%s' does not have a SimpleConstructionScript (not an Actor Blueprint)"), - *Blueprint)); - } - - // Check for duplicate component names - const TArray& ExistingNodes = SCS->GetAllNodes(); - for (USCS_Node* Existing : ExistingNodes) - { - if (Existing && Existing->GetVariableName().ToString().Equals(Component, ESearchCase::IgnoreCase)) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf( - TEXT("A component named '%s' already exists in Blueprint '%s'"), - *Component, *Blueprint)); - } - } - - // Resolve the component class by name - // Try multiple name variants: exact name, with U prefix, without U prefix - UClass* ComponentClassObj = nullptr; - - TArray NamesToTry; - NamesToTry.Add(ComponentClass); - if (!ComponentClass.StartsWith(TEXT("U"))) - { - NamesToTry.Add(FString::Printf(TEXT("U%s"), *ComponentClass)); - } - else - { - // Also try without U prefix - NamesToTry.Add(ComponentClass.Mid(1)); - } - - for (TObjectIterator It; It; ++It) - { - if (!It->IsChildOf(UActorComponent::StaticClass())) - { - continue; - } - - FString ClassName = It->GetName(); - for (const FString& NameToTry : NamesToTry) - { - if (ClassName.Equals(NameToTry, ESearchCase::IgnoreCase)) - { - ComponentClassObj = *It; - break; - } - } - - if (ComponentClassObj) - { - break; - } - } - - if (!ComponentClassObj) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf( - TEXT("Component class '%s' not found or is not a subclass of UActorComponent. " - "Common classes: StaticMeshComponent, SkeletalMeshComponent, AudioComponent, " - "SceneComponent, BoxCollisionComponent, SphereCollisionComponent, CapsuleComponent, " - "ArrowComponent, ChildActorComponent, SpotLightComponent, PointLightComponent, " - "WidgetComponent, BillboardComponent"), - *ComponentClass)); - } - - // If parent component specified, find its SCS node - USCS_Node* ParentSCSNode = nullptr; - if (!ParentComponent.IsEmpty()) - { - for (USCS_Node* Node : ExistingNodes) - { - if (Node && Node->GetVariableName().ToString().Equals(ParentComponent, ESearchCase::IgnoreCase)) - { - ParentSCSNode = Node; - break; - } - } - - if (!ParentSCSNode) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf( - TEXT("Parent component '%s' not found in Blueprint '%s'"), - *ParentComponent, *Blueprint)); - } - } - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Adding component '%s' (%s) to Blueprint '%s'"), - *Component, *ComponentClassObj->GetName(), *Blueprint); - - // Create the SCS node - USCS_Node* NewNode = SCS->CreateNode(ComponentClassObj, FName(*Component)); - if (!NewNode) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf( - TEXT("Failed to create SCS node for component '%s' with class '%s'"), - *Component, *ComponentClassObj->GetName())); - } - - // Add to the hierarchy - if (ParentSCSNode) - { - ParentSCSNode->AddChildNode(NewNode); - } - else - { - SCS->AddNode(NewNode); - } - - FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP); - bool bSaved = MCPUtils::SaveBlueprintPackage(BP); - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Added component '%s' (%s) to '%s' (parent: %s, saved: %s)"), - *Component, *ComponentClassObj->GetName(), *Blueprint, - ParentSCSNode ? *ParentComponent : TEXT("(root)"), - bSaved ? TEXT("true") : TEXT("false")); - - Result->SetStringField(TEXT("component"), NewNode->GetVariableName().ToString()); - Result->SetStringField(TEXT("componentClass"), ComponentClassObj->GetName()); - if (ParentSCSNode) - { - Result->SetStringField(TEXT("parentComponent"), ParentSCSNode->GetVariableName().ToString()); - } - Result->SetBoolField(TEXT("saved"), bSaved); - } -}; - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -UCLASS() -class UMCPHandler_RemoveBlueprintComponent : public UObject, public IMCPHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Description="Blueprint name or package path")) - FString Blueprint; - - UPROPERTY(meta=(Description="Component to remove")) - FString Component; - - virtual FString GetDescription() const override - { - return TEXT("Remove a component from a Blueprint's SimpleConstructionScript."); - } - - virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override - { - MCPAssets Assets; - if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; - UBlueprint* BP = Assets.Object(); - - USimpleConstructionScript* SCS = BP->SimpleConstructionScript; - if (!SCS) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf( - TEXT("Blueprint '%s' does not have a SimpleConstructionScript (not an Actor Blueprint)"), - *Blueprint)); - } - - // Find the node to remove - USCS_Node* NodeToRemove = nullptr; - const TArray& AllNodes = SCS->GetAllNodes(); - for (USCS_Node* Node : AllNodes) - { - if (Node && Node->GetVariableName().ToString().Equals(Component, ESearchCase::IgnoreCase)) - { - NodeToRemove = Node; - break; - } - } - - if (!NodeToRemove) - { - // Build list of component names for the error message - TArray> CompList; - for (USCS_Node* Node : AllNodes) - { - if (Node) - { - CompList.Add(MakeShared(Node->GetVariableName().ToString())); - } - } - - MCPUtils::MakeErrorJson(Result, FString::Printf( - TEXT("Component '%s' not found in Blueprint '%s'"), - *Component, *Blueprint)); - Result->SetArrayField(TEXT("existingComponents"), CompList); - return; - } - - // Prevent removing the root scene component if it has children - const TArray& RootNodes = SCS->GetRootNodes(); - if (RootNodes.Contains(NodeToRemove) && NodeToRemove->GetChildNodes().Num() > 0) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf( - TEXT("Cannot remove component '%s' because it is a root component with %d child(ren). " - "Remove or re-parent the children first."), - *Component, NodeToRemove->GetChildNodes().Num())); - } - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Removing component '%s' from Blueprint '%s'"), - *Component, *Blueprint); - - // Remove the node (promotes children to parent if it has any — but we've guarded root above) - SCS->RemoveNodeAndPromoteChildren(NodeToRemove); - - FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP); - bool bSaved = MCPUtils::SaveBlueprintPackage(BP); - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Removed component '%s' from '%s' (saved: %s)"), - *Component, *Blueprint, bSaved ? TEXT("true") : TEXT("false")); - - Result->SetBoolField(TEXT("saved"), bSaved); - } -}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/MCPHandlers_Discovery.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/MCPHandlers_Discovery.h deleted file mode 100644 index f4ea0e0e..00000000 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/MCPHandlers_Discovery.h +++ /dev/null @@ -1,645 +0,0 @@ -#pragma once - -#include "CoreMinimal.h" -#include "MCPHandler.h" -#include "MCPAssetFinder.h" -#include "MCPUtils.h" -#include "Engine/Blueprint.h" -#include "EdGraph/EdGraph.h" -#include "EdGraph/EdGraphNode.h" -#include "EdGraph/EdGraphPin.h" -#include "EdGraphSchema_K2.h" -#include "UObject/UObjectIterator.h" -#include "MCPHandlers_Discovery.generated.h" - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -// ============================================================ -// HandleGetPinInfo — detailed information about a specific pin -// ============================================================ - -UCLASS() -class UMCPHandler_GetPinDetails : public UObject, public IMCPHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Description="Blueprint name or package path")) - FString Blueprint; - - UPROPERTY(meta=(Description="Node to look up (GUID)")) - FString Node; - - UPROPERTY(meta=(Description="Pin name on the node")) - FString PinName; - - virtual FString GetDescription() const override - { - return TEXT("Get detailed information about a specific pin on a blueprint node, " - "including type, connections, and default values."); - } - - virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override - { - MCPAssets Assets; - if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; - UBlueprint* BP = Assets.Object(); - - UEdGraph* Graph = nullptr; - UEdGraphNode* FoundNode = MCPUtils::FindNodeByGuid(BP, Node, &Graph); - if (!FoundNode) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found"), *Node)); - } - - UEdGraphPin* Pin = FoundNode->FindPin(FName(*PinName)); - if (!Pin) - { - // List available pins - TArray> AvailPins; - for (UEdGraphPin* P : FoundNode->Pins) - { - if (P) - { - TSharedRef PinObj = MakeShared(); - PinObj->SetStringField(TEXT("name"), P->PinName.ToString()); - PinObj->SetStringField(TEXT("direction"), P->Direction == EGPD_Input ? TEXT("Input") : TEXT("Output")); - PinObj->SetStringField(TEXT("type"), P->PinType.PinCategory.ToString()); - AvailPins.Add(MakeShared(PinObj)); - } - } - MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Pin '%s' not found on node '%s'"), *PinName, *Node)); - Result->SetArrayField(TEXT("availablePins"), AvailPins); - return; - } - - Result->SetStringField(TEXT("pinName"), Pin->PinName.ToString()); - Result->SetStringField(TEXT("direction"), Pin->Direction == EGPD_Input ? TEXT("Input") : TEXT("Output")); - Result->SetStringField(TEXT("type"), Pin->PinType.PinCategory.ToString()); - - if (!Pin->PinType.PinSubCategory.IsNone()) - { - Result->SetStringField(TEXT("subCategory"), Pin->PinType.PinSubCategory.ToString()); - } - if (Pin->PinType.PinSubCategoryObject.IsValid()) - { - Result->SetStringField(TEXT("subtype"), Pin->PinType.PinSubCategoryObject->GetName()); - } - - Result->SetBoolField(TEXT("isArray"), Pin->PinType.IsArray()); - Result->SetBoolField(TEXT("isSet"), Pin->PinType.IsSet()); - Result->SetBoolField(TEXT("isMap"), Pin->PinType.IsMap()); - Result->SetBoolField(TEXT("isReference"), Pin->PinType.bIsReference); - Result->SetBoolField(TEXT("isConst"), Pin->PinType.bIsConst); - - if (!Pin->DefaultValue.IsEmpty()) - { - Result->SetStringField(TEXT("defaultValue"), Pin->DefaultValue); - } - if (!Pin->DefaultTextValue.IsEmpty()) - { - Result->SetStringField(TEXT("defaultTextValue"), Pin->DefaultTextValue.ToString()); - } - if (Pin->DefaultObject) - { - Result->SetStringField(TEXT("defaultObject"), Pin->DefaultObject->GetPathName()); - } - - // Connected pins - if (Pin->LinkedTo.Num() > 0) - { - TArray> Conns; - for (UEdGraphPin* Linked : Pin->LinkedTo) - { - if (!Linked || !Linked->GetOwningNode()) continue; - TSharedRef CJ = MakeShared(); - CJ->SetStringField(TEXT("nodeId"), Linked->GetOwningNode()->NodeGuid.ToString()); - CJ->SetStringField(TEXT("pinName"), Linked->PinName.ToString()); - CJ->SetStringField(TEXT("nodeTitle"), Linked->GetOwningNode()->GetNodeTitle(ENodeTitleType::FullTitle).ToString()); - Conns.Add(MakeShared(CJ)); - } - Result->SetArrayField(TEXT("connectedTo"), Conns); - } - } -}; - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -// ============================================================ -// HandleCheckPinCompatibility — pre-flight check for connect_pins -// ============================================================ - -UCLASS() -class UMCPHandler_CheckPinConnectionCompatibility : public UObject, public IMCPHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Description="Blueprint name or package path")) - FString Blueprint; - - UPROPERTY(meta=(Description="Source node (GUID)")) - FString SourceNode; - - UPROPERTY(meta=(Description="Source pin name")) - FString SourcePinName; - - UPROPERTY(meta=(Description="Target node (GUID)")) - FString TargetNode; - - UPROPERTY(meta=(Description="Target pin name")) - FString TargetPinName; - - virtual FString GetDescription() const override - { - return TEXT("Check whether two pins can be connected, and what kind of connection would result. " - "Use as a pre-flight check before connect_blueprint_pins."); - } - - virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override - { - MCPAssets Assets; - if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; - UBlueprint* BP = Assets.Object(); - - UEdGraph* SourceGraph = nullptr; - UEdGraphNode* FoundSourceNode = MCPUtils::FindNodeByGuid(BP, SourceNode, &SourceGraph); - if (!FoundSourceNode) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Source node '%s' not found"), *SourceNode)); - } - - UEdGraphNode* FoundTargetNode = MCPUtils::FindNodeByGuid(BP, TargetNode); - if (!FoundTargetNode) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Target node '%s' not found"), *TargetNode)); - } - - UEdGraphPin* SourcePin = FoundSourceNode->FindPin(FName(*SourcePinName)); - if (!SourcePin) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Source pin '%s' not found on node '%s'"), *SourcePinName, *SourceNode)); - } - - UEdGraphPin* TargetPin = FoundTargetNode->FindPin(FName(*TargetPinName)); - if (!TargetPin) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Target pin '%s' not found on node '%s'"), *TargetPinName, *TargetNode)); - } - - const UEdGraphSchema* Schema = SourceGraph ? SourceGraph->GetSchema() : nullptr; - if (!Schema) - { - return MCPUtils::MakeErrorJson(Result, TEXT("Graph schema not found")); - } - - // Check compatibility using the schema - const FPinConnectionResponse Response = Schema->CanCreateConnection(SourcePin, TargetPin); - - bool bCompatible = (Response.Response != ECanCreateConnectionResponse::CONNECT_RESPONSE_DISALLOW); - Result->SetBoolField(TEXT("compatible"), bCompatible); - - // Decode the response type - FString ResponseType; - switch (Response.Response) - { - case ECanCreateConnectionResponse::CONNECT_RESPONSE_MAKE: - ResponseType = TEXT("direct"); - break; - case ECanCreateConnectionResponse::CONNECT_RESPONSE_BREAK_OTHERS_A: - ResponseType = TEXT("breakSourceConnections"); - break; - case ECanCreateConnectionResponse::CONNECT_RESPONSE_BREAK_OTHERS_B: - ResponseType = TEXT("breakTargetConnections"); - break; - case ECanCreateConnectionResponse::CONNECT_RESPONSE_BREAK_OTHERS_AB: - ResponseType = TEXT("breakBothConnections"); - break; - case ECanCreateConnectionResponse::CONNECT_RESPONSE_MAKE_WITH_CONVERSION_NODE: - ResponseType = TEXT("requiresConversion"); - break; - case ECanCreateConnectionResponse::CONNECT_RESPONSE_MAKE_WITH_PROMOTION: - ResponseType = TEXT("requiresPromotion"); - break; - case ECanCreateConnectionResponse::CONNECT_RESPONSE_DISALLOW: - default: - ResponseType = TEXT("disallowed"); - break; - } - Result->SetStringField(TEXT("connectionType"), ResponseType); - - if (!Response.Message.IsEmpty()) - { - Result->SetStringField(TEXT("message"), Response.Message.ToString()); - } - - // Include pin type info for context - Result->SetStringField(TEXT("sourcePinType"), SourcePin->PinType.PinCategory.ToString()); - if (SourcePin->PinType.PinSubCategoryObject.IsValid()) - Result->SetStringField(TEXT("sourcePinSubtype"), SourcePin->PinType.PinSubCategoryObject->GetName()); - Result->SetStringField(TEXT("targetPinType"), TargetPin->PinType.PinCategory.ToString()); - if (TargetPin->PinType.PinSubCategoryObject.IsValid()) - Result->SetStringField(TEXT("targetPinSubtype"), TargetPin->PinType.PinSubCategoryObject->GetName()); - } -}; - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -// ============================================================ -// HandleListClasses — discover available UClasses -// ============================================================ - -UCLASS() -class UMCPHandler_SearchUnrealClasses : public UObject, public IMCPHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Optional, Description="Substring filter for class names")) - FString Filter; - - UPROPERTY(meta=(Optional, Description="Parent class name to restrict results to subclasses")) - FString ParentClass; - - UPROPERTY(meta=(Optional, Description="Maximum number of results to return (1-500, default 100)")) - int32 Limit = 100; - - virtual FString GetDescription() const override - { - return TEXT("Search for available UClasses by name substring and/or parent class. " - "Returns class metadata including flags, parent class, and package."); - } - - virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override - { - if (Json->HasField(TEXT("limit"))) - { - Limit = FMath::Clamp(Limit, 1, 500); - } - - UClass* ParentClassObj = nullptr; - if (!ParentClass.IsEmpty()) - { - for (TObjectIterator It; It; ++It) - { - if (It->GetName() == ParentClass || It->GetName() == ParentClass + TEXT("_C")) - { - ParentClassObj = *It; - break; - } - } - if (!ParentClassObj) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Parent class '%s' not found"), *ParentClass)); - } - } - - TArray> ClassList; - int32 TotalMatched = 0; - - for (TObjectIterator It; It; ++It) - { - UClass* Class = *It; - if (!Class) continue; - - // Skip internal/deprecated classes - if (Class->HasAnyClassFlags(CLASS_Deprecated | CLASS_NewerVersionExists)) continue; - - // Apply parent filter - if (ParentClassObj && !Class->IsChildOf(ParentClassObj)) continue; - - FString ClassName = Class->GetName(); - - // Apply name filter - if (!Filter.IsEmpty() && !ClassName.Contains(Filter, ESearchCase::IgnoreCase)) - { - continue; - } - - TotalMatched++; - if (ClassList.Num() >= Limit) continue; // Count but don't add beyond limit - - TSharedRef ClassObj = MakeShared(); - ClassObj->SetStringField(TEXT("name"), ClassName); - ClassObj->SetStringField(TEXT("fullPath"), Class->GetPathName()); - - // Determine if it's a Blueprint-generated class - bool bIsBlueprint = Class->ClassGeneratedBy != nullptr; - ClassObj->SetBoolField(TEXT("isBlueprint"), bIsBlueprint); - - // Parent class - if (Class->GetSuperClass()) - { - ClassObj->SetStringField(TEXT("parentClass"), Class->GetSuperClass()->GetName()); - } - - // Module/package info - UPackage* Package = Class->GetOuterUPackage(); - if (Package) - { - ClassObj->SetStringField(TEXT("package"), Package->GetName()); - } - - // Flags - TArray> Flags; - if (Class->HasAnyClassFlags(CLASS_Abstract)) Flags.Add(MakeShared(TEXT("Abstract"))); - if (Class->HasAnyClassFlags(CLASS_Interface)) Flags.Add(MakeShared(TEXT("Interface"))); - if (Class->HasAnyClassFlags(CLASS_MinimalAPI)) Flags.Add(MakeShared(TEXT("MinimalAPI"))); - if (Flags.Num() > 0) - { - ClassObj->SetArrayField(TEXT("flags"), Flags); - } - - ClassList.Add(MakeShared(ClassObj)); - } - - Result->SetNumberField(TEXT("count"), ClassList.Num()); - Result->SetNumberField(TEXT("totalMatched"), TotalMatched); - if (TotalMatched > Limit) - { - Result->SetBoolField(TEXT("truncated"), true); - Result->SetNumberField(TEXT("limit"), Limit); - } - Result->SetArrayField(TEXT("classes"), ClassList); - } -}; - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -// ============================================================ -// HandleListFunctions — list Blueprint-callable functions on a class -// ============================================================ - -UCLASS() -class UMCPHandler_ListClassFunctions : public UObject, public IMCPHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Description="Class name to list functions for")) - FString ClassName; - - UPROPERTY(meta=(Optional, Description="Substring filter for function names")) - FString Filter; - - virtual FString GetDescription() const override - { - return TEXT("List Blueprint-callable functions on a UClass, including parameter info and flags."); - } - - virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override - { - // Find the class - UClass* FoundClass = nullptr; - for (TObjectIterator It; It; ++It) - { - if (It->GetName() == ClassName || It->GetName() == ClassName + TEXT("_C")) - { - FoundClass = *It; - break; - } - } - if (!FoundClass) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Class '%s' not found"), *ClassName)); - } - - TArray> FuncList; - - for (TFieldIterator FuncIt(FoundClass); FuncIt; ++FuncIt) - { - UFunction* Func = *FuncIt; - if (!Func) continue; - - // Only include Blueprint-callable functions - if (!Func->HasAnyFunctionFlags(FUNC_BlueprintCallable | FUNC_BlueprintPure | FUNC_BlueprintEvent)) continue; - - FString FuncName = Func->GetName(); - - // Apply filter - if (!Filter.IsEmpty() && !FuncName.Contains(Filter, ESearchCase::IgnoreCase)) - { - continue; - } - - TSharedRef FuncObj = MakeShared(); - FuncObj->SetStringField(TEXT("name"), FuncName); - - // Determine the owning class - UClass* OwnerClass = Func->GetOwnerClass(); - if (OwnerClass) - { - FuncObj->SetStringField(TEXT("definedIn"), OwnerClass->GetName()); - } - - // Function flags - FuncObj->SetBoolField(TEXT("isPure"), Func->HasAnyFunctionFlags(FUNC_BlueprintPure)); - FuncObj->SetBoolField(TEXT("isStatic"), Func->HasAnyFunctionFlags(FUNC_Static)); - FuncObj->SetBoolField(TEXT("isEvent"), Func->HasAnyFunctionFlags(FUNC_BlueprintEvent)); - FuncObj->SetBoolField(TEXT("isConst"), Func->HasAnyFunctionFlags(FUNC_Const)); - - // Parameters - TArray> Params; - FString ReturnType; - for (TFieldIterator PropIt(Func); PropIt; ++PropIt) - { - FProperty* Prop = *PropIt; - if (!Prop) continue; - - FString PropType = Prop->GetCPPType(); - - if (Prop->HasAnyPropertyFlags(CPF_ReturnParm)) - { - ReturnType = PropType; - continue; - } - - if (Prop->HasAnyPropertyFlags(CPF_Parm)) - { - TSharedRef ParamObj = MakeShared(); - ParamObj->SetStringField(TEXT("name"), Prop->GetName()); - ParamObj->SetStringField(TEXT("type"), PropType); - ParamObj->SetBoolField(TEXT("isOutput"), Prop->HasAnyPropertyFlags(CPF_OutParm) && !Prop->HasAnyPropertyFlags(CPF_ReferenceParm)); - ParamObj->SetBoolField(TEXT("isReference"), Prop->HasAnyPropertyFlags(CPF_ReferenceParm)); - Params.Add(MakeShared(ParamObj)); - } - } - FuncObj->SetArrayField(TEXT("parameters"), Params); - if (!ReturnType.IsEmpty()) - { - FuncObj->SetStringField(TEXT("returnType"), ReturnType); - } - - FuncList.Add(MakeShared(FuncObj)); - } - - Result->SetStringField(TEXT("className"), FoundClass->GetName()); - Result->SetNumberField(TEXT("count"), FuncList.Num()); - Result->SetArrayField(TEXT("functions"), FuncList); - } -}; - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -// ============================================================ -// HandleListProperties — list properties on a class -// ============================================================ - -UCLASS() -class UMCPHandler_ListClassProperties : public UObject, public IMCPHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Description="Class name to list properties for")) - FString ClassName; - - UPROPERTY(meta=(Optional, Description="Substring filter for property names")) - FString Filter; - - virtual FString GetDescription() const override - { - return TEXT("List properties on a UClass, including type, owning class, and property flags."); - } - - virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override - { - // Find the class - UClass* FoundClass = nullptr; - for (TObjectIterator It; It; ++It) - { - if (It->GetName() == ClassName || It->GetName() == ClassName + TEXT("_C")) - { - FoundClass = *It; - break; - } - } - if (!FoundClass) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Class '%s' not found"), *ClassName)); - } - - TArray> PropList; - - for (TFieldIterator PropIt(FoundClass); PropIt; ++PropIt) - { - FProperty* Prop = *PropIt; - if (!Prop) continue; - - FString PropName = Prop->GetName(); - - // Apply filter - if (!Filter.IsEmpty() && !PropName.Contains(Filter, ESearchCase::IgnoreCase)) - { - continue; - } - - TSharedRef PropObj = MakeShared(); - PropObj->SetStringField(TEXT("name"), PropName); - PropObj->SetStringField(TEXT("type"), Prop->GetCPPType()); - - // Determine the owning class - UClass* OwnerClass = Prop->GetOwnerClass(); - if (OwnerClass) - { - PropObj->SetStringField(TEXT("definedIn"), OwnerClass->GetName()); - } - - // Property flags - TArray> Flags; - if (Prop->HasAnyPropertyFlags(CPF_BlueprintVisible)) Flags.Add(MakeShared(TEXT("BlueprintVisible"))); - if (Prop->HasAnyPropertyFlags(CPF_BlueprintReadOnly)) Flags.Add(MakeShared(TEXT("BlueprintReadOnly"))); - if (Prop->HasAnyPropertyFlags(CPF_Edit)) Flags.Add(MakeShared(TEXT("EditAnywhere"))); - if (Prop->HasAnyPropertyFlags(CPF_EditConst)) Flags.Add(MakeShared(TEXT("VisibleOnly"))); - if (Prop->HasAnyPropertyFlags(CPF_Config)) Flags.Add(MakeShared(TEXT("Config"))); - if (Prop->HasAnyPropertyFlags(CPF_SaveGame)) Flags.Add(MakeShared(TEXT("SaveGame"))); - if (Prop->HasAnyPropertyFlags(CPF_Transient)) Flags.Add(MakeShared(TEXT("Transient"))); - if (Prop->HasAnyPropertyFlags(CPF_RepNotify)) Flags.Add(MakeShared(TEXT("RepNotify"))); - if (Flags.Num() > 0) - { - PropObj->SetArrayField(TEXT("flags"), Flags); - } - - PropList.Add(MakeShared(PropObj)); - } - - Result->SetStringField(TEXT("className"), FoundClass->GetName()); - Result->SetNumberField(TEXT("count"), PropList.Num()); - Result->SetArrayField(TEXT("properties"), PropList); - } -}; - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -UCLASS() -class UMCPHandler_ShowCommands : public UObject, public IMCPHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Optional, Description="If true, return full details including parameter types and descriptions")) - bool Verbose = false; - - virtual FString GetDescription() const override - { - return TEXT("List all available commands with their descriptions."); - } - - // Collect all handler classes sorted by tool name. - TArray> CollectHandlers() const - { - TArray> Handlers; - for (TObjectIterator It; It; ++It) - { - UClass* Class = *It; - if (Class->HasAnyClassFlags(CLASS_Abstract)) continue; - const IMCPHandler* Handler = Cast(Class->GetDefaultObject()); - if (!Handler) continue; - FString ToolName = MCPUtils::GetToolName(Class); - Handlers.Add({ToolName, Class}); - } - Handlers.Sort(); - return Handlers; - } - - virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override - { - auto Handlers = CollectHandlers(); - Result.Appendf(TEXT("%d commands:\n"), Handlers.Num()); - - for (const auto& Pair : Handlers) - { - if (Verbose) - { - MCPUtils::FormatCommandHelp(Pair.Value, Result); - continue; - } - - // Non-verbose: just the signature line - UClass* Class = Pair.Value; - Result.Append(Pair.Key); - Result.Append(TEXT("(")); - bool bFirst = true; - for (TFieldIterator PropIt(Class, EFieldIterationFlags::None); PropIt; ++PropIt) - { - if (!bFirst) Result.Append(TEXT(",")); - bFirst = false; - if (PropIt->HasMetaData(TEXT("Optional"))) Result.Append(TEXT("?")); - Result.Append(MCPUtils::PropertyNameToJsonKey(PropIt->GetName())); - } - Result.Append(TEXT(")\n")); - } - } -}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/MCPHandlers_Graphs.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/MCPHandlers_Graphs.h deleted file mode 100644 index f03b1362..00000000 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/MCPHandlers_Graphs.h +++ /dev/null @@ -1,554 +0,0 @@ -#pragma once - -#include "CoreMinimal.h" -#include "MCPHandler.h" -#include "MCPAssetFinder.h" -#include "MCPUtils.h" -#include "Engine/Blueprint.h" -#include "EdGraph/EdGraph.h" -#include "EdGraph/EdGraphNode.h" -#include "EdGraphSchema_K2.h" -#include "K2Node_CustomEvent.h" -#include "Kismet2/BlueprintEditorUtils.h" -#include "Kismet2/KismetEditorUtilities.h" -#include "UObject/UObjectIterator.h" -#include "MCPHandlers_Graphs.generated.h" - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -UCLASS() -class UMCPHandler_ReparentBlueprint : public UObject, public IMCPHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Description="Blueprint name or package path")) - FString Blueprint; - - UPROPERTY(meta=(Description="Name of the new parent class (C++ class name or Blueprint name)")) - FString NewParentClass; - - virtual FString GetDescription() const override - { - return TEXT("Change a Blueprint's parent class. Accepts C++ class names or Blueprint names."); - } - - virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override - { - // Load Blueprint - MCPAssets Assets; - if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; - UBlueprint* BP = Assets.Object(); - - FString OldParentName = BP->ParentClass ? BP->ParentClass->GetName() : TEXT("None"); - - // Find the new parent class - // Try C++ class first (e.g. "WebUIHUD" finds /Script/ModuleName.WebUIHUD) - UClass* NewParentClassObj = nullptr; - - // Search across all packages for native classes - for (TObjectIterator It; It; ++It) - { - if (It->GetName() == NewParentClass) - { - NewParentClassObj = *It; - break; - } - } - - // If not found as C++ class, try loading as a Blueprint asset - if (!NewParentClassObj) - { - MCPAssets ParentAssets; - if (!ParentAssets.Exact(NewParentClass).AllContent().Errors(Result).ETwo().Load()) return; - if (!ParentAssets.Objects().IsEmpty()) - { - if (ParentAssets.Object()->GeneratedClass) - NewParentClassObj = ParentAssets.Object()->GeneratedClass; - } - } - - if (!NewParentClassObj) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf( - TEXT("Could not find class '%s'. Provide a C++ class name (e.g. 'WebUIHUD') or Blueprint name."), - *NewParentClass)); - } - - // Validate: new parent must be compatible - if (BP->ParentClass && !NewParentClassObj->IsChildOf(BP->ParentClass->GetSuperClass()) && - BP->ParentClass != NewParentClassObj) - { - // Just warn, don't block — the user may intentionally reparent to a sibling - UE_LOG(LogTemp, Warning, - TEXT("BlueprintMCP: Reparenting '%s' from '%s' to '%s' — classes are not in a direct hierarchy"), - *Blueprint, *OldParentName, *NewParentClassObj->GetName()); - } - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Reparenting '%s' from '%s' to '%s'"), - *Blueprint, *OldParentName, *NewParentClassObj->GetName()); - - // Perform reparent - BP->PreEditChange(nullptr); - BP->ParentClass = NewParentClassObj; - BP->PostEditChange(); - - // Refresh all nodes to pick up new parent's functions/variables - FBlueprintEditorUtils::RefreshAllNodes(BP); - - // Compile - FKismetEditorUtilities::CompileBlueprint(BP); - - // Save - bool bSaved = MCPUtils::SaveBlueprintPackage(BP); - - FString NewParentActualName = NewParentClassObj->GetName(); - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Reparent complete, save %s"), - bSaved ? TEXT("succeeded") : TEXT("failed")); - - Result->SetStringField(TEXT("oldParentClass"), OldParentName); - Result->SetStringField(TEXT("newParentClass"), NewParentActualName); - Result->SetBoolField(TEXT("saved"), bSaved); - } -}; - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -UCLASS() -class UMCPHandler_CreateBlueprintAsset : public UObject, public IMCPHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Description="New Blueprint asset name")) - FString Blueprint; - - UPROPERTY(meta=(Description="Package path where the asset will be created (must start with /Game)")) - FString PackagePath; - - UPROPERTY(meta=(Description="Parent class name (C++ class name or Blueprint name)")) - FString ParentClass; - - UPROPERTY(meta=(Optional, Description="Blueprint type: Normal, Interface, FunctionLibrary, or MacroLibrary (default: Normal)")) - FString BlueprintType; - - virtual FString GetDescription() const override - { - return TEXT("Create a new Blueprint asset with a specified parent class and type."); - } - - virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override - { - // Validate packagePath starts with /Game - if (!PackagePath.StartsWith(TEXT("/Game"))) - { - return MCPUtils::MakeErrorJson(Result, TEXT("packagePath must start with '/Game'")); - } - - // Check if asset already exists - FString FullAssetPath = PackagePath / Blueprint; - MCPAssets ExistCheck; - if (!ExistCheck.Exact(Blueprint).Errors(Result).EAny().Info()) return; - - // Resolve parent class — try C++ class first, then Blueprint - UClass* ParentClassObj = nullptr; - - for (TObjectIterator It; It; ++It) - { - if (It->GetName() == ParentClass) - { - ParentClassObj = *It; - break; - } - } - - if (!ParentClassObj) - { - MCPAssets ParentAssets; - if (!ParentAssets.Exact(ParentClass).AllContent().Errors(Result).ETwo().Load()) return; - if (!ParentAssets.Objects().IsEmpty()) - { - if (ParentAssets.Object()->GeneratedClass) - ParentClassObj = ParentAssets.Object()->GeneratedClass; - } - } - - if (!ParentClassObj) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf( - TEXT("Could not find parent class '%s'. Provide a C++ class name (e.g. 'Actor', 'Pawn') or Blueprint name."), - *ParentClass)); - } - - // Map blueprintType string to EBlueprintType - EBlueprintType BlueprintTypeEnum = BPTYPE_Normal; - if (!BlueprintType.IsEmpty()) - { - if (!MCPUtils::StringToEnum(BlueprintType, BlueprintTypeEnum, Result, TEXT("BPTYPE_"))) return; - } - - // For Interface type, parent must be UInterface - if ((BlueprintTypeEnum == BPTYPE_Interface) && !ParentClassObj->IsChildOf(UInterface::StaticClass())) - { - // Use the engine's standard BlueprintInterface parent - ParentClassObj = UInterface::StaticClass(); - } - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Creating Blueprint '%s' in '%s' with parent '%s' (type=%s)"), - *Blueprint, *PackagePath, *ParentClassObj->GetName(), *BlueprintType); - - // Create the package - FString FullPackagePath = PackagePath / Blueprint; - UPackage* Package = CreatePackage(*FullPackagePath); - if (!Package) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Failed to create package at '%s'"), *FullPackagePath)); - } - - // Create the Blueprint - UBlueprint* NewBP = FKismetEditorUtilities::CreateBlueprint( - ParentClassObj, - Package, - FName(*Blueprint), - BlueprintTypeEnum, - UBlueprint::StaticClass(), - UBlueprintGeneratedClass::StaticClass() - ); - - if (!NewBP) - { - return MCPUtils::MakeErrorJson(Result, TEXT("FKismetEditorUtilities::CreateBlueprint returned null")); - } - - // Compile - FKismetEditorUtilities::CompileBlueprint(NewBP); - - // Save - bool bSaved = MCPUtils::SaveBlueprintPackage(NewBP); - - - // Collect graph names - TArray> GraphNames = MCPUtils::AllGraphNamesJson(NewBP); - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Created Blueprint '%s' with %d graphs (saved: %s)"), - *Blueprint, GraphNames.Num(), bSaved ? TEXT("true") : TEXT("false")); - - Result->SetStringField(TEXT("assetPath"), FullAssetPath); - Result->SetStringField(TEXT("parentClass"), ParentClassObj->GetName()); - Result->SetBoolField(TEXT("saved"), bSaved); - Result->SetArrayField(TEXT("graphs"), GraphNames); - } -}; - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -UCLASS() -class UMCPHandler_CreateBlueprintGraph : public UObject, public IMCPHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Description="Blueprint name or package path")) - FString Blueprint; - - UPROPERTY(meta=(Description="Name for the new graph")) - FString Graph; - - UPROPERTY(meta=(Description="Type of graph: function, macro, or customEvent")) - FString GraphType; - - virtual FString GetDescription() const override - { - return TEXT("Create a new function, macro, or custom event graph in a Blueprint."); - } - - virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override - { - if (GraphType != TEXT("function") && GraphType != TEXT("macro") && GraphType != TEXT("customEvent")) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf( - TEXT("Invalid graphType '%s'. Valid values: function, macro, customEvent"), *GraphType)); - } - - // Load Blueprint - MCPAssets Assets; - if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; - UBlueprint* BP = Assets.Object(); - - // Check graph name uniqueness - if (!MCPUtils::AllGraphsNamed(BP, Graph).IsEmpty()) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf( - TEXT("A graph named '%s' already exists in Blueprint '%s'"), *Graph, *Blueprint)); - } - - // Also check for existing custom events with the same name - if (GraphType == TEXT("customEvent")) - { - for (UK2Node_CustomEvent* CE : MCPUtils::AllNodes(BP)) - { - if (CE->CustomFunctionName == FName(*Graph)) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf( - TEXT("A custom event named '%s' already exists in Blueprint '%s'"), *Graph, *Blueprint)); - } - } - } - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Creating %s graph '%s' in Blueprint '%s'"), - *GraphType, *Graph, *Blueprint); - - FString CreatedNodeId; - - if (GraphType == TEXT("function")) - { - UEdGraph* NewGraph = FBlueprintEditorUtils::CreateNewGraph(BP, FName(*Graph), - UEdGraph::StaticClass(), UEdGraphSchema_K2::StaticClass()); - if (!NewGraph) - { - return MCPUtils::MakeErrorJson(Result, TEXT("Failed to create function graph")); - } - FBlueprintEditorUtils::AddFunctionGraph(BP, NewGraph, /*bIsUserCreated=*/true, /*SignatureFromObject=*/static_cast(nullptr)); - } - else if (GraphType == TEXT("macro")) - { - UEdGraph* NewGraph = FBlueprintEditorUtils::CreateNewGraph(BP, FName(*Graph), - UEdGraph::StaticClass(), UEdGraphSchema_K2::StaticClass()); - if (!NewGraph) - { - return MCPUtils::MakeErrorJson(Result, TEXT("Failed to create macro graph")); - } - FBlueprintEditorUtils::AddMacroGraph(BP, NewGraph, /*bIsUserCreated=*/true, /*SignatureFromClass=*/nullptr); - } - else // customEvent - { - // Find the EventGraph (first UbergraphPage) - UEdGraph* EventGraph = nullptr; - if (BP->UbergraphPages.Num() > 0) - { - EventGraph = BP->UbergraphPages[0]; - } - if (!EventGraph) - { - return MCPUtils::MakeErrorJson(Result, TEXT("Blueprint has no EventGraph to add a custom event to")); - } - - // Create a custom event node in the EventGraph - UK2Node_CustomEvent* NewEvent = NewObject(EventGraph); - NewEvent->CustomFunctionName = FName(*Graph); - NewEvent->bIsEditable = true; - EventGraph->AddNode(NewEvent, /*bFromUI=*/false, /*bSelectNewNode=*/false); - NewEvent->CreateNewGuid(); - NewEvent->PostPlacedNewNode(); - NewEvent->AllocateDefaultPins(); - CreatedNodeId = NewEvent->NodeGuid.ToString(); - } - - FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP); - bool bSaved = MCPUtils::SaveBlueprintPackage(BP); - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Created %s graph '%s' in '%s' (saved: %s)"), - *GraphType, *Graph, *Blueprint, bSaved ? TEXT("true") : TEXT("false")); - - Result->SetBoolField(TEXT("saved"), bSaved); - if (!CreatedNodeId.IsEmpty()) - { - Result->SetStringField(TEXT("nodeId"), CreatedNodeId); - } - } -}; - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -UCLASS() -class UMCPHandler_DeleteBlueprintGraph : public UObject, public IMCPHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Description="Blueprint name or package path")) - FString Blueprint; - - UPROPERTY(meta=(Description="Name of the graph to delete")) - FString Graph; - - virtual FString GetDescription() const override - { - return TEXT("Delete a function or macro graph from a Blueprint. Cannot delete EventGraph pages."); - } - - virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override - { - MCPAssets Assets; - if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; - UBlueprint* BP = Assets.Object(); - - // Find the graph - UEdGraph* TargetGraph = nullptr; - FString GraphType; - - for (UEdGraph* CandidateGraph : BP->FunctionGraphs) - { - if (CandidateGraph && CandidateGraph->GetName().Equals(Graph, ESearchCase::IgnoreCase)) - { - TargetGraph = CandidateGraph; - GraphType = TEXT("function"); - break; - } - } - if (!TargetGraph) - { - for (UEdGraph* CandidateGraph : BP->MacroGraphs) - { - if (CandidateGraph && CandidateGraph->GetName().Equals(Graph, ESearchCase::IgnoreCase)) - { - TargetGraph = CandidateGraph; - GraphType = TEXT("macro"); - break; - } - } - } - - // Check if it's an UbergraphPage (EventGraph) — disallow deletion - if (!TargetGraph) - { - for (UEdGraph* CandidateGraph : BP->UbergraphPages) - { - if (CandidateGraph && CandidateGraph->GetName().Equals(Graph, ESearchCase::IgnoreCase)) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf( - TEXT("Cannot delete UbergraphPage '%s'. EventGraph and other Ubergraph pages cannot be deleted."), - *Graph)); - } - } - return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Graph '%s' not found in Blueprint '%s'"), *Graph, *Blueprint)); - } - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Deleting %s graph '%s' from Blueprint '%s'"), - *GraphType, *Graph, *Blueprint); - - // Count nodes for reporting - int32 NodeCount = TargetGraph->Nodes.Num(); - - // Remove the graph - FBlueprintEditorUtils::RemoveGraph(BP, TargetGraph, EGraphRemoveFlags::Default); - - FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP); - bool bSaved = MCPUtils::SaveBlueprintPackage(BP); - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Deleted graph '%s' (%d nodes), save %s"), - *Graph, NodeCount, bSaved ? TEXT("true") : TEXT("false")); - - Result->SetStringField(TEXT("graphType"), GraphType); - Result->SetNumberField(TEXT("nodeCount"), NodeCount); - Result->SetBoolField(TEXT("saved"), bSaved); - } -}; - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -UCLASS() -class UMCPHandler_RenameBlueprintGraph : public UObject, public IMCPHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Description="Blueprint name or package path")) - FString Blueprint; - - UPROPERTY(meta=(Description="Current name of the graph to rename")) - FString Graph; - - UPROPERTY(meta=(Description="New name for the graph")) - FString NewName; - - virtual FString GetDescription() const override - { - return TEXT("Rename a function or macro graph in a Blueprint. Cannot rename EventGraph pages."); - } - - virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override - { - MCPAssets Assets; - if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; - UBlueprint* BP = Assets.Object(); - - // Check if it's an UbergraphPage — disallow rename - for (UEdGraph* CandidateGraph : BP->UbergraphPages) - { - if (CandidateGraph && CandidateGraph->GetName().Equals(Graph, ESearchCase::IgnoreCase)) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf( - TEXT("Cannot rename UbergraphPage '%s'. EventGraph and other Ubergraph pages cannot be renamed."), - *Graph)); - } - } - - // Find the graph in FunctionGraphs or MacroGraphs - UEdGraph* TargetGraph = nullptr; - FString GraphType; - - for (UEdGraph* CandidateGraph : BP->FunctionGraphs) - { - if (CandidateGraph && CandidateGraph->GetName().Equals(Graph, ESearchCase::IgnoreCase)) - { - TargetGraph = CandidateGraph; - GraphType = TEXT("function"); - break; - } - } - if (!TargetGraph) - { - for (UEdGraph* CandidateGraph : BP->MacroGraphs) - { - if (CandidateGraph && CandidateGraph->GetName().Equals(Graph, ESearchCase::IgnoreCase)) - { - TargetGraph = CandidateGraph; - GraphType = TEXT("macro"); - break; - } - } - } - - if (!TargetGraph) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Graph '%s' not found in Blueprint '%s'"), *Graph, *Blueprint)); - } - - // Check for name collision - for (UEdGraph* Existing : MCPUtils::AllGraphsNamed(BP, NewName)) - { - if (Existing != TargetGraph) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf( - TEXT("A graph named '%s' already exists in Blueprint '%s'"), *NewName, *Blueprint)); - } - } - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Renaming %s graph '%s' to '%s' in Blueprint '%s'"), - *GraphType, *Graph, *NewName, *Blueprint); - - FBlueprintEditorUtils::RenameGraph(TargetGraph, NewName); - - FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP); - bool bSaved = MCPUtils::SaveBlueprintPackage(BP); - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Renamed graph '%s' to '%s', save %s"), - *Graph, *NewName, bSaved ? TEXT("true") : TEXT("false")); - - Result->SetStringField(TEXT("newName"), TargetGraph->GetName()); - Result->SetStringField(TEXT("graphType"), GraphType); - Result->SetBoolField(TEXT("saved"), bSaved); - } -}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/MCPHandlers_Interfaces.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/MCPHandlers_Interfaces.h deleted file mode 100644 index b8593b50..00000000 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/MCPHandlers_Interfaces.h +++ /dev/null @@ -1,303 +0,0 @@ -#pragma once - -#include "CoreMinimal.h" -#include "MCPHandler.h" -#include "MCPAssetFinder.h" -#include "MCPServer.h" -#include "MCPUtils.h" -#include "Engine/Blueprint.h" -#include "EdGraph/EdGraph.h" -#include "Kismet2/BlueprintEditorUtils.h" -#include "UObject/UObjectIterator.h" -#include "MCPHandlers_Interfaces.generated.h" - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -UCLASS() -class UMCPHandler_ListBlueprintInterfaces : public UObject, public IMCPHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Description="Blueprint name or package path")) - FString Blueprint; - - virtual FString GetDescription() const override - { - return TEXT("List all Blueprint Interfaces implemented by a Blueprint, " - "including their function graphs."); - } - - virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override - { - - MCPAssets Assets; - if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; - UBlueprint* BP = Assets.Object(); - - TArray> InterfacesArr; - for (const FBPInterfaceDescription& IfaceDesc : BP->ImplementedInterfaces) - { - if (!IfaceDesc.Interface) - { - continue; - } - - TSharedRef IfaceObj = MakeShared(); - IfaceObj->SetStringField(TEXT("name"), IfaceDesc.Interface->GetName()); - IfaceObj->SetStringField(TEXT("classPath"), IfaceDesc.Interface->GetPathName()); - - TArray> FuncArr; - for (const UEdGraph* Graph : IfaceDesc.Graphs) - { - if (Graph) - { - FuncArr.Add(MakeShared(Graph->GetName())); - } - } - IfaceObj->SetArrayField(TEXT("functions"), FuncArr); - - InterfacesArr.Add(MakeShared(IfaceObj)); - } - - Result->SetNumberField(TEXT("count"), InterfacesArr.Num()); - Result->SetArrayField(TEXT("interfaces"), InterfacesArr); - } -}; - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -UCLASS() -class UMCPHandler_AddBlueprintInterface : public UObject, public IMCPHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Description="Blueprint name or package path")) - FString Blueprint; - - UPROPERTY(meta=(Description="Interface name (e.g. 'BPI_MyInterface') or native UInterface class name")) - FString InterfaceName; - - virtual FString GetDescription() const override - { - return TEXT("Add a Blueprint Interface implementation to a Blueprint. " - "Creates stub function graphs for each interface function."); - } - - virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override - { - - MCPAssets Assets; - if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; - UBlueprint* BP = Assets.Object(); - - // Resolve the interface class - UClass* InterfaceClass = nullptr; - - // Strategy 1: Search loaded UInterface classes by name - for (TObjectIterator It; It; ++It) - { - if (!It->IsChildOf(UInterface::StaticClass())) - { - continue; - } - - FString ClassName = It->GetName(); - // Match by class name (e.g. "BPI_Foo_C") or by trimmed name (e.g. "BPI_Foo") - if (ClassName.Equals(InterfaceName, ESearchCase::IgnoreCase)) - { - InterfaceClass = *It; - break; - } - // Strip the generated "_C" suffix for comparison - FString TrimmedName = ClassName; - if (TrimmedName.EndsWith(TEXT("_C"))) - { - TrimmedName = TrimmedName.LeftChop(2); - } - if (TrimmedName.Equals(InterfaceName, ESearchCase::IgnoreCase)) - { - InterfaceClass = *It; - break; - } - } - - // Strategy 2: Try loading as a Blueprint Interface asset - if (!InterfaceClass) - { - MCPAssets IfaceAssets; - if (!IfaceAssets.Exact(InterfaceName).AllContent().Errors(Result).ETwo().Load()) return; - if (!IfaceAssets.Objects().IsEmpty()) - { - UClass* GenClass = IfaceAssets.Object()->GeneratedClass; - if (GenClass && GenClass->IsChildOf(UInterface::StaticClass())) - InterfaceClass = GenClass; - } - } - - if (!InterfaceClass) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf( - TEXT("Interface '%s' not found. Provide a Blueprint Interface asset name (e.g. 'BPI_MyInterface') or a native UInterface class name."), - *InterfaceName)); - } - - // Check for duplicates - for (const FBPInterfaceDescription& IfaceDesc : BP->ImplementedInterfaces) - { - if (IfaceDesc.Interface == InterfaceClass) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf( - TEXT("Interface '%s' is already implemented by Blueprint '%s'"), - *InterfaceName, *Blueprint)); - } - } - - FTopLevelAssetPath InterfacePath = InterfaceClass->GetClassPathName(); - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Adding interface '%s' to Blueprint '%s'"), - *InterfaceClass->GetName(), *Blueprint); - - bool bAdded = FBlueprintEditorUtils::ImplementNewInterface(BP, InterfacePath); - if (!bAdded) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf( - TEXT("FBlueprintEditorUtils::ImplementNewInterface failed for interface '%s' on Blueprint '%s'"), - *InterfaceName, *Blueprint)); - } - - // Collect stub function graph names from the newly added interface entry - TArray AddedFunctions; - for (const FBPInterfaceDescription& IfaceDesc : BP->ImplementedInterfaces) - { - if (IfaceDesc.Interface == InterfaceClass) - { - for (const UEdGraph* Graph : IfaceDesc.Graphs) - { - if (Graph) - { - AddedFunctions.Add(Graph->GetName()); - } - } - break; - } - } - - FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP); - - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Added interface '%s' to '%s' (%d function stubs)"), - *InterfaceClass->GetName(), *Blueprint, AddedFunctions.Num()); - - Result->SetStringField(TEXT("interfaceName"), InterfaceClass->GetName()); - Result->SetStringField(TEXT("interfacePath"), InterfaceClass->GetPathName()); - - TArray> FuncArr; - for (const FString& FuncName : AddedFunctions) - { - FuncArr.Add(MakeShared(FuncName)); - } - Result->SetArrayField(TEXT("functionGraphsAdded"), FuncArr); - } -}; - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -UCLASS() -class UMCPHandler_RemoveBlueprintInterface : public UObject, public IMCPHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Description="Blueprint name or package path")) - FString Blueprint; - - UPROPERTY(meta=(Description="Interface name to remove")) - FString InterfaceName; - - UPROPERTY(meta=(Optional, Description="If true, keep the function graphs as regular functions")) - bool PreserveFunctions = false; - - virtual FString GetDescription() const override - { - return TEXT("Remove a Blueprint Interface implementation from a Blueprint. " - "Optionally preserve the function graphs as regular functions."); - } - - virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override - { - - MCPAssets Assets; - if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; - UBlueprint* BP = Assets.Object(); - - // Find the interface in ImplementedInterfaces by name (case-insensitive) - UClass* FoundInterface = nullptr; - for (const FBPInterfaceDescription& IfaceDesc : BP->ImplementedInterfaces) - { - if (!IfaceDesc.Interface) - { - continue; - } - - FString ClassName = IfaceDesc.Interface->GetName(); - if (ClassName.Equals(InterfaceName, ESearchCase::IgnoreCase)) - { - FoundInterface = IfaceDesc.Interface; - break; - } - // Strip "_C" suffix for comparison - FString TrimmedName = ClassName; - if (TrimmedName.EndsWith(TEXT("_C"))) - { - TrimmedName = TrimmedName.LeftChop(2); - } - if (TrimmedName.Equals(InterfaceName, ESearchCase::IgnoreCase)) - { - FoundInterface = IfaceDesc.Interface; - break; - } - } - - if (!FoundInterface) - { - // Build helpful error with list of implemented interfaces - TArray> IfaceList; - for (const FBPInterfaceDescription& IfaceDesc : BP->ImplementedInterfaces) - { - if (IfaceDesc.Interface) - { - IfaceList.Add(MakeShared(IfaceDesc.Interface->GetName())); - } - } - - MCPUtils::MakeErrorJson(Result, FString::Printf( - TEXT("Interface '%s' is not implemented by Blueprint '%s'"), - *InterfaceName, *Blueprint)); - Result->SetArrayField(TEXT("implementedInterfaces"), IfaceList); - return; - } - - FTopLevelAssetPath InterfacePath = FoundInterface->GetClassPathName(); - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Removing interface '%s' from Blueprint '%s' (preserveFunctions: %s)"), - *FoundInterface->GetName(), *Blueprint, PreserveFunctions ? TEXT("true") : TEXT("false")); - - FBlueprintEditorUtils::RemoveInterface(BP, InterfacePath, PreserveFunctions); - - FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP); - - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Removed interface '%s' from '%s'"), - *FoundInterface->GetName(), *Blueprint); - - Result->SetStringField(TEXT("interfaceName"), FoundInterface->GetName()); - } -}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/MCPHandlers_MaterialInstance.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/MCPHandlers_MaterialInstance.h deleted file mode 100644 index 19ae363d..00000000 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/MCPHandlers_MaterialInstance.h +++ /dev/null @@ -1,722 +0,0 @@ -#pragma once - -#include "CoreMinimal.h" -#include "MCPHandler.h" -#include "MCPAssetFinder.h" -#include "MCPUtils.h" -#include "Materials/Material.h" -#include "Materials/MaterialInterface.h" -#include "Materials/MaterialInstanceConstant.h" -#include "Materials/MaterialExpressionScalarParameter.h" -#include "Materials/MaterialExpressionVectorParameter.h" -#include "Materials/MaterialExpressionTextureSampleParameter2D.h" -#include "Materials/MaterialExpressionStaticSwitchParameter.h" -#include "Factories/MaterialInstanceConstantFactoryNew.h" -#include "AssetToolsModule.h" -#include "IAssetTools.h" -#include "Engine/Texture.h" -#include "MCPHandlers_MaterialInstance.generated.h" - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -UCLASS() -class UMCPHandler_CreateMaterialInstanceAsset : public UObject, public IMCPHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Description="Name for the new Material Instance asset")) - FString Name; - - UPROPERTY(meta=(Description="Package path where the asset will be created (must start with /Game)")) - FString PackagePath; - - UPROPERTY(meta=(Description="Parent material name or path (Material or Material Instance)")) - FString ParentMaterial; - - virtual FString GetDescription() const override - { - return TEXT("Create a new Material Instance Constant asset with a specified parent material."); - } - - virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override - { - // Validate packagePath starts with /Game - if (!PackagePath.StartsWith(TEXT("/Game"))) - { - return MCPUtils::MakeErrorJson(Result, TEXT("packagePath must start with '/Game'")); - } - - // Check if asset already exists - { - MCPAssets ExistCheck; - if (!ExistCheck.Exact(Name).Errors(Result).EAny().Info()) return; - } - - // Load parent material — try as Material first, then as Material Instance - UMaterialInterface* ParentMaterialObj = nullptr; - { - MCPAssets MatAssets; - if (MatAssets.Exact(ParentMaterial).ETwo().Load() && !MatAssets.Objects().IsEmpty()) - { - ParentMaterialObj = MatAssets.Object(); - } - else - { - MCPAssets MIAssets; - if (MIAssets.Exact(ParentMaterial).ETwo().Load() && !MIAssets.Objects().IsEmpty()) - { - ParentMaterialObj = MIAssets.Object(); - } - } - } - - if (!ParentMaterialObj) - { - // Also try LoadObject as a fallback with the raw path - ParentMaterialObj = LoadObject(nullptr, *ParentMaterial); - } - - if (!ParentMaterialObj) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf( - TEXT("Parent material '%s' not found. Provide a Material or Material Instance name/path."), - *ParentMaterial)); - } - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Creating Material Instance '%s' in '%s' with parent '%s'"), - *Name, *PackagePath, *ParentMaterialObj->GetName()); - - // Create via factory + AssetTools - IAssetTools& AssetTools = FModuleManager::LoadModuleChecked("AssetTools").Get(); - UMaterialInstanceConstantFactoryNew* Factory = NewObject(); - - UObject* NewAsset = AssetTools.CreateAsset(Name, PackagePath, UMaterialInstanceConstant::StaticClass(), Factory); - if (!NewAsset) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Failed to create Material Instance asset '%s' in '%s'"), *Name, *PackagePath)); - } - - UMaterialInstanceConstant* MI = Cast(NewAsset); - if (!MI) - { - return MCPUtils::MakeErrorJson(Result, TEXT("Created asset is not a UMaterialInstanceConstant")); - } - - // Set parent - MI->PreEditChange(nullptr); - MI->Parent = ParentMaterialObj; - MI->PostEditChange(); - - // Save - bool bSaved = MCPUtils::SaveGenericPackage(MI); - - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Created Material Instance '%s' with parent '%s' (saved: %s)"), - *Name, *ParentMaterialObj->GetName(), bSaved ? TEXT("true") : TEXT("false")); - - Result->SetStringField(TEXT("path"), MI->GetPathName()); - Result->SetStringField(TEXT("parent"), ParentMaterialObj->GetPathName()); - Result->SetBoolField(TEXT("saved"), bSaved); - } -}; - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -UCLASS() -class UMCPHandler_SetMaterialInstanceParameter : public UObject, public IMCPHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Description="Material Instance name or path")) - FString MaterialInstance; - - UPROPERTY(meta=(Description="Parameter name to set")) - FString ParameterName; - - UPROPERTY(meta=(Description="Value to set (number for scalar, object with r/g/b/a for vector, string path for texture, bool for staticSwitch)")) - FMCPJsonObject Value; - - UPROPERTY(meta=(Optional, Description="Parameter type: scalar, vector, texture, staticSwitch. Auto-detected from parent if omitted.")) - FString Type; - - UPROPERTY(meta=(Optional, Description="If true, validate without applying changes")) - bool DryRun = false; - - virtual FString GetDescription() const override - { - return TEXT("Set a parameter override on a Material Instance. " - "Supports scalar, vector, texture, and static switch parameter types."); - } - - virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override - { - if (!Json->HasField(TEXT("value"))) - { - return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: value")); - } - - // Load the Material Instance - MCPAssets Assets; - if (!Assets.Exact(MaterialInstance).Errors(Result).ENone().ETwo().Load()) return; - UMaterialInstanceConstant* MI = Assets.Object(); - - // Determine the parameter type — explicit or auto-detect from parent - FString TypeStr = Type; - - // Auto-detect type from parent material's parameters if not provided - if (TypeStr.IsEmpty()) - { - UMaterialInterface* ParentMat = MI->Parent; - while (ParentMat) - { - UMaterial* BaseMat = ParentMat->GetMaterial(); - if (BaseMat) - { - // Check scalar parameters - for (UMaterialExpression* Expr : BaseMat->GetExpressions()) - { - if (auto* SP = Cast(Expr)) - { - if (SP->ParameterName.ToString() == ParameterName) - { - TypeStr = TEXT("scalar"); - break; - } - } - else if (auto* VP = Cast(Expr)) - { - if (VP->ParameterName.ToString() == ParameterName) - { - TypeStr = TEXT("vector"); - break; - } - } - else if (auto* TP = Cast(Expr)) - { - if (TP->ParameterName.ToString() == ParameterName) - { - TypeStr = TEXT("texture"); - break; - } - } - else if (auto* SSP = Cast(Expr)) - { - if (SSP->ParameterName.ToString() == ParameterName) - { - TypeStr = TEXT("staticSwitch"); - break; - } - } - } - break; // Only need to check the base material - } - // Walk up the parent chain if it's an MI parented to another MI - UMaterialInstanceConstant* ParentMI = Cast(ParentMat); - if (ParentMI) - { - ParentMat = ParentMI->Parent; - } - else - { - break; - } - } - } - - if (TypeStr.IsEmpty()) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf( - TEXT("Could not determine parameter type for '%s'. Specify the 'type' field explicitly (scalar, vector, texture, staticSwitch)."), - *ParameterName)); - } - - FString NewValueDescription; - FMaterialParameterInfo ParamInfo(*ParameterName); - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: %s parameter '%s' (type=%s) on Material Instance '%s'"), - DryRun ? TEXT("[DRY RUN] Setting") : TEXT("Setting"), - *ParameterName, *TypeStr, *MaterialInstance); - - if (TypeStr.Equals(TEXT("scalar"), ESearchCase::IgnoreCase)) - { - // Scalar parameter — value is a number - double FloatValue = Json->GetNumberField(TEXT("value")); - - if (!DryRun) - { - MI->SetScalarParameterValueEditorOnly(ParamInfo, (float)FloatValue); - } - NewValueDescription = FString::Printf(TEXT("%f"), FloatValue); - } - else if (TypeStr.Equals(TEXT("vector"), ESearchCase::IgnoreCase)) - { - // Vector parameter — value is { r, g, b, a? } - const TSharedPtr* ValueObj = nullptr; - if (!Json->TryGetObjectField(TEXT("value"), ValueObj) || !ValueObj || !(*ValueObj).IsValid()) - { - return MCPUtils::MakeErrorJson(Result, TEXT("For vector parameters, 'value' must be an object with r, g, b (and optional a) fields.")); - } - - double R = (*ValueObj)->GetNumberField(TEXT("r")); - double G = (*ValueObj)->GetNumberField(TEXT("g")); - double B = (*ValueObj)->GetNumberField(TEXT("b")); - double A = (*ValueObj)->HasField(TEXT("a")) ? (*ValueObj)->GetNumberField(TEXT("a")) : 1.0; - - FLinearColor Color((float)R, (float)G, (float)B, (float)A); - - if (!DryRun) - { - MI->SetVectorParameterValueEditorOnly(ParamInfo, Color); - } - NewValueDescription = FString::Printf(TEXT("(R=%f, G=%f, B=%f, A=%f)"), R, G, B, A); - } - else if (TypeStr.Equals(TEXT("texture"), ESearchCase::IgnoreCase)) - { - // Texture parameter — value is a texture path string - FString TexturePath = Json->GetStringField(TEXT("value")); - if (TexturePath.IsEmpty()) - { - return MCPUtils::MakeErrorJson(Result, TEXT("For texture parameters, 'value' must be a texture asset path string.")); - } - - UTexture* TextureObj = LoadObject(nullptr, *TexturePath); - if (!TextureObj) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Could not load texture at path '%s'"), *TexturePath)); - } - - if (!DryRun) - { - MI->SetTextureParameterValueEditorOnly(ParamInfo, TextureObj); - } - NewValueDescription = TexturePath; - } - else if (TypeStr.Equals(TEXT("staticSwitch"), ESearchCase::IgnoreCase)) - { - // Static switch parameter — value is a bool - bool bSwitchValue = Json->GetBoolField(TEXT("value")); - - if (!DryRun) - { - // Modify static parameters - FStaticParameterSet StaticParams; - MI->GetStaticParameterValues(StaticParams); - - bool bFound = false; - for (FStaticSwitchParameter& Param : StaticParams.StaticSwitchParameters) - { - if (Param.ParameterInfo.Name == FName(*ParameterName)) - { - Param.Value = bSwitchValue; - Param.bOverride = true; - bFound = true; - break; - } - } - - if (!bFound) - { - // Add new static switch parameter entry - FStaticSwitchParameter NewParam; - NewParam.ParameterInfo.Name = FName(*ParameterName); - NewParam.Value = bSwitchValue; - NewParam.bOverride = true; - StaticParams.StaticSwitchParameters.Add(NewParam); - } - - MI->UpdateStaticPermutation(StaticParams); - } - NewValueDescription = bSwitchValue ? TEXT("true") : TEXT("false"); - } - else - { - return MCPUtils::MakeErrorJson(Result, FString::Printf( - TEXT("Unknown parameter type '%s'. Valid types: scalar, vector, texture, staticSwitch"), - *TypeStr)); - } - - if (!DryRun) - { - MI->PreEditChange(nullptr); - MI->PostEditChange(); - MI->MarkPackageDirty(); - MCPUtils::SaveGenericPackage(MI); - } - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: %s parameter '%s' = %s on '%s'"), - DryRun ? TEXT("[DRY RUN] Would set") : TEXT("Set"), - *ParameterName, *NewValueDescription, *MaterialInstance); - - Result->SetStringField(TEXT("type"), TypeStr); - Result->SetStringField(TEXT("newValue"), NewValueDescription); - if (DryRun) - { - Result->SetBoolField(TEXT("dryRun"), true); - } - } -}; - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -UCLASS() -class UMCPHandler_DumpMaterialInstanceParameters : public UObject, public IMCPHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Description="Material Instance name or path to inspect")) - FString MaterialInstance; - - virtual FString GetDescription() const override - { - return TEXT("List all parameters on a Material Instance, including overridden and inherited parameters."); - } - - virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override - { - MCPAssets Assets; - if (!Assets.Exact(MaterialInstance).Errors(Result).ENone().ETwo().Load()) return; - UMaterialInstanceConstant* MI = Assets.Object(); - - Result->SetStringField(TEXT("name"), MI->GetName()); - Result->SetStringField(TEXT("path"), MI->GetPathName()); - - // Parent info - if (MI->Parent) - { - Result->SetStringField(TEXT("parent"), MI->Parent->GetPathName()); - } - - // Build parent chain - TArray> ParentChainArr; - { - UMaterialInterface* Current = MI->Parent; - while (Current) - { - TSharedRef ParentObj = MakeShared(); - ParentObj->SetStringField(TEXT("name"), Current->GetName()); - ParentObj->SetStringField(TEXT("path"), Current->GetPathName()); - ParentObj->SetStringField(TEXT("class"), Current->GetClass()->GetName()); - ParentChainArr.Add(MakeShared(ParentObj)); - - UMaterialInstanceConstant* ParentMI = Cast(Current); - if (ParentMI) - { - Current = ParentMI->Parent; - } - else - { - break; // Reached the root Material - } - } - } - Result->SetArrayField(TEXT("parentChain"), ParentChainArr); - - // Scalar parameters - TArray> ScalarArr; - for (const FScalarParameterValue& Param : MI->ScalarParameterValues) - { - TSharedRef PObj = MakeShared(); - PObj->SetStringField(TEXT("name"), Param.ParameterInfo.Name.ToString()); - PObj->SetNumberField(TEXT("value"), Param.ParameterValue); - PObj->SetBoolField(TEXT("isOverridden"), true); // Present in ScalarParameterValues means it's overridden - ScalarArr.Add(MakeShared(PObj)); - } - Result->SetArrayField(TEXT("scalarParameters"), ScalarArr); - - // Vector parameters - TArray> VectorArr; - for (const FVectorParameterValue& Param : MI->VectorParameterValues) - { - TSharedRef PObj = MakeShared(); - PObj->SetStringField(TEXT("name"), Param.ParameterInfo.Name.ToString()); - PObj->SetNumberField(TEXT("r"), Param.ParameterValue.R); - PObj->SetNumberField(TEXT("g"), Param.ParameterValue.G); - PObj->SetNumberField(TEXT("b"), Param.ParameterValue.B); - PObj->SetNumberField(TEXT("a"), Param.ParameterValue.A); - PObj->SetBoolField(TEXT("isOverridden"), true); - VectorArr.Add(MakeShared(PObj)); - } - Result->SetArrayField(TEXT("vectorParameters"), VectorArr); - - // Texture parameters - TArray> TextureArr; - for (const FTextureParameterValue& Param : MI->TextureParameterValues) - { - TSharedRef PObj = MakeShared(); - PObj->SetStringField(TEXT("name"), Param.ParameterInfo.Name.ToString()); - if (Param.ParameterValue) - { - PObj->SetStringField(TEXT("texture"), Param.ParameterValue->GetPathName()); - } - else - { - PObj->SetStringField(TEXT("texture"), TEXT("None")); - } - PObj->SetBoolField(TEXT("isOverridden"), true); - TextureArr.Add(MakeShared(PObj)); - } - Result->SetArrayField(TEXT("textureParameters"), TextureArr); - - // Static switch parameters - TArray> StaticSwitchArr; - { - FStaticParameterSet StaticParams; - MI->GetStaticParameterValues(StaticParams); - - for (const FStaticSwitchParameter& Param : StaticParams.StaticSwitchParameters) - { - TSharedRef PObj = MakeShared(); - PObj->SetStringField(TEXT("name"), Param.ParameterInfo.Name.ToString()); - PObj->SetBoolField(TEXT("value"), Param.Value); - PObj->SetBoolField(TEXT("isOverridden"), Param.bOverride); - StaticSwitchArr.Add(MakeShared(PObj)); - } - } - Result->SetArrayField(TEXT("staticSwitchParameters"), StaticSwitchArr); - - // Also report inherited parameters from the parent material for discoverability - TArray> InheritedScalarArr; - TArray> InheritedVectorArr; - TArray> InheritedTextureArr; - TArray> InheritedStaticSwitchArr; - { - UMaterial* BaseMat = MI->GetMaterial(); - if (BaseMat) - { - // Collect names of already-overridden parameters for filtering - TSet OverriddenScalars; - for (const FScalarParameterValue& P : MI->ScalarParameterValues) - { - OverriddenScalars.Add(P.ParameterInfo.Name.ToString()); - } - TSet OverriddenVectors; - for (const FVectorParameterValue& P : MI->VectorParameterValues) - { - OverriddenVectors.Add(P.ParameterInfo.Name.ToString()); - } - TSet OverriddenTextures; - for (const FTextureParameterValue& P : MI->TextureParameterValues) - { - OverriddenTextures.Add(P.ParameterInfo.Name.ToString()); - } - TSet OverriddenStaticSwitches; - { - FStaticParameterSet SP; - MI->GetStaticParameterValues(SP); - for (const FStaticSwitchParameter& P : SP.StaticSwitchParameters) - { - if (P.bOverride) - { - OverriddenStaticSwitches.Add(P.ParameterInfo.Name.ToString()); - } - } - } - - for (UMaterialExpression* Expr : BaseMat->GetExpressions()) - { - if (auto* SP = Cast(Expr)) - { - if (!OverriddenScalars.Contains(SP->ParameterName.ToString())) - { - TSharedRef PObj = MakeShared(); - PObj->SetStringField(TEXT("name"), SP->ParameterName.ToString()); - PObj->SetNumberField(TEXT("defaultValue"), SP->DefaultValue); - PObj->SetBoolField(TEXT("isOverridden"), false); - InheritedScalarArr.Add(MakeShared(PObj)); - } - } - else if (auto* VP = Cast(Expr)) - { - if (!OverriddenVectors.Contains(VP->ParameterName.ToString())) - { - TSharedRef PObj = MakeShared(); - PObj->SetStringField(TEXT("name"), VP->ParameterName.ToString()); - PObj->SetNumberField(TEXT("r"), VP->DefaultValue.R); - PObj->SetNumberField(TEXT("g"), VP->DefaultValue.G); - PObj->SetNumberField(TEXT("b"), VP->DefaultValue.B); - PObj->SetNumberField(TEXT("a"), VP->DefaultValue.A); - PObj->SetBoolField(TEXT("isOverridden"), false); - InheritedVectorArr.Add(MakeShared(PObj)); - } - } - else if (auto* TP = Cast(Expr)) - { - if (!OverriddenTextures.Contains(TP->ParameterName.ToString())) - { - TSharedRef PObj = MakeShared(); - PObj->SetStringField(TEXT("name"), TP->ParameterName.ToString()); - if (TP->Texture) - { - PObj->SetStringField(TEXT("defaultTexture"), TP->Texture->GetPathName()); - } - else - { - PObj->SetStringField(TEXT("defaultTexture"), TEXT("None")); - } - PObj->SetBoolField(TEXT("isOverridden"), false); - InheritedTextureArr.Add(MakeShared(PObj)); - } - } - else if (auto* SSP = Cast(Expr)) - { - if (!OverriddenStaticSwitches.Contains(SSP->ParameterName.ToString())) - { - TSharedRef PObj = MakeShared(); - PObj->SetStringField(TEXT("name"), SSP->ParameterName.ToString()); - PObj->SetBoolField(TEXT("defaultValue"), SSP->DefaultValue); - PObj->SetBoolField(TEXT("isOverridden"), false); - InheritedStaticSwitchArr.Add(MakeShared(PObj)); - } - } - } - } - } - - // Merge inherited (non-overridden) params into the arrays - for (const TSharedPtr& V : InheritedScalarArr) - { - ScalarArr.Add(V); - } - for (const TSharedPtr& V : InheritedVectorArr) - { - VectorArr.Add(V); - } - for (const TSharedPtr& V : InheritedTextureArr) - { - TextureArr.Add(V); - } - for (const TSharedPtr& V : InheritedStaticSwitchArr) - { - StaticSwitchArr.Add(V); - } - - // Update arrays with merged data - Result->SetArrayField(TEXT("scalarParameters"), ScalarArr); - Result->SetArrayField(TEXT("vectorParameters"), VectorArr); - Result->SetArrayField(TEXT("textureParameters"), TextureArr); - Result->SetArrayField(TEXT("staticSwitchParameters"), StaticSwitchArr); - } -}; - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -UCLASS() -class UMCPHandler_ReparentMaterialInstance : public UObject, public IMCPHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Description="Material Instance name or path to reparent")) - FString MaterialInstance; - - UPROPERTY(meta=(Description="New parent material name or path (Material or Material Instance)")) - FString NewParent; - - UPROPERTY(meta=(Optional, Description="If true, validate without applying changes")) - bool DryRun = false; - - virtual FString GetDescription() const override - { - return TEXT("Change the parent material of a Material Instance. " - "Validates against circular parent chains."); - } - - virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override - { - // Load the Material Instance - MCPAssets Assets; - if (!Assets.Exact(MaterialInstance).Errors(Result).ENone().ETwo().Load()) return; - UMaterialInstanceConstant* MI = Assets.Object(); - - // Capture old parent - FString OldParentPath = MI->Parent ? MI->Parent->GetPathName() : TEXT("None"); - - // Load new parent — try as Material first, then as Material Instance - UMaterialInterface* NewParentObj = nullptr; - { - MCPAssets MatAssets; - if (MatAssets.Exact(NewParent).ETwo().Load() && !MatAssets.Objects().IsEmpty()) - { - NewParentObj = MatAssets.Object(); - } - else - { - MCPAssets MIAssets; - if (MIAssets.Exact(NewParent).ETwo().Load() && !MIAssets.Objects().IsEmpty()) - { - NewParentObj = MIAssets.Object(); - } - } - } - - if (!NewParentObj) - { - // Try LoadObject as a fallback - NewParentObj = LoadObject(nullptr, *NewParent); - } - - if (!NewParentObj) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf( - TEXT("New parent material '%s' not found. Provide a Material or Material Instance name/path."), - *NewParent)); - } - - // Prevent circular parenting — check if NewParent is this MI or has this MI in its chain - { - UMaterialInterface* Check = NewParentObj; - while (Check) - { - if (Check == MI) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf( - TEXT("Cannot reparent '%s' to '%s' — this would create a circular parent chain."), - *MaterialInstance, *NewParent)); - } - UMaterialInstanceConstant* CheckMI = Cast(Check); - if (CheckMI) - { - Check = CheckMI->Parent; - } - else - { - break; - } - } - } - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: %s Material Instance '%s': parent '%s' -> '%s'"), - DryRun ? TEXT("[DRY RUN] Reparenting") : TEXT("Reparenting"), - *MaterialInstance, *OldParentPath, *NewParentObj->GetPathName()); - - if (!DryRun) - { - MI->PreEditChange(nullptr); - MI->Parent = NewParentObj; - MI->PostEditChange(); - - bool bSaved = MCPUtils::SaveGenericPackage(MI); - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Reparented Material Instance '%s' (saved: %s)"), - *MaterialInstance, bSaved ? TEXT("true") : TEXT("false")); - } - - Result->SetStringField(TEXT("oldParent"), OldParentPath); - Result->SetStringField(TEXT("newParent"), NewParentObj->GetPathName()); - if (DryRun) - { - Result->SetBoolField(TEXT("dryRun"), true); - } - } -}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/MCPHandlers_MaterialMutation.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/MCPHandlers_MaterialMutation.h deleted file mode 100644 index 31c8b16a..00000000 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/MCPHandlers_MaterialMutation.h +++ /dev/null @@ -1,1481 +0,0 @@ -#pragma once - -#include "CoreMinimal.h" -#include "MCPHandler.h" -#include "MCPAssetFinder.h" -#include "MCPUtils.h" -#include "Materials/Material.h" -#include "MaterialDomain.h" -#include "Materials/MaterialInstanceConstant.h" -#include "Materials/MaterialFunction.h" -#include "Materials/MaterialExpression.h" -#include "Materials/MaterialExpressionScalarParameter.h" -#include "Materials/MaterialExpressionVectorParameter.h" -#include "Materials/MaterialExpressionTextureObjectParameter.h" -#include "Materials/MaterialExpressionTextureSampleParameter2D.h" -#include "Materials/MaterialExpressionStaticSwitchParameter.h" -#include "Materials/MaterialExpressionConstant.h" -#include "Materials/MaterialExpressionConstant3Vector.h" -#include "Materials/MaterialExpressionConstant4Vector.h" -#include "Materials/MaterialExpressionTextureSample.h" -#include "Materials/MaterialExpressionTextureCoordinate.h" -#include "Materials/MaterialExpressionComponentMask.h" -#include "Materials/MaterialExpressionCustom.h" -#include "Materials/MaterialExpressionFunctionInput.h" -#include "Materials/MaterialExpressionFunctionOutput.h" -#include "Materials/MaterialExpressionMaterialFunctionCall.h" -#include "MaterialGraph/MaterialGraph.h" -#include "MaterialGraph/MaterialGraphNode.h" -#include "MaterialGraph/MaterialGraphSchema.h" -#include "Factories/MaterialFactoryNew.h" -#include "Factories/MaterialFunctionFactoryNew.h" -#include "AssetToolsModule.h" -#include "IAssetTools.h" -#include "AssetRegistry/AssetRegistryModule.h" -#include "EdGraph/EdGraph.h" -#include "EdGraph/EdGraphNode.h" -#include "Serialization/JsonReader.h" -#include "Serialization/JsonWriter.h" -#include "Serialization/JsonSerializer.h" -#include "Misc/Guid.h" -#include "Misc/FileHelper.h" -#include "Misc/Paths.h" -#include "UObject/SavePackage.h" -#include "UObject/UObjectIterator.h" -#include "Kismet2/BlueprintEditorUtils.h" -#include "MCPHandlers_MaterialMutation.generated.h" - -// SEH wrapper defined in BlueprintMCPServer.cpp — catches crashes from abstract/invalid expression classes. -// Wraps the entire creation + registration + PostEditChange flow so that if the expression crashes -// (e.g. UMaterialExpressionParameter), it cleans up and returns -1 instead of terminating the process. -#if PLATFORM_WINDOWS -extern int32 TryAddMaterialExpressionSEH( - UObject* Owner, UClass* ExprClass, UMaterial* Material, UMaterialFunction* MatFunc, - int32 PosX, int32 PosY, UMaterialExpression** OutExpr); -#endif - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -UCLASS() -class UMCPHandler_CreateMaterialAsset : public UObject, public IMCPHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Description="Name for the new material asset")) - FString Name; - - UPROPERTY(meta=(Description="Package path where the asset will be created (must start with /Game)")) - FString PackagePath; - - UPROPERTY(meta=(Optional, Description="Material domain: Surface, DeferredDecal, LightFunction, Volume, PostProcess, UI")) - FString Domain; - - UPROPERTY(meta=(Optional, Description="Blend mode: Opaque, Masked, Translucent, Additive, Modulate")) - FString BlendMode; - - UPROPERTY(meta=(Optional, Description="Whether the material is two-sided")) - bool TwoSided = false; - - virtual FString GetDescription() const override - { - return TEXT("Create a new UMaterial asset with optional domain, blend mode, and two-sided settings."); - } - - virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override - { - if (!PackagePath.StartsWith(TEXT("/Game"))) - { - return MCPUtils::MakeErrorJson(Result, TEXT("packagePath must start with '/Game'")); - } - - // Check if asset already exists - MCPAssets ExistCheck; - if (!ExistCheck.Exact(Name).Errors(Result).EAny().Info()) return; - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Creating Material '%s' in '%s'"), *Name, *PackagePath); - - // Create via IAssetTools + factory - IAssetTools& AssetTools = FModuleManager::LoadModuleChecked("AssetTools").Get(); - UMaterialFactoryNew* Factory = NewObject(); - UObject* NewAsset = AssetTools.CreateAsset(Name, PackagePath, UMaterial::StaticClass(), Factory); - - if (!NewAsset) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Failed to create Material '%s' in '%s'"), *Name, *PackagePath)); - } - - UMaterial* MaterialObj = Cast(NewAsset); - if (!MaterialObj) - { - return MCPUtils::MakeErrorJson(Result, TEXT("Created asset is not a UMaterial")); - } - - // Apply optional properties - bool bHasTwoSided = Json->HasField(TEXT("twoSided")); - - MaterialObj->PreEditChange(nullptr); - - // Parse domain - if (!Domain.IsEmpty()) - { - if (Domain == TEXT("Surface")) - MaterialObj->MaterialDomain = MD_Surface; - else if (Domain == TEXT("DeferredDecal")) - MaterialObj->MaterialDomain = MD_DeferredDecal; - else if (Domain == TEXT("LightFunction")) - MaterialObj->MaterialDomain = MD_LightFunction; - else if (Domain == TEXT("Volume")) - MaterialObj->MaterialDomain = MD_Volume; - else if (Domain == TEXT("PostProcess")) - MaterialObj->MaterialDomain = MD_PostProcess; - else if (Domain == TEXT("UI")) - MaterialObj->MaterialDomain = MD_UI; - } - - // Parse blend mode - if (!BlendMode.IsEmpty()) - { - if (BlendMode == TEXT("Opaque")) - MaterialObj->BlendMode = BLEND_Opaque; - else if (BlendMode == TEXT("Masked")) - MaterialObj->BlendMode = BLEND_Masked; - else if (BlendMode == TEXT("Translucent")) - MaterialObj->BlendMode = BLEND_Translucent; - else if (BlendMode == TEXT("Additive")) - MaterialObj->BlendMode = BLEND_Additive; - else if (BlendMode == TEXT("Modulate")) - MaterialObj->BlendMode = BLEND_Modulate; - } - - if (bHasTwoSided) - { - MaterialObj->TwoSided = TwoSided; - } - - MaterialObj->PostEditChange(); - - // Save - bool bSaved = MCPUtils::SaveMaterialPackage(MaterialObj); - - - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Created Material '%s' (saved: %s)"), - *Name, bSaved ? TEXT("true") : TEXT("false")); - - Result->SetStringField(TEXT("path"), MaterialObj->GetPathName()); - Result->SetStringField(TEXT("domain"), MCPUtils::EnumToString(MaterialObj->MaterialDomain, TEXT("MD_"))); - Result->SetStringField(TEXT("blendMode"), MCPUtils::EnumToString(MaterialObj->BlendMode, TEXT("BLEND_"))); - Result->SetBoolField(TEXT("twoSided"), MaterialObj->TwoSided != 0); - Result->SetBoolField(TEXT("saved"), bSaved); - } -}; - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -UCLASS() -class UMCPHandler_SetMaterialProperty : public UObject, public IMCPHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Description="Material name or package path")) - FString Material; - - UPROPERTY(meta=(Description="Property name to set (domain, blendMode, twoSided, shadingModel, opacity, opacityMaskClipValue, bUsedWithSkeletalMesh, bUsedWithMorphTargets, bUsedWithNiagaraSprites, ditheredLODTransition, bAllowNegativeEmissiveColor)")) - FString Property; - - UPROPERTY(meta=(Optional, Description="If true, preview the change without applying it")) - bool DryRun = false; - - virtual FString GetDescription() const override - { - return TEXT("Set a top-level material property such as domain, blend mode, shading model, or usage flags. " - "The 'value' field in the JSON payload provides the new value."); - } - - virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override - { - if (!Json->HasField(TEXT("value"))) - { - return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: value")); - } - - // Load material - MCPAssets Assets; - if (!Assets.Exact(Material).Errors(Result).ENone().ETwo().Load()) return; - UMaterial* MaterialObj = Assets.Object(); - - FString OldValue; - FString NewValue; - - if (Property == TEXT("domain")) - { - FString ValueStr = Json->GetStringField(TEXT("value")); - OldValue = MCPUtils::EnumToString(MaterialObj->MaterialDomain, TEXT("MD_")); - - EMaterialDomain NewDomain; - if (!MCPUtils::StringToEnum(ValueStr, NewDomain, Result, TEXT("MD_"))) return; - NewValue = MCPUtils::EnumToString(NewDomain, TEXT("MD_")); - - if (!DryRun) - { - MaterialObj->PreEditChange(nullptr); - MaterialObj->MaterialDomain = NewDomain; - MaterialObj->PostEditChange(); - } - } - else if (Property == TEXT("blendMode")) - { - FString ValueStr = Json->GetStringField(TEXT("value")); - OldValue = MCPUtils::EnumToString(MaterialObj->BlendMode, TEXT("BLEND_")); - - EBlendMode NewBlend; - if (!MCPUtils::StringToEnum(ValueStr, NewBlend, Result, TEXT("BLEND_"))) return; - NewValue = MCPUtils::EnumToString(NewBlend, TEXT("BLEND_")); - - if (!DryRun) - { - MaterialObj->PreEditChange(nullptr); - MaterialObj->BlendMode = NewBlend; - MaterialObj->PostEditChange(); - } - } - else if (Property == TEXT("twoSided")) - { - bool bValue = Json->GetBoolField(TEXT("value")); - OldValue = MaterialObj->TwoSided ? TEXT("true") : TEXT("false"); - NewValue = bValue ? TEXT("true") : TEXT("false"); - - if (!DryRun) - { - MaterialObj->PreEditChange(nullptr); - MaterialObj->TwoSided = bValue ? 1 : 0; - MaterialObj->PostEditChange(); - } - } - else if (Property == TEXT("shadingModel")) - { - FString ValueStr = Json->GetStringField(TEXT("value")); - OldValue = MCPUtils::EnumToString(MaterialObj->GetShadingModels().GetFirstShadingModel(), TEXT("MSM_")); - - EMaterialShadingModel NewModel; - if (!MCPUtils::StringToEnum(ValueStr, NewModel, Result, TEXT("MSM_"))) return; - NewValue = MCPUtils::EnumToString(NewModel, TEXT("MSM_")); - - if (!DryRun) - { - MaterialObj->PreEditChange(nullptr); - MaterialObj->SetShadingModel(NewModel); - MaterialObj->PostEditChange(); - } - } - else if (Property == TEXT("opacity") || Property == TEXT("opacityMaskClipValue")) - { - double OpacityValue = Json->GetNumberField(TEXT("value")); - OldValue = FString::Printf(TEXT("%f"), MaterialObj->OpacityMaskClipValue); - NewValue = FString::Printf(TEXT("%f"), OpacityValue); - - if (!DryRun) - { - MaterialObj->PreEditChange(nullptr); - MaterialObj->OpacityMaskClipValue = (float)OpacityValue; - MaterialObj->PostEditChange(); - } - } - else if (Property == TEXT("bUsedWithSkeletalMesh")) - { - bool bValue = Json->GetBoolField(TEXT("value")); - OldValue = MaterialObj->bUsedWithSkeletalMesh ? TEXT("true") : TEXT("false"); - NewValue = bValue ? TEXT("true") : TEXT("false"); - - if (!DryRun) - { - MaterialObj->PreEditChange(nullptr); - MaterialObj->bUsedWithSkeletalMesh = bValue ? 1 : 0; - MaterialObj->PostEditChange(); - } - } - else if (Property == TEXT("bUsedWithMorphTargets")) - { - bool bValue = Json->GetBoolField(TEXT("value")); - OldValue = MaterialObj->bUsedWithMorphTargets ? TEXT("true") : TEXT("false"); - NewValue = bValue ? TEXT("true") : TEXT("false"); - - if (!DryRun) - { - MaterialObj->PreEditChange(nullptr); - MaterialObj->bUsedWithMorphTargets = bValue ? 1 : 0; - MaterialObj->PostEditChange(); - } - } - else if (Property == TEXT("bUsedWithNiagaraSprites")) - { - bool bValue = Json->GetBoolField(TEXT("value")); - OldValue = MaterialObj->bUsedWithNiagaraSprites ? TEXT("true") : TEXT("false"); - NewValue = bValue ? TEXT("true") : TEXT("false"); - - if (!DryRun) - { - MaterialObj->PreEditChange(nullptr); - MaterialObj->bUsedWithNiagaraSprites = bValue ? 1 : 0; - MaterialObj->PostEditChange(); - } - } - else if (Property == TEXT("ditheredLODTransition") || Property == TEXT("DitheredLODTransition")) - { - bool bValue = Json->GetBoolField(TEXT("value")); - OldValue = MaterialObj->DitheredLODTransition ? TEXT("true") : TEXT("false"); - NewValue = bValue ? TEXT("true") : TEXT("false"); - - if (!DryRun) - { - MaterialObj->PreEditChange(nullptr); - MaterialObj->DitheredLODTransition = bValue ? 1 : 0; - MaterialObj->PostEditChange(); - } - } - else if (Property == TEXT("bAllowNegativeEmissiveColor")) - { - bool bValue = Json->GetBoolField(TEXT("value")); - OldValue = MaterialObj->bAllowNegativeEmissiveColor ? TEXT("true") : TEXT("false"); - NewValue = bValue ? TEXT("true") : TEXT("false"); - - if (!DryRun) - { - MaterialObj->PreEditChange(nullptr); - MaterialObj->bAllowNegativeEmissiveColor = bValue ? 1 : 0; - MaterialObj->PostEditChange(); - } - } - else - { - return MCPUtils::MakeErrorJson(Result, FString::Printf( - TEXT("Unknown property '%s'. Valid properties: domain, blendMode, twoSided, shadingModel, opacity, " - "opacityMaskClipValue, bUsedWithSkeletalMesh, bUsedWithMorphTargets, bUsedWithNiagaraSprites, " - "ditheredLODTransition, bAllowNegativeEmissiveColor"), - *Property)); - } - - // Save if not dry run - bool bSaved = false; - if (!DryRun) - { - bSaved = MCPUtils::SaveMaterialPackage(MaterialObj); - } - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: %sSet material property '%s' on '%s': '%s' -> '%s'"), - DryRun ? TEXT("[DRY RUN] ") : TEXT(""), - *Property, *Material, *OldValue, *NewValue); - - Result->SetStringField(TEXT("material"), MaterialObj->GetName()); - Result->SetStringField(TEXT("oldValue"), OldValue); - Result->SetStringField(TEXT("newValue"), NewValue); - Result->SetBoolField(TEXT("dryRun"), DryRun); - if (!DryRun) - { - Result->SetBoolField(TEXT("saved"), bSaved); - } - } -}; - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -UCLASS() -class UMCPHandler_AddMaterialExpression : public UObject, public IMCPHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Optional, Description="Material name or package path (specify this or materialFunction, not both)")) - FString Material; - - UPROPERTY(meta=(Optional, Description="Material function name or package path (specify this or material, not both)")) - FString MaterialFunction; - - UPROPERTY(meta=(Description="Expression class name without 'MaterialExpression' prefix (e.g. 'Constant', 'ScalarParameter', 'Add', 'Multiply', 'Lerp')")) - FString ExpressionClass; - - UPROPERTY(meta=(Optional, Description="X position in the material graph editor")) - int32 PosX = 0; - - UPROPERTY(meta=(Optional, Description="Y position in the material graph editor")) - int32 PosY = 0; - - UPROPERTY(meta=(Optional, Description="If true, preview the change without applying it")) - bool DryRun = false; - - virtual FString GetDescription() const override - { - return TEXT("Add a new expression node to a material or material function graph."); - } - - virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override - { - if (Material.IsEmpty() && MaterialFunction.IsEmpty()) - { - return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: 'material' or 'materialFunction'")); - } - - // Map string class name to UClass via dynamic lookup - UClass* ExprClass = nullptr; - - // Convenience aliases for backward compatibility - static TMap Aliases = { - {TEXT("Lerp"), TEXT("LinearInterpolate")}, - }; - - FString LookupName = ExpressionClass; - if (const FString* Alias = Aliases.Find(ExpressionClass)) - { - LookupName = *Alias; - } - - // Dynamic lookup: find UMaterialExpression via UClass iteration - FString FullClassName = FString::Printf(TEXT("MaterialExpression%s"), *LookupName); - for (TObjectIterator It; It; ++It) - { - if (It->GetName() == FullClassName && It->IsChildOf(UMaterialExpression::StaticClass())) - { - ExprClass = *It; - break; - } - } - - if (!ExprClass) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf( - TEXT("Unknown expression class '%s'. Use the UMaterialExpression subclass name without the 'MaterialExpression' prefix " - "(e.g. 'Constant', 'ScalarParameter', 'Add', 'Multiply', 'Lerp', 'Subtract', 'Fresnel', 'Comment', etc.)"), - *ExpressionClass)); - } - if (ExprClass->HasAnyClassFlags(CLASS_Abstract)) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf( - TEXT("Expression class '%s' is abstract and cannot be instantiated."), *ExpressionClass)); - } - - // Load material or material function - UMaterial* MaterialObj = nullptr; - UMaterialFunction* MatFunc = nullptr; - UObject* Owner = nullptr; - FString AssetDisplayName; - - if (!MaterialFunction.IsEmpty()) - { - if (!Material.IsEmpty()) - { - return MCPUtils::MakeErrorJson(Result, TEXT("Specify either 'material' or 'materialFunction', not both")); - } - MCPAssets MFAssets; - if (!MFAssets.Exact(MaterialFunction).Errors(Result).ENone().ETwo().Load()) return; - MatFunc = MFAssets.Object(); - Owner = MatFunc; - AssetDisplayName = MatFunc->GetName(); - } - else - { - MCPAssets MatAssets; - if (!MatAssets.Exact(Material).Errors(Result).ENone().ETwo().Load()) return; - MaterialObj = MatAssets.Object(); - Owner = MaterialObj; - AssetDisplayName = MaterialObj->GetName(); - } - - if (DryRun) - { - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: [DRY RUN] Would add expression '%s' to '%s' at (%d, %d)"), - *ExpressionClass, *AssetDisplayName, PosX, PosY); - - Result->SetBoolField(TEXT("dryRun"), true); - Result->SetStringField(TEXT("material"), AssetDisplayName); - return; - } - - // Ensure the MaterialGraph exists (commandlet mode doesn't auto-create it) - if (MaterialObj) MCPUtils::EnsureMaterialGraph(MaterialObj); - - // Create, register, and PostEditChange the expression — all inside an SEH wrapper because - // some classes (e.g. UMaterialExpressionParameter) lack CLASS_Abstract but crash during - // PostEditChange. The SEH wrapper cleans up the bad expression on crash. - UMaterialExpression* NewExpr = nullptr; -#if PLATFORM_WINDOWS - int32 CreateResult = TryAddMaterialExpressionSEH(Owner, ExprClass, MaterialObj, MatFunc, PosX, PosY, &NewExpr); - if (CreateResult != 0 || !NewExpr) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf( - TEXT("Expression class '%s' cannot be instantiated (may be abstract or have internal errors)."), - *ExpressionClass)); - } -#else - NewExpr = NewObject(Owner, ExprClass); - if (!NewExpr) - { - return MCPUtils::MakeErrorJson(Result, TEXT("Failed to create material expression object")); - } - NewExpr->MaterialExpressionEditorX = PosX; - NewExpr->MaterialExpressionEditorY = PosY; - if (MaterialObj) - { - MaterialObj->GetExpressionCollection().AddExpression(NewExpr); - if (MaterialObj->MaterialGraph) - { - MaterialObj->MaterialGraph->RebuildGraph(); - } - MaterialObj->PreEditChange(nullptr); - MaterialObj->PostEditChange(); - MaterialObj->MarkPackageDirty(); - } - else if (MatFunc) - { - MatFunc->GetExpressionCollection().AddExpression(NewExpr); - MatFunc->PreEditChange(nullptr); - MatFunc->PostEditChange(); - MatFunc->MarkPackageDirty(); - } -#endif - - // Save - bool bSaved = MaterialObj ? MCPUtils::SaveMaterialPackage(MaterialObj) : MCPUtils::SaveGenericPackage(MatFunc); - - // Find the node GUID from the material graph (only for materials) - FString NodeGuid; - if (MaterialObj && MaterialObj->MaterialGraph) - { - for (UEdGraphNode* Node : MaterialObj->MaterialGraph->Nodes) - { - UMaterialGraphNode* MatNode = Cast(Node); - if (MatNode && MatNode->MaterialExpression == NewExpr) - { - NodeGuid = Node->NodeGuid.ToString(); - break; - } - } - } - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Added expression '%s' to '%s' (nodeId: %s, saved: %s)"), - *ExpressionClass, *AssetDisplayName, *NodeGuid, bSaved ? TEXT("true") : TEXT("false")); - - // Serialize the expression details - TSharedPtr ExprDetails = MCPUtils::SerializeMaterialExpression(NewExpr); - - Result->SetStringField(TEXT("material"), AssetDisplayName); - Result->SetStringField(TEXT("nodeId"), NodeGuid); - if (ExprDetails.IsValid()) - { - Result->SetObjectField(TEXT("expression"), ExprDetails); - } - Result->SetBoolField(TEXT("saved"), bSaved); - } -}; - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -UCLASS() -class UMCPHandler_DeleteMaterialExpression : public UObject, public IMCPHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Optional, Description="Material name or package path (specify this or materialFunction)")) - FString Material; - - UPROPERTY(meta=(Optional, Description="Material function name or package path (specify this or material)")) - FString MaterialFunction; - - UPROPERTY(meta=(Description="Node GUID of the expression to delete")) - FString Node; - - UPROPERTY(meta=(Optional, Description="If true, preview the change without applying it")) - bool DryRun = false; - - virtual FString GetDescription() const override - { - return TEXT("Remove an expression node from a material or material function graph."); - } - - virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override - { - if (Material.IsEmpty() && MaterialFunction.IsEmpty()) - { - return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: 'material' or 'materialFunction'")); - } - - // Load material or material function - UMaterial* MaterialObj = nullptr; - UMaterialFunction* MatFunc = nullptr; - FString AssetDisplayName; - - if (!MaterialFunction.IsEmpty()) - { - MCPAssets MFAssets; - if (!MFAssets.Exact(MaterialFunction).Errors(Result).ENone().ETwo().Load()) return; - MatFunc = MFAssets.Object(); - AssetDisplayName = MatFunc->GetName(); - } - else - { - MCPAssets MatAssets; - if (!MatAssets.Exact(Material).Errors(Result).ENone().ETwo().Load()) return; - MaterialObj = MatAssets.Object(); - AssetDisplayName = MaterialObj->GetName(); - } - - // For materials, we need the graph to find nodes by GUID - if (MaterialObj) MCPUtils::EnsureMaterialGraph(MaterialObj); - UEdGraph* Graph = MaterialObj ? (UEdGraph*)MaterialObj->MaterialGraph : (MatFunc ? MatFunc->MaterialGraph : nullptr); - if (!Graph) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("'%s' has no material graph"), *AssetDisplayName)); - } - - // Find the node by GUID - UMaterialGraphNode* TargetMatNode = nullptr; - for (UEdGraphNode* GraphNode : Graph->Nodes) - { - if (!GraphNode) continue; - if (GraphNode->NodeGuid.ToString() == Node) - { - TargetMatNode = Cast(GraphNode); - break; - } - } - - if (!TargetMatNode) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found in material graph"), *Node)); - } - - if (!TargetMatNode->MaterialExpression) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' has no associated material expression"), *Node)); - } - - // Capture info before deletion - FString DeletedNodeTitle = TargetMatNode->GetNodeTitle(ENodeTitleType::FullTitle).ToString(); - FString DeletedExprClass = TargetMatNode->MaterialExpression->GetClass()->GetName(); - - if (DryRun) - { - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: [DRY RUN] Would delete expression '%s' (nodeId: %s) from '%s'"), - *DeletedExprClass, *Node, *AssetDisplayName); - - Result->SetBoolField(TEXT("dryRun"), true); - Result->SetStringField(TEXT("material"), AssetDisplayName); - Result->SetStringField(TEXT("deletedNodeTitle"), DeletedNodeTitle); - Result->SetStringField(TEXT("deletedExpressionClass"), DeletedExprClass); - return; - } - - // Remove the expression - UMaterialExpression* ExprToRemove = TargetMatNode->MaterialExpression; - if (MaterialObj) - { - MaterialObj->GetExpressionCollection().RemoveExpression(ExprToRemove); - } - else - { - MatFunc->GetExpressionCollection().RemoveExpression(ExprToRemove); - } - ExprToRemove->MarkAsGarbage(); - - // Rebuild graph - Graph->NotifyGraphChanged(); - - UObject* Asset = MaterialObj ? (UObject*)MaterialObj : (UObject*)MatFunc; - Asset->PreEditChange(nullptr); - Asset->PostEditChange(); - Asset->MarkPackageDirty(); - - // Save - bool bSaved = MaterialObj ? MCPUtils::SaveMaterialPackage(MaterialObj) : MCPUtils::SaveGenericPackage(MatFunc); - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Deleted expression '%s' (nodeId: %s) from '%s' (saved: %s)"), - *DeletedExprClass, *Node, *AssetDisplayName, bSaved ? TEXT("true") : TEXT("false")); - - Result->SetStringField(TEXT("material"), AssetDisplayName); - Result->SetStringField(TEXT("deletedNodeTitle"), DeletedNodeTitle); - Result->SetStringField(TEXT("deletedExpressionClass"), DeletedExprClass); - Result->SetBoolField(TEXT("saved"), bSaved); - } -}; - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -UCLASS() -class UMCPHandler_ConnectMaterialExpressionPins : public UObject, public IMCPHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Optional, Description="Material name or package path (specify this or materialFunction)")) - FString Material; - - UPROPERTY(meta=(Optional, Description="Material function name or package path (specify this or material)")) - FString MaterialFunction; - - UPROPERTY(meta=(Description="Node GUID of the source (output) node")) - FString SourceNode; - - UPROPERTY(meta=(Description="Pin name on the source node")) - FString SourcePinName; - - UPROPERTY(meta=(Description="Node GUID of the target (input) node")) - FString TargetNode; - - UPROPERTY(meta=(Description="Pin name on the target node")) - FString TargetPinName; - - UPROPERTY(meta=(Optional, Description="If true, preview the change without applying it")) - bool DryRun = false; - - virtual FString GetDescription() const override - { - return TEXT("Connect two pins in a material or material function graph."); - } - - virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override - { - if (Material.IsEmpty() && MaterialFunction.IsEmpty()) - { - return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: 'material' or 'materialFunction'")); - } - - // Load material or material function - UMaterial* MaterialObj = nullptr; - UMaterialFunction* MatFunc = nullptr; - FString AssetDisplayName; - - if (!MaterialFunction.IsEmpty()) - { - MCPAssets MFAssets; - if (!MFAssets.Exact(MaterialFunction).Errors(Result).ENone().ETwo().Load()) return; - MatFunc = MFAssets.Object(); - AssetDisplayName = MatFunc->GetName(); - } - else - { - MCPAssets MatAssets; - if (!MatAssets.Exact(Material).Errors(Result).ENone().ETwo().Load()) return; - MaterialObj = MatAssets.Object(); - AssetDisplayName = MaterialObj->GetName(); - } - - if (MaterialObj) MCPUtils::EnsureMaterialGraph(MaterialObj); - UEdGraph* Graph = MaterialObj ? (UEdGraph*)MaterialObj->MaterialGraph : (MatFunc ? MatFunc->MaterialGraph : nullptr); - if (!Graph) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("'%s' has no material graph"), *AssetDisplayName)); - } - - // Find source and target nodes by GUID - UEdGraphNode* SourceGraphNode = nullptr; - UEdGraphNode* TargetGraphNode = nullptr; - - for (UEdGraphNode* GraphNode : Graph->Nodes) - { - if (!GraphNode) continue; - if (GraphNode->NodeGuid.ToString() == SourceNode) - SourceGraphNode = GraphNode; - if (GraphNode->NodeGuid.ToString() == TargetNode) - TargetGraphNode = GraphNode; - if (SourceGraphNode && TargetGraphNode) - break; - } - - if (!SourceGraphNode) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Source node '%s' not found in material graph"), *SourceNode)); - } - if (!TargetGraphNode) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Target node '%s' not found in material graph"), *TargetNode)); - } - - // Find pins - UEdGraphPin* SourcePin = SourceGraphNode->FindPin(FName(*SourcePinName)); - if (!SourcePin) - { - // List available pins for debugging - TArray> PinNames; - for (UEdGraphPin* P : SourceGraphNode->Pins) - { - if (P) PinNames.Add(MakeShared( - FString::Printf(TEXT("%s (%s)"), *P->PinName.ToString(), - P->Direction == EGPD_Input ? TEXT("Input") : TEXT("Output")))); - } - MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Source pin '%s' not found on node '%s'"), - *SourcePinName, *SourceNode)); - Result->SetArrayField(TEXT("availablePins"), PinNames); - return; - } - - UEdGraphPin* TargetPin = TargetGraphNode->FindPin(FName(*TargetPinName)); - if (!TargetPin) - { - TArray> PinNames; - for (UEdGraphPin* P : TargetGraphNode->Pins) - { - if (P) PinNames.Add(MakeShared( - FString::Printf(TEXT("%s (%s)"), *P->PinName.ToString(), - P->Direction == EGPD_Input ? TEXT("Input") : TEXT("Output")))); - } - MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Target pin '%s' not found on node '%s'"), - *TargetPinName, *TargetNode)); - Result->SetArrayField(TEXT("availablePins"), PinNames); - return; - } - - if (DryRun) - { - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: [DRY RUN] Would connect %s.%s -> %s.%s in '%s'"), - *SourceNode, *SourcePinName, *TargetNode, *TargetPinName, *AssetDisplayName); - - Result->SetBoolField(TEXT("dryRun"), true); - Result->SetStringField(TEXT("material"), AssetDisplayName); - return; - } - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Connecting %s.%s -> %s.%s in '%s'"), - *SourceNode, *SourcePinName, *TargetNode, *TargetPinName, *AssetDisplayName); - - // Try to connect via the schema - const UEdGraphSchema* Schema = Graph->GetSchema(); - if (!Schema) - { - return MCPUtils::MakeErrorJson(Result, TEXT("Material graph schema not found")); - } - - bool bConnected = Schema->TryCreateConnection(SourcePin, TargetPin); - - Result->SetStringField(TEXT("material"), AssetDisplayName); - - if (!bConnected) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf( - TEXT("Cannot connect %s.%s to %s.%s — types may be incompatible"), - *SourceNode, *SourcePinName, *TargetNode, *TargetPinName)); - } - - // Save - UObject* Asset = MaterialObj ? (UObject*)MaterialObj : (UObject*)MatFunc; - Asset->PreEditChange(nullptr); - Asset->PostEditChange(); - bool bSaved = MaterialObj ? MCPUtils::SaveMaterialPackage(MaterialObj) : MCPUtils::SaveGenericPackage(MatFunc); - Result->SetBoolField(TEXT("saved"), bSaved); - } -}; - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -UCLASS() -class UMCPHandler_DisconnectMaterialExpressionPin : public UObject, public IMCPHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Optional, Description="Material name or package path (specify this or materialFunction)")) - FString Material; - - UPROPERTY(meta=(Optional, Description="Material function name or package path (specify this or material)")) - FString MaterialFunction; - - UPROPERTY(meta=(Description="Node GUID of the node whose pin to disconnect")) - FString Node; - - UPROPERTY(meta=(Description="Pin name to disconnect")) - FString PinName; - - UPROPERTY(meta=(Optional, Description="If true, preview the change without applying it")) - bool DryRun = false; - - virtual FString GetDescription() const override - { - return TEXT("Break all connections on a specific pin in a material or material function graph."); - } - - virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override - { - if (Material.IsEmpty() && MaterialFunction.IsEmpty()) - { - return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: 'material' or 'materialFunction'")); - } - - // Load material or material function - UMaterial* MaterialObj = nullptr; - UMaterialFunction* MatFunc = nullptr; - FString AssetDisplayName; - - if (!MaterialFunction.IsEmpty()) - { - MCPAssets MFAssets; - if (!MFAssets.Exact(MaterialFunction).Errors(Result).ENone().ETwo().Load()) return; - MatFunc = MFAssets.Object(); - AssetDisplayName = MatFunc->GetName(); - } - else - { - MCPAssets MatAssets; - if (!MatAssets.Exact(Material).Errors(Result).ENone().ETwo().Load()) return; - MaterialObj = MatAssets.Object(); - AssetDisplayName = MaterialObj->GetName(); - } - - if (MaterialObj) MCPUtils::EnsureMaterialGraph(MaterialObj); - UEdGraph* Graph = MaterialObj ? (UEdGraph*)MaterialObj->MaterialGraph : (MatFunc ? MatFunc->MaterialGraph : nullptr); - if (!Graph) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("'%s' has no material graph"), *AssetDisplayName)); - } - - // Find node by GUID - UEdGraphNode* TargetGraphNode = nullptr; - for (UEdGraphNode* GraphNode : Graph->Nodes) - { - if (!GraphNode) continue; - if (GraphNode->NodeGuid.ToString() == Node) - { - TargetGraphNode = GraphNode; - break; - } - } - - if (!TargetGraphNode) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found in material graph"), *Node)); - } - - // Find pin - UEdGraphPin* Pin = TargetGraphNode->FindPin(FName(*PinName)); - if (!Pin) - { - TArray> PinNames; - for (UEdGraphPin* P : TargetGraphNode->Pins) - { - if (P) PinNames.Add(MakeShared( - FString::Printf(TEXT("%s (%s)"), *P->PinName.ToString(), - P->Direction == EGPD_Input ? TEXT("Input") : TEXT("Output")))); - } - MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Pin '%s' not found on node '%s'"), - *PinName, *Node)); - Result->SetArrayField(TEXT("availablePins"), PinNames); - return; - } - - int32 BrokenCount = Pin->LinkedTo.Num(); - - if (DryRun) - { - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: [DRY RUN] Would disconnect pin '%s' on node '%s' in '%s' (%d links)"), - *PinName, *Node, *AssetDisplayName, BrokenCount); - - Result->SetBoolField(TEXT("dryRun"), true); - Result->SetStringField(TEXT("material"), AssetDisplayName); - Result->SetNumberField(TEXT("brokenLinkCount"), BrokenCount); - return; - } - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Disconnecting pin '%s' on node '%s' in '%s' (%d links)"), - *PinName, *Node, *AssetDisplayName, BrokenCount); - - // Break all links - Pin->BreakAllPinLinks(); - - UObject* Asset = MaterialObj ? (UObject*)MaterialObj : (UObject*)MatFunc; - Asset->PreEditChange(nullptr); - Asset->PostEditChange(); - - // Save - bool bSaved = MaterialObj ? MCPUtils::SaveMaterialPackage(MaterialObj) : MCPUtils::SaveGenericPackage(MatFunc); - - Result->SetStringField(TEXT("material"), AssetDisplayName); - Result->SetNumberField(TEXT("brokenLinkCount"), BrokenCount); - Result->SetBoolField(TEXT("saved"), bSaved); - } -}; - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -UCLASS() -class UMCPHandler_SetMaterialExpressionProperty : public UObject, public IMCPHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Optional, Description="Material name or package path (specify this or materialFunction)")) - FString Material; - - UPROPERTY(meta=(Optional, Description="Material function name or package path (specify this or material)")) - FString MaterialFunction; - - UPROPERTY(meta=(Description="Node GUID of the expression to modify")) - FString Node; - - virtual FString GetDescription() const override - { - return TEXT("Set the value or properties on a material expression node. " - "The 'value' field in the JSON payload provides the new value, whose format depends on the expression type."); - } - - virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override - { - if (Material.IsEmpty() && MaterialFunction.IsEmpty()) - { - return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: 'material' or 'materialFunction'")); - } - - if (!Json->HasField(TEXT("value"))) - { - return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: value")); - } - - // Load material or material function - UMaterial* MaterialObj = nullptr; - UMaterialFunction* MatFunc = nullptr; - FString AssetDisplayName; - - if (!MaterialFunction.IsEmpty()) - { - MCPAssets MFAssets; - if (!MFAssets.Exact(MaterialFunction).Errors(Result).ENone().ETwo().Load()) return; - MatFunc = MFAssets.Object(); - AssetDisplayName = MatFunc->GetName(); - } - else - { - MCPAssets MatAssets; - if (!MatAssets.Exact(Material).Errors(Result).ENone().ETwo().Load()) return; - MaterialObj = MatAssets.Object(); - AssetDisplayName = MaterialObj->GetName(); - } - - if (MaterialObj) MCPUtils::EnsureMaterialGraph(MaterialObj); - UEdGraph* Graph = MaterialObj ? (UEdGraph*)MaterialObj->MaterialGraph : (MatFunc ? MatFunc->MaterialGraph : nullptr); - if (!Graph) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("'%s' has no material graph"), *AssetDisplayName)); - } - - // Find the node by GUID - UMaterialGraphNode* TargetMatNode = nullptr; - for (UEdGraphNode* GraphNode : Graph->Nodes) - { - if (!GraphNode) continue; - if (GraphNode->NodeGuid.ToString() == Node) - { - TargetMatNode = Cast(GraphNode); - break; - } - } - - if (!TargetMatNode) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found in material graph"), *Node)); - } - - UMaterialExpression* Expr = TargetMatNode->MaterialExpression; - if (!Expr) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' has no associated material expression"), *Node)); - } - - FString ExprType; - FString NewValueStr; - - UObject* Asset = MaterialObj ? (UObject*)MaterialObj : (UObject*)MatFunc; - Asset->PreEditChange(nullptr); - - // Handle based on expression type - if (UMaterialExpressionConstant* ConstExpr = Cast(Expr)) - { - ExprType = TEXT("Constant"); - double Value = Json->GetNumberField(TEXT("value")); - ConstExpr->R = (float)Value; - NewValueStr = FString::Printf(TEXT("%f"), Value); - } - else if (UMaterialExpressionConstant3Vector* C3Expr = Cast(Expr)) - { - ExprType = TEXT("Constant3Vector"); - const TSharedPtr* ValueObj = nullptr; - if (Json->TryGetObjectField(TEXT("value"), ValueObj) && ValueObj && (*ValueObj).IsValid()) - { - double R = 0, G = 0, B = 0; - (*ValueObj)->TryGetNumberField(TEXT("r"), R); - (*ValueObj)->TryGetNumberField(TEXT("g"), G); - (*ValueObj)->TryGetNumberField(TEXT("b"), B); - C3Expr->Constant = FLinearColor((float)R, (float)G, (float)B); - NewValueStr = FString::Printf(TEXT("(%f, %f, %f)"), R, G, B); - } - else - { - Asset->PostEditChange(); - return MCPUtils::MakeErrorJson(Result, TEXT("Constant3Vector requires value as object {r, g, b}")); - } - } - else if (UMaterialExpressionConstant4Vector* C4Expr = Cast(Expr)) - { - ExprType = TEXT("Constant4Vector"); - const TSharedPtr* ValueObj = nullptr; - if (Json->TryGetObjectField(TEXT("value"), ValueObj) && ValueObj && (*ValueObj).IsValid()) - { - double R = 0, G = 0, B = 0, A = 1; - (*ValueObj)->TryGetNumberField(TEXT("r"), R); - (*ValueObj)->TryGetNumberField(TEXT("g"), G); - (*ValueObj)->TryGetNumberField(TEXT("b"), B); - (*ValueObj)->TryGetNumberField(TEXT("a"), A); - C4Expr->Constant = FLinearColor((float)R, (float)G, (float)B, (float)A); - NewValueStr = FString::Printf(TEXT("(%f, %f, %f, %f)"), R, G, B, A); - } - else - { - Asset->PostEditChange(); - return MCPUtils::MakeErrorJson(Result, TEXT("Constant4Vector requires value as object {r, g, b, a}")); - } - } - else if (UMaterialExpressionScalarParameter* SPExpr = Cast(Expr)) - { - ExprType = TEXT("ScalarParameter"); - double Value = Json->GetNumberField(TEXT("value")); - SPExpr->DefaultValue = (float)Value; - NewValueStr = FString::Printf(TEXT("%f"), Value); - - FString ParamName; - if (Json->TryGetStringField(TEXT("parameterName"), ParamName) && !ParamName.IsEmpty()) - { - SPExpr->ParameterName = FName(*ParamName); - } - } - else if (UMaterialExpressionVectorParameter* VPExpr = Cast(Expr)) - { - ExprType = TEXT("VectorParameter"); - const TSharedPtr* ValueObj = nullptr; - if (Json->TryGetObjectField(TEXT("value"), ValueObj) && ValueObj && (*ValueObj).IsValid()) - { - double R = 0, G = 0, B = 0, A = 1; - (*ValueObj)->TryGetNumberField(TEXT("r"), R); - (*ValueObj)->TryGetNumberField(TEXT("g"), G); - (*ValueObj)->TryGetNumberField(TEXT("b"), B); - (*ValueObj)->TryGetNumberField(TEXT("a"), A); - VPExpr->DefaultValue = FLinearColor((float)R, (float)G, (float)B, (float)A); - NewValueStr = FString::Printf(TEXT("(%f, %f, %f, %f)"), R, G, B, A); - } - else - { - Asset->PostEditChange(); - return MCPUtils::MakeErrorJson(Result, TEXT("VectorParameter requires value as object {r, g, b, a}")); - } - - FString ParamName; - if (Json->TryGetStringField(TEXT("parameterName"), ParamName) && !ParamName.IsEmpty()) - { - VPExpr->ParameterName = FName(*ParamName); - } - } - else if (UMaterialExpressionTextureCoordinate* TCExpr = Cast(Expr)) - { - ExprType = TEXT("TextureCoordinate"); - const TSharedPtr* ValueObj = nullptr; - if (Json->TryGetObjectField(TEXT("value"), ValueObj) && ValueObj && (*ValueObj).IsValid()) - { - double CoordIndex = 0, UTiling = 1, VTiling = 1; - (*ValueObj)->TryGetNumberField(TEXT("coordinateIndex"), CoordIndex); - (*ValueObj)->TryGetNumberField(TEXT("uTiling"), UTiling); - (*ValueObj)->TryGetNumberField(TEXT("vTiling"), VTiling); - TCExpr->CoordinateIndex = (int32)CoordIndex; - TCExpr->UTiling = (float)UTiling; - TCExpr->VTiling = (float)VTiling; - NewValueStr = FString::Printf(TEXT("(index=%d, uTiling=%f, vTiling=%f)"), (int32)CoordIndex, UTiling, VTiling); - } - else - { - Asset->PostEditChange(); - return MCPUtils::MakeErrorJson(Result, TEXT("TextureCoordinate requires value as object {coordinateIndex, uTiling, vTiling}")); - } - } - else if (UMaterialExpressionCustom* CustomExpr = Cast(Expr)) - { - ExprType = TEXT("Custom"); - FString Code; - if (Json->TryGetStringField(TEXT("code"), Code)) - { - CustomExpr->Code = Code; - NewValueStr = FString::Printf(TEXT("Code: %d chars"), Code.Len()); - } - else if (Json->HasField(TEXT("value"))) - { - // Also accept code via value field as string - FString ValueStr = Json->GetStringField(TEXT("value")); - if (!ValueStr.IsEmpty()) - { - CustomExpr->Code = ValueStr; - NewValueStr = FString::Printf(TEXT("Code: %d chars"), ValueStr.Len()); - } - } - - FString OutputTypeStr; - if (Json->TryGetStringField(TEXT("outputType"), OutputTypeStr) && !OutputTypeStr.IsEmpty()) - { - int64 EnumVal = StaticEnum()->GetValueByNameString(OutputTypeStr); - if (EnumVal != INDEX_NONE) - { - CustomExpr->OutputType = (ECustomMaterialOutputType)EnumVal; - } - } - } - else if (UMaterialExpressionComponentMask* CMExpr = Cast(Expr)) - { - ExprType = TEXT("ComponentMask"); - const TSharedPtr* ValueObj = nullptr; - if (Json->TryGetObjectField(TEXT("value"), ValueObj) && ValueObj && (*ValueObj).IsValid()) - { - bool bR = false, bG = false, bB = false, bA = false; - (*ValueObj)->TryGetBoolField(TEXT("r"), bR); - (*ValueObj)->TryGetBoolField(TEXT("g"), bG); - (*ValueObj)->TryGetBoolField(TEXT("b"), bB); - (*ValueObj)->TryGetBoolField(TEXT("a"), bA); - CMExpr->R = bR ? 1 : 0; - CMExpr->G = bG ? 1 : 0; - CMExpr->B = bB ? 1 : 0; - CMExpr->A = bA ? 1 : 0; - NewValueStr = FString::Printf(TEXT("(R=%s, G=%s, B=%s, A=%s)"), - bR ? TEXT("true") : TEXT("false"), - bG ? TEXT("true") : TEXT("false"), - bB ? TEXT("true") : TEXT("false"), - bA ? TEXT("true") : TEXT("false")); - } - else - { - Asset->PostEditChange(); - return MCPUtils::MakeErrorJson(Result, TEXT("ComponentMask requires value as object {r, g, b, a} (booleans)")); - } - } - else - { - Asset->PostEditChange(); - return MCPUtils::MakeErrorJson(Result, FString::Printf( - TEXT("Expression type '%s' does not support direct value setting. Supported types: Constant, " - "Constant3Vector, Constant4Vector, ScalarParameter, VectorParameter, TextureCoordinate, " - "Custom, ComponentMask"), - *Expr->GetClass()->GetName())); - } - - Asset->PostEditChange(); - Asset->MarkPackageDirty(); - - // Save - bool bSaved = MaterialObj ? MCPUtils::SaveMaterialPackage(MaterialObj) : MCPUtils::SaveGenericPackage(MatFunc); - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Set expression value on node '%s' (%s) in '%s': %s"), - *Node, *ExprType, *AssetDisplayName, *NewValueStr); - - Result->SetStringField(TEXT("material"), AssetDisplayName); - Result->SetStringField(TEXT("expressionType"), ExprType); - Result->SetStringField(TEXT("newValue"), NewValueStr); - Result->SetBoolField(TEXT("saved"), bSaved); - } -}; - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -UCLASS() -class UMCPHandler_SetMaterialExpressionPosition : public UObject, public IMCPHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Optional, Description="Material name or package path (specify this or materialFunction)")) - FString Material; - - UPROPERTY(meta=(Optional, Description="Material function name or package path (specify this or material)")) - FString MaterialFunction; - - UPROPERTY(meta=(Description="Node GUID of the expression to reposition")) - FString Node; - - UPROPERTY(meta=(Description="New X position")) - int32 PosX = 0; - - UPROPERTY(meta=(Description="New Y position")) - int32 PosY = 0; - - UPROPERTY(meta=(Optional, Description="If true, preview the change without applying it")) - bool DryRun = false; - - virtual FString GetDescription() const override - { - return TEXT("Reposition a material expression node in the material graph editor."); - } - - virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override - { - if (Material.IsEmpty() && MaterialFunction.IsEmpty()) - { - return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: 'material' or 'materialFunction'")); - } - - // Load material or material function - UMaterial* MaterialObj = nullptr; - UMaterialFunction* MatFunc = nullptr; - FString AssetDisplayName; - - if (!MaterialFunction.IsEmpty()) - { - MCPAssets MFAssets; - if (!MFAssets.Exact(MaterialFunction).Errors(Result).ENone().ETwo().Load()) return; - MatFunc = MFAssets.Object(); - AssetDisplayName = MatFunc->GetName(); - } - else - { - MCPAssets MatAssets; - if (!MatAssets.Exact(Material).Errors(Result).ENone().ETwo().Load()) return; - MaterialObj = MatAssets.Object(); - AssetDisplayName = MaterialObj->GetName(); - } - - if (MaterialObj) MCPUtils::EnsureMaterialGraph(MaterialObj); - UEdGraph* Graph = MaterialObj ? (UEdGraph*)MaterialObj->MaterialGraph : (MatFunc ? MatFunc->MaterialGraph : nullptr); - if (!Graph) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("'%s' has no material graph"), *AssetDisplayName)); - } - - // Find node by GUID - UMaterialGraphNode* TargetMatNode = nullptr; - for (UEdGraphNode* GraphNode : Graph->Nodes) - { - if (!GraphNode) continue; - if (GraphNode->NodeGuid.ToString() == Node) - { - TargetMatNode = Cast(GraphNode); - break; - } - } - - if (!TargetMatNode) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found in material graph"), *Node)); - } - - if (DryRun) - { - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: [DRY RUN] Would move node '%s' to (%d, %d) in '%s'"), - *Node, PosX, PosY, *AssetDisplayName); - - Result->SetBoolField(TEXT("dryRun"), true); - Result->SetStringField(TEXT("material"), AssetDisplayName); - return; - } - - // Set position on the graph node - TargetMatNode->NodePosX = PosX; - TargetMatNode->NodePosY = PosY; - - // Also update the underlying expression position - if (TargetMatNode->MaterialExpression) - { - TargetMatNode->MaterialExpression->MaterialExpressionEditorX = PosX; - TargetMatNode->MaterialExpression->MaterialExpressionEditorY = PosY; - } - - UObject* Asset = MaterialObj ? (UObject*)MaterialObj : (UObject*)MatFunc; - Asset->PreEditChange(nullptr); - Asset->PostEditChange(); - - // Save - bool bSaved = MaterialObj ? MCPUtils::SaveMaterialPackage(MaterialObj) : MCPUtils::SaveGenericPackage(MatFunc); - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Moved node '%s' to (%d, %d) in '%s' (saved: %s)"), - *Node, PosX, PosY, *AssetDisplayName, bSaved ? TEXT("true") : TEXT("false")); - - Result->SetStringField(TEXT("material"), AssetDisplayName); - Result->SetBoolField(TEXT("saved"), bSaved); - } -}; - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -UCLASS() -class UMCPHandler_CreateMaterialFunctionAsset : public UObject, public IMCPHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Description="Name for the new material function asset")) - FString Name; - - UPROPERTY(meta=(Description="Package path where the asset will be created (must start with /Game)")) - FString PackagePath; - - UPROPERTY(meta=(Optional, Description="Description for the material function")) - FString Description; - - virtual FString GetDescription() const override - { - return TEXT("Create a new UMaterialFunction asset with an optional description."); - } - - virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override - { - if (!PackagePath.StartsWith(TEXT("/Game"))) - { - return MCPUtils::MakeErrorJson(Result, TEXT("packagePath must start with '/Game'")); - } - - // Check if asset already exists - MCPAssets ExistCheck; - if (!ExistCheck.Exact(Name).Errors(Result).EAny().Info()) return; - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Creating Material Function '%s' in '%s'"), *Name, *PackagePath); - - // Create via IAssetTools + factory - IAssetTools& AssetTools = FModuleManager::LoadModuleChecked("AssetTools").Get(); - UMaterialFunctionFactoryNew* Factory = NewObject(); - UObject* NewAsset = AssetTools.CreateAsset(Name, PackagePath, UMaterialFunction::StaticClass(), Factory); - - if (!NewAsset) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Failed to create Material Function '%s' in '%s'"), *Name, *PackagePath)); - } - - UMaterialFunction* MF = Cast(NewAsset); - if (!MF) - { - return MCPUtils::MakeErrorJson(Result, TEXT("Created asset is not a UMaterialFunction")); - } - - // Set optional description - if (!Description.IsEmpty()) - { - MF->Description = Description; - } - - // Save - bool bSaved = MCPUtils::SaveGenericPackage(MF); - - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Created Material Function '%s' (saved: %s)"), - *Name, bSaved ? TEXT("true") : TEXT("false")); - - Result->SetStringField(TEXT("path"), MF->GetPathName()); - Result->SetBoolField(TEXT("saved"), bSaved); - } -}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/MCPHandlers_MaterialRead.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/MCPHandlers_MaterialRead.h deleted file mode 100644 index 5686d462..00000000 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/MCPHandlers_MaterialRead.h +++ /dev/null @@ -1,962 +0,0 @@ -#pragma once - -#include "CoreMinimal.h" -#include "MCPHandler.h" -#include "MCPAssetFinder.h" -#include "MCPUtils.h" -#include "Materials/Material.h" -#include "MaterialDomain.h" -#include "Materials/MaterialInstanceConstant.h" -#include "Materials/MaterialFunction.h" -#include "Materials/MaterialExpression.h" -#include "Materials/MaterialExpressionScalarParameter.h" -#include "Materials/MaterialExpressionVectorParameter.h" -#include "Materials/MaterialExpressionTextureObjectParameter.h" -#include "Materials/MaterialExpressionTextureSampleParameter2D.h" -#include "Materials/MaterialExpressionStaticSwitchParameter.h" -#include "Materials/MaterialExpressionConstant.h" -#include "Materials/MaterialExpressionConstant3Vector.h" -#include "Materials/MaterialExpressionConstant4Vector.h" -#include "Materials/MaterialExpressionTextureSample.h" -#include "Materials/MaterialExpressionTextureCoordinate.h" -#include "Materials/MaterialExpressionComponentMask.h" -#include "Materials/MaterialExpressionCustom.h" -#include "Materials/MaterialExpressionFunctionInput.h" -#include "Materials/MaterialExpressionFunctionOutput.h" -#include "Materials/MaterialExpressionMaterialFunctionCall.h" -#include "MaterialGraph/MaterialGraph.h" -#include "MaterialGraph/MaterialGraphNode.h" -#include "MaterialGraph/MaterialGraphNode_Root.h" -#include "MaterialGraph/MaterialGraphSchema.h" -#include "Kismet2/BlueprintEditorUtils.h" -#include "AssetRegistry/AssetRegistryModule.h" -#include "AssetRegistry/IAssetRegistry.h" -#include "EdGraph/EdGraph.h" -#include "EdGraph/EdGraphNode.h" -#include "MCPHandlers_MaterialRead.generated.h" - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -UCLASS() -class UMCPHandler_ListMaterialAssets : public UObject, public IMCPHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Optional, Description="Filter string to match against material name or path")) - FString Filter; - - UPROPERTY(meta=(Optional, Description="Type filter: 'all', 'material', or 'instance'")) - FString Type; - - virtual FString GetDescription() const override - { - return TEXT("List Material and MaterialInstance assets, optionally filtered by name and type."); - } - - virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override - { - bool bIncludeMaterials = Type.IsEmpty() || Type == TEXT("all") || Type == TEXT("material"); - bool bIncludeInstances = Type.IsEmpty() || Type == TEXT("all") || Type == TEXT("instance"); - - MCPAssets Assets; - if (bIncludeMaterials) Assets.Scan(UMaterial::StaticClass()); - if (bIncludeInstances) Assets.Scan(UMaterialInstanceConstant::StaticClass()); - Assets.Substring(Filter).NoDerived().Info(); - - TArray> Entries; - for (const FAssetData& Asset : Assets.AllData()) - { - TSharedRef Entry = MakeShared(); - Entry->SetStringField(TEXT("name"), Asset.AssetName.ToString()); - Entry->SetStringField(TEXT("path"), Asset.PackageName.ToString()); - Entry->SetStringField(TEXT("type"), - Asset.AssetClassPath.GetAssetName() == TEXT("MaterialInstanceConstant") - ? TEXT("MaterialInstance") : TEXT("Material")); - Entries.Add(MakeShared(Entry)); - } - - Result->SetNumberField(TEXT("count"), Entries.Num()); - Result->SetArrayField(TEXT("materials"), Entries); - } -}; - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -UCLASS() -class UMCPHandler_DumpMaterial : public UObject, public IMCPHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Description="Material or MaterialInstance name or package path")) - FString Material; - - virtual FString GetDescription() const override - { - return TEXT("Get detailed info about a material or material instance, including parameters, usage flags, and referenced textures."); - } - - virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override - { - FString DecodedName = MCPUtils::UrlDecode(Material); - - // Try loading as UMaterial or UMaterialInstanceConstant - MCPAssets Assets; - Assets.Scan(UMaterial::StaticClass()); - Assets.Scan(UMaterialInstanceConstant::StaticClass()); - if (!Assets.Exact(DecodedName).Errors(Result).ENone().ETwo().Load()) return; - UMaterialInterface* LoadedObj = Assets.Object(); - - if (UMaterial* MaterialObj = Cast(LoadedObj)) - { - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: GetMaterial — loaded material '%s'"), *MaterialObj->GetName()); - - Result->SetStringField(TEXT("name"), MaterialObj->GetName()); - Result->SetStringField(TEXT("path"), MaterialObj->GetPathName()); - Result->SetStringField(TEXT("type"), TEXT("Material")); - - // Material domain - FString DomainStr = TEXT("Unknown"); - if (const UEnum* DomainEnum = StaticEnum()) - { - DomainStr = DomainEnum->GetNameStringByValue((int64)MaterialObj->MaterialDomain); - } - Result->SetStringField(TEXT("domain"), DomainStr); - - // Blend mode - FString BlendModeStr = TEXT("Unknown"); - if (const UEnum* BlendEnum = StaticEnum()) - { - BlendModeStr = BlendEnum->GetNameStringByValue((int64)MaterialObj->BlendMode); - } - Result->SetStringField(TEXT("blendMode"), BlendModeStr); - - // Shading models - TArray> ShadingModels; - FMaterialShadingModelField SMField = MaterialObj->GetShadingModels(); - if (const UEnum* SMEnum = StaticEnum()) - { - for (int32 i = 0; i < SMEnum->NumEnums() - 1; ++i) - { - EMaterialShadingModel SM = (EMaterialShadingModel)SMEnum->GetValueByIndex(i); - if (SMField.HasShadingModel(SM)) - { - ShadingModels.Add(MakeShared(SMEnum->GetNameStringByIndex(i))); - } - } - } - Result->SetArrayField(TEXT("shadingModels"), ShadingModels); - - // Two-sided - Result->SetBoolField(TEXT("twoSided"), MaterialObj->IsTwoSided()); - - // Expression count - auto Expressions = MaterialObj->GetExpressions(); - Result->SetNumberField(TEXT("expressionCount"), Expressions.Num()); - - // Parameters — iterate expressions for parameter types - TArray> Parameters; - for (UMaterialExpression* Expr : Expressions) - { - if (!Expr) continue; - - TSharedRef ParamObj = MakeShared(); - bool bIsParam = false; - - if (auto* SP = Cast(Expr)) - { - bIsParam = true; - ParamObj->SetStringField(TEXT("name"), SP->ParameterName.ToString()); - ParamObj->SetStringField(TEXT("type"), TEXT("Scalar")); - ParamObj->SetStringField(TEXT("group"), SP->Group.ToString()); - ParamObj->SetNumberField(TEXT("defaultValue"), SP->DefaultValue); - } - else if (auto* VP = Cast(Expr)) - { - bIsParam = true; - ParamObj->SetStringField(TEXT("name"), VP->ParameterName.ToString()); - ParamObj->SetStringField(TEXT("type"), TEXT("Vector")); - ParamObj->SetStringField(TEXT("group"), VP->Group.ToString()); - TSharedRef DefVal = MakeShared(); - DefVal->SetNumberField(TEXT("r"), VP->DefaultValue.R); - DefVal->SetNumberField(TEXT("g"), VP->DefaultValue.G); - DefVal->SetNumberField(TEXT("b"), VP->DefaultValue.B); - DefVal->SetNumberField(TEXT("a"), VP->DefaultValue.A); - ParamObj->SetObjectField(TEXT("defaultValue"), DefVal); - } - else if (auto* TP = Cast(Expr)) - { - bIsParam = true; - ParamObj->SetStringField(TEXT("name"), TP->ParameterName.ToString()); - ParamObj->SetStringField(TEXT("type"), TEXT("Texture")); - ParamObj->SetStringField(TEXT("group"), TP->Group.ToString()); - if (TP->Texture) - ParamObj->SetStringField(TEXT("defaultValue"), TP->Texture->GetPathName()); - } - else if (auto* SSP = Cast(Expr)) - { - bIsParam = true; - ParamObj->SetStringField(TEXT("name"), SSP->ParameterName.ToString()); - ParamObj->SetStringField(TEXT("type"), TEXT("StaticSwitch")); - ParamObj->SetStringField(TEXT("group"), SSP->Group.ToString()); - ParamObj->SetBoolField(TEXT("defaultValue"), SSP->DefaultValue); - } - - if (bIsParam) - { - Parameters.Add(MakeShared(ParamObj)); - } - } - Result->SetArrayField(TEXT("parameters"), Parameters); - - // Referenced textures - TArray> ReferencedTextures; - auto RefTexObjs = MaterialObj->GetReferencedTextures(); - for (const TObjectPtr& TexObj : RefTexObjs) - { - if (TexObj) - { - ReferencedTextures.Add(MakeShared(TexObj->GetPathName())); - } - } - Result->SetArrayField(TEXT("referencedTextures"), ReferencedTextures); - - // Graph node count - int32 GraphNodeCount = 0; - if (MaterialObj->MaterialGraph) - { - GraphNodeCount = MaterialObj->MaterialGraph->Nodes.Num(); - } - Result->SetNumberField(TEXT("graphNodeCount"), GraphNodeCount); - - // Usage flags - TSharedRef UsageFlags = MakeShared(); - UsageFlags->SetBoolField(TEXT("bUsedWithSkeletalMesh"), MaterialObj->bUsedWithSkeletalMesh != 0); - UsageFlags->SetBoolField(TEXT("bUsedWithMorphTargets"), MaterialObj->bUsedWithMorphTargets != 0); - UsageFlags->SetBoolField(TEXT("bUsedWithNiagaraSprites"), MaterialObj->bUsedWithNiagaraSprites != 0); - UsageFlags->SetBoolField(TEXT("bUsedWithParticleSprites"), MaterialObj->bUsedWithParticleSprites != 0); - UsageFlags->SetBoolField(TEXT("bUsedWithStaticLighting"), MaterialObj->bUsedWithStaticLighting != 0); - Result->SetObjectField(TEXT("usageFlags"), UsageFlags); - - // Opacity mask clip value - Result->SetNumberField(TEXT("opacityMaskClipValue"), MaterialObj->OpacityMaskClipValue); - - // Additional settings - Result->SetBoolField(TEXT("ditheredLODTransition"), MaterialObj->DitheredLODTransition != 0); - Result->SetBoolField(TEXT("bAllowNegativeEmissiveColor"), MaterialObj->bAllowNegativeEmissiveColor != 0); - - // Texture sample count (simple expression scan) - int32 TextureSampleCount = 0; - for (UMaterialExpression* Expr : Expressions) - { - if (Expr && Expr->IsA()) - { - TextureSampleCount++; - } - } - Result->SetNumberField(TEXT("textureSampleCount"), TextureSampleCount); - - return; - } - - if (UMaterialInstanceConstant* MI = Cast(LoadedObj)) - { - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: GetMaterial — loaded material instance '%s'"), *MI->GetName()); - - Result->SetStringField(TEXT("name"), MI->GetName()); - Result->SetStringField(TEXT("path"), MI->GetPathName()); - Result->SetStringField(TEXT("type"), TEXT("MaterialInstance")); - - if (MI->Parent) - { - Result->SetStringField(TEXT("parent"), MI->Parent->GetName()); - Result->SetStringField(TEXT("parentPath"), MI->Parent->GetPathName()); - } - - // Overridden parameters - TArray> OverriddenParams; - - // Scalar parameters - for (const FScalarParameterValue& Param : MI->ScalarParameterValues) - { - TSharedRef PObj = MakeShared(); - PObj->SetStringField(TEXT("name"), Param.ParameterInfo.Name.ToString()); - PObj->SetStringField(TEXT("type"), TEXT("Scalar")); - PObj->SetNumberField(TEXT("value"), Param.ParameterValue); - OverriddenParams.Add(MakeShared(PObj)); - } - - // Vector parameters - for (const FVectorParameterValue& Param : MI->VectorParameterValues) - { - TSharedRef PObj = MakeShared(); - PObj->SetStringField(TEXT("name"), Param.ParameterInfo.Name.ToString()); - PObj->SetStringField(TEXT("type"), TEXT("Vector")); - TSharedRef Val = MakeShared(); - Val->SetNumberField(TEXT("r"), Param.ParameterValue.R); - Val->SetNumberField(TEXT("g"), Param.ParameterValue.G); - Val->SetNumberField(TEXT("b"), Param.ParameterValue.B); - Val->SetNumberField(TEXT("a"), Param.ParameterValue.A); - PObj->SetObjectField(TEXT("value"), Val); - OverriddenParams.Add(MakeShared(PObj)); - } - - // Texture parameters - for (const FTextureParameterValue& Param : MI->TextureParameterValues) - { - TSharedRef PObj = MakeShared(); - PObj->SetStringField(TEXT("name"), Param.ParameterInfo.Name.ToString()); - PObj->SetStringField(TEXT("type"), TEXT("Texture")); - if (Param.ParameterValue) - PObj->SetStringField(TEXT("value"), Param.ParameterValue->GetPathName()); - else - PObj->SetStringField(TEXT("value"), TEXT("None")); - OverriddenParams.Add(MakeShared(PObj)); - } - - // Static switch parameters - for (const FStaticSwitchParameter& Param : MI->GetStaticParameters().StaticSwitchParameters) - { - TSharedRef PObj = MakeShared(); - PObj->SetStringField(TEXT("name"), Param.ParameterInfo.Name.ToString()); - PObj->SetStringField(TEXT("type"), TEXT("StaticSwitch")); - PObj->SetBoolField(TEXT("value"), Param.Value); - PObj->SetBoolField(TEXT("overridden"), Param.bOverride); - OverriddenParams.Add(MakeShared(PObj)); - } - - Result->SetArrayField(TEXT("overriddenParameters"), OverriddenParams); - - return; - } - - MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Material or MaterialInstance '%s' not found. Use list_materials to see available assets."), *DecodedName)); - } -}; - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -UCLASS() -class UMCPHandler_DumpMaterialExpressionGraph : public UObject, public IMCPHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Description="Material name or package path")) - FString Material; - - virtual FString GetDescription() const override - { - return TEXT("Get the serialized expression graph for a material, including all nodes and connections."); - } - - virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override - { - FString DecodedName = MCPUtils::UrlDecode(Material); - - MCPAssets Assets; - if (!Assets.Exact(DecodedName).Errors(Result).ENone().ETwo().Load()) return; - UMaterial* MaterialObj = Assets.Object(); - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: GetMaterialGraph — material '%s'"), *MaterialObj->GetName()); - - // Ensure the material graph is built - if (!MaterialObj->MaterialGraph) - { - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: GetMaterialGraph — MaterialGraph is null, attempting rebuild")); - // The material graph is built lazily by the material editor; force-create it - MaterialObj->MaterialGraph = CastChecked( - FBlueprintEditorUtils::CreateNewGraph(MaterialObj, NAME_None, UMaterialGraph::StaticClass(), UMaterialGraphSchema::StaticClass())); - MaterialObj->MaterialGraph->Material = MaterialObj; - MaterialObj->MaterialGraph->RebuildGraph(); - } - - if (!MaterialObj->MaterialGraph) - { - return MCPUtils::MakeErrorJson(Result, TEXT("Could not build MaterialGraph for this material")); - } - - TSharedPtr GraphJson = MCPUtils::SerializeGraph(MaterialObj->MaterialGraph); - if (!GraphJson.IsValid()) - { - return MCPUtils::MakeErrorJson(Result, TEXT("Failed to serialize material graph")); - } - - MCPUtils::CopyJsonFields(GraphJson.Get(), Result); - - // Add material name context - Result->SetStringField(TEXT("material"), MaterialObj->GetName()); - Result->SetStringField(TEXT("materialPath"), MaterialObj->GetPathName()); - } -}; - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -UCLASS() -class UMCPHandler_DescribeMaterialInEnglish : public UObject, public IMCPHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Description="Material name or package path")) - FString Material; - - virtual FString GetDescription() const override - { - return TEXT("Generate a human-readable description of a material by tracing its expression graph from the root node inputs."); - } - - virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override - { - MCPAssets Assets; - if (!Assets.Exact(Material).Errors(Result).ENone().ETwo().Load()) return; - UMaterial* MaterialObj = Assets.Object(); - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: DescribeMaterial — '%s'"), *MaterialObj->GetName()); - - // Ensure material graph is built - if (!MaterialObj->MaterialGraph) - { - MaterialObj->MaterialGraph = CastChecked( - FBlueprintEditorUtils::CreateNewGraph(MaterialObj, NAME_None, UMaterialGraph::StaticClass(), UMaterialGraphSchema::StaticClass())); - MaterialObj->MaterialGraph->Material = MaterialObj; - MaterialObj->MaterialGraph->RebuildGraph(); - } - - if (!MaterialObj->MaterialGraph) - { - return MCPUtils::MakeErrorJson(Result, TEXT("Could not build MaterialGraph for this material")); - } - - // Recursive helper: trace backwards from a pin and build a description string - TFunction TracePin = [&TracePin](UEdGraphPin* Pin, int32 Depth) -> FString - { - if (!Pin || Depth > 10) - return TEXT("(unknown)"); - - // If no connections, report as unconnected - if (Pin->LinkedTo.Num() == 0) - { - if (!Pin->DefaultValue.IsEmpty()) - return FString::Printf(TEXT("(default: %s)"), *Pin->DefaultValue); - return TEXT("(unconnected)"); - } - - TArray Sources; - for (UEdGraphPin* LinkedPin : Pin->LinkedTo) - { - if (!LinkedPin || !LinkedPin->GetOwningNode()) continue; - - UEdGraphNode* SourceNode = LinkedPin->GetOwningNode(); - FString NodeDesc; - - // Check if this is a material graph node - if (UMaterialGraphNode* MatNode = Cast(SourceNode)) - { - UMaterialExpression* Expr = MatNode->MaterialExpression; - if (!Expr) - { - NodeDesc = TEXT("(null expression)"); - } - else if (auto* SP = Cast(Expr)) - { - NodeDesc = FString::Printf(TEXT("ScalarParam \"%s\" (default: %.4f)"), *SP->ParameterName.ToString(), SP->DefaultValue); - } - else if (auto* VP = Cast(Expr)) - { - NodeDesc = FString::Printf(TEXT("VectorParam \"%s\" (default: R=%.2f G=%.2f B=%.2f A=%.2f)"), - *VP->ParameterName.ToString(), VP->DefaultValue.R, VP->DefaultValue.G, VP->DefaultValue.B, VP->DefaultValue.A); - } - else if (auto* TP = Cast(Expr)) - { - FString TexName = TP->Texture ? TP->Texture->GetName() : TEXT("None"); - NodeDesc = FString::Printf(TEXT("TextureParam \"%s\" (%s)"), *TP->ParameterName.ToString(), *TexName); - } - else if (auto* SSP = Cast(Expr)) - { - NodeDesc = FString::Printf(TEXT("StaticSwitchParam \"%s\" (default: %s)"), - *SSP->ParameterName.ToString(), SSP->DefaultValue ? TEXT("true") : TEXT("false")); - } - else if (auto* SC = Cast(Expr)) - { - NodeDesc = FString::Printf(TEXT("Constant(%.4f)"), SC->R); - } - else if (auto* C3 = Cast(Expr)) - { - NodeDesc = FString::Printf(TEXT("Constant3(R=%.2f G=%.2f B=%.2f)"), C3->Constant.R, C3->Constant.G, C3->Constant.B); - } - else if (auto* C4 = Cast(Expr)) - { - NodeDesc = FString::Printf(TEXT("Constant4(R=%.2f G=%.2f B=%.2f A=%.2f)"), C4->Constant.R, C4->Constant.G, C4->Constant.B, C4->Constant.A); - } - else if (auto* TS = Cast(Expr)) - { - FString TexName = TS->Texture ? TS->Texture->GetName() : TEXT("None"); - NodeDesc = FString::Printf(TEXT("TextureSample(%s)"), *TexName); - } - else if (auto* MFC = Cast(Expr)) - { - FString FuncName = MFC->MaterialFunction ? MFC->MaterialFunction->GetName() : TEXT("None"); - NodeDesc = FString::Printf(TEXT("FunctionCall(%s)"), *FuncName); - } - else - { - NodeDesc = Expr->GetClass()->GetName(); - } - - // If the source node has input pins with connections, recurse - TArray InputDescs; - for (UEdGraphPin* InputPin : SourceNode->Pins) - { - if (!InputPin || InputPin->Direction != EGPD_Input || InputPin->LinkedTo.Num() == 0) continue; - FString InputDesc = TracePin(InputPin, Depth + 1); - InputDescs.Add(InputDesc); - } - - if (InputDescs.Num() > 0) - { - NodeDesc += TEXT(" <- (") + FString::Join(InputDescs, TEXT(", ")) + TEXT(")"); - } - } - else - { - // Non-material node (e.g., root, comment), just use title - NodeDesc = SourceNode->GetNodeTitle(ENodeTitleType::FullTitle).ToString(); - } - - Sources.Add(NodeDesc); - } - - if (Sources.Num() == 1) - return Sources[0]; - - return TEXT("(") + FString::Join(Sources, TEXT(", ")) + TEXT(")"); - }; - - // Find root node and trace each input - TArray> InputDescriptions; - - UMaterialGraphNode_Root* RootNode = nullptr; - for (UEdGraphNode* Node : MaterialObj->MaterialGraph->Nodes) - { - RootNode = Cast(Node); - if (RootNode) break; - } - - if (!RootNode) - { - return MCPUtils::MakeErrorJson(Result, TEXT("Could not find root node in material graph")); - } - - for (UEdGraphPin* Pin : RootNode->Pins) - { - if (!Pin || Pin->Direction != EGPD_Input) continue; - - FString PinName = Pin->PinName.ToString(); - FString Description; - - if (Pin->LinkedTo.Num() == 0) - { - Description = TEXT("(unconnected)"); - } - else - { - Description = TracePin(Pin, 0); - } - - TSharedRef InputObj = MakeShared(); - InputObj->SetStringField(TEXT("input"), PinName); - InputObj->SetStringField(TEXT("chain"), Description); - InputObj->SetBoolField(TEXT("connected"), Pin->LinkedTo.Num() > 0); - InputDescriptions.Add(MakeShared(InputObj)); - } - - Result->SetStringField(TEXT("material"), MaterialObj->GetName()); - Result->SetStringField(TEXT("materialPath"), MaterialObj->GetPathName()); - Result->SetArrayField(TEXT("inputs"), InputDescriptions); - - // Also include a compact text description - FString TextDesc; - for (const TSharedPtr& Val : InputDescriptions) - { - TSharedPtr Obj = Val->AsObject(); - if (!Obj.IsValid()) continue; - FString InputName = Obj->GetStringField(TEXT("input")); - FString Chain = Obj->GetStringField(TEXT("chain")); - bool bConnected = Obj->GetBoolField(TEXT("connected")); - if (bConnected) - { - TextDesc += FString::Printf(TEXT("%s <- %s\n"), *InputName, *Chain); - } - } - if (!TextDesc.IsEmpty()) - { - Result->SetStringField(TEXT("description"), TextDesc); - } - } -}; - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -UCLASS() -class UMCPHandler_SearchWithinMaterials : public UObject, public IMCPHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Description="Search query string to match against material names, expression classes, and parameter names")) - FString Query; - - UPROPERTY(meta=(Optional, Description="Maximum number of results to return (default 50, max 200)")) - int32 MaxResults = 50; - - virtual FString GetDescription() const override - { - return TEXT("Search across all materials for matching material names, expression types, and parameter names."); - } - - virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override - { - FString DecodedQuery = MCPUtils::UrlDecode(Query); - - MaxResults = FMath::Clamp(MaxResults, 1, 200); - - TArray> Results; - - MCPAssets AllMaterials; - AllMaterials.Info(); - - for (const FAssetData& Asset : AllMaterials.AllData()) - { - if (Results.Num() >= MaxResults) break; - - FString MatName = Asset.AssetName.ToString(); - - // Check material name first - bool bNameMatch = MatName.Contains(DecodedQuery, ESearchCase::IgnoreCase); - - UMaterial* MaterialObj = Cast(const_cast(Asset).GetAsset()); - if (!MaterialObj) continue; - - auto Expressions = MaterialObj->GetExpressions(); - - if (bNameMatch) - { - // Add a match for the material itself - TSharedRef R = MakeShared(); - R->SetStringField(TEXT("material"), MatName); - R->SetStringField(TEXT("materialPath"), Asset.PackageName.ToString()); - R->SetStringField(TEXT("matchType"), TEXT("materialName")); - Results.Add(MakeShared(R)); - } - - // Search expressions - for (UMaterialExpression* Expr : Expressions) - { - if (!Expr || Results.Num() >= MaxResults) continue; - - FString ExprDesc = Expr->GetDescription(); - FString ExprClass = Expr->GetClass()->GetName(); - - // Check parameter name - FString ParamName; - if (auto* SP = Cast(Expr)) - ParamName = SP->ParameterName.ToString(); - else if (auto* VP = Cast(Expr)) - ParamName = VP->ParameterName.ToString(); - else if (auto* TP = Cast(Expr)) - ParamName = TP->ParameterName.ToString(); - else if (auto* SSP = Cast(Expr)) - ParamName = SSP->ParameterName.ToString(); - - bool bExprMatch = ExprDesc.Contains(DecodedQuery, ESearchCase::IgnoreCase) || - ExprClass.Contains(DecodedQuery, ESearchCase::IgnoreCase) || - (!ParamName.IsEmpty() && ParamName.Contains(DecodedQuery, ESearchCase::IgnoreCase)); - - if (bExprMatch) - { - TSharedRef R = MakeShared(); - R->SetStringField(TEXT("material"), MatName); - R->SetStringField(TEXT("materialPath"), Asset.PackageName.ToString()); - R->SetStringField(TEXT("matchType"), TEXT("expression")); - R->SetStringField(TEXT("expressionClass"), ExprClass); - if (!ExprDesc.IsEmpty()) - R->SetStringField(TEXT("description"), ExprDesc); - if (!ParamName.IsEmpty()) - R->SetStringField(TEXT("parameterName"), ParamName); - Results.Add(MakeShared(R)); - } - } - } - - Result->SetStringField(TEXT("query"), DecodedQuery); - Result->SetNumberField(TEXT("resultCount"), Results.Num()); - Result->SetArrayField(TEXT("results"), Results); - } -}; - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -UCLASS() -class UMCPHandler_FindMaterialReferences : public UObject, public IMCPHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Description="Material or MaterialInstance name or package path")) - FString Material; - - virtual FString GetDescription() const override - { - return TEXT("Find all assets that reference a given material or material instance."); - } - - virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override - { - // Try to find the material's package path (search both Material and MaterialInstance) - MCPAssets Assets; - Assets.Scan().Scan(); - if (!Assets.Exact(Material).Errors(Result).ENone().ETwo().Info()) return; - FString PackagePath = Assets.OneData().PackageName.ToString(); - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: FindMaterialReferences — '%s' (package: %s)"), *Material, *PackagePath); - - IAssetRegistry& Registry = *IAssetRegistry::Get(); - - TArray Referencers; - Registry.GetReferencers(FName(*PackagePath), Referencers); - - TArray> RefArray; - for (const FName& Ref : Referencers) - { - FString RefStr = Ref.ToString(); - // Skip self-reference - if (RefStr == PackagePath) continue; - RefArray.Add(MakeShared(RefStr)); - } - - Result->SetStringField(TEXT("packagePath"), PackagePath); - Result->SetNumberField(TEXT("totalReferencers"), RefArray.Num()); - Result->SetArrayField(TEXT("referencers"), RefArray); - } -}; - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -UCLASS() -class UMCPHandler_ListMaterialFunctionAssets : public UObject, public IMCPHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Optional, Description="Filter string to match against function name or path")) - FString Filter; - - virtual FString GetDescription() const override - { - return TEXT("List MaterialFunction assets, optionally filtered by name or path."); - } - - virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override - { - MCPAssets Assets; - Assets.Substring(Filter).Info(); - - TArray> Entries; - for (const FAssetData& Asset : Assets.AllData()) - { - TSharedRef Entry = MakeShared(); - Entry->SetStringField(TEXT("name"), Asset.AssetName.ToString()); - Entry->SetStringField(TEXT("path"), Asset.PackageName.ToString()); - Entries.Add(MakeShared(Entry)); - } - - Result->SetNumberField(TEXT("count"), Entries.Num()); - Result->SetArrayField(TEXT("functions"), Entries); - } -}; - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -UCLASS() -class UMCPHandler_DumpMaterialFunction : public UObject, public IMCPHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Description="MaterialFunction name or package path")) - FString MaterialFunction; - - virtual FString GetDescription() const override - { - return TEXT("Get detailed info about a material function, including its inputs, outputs, and expressions."); - } - - virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override - { - FString DecodedName = MCPUtils::UrlDecode(MaterialFunction); - - MCPAssets Assets; - if (!Assets.Exact(DecodedName).Errors(Result).ENone().ETwo().Load()) return; - UMaterialFunction* MF = Assets.Object(); - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: GetMaterialFunction — '%s'"), *MF->GetName()); - - Result->SetStringField(TEXT("name"), MF->GetName()); - Result->SetStringField(TEXT("path"), MF->GetPathName()); - Result->SetStringField(TEXT("description"), MF->GetDescription()); - - // Expression count - auto Expressions = MF->GetExpressions(); - Result->SetNumberField(TEXT("expressionCount"), Expressions.Num()); - - // List function inputs and outputs from expressions - TArray> Inputs; - TArray> Outputs; - TArray> ExpressionList; - - { - for (UMaterialExpression* Expr : Expressions) - { - if (!Expr) continue; - - if (auto* FI = Cast(Expr)) - { - TSharedRef InputObj = MakeShared(); - InputObj->SetStringField(TEXT("name"), FI->InputName.ToString()); - InputObj->SetStringField(TEXT("type"), TEXT("FunctionInput")); - InputObj->SetNumberField(TEXT("posX"), FI->MaterialExpressionEditorX); - InputObj->SetNumberField(TEXT("posY"), FI->MaterialExpressionEditorY); - Inputs.Add(MakeShared(InputObj)); - } - else if (auto* FO = Cast(Expr)) - { - TSharedRef OutputObj = MakeShared(); - OutputObj->SetStringField(TEXT("name"), FO->OutputName.ToString()); - OutputObj->SetStringField(TEXT("type"), TEXT("FunctionOutput")); - OutputObj->SetNumberField(TEXT("posX"), FO->MaterialExpressionEditorX); - OutputObj->SetNumberField(TEXT("posY"), FO->MaterialExpressionEditorY); - Outputs.Add(MakeShared(OutputObj)); - } - - // Serialize every expression - TSharedPtr ExprJson = MCPUtils::SerializeMaterialExpression(Expr); - if (ExprJson.IsValid()) - { - ExpressionList.Add(MakeShared(ExprJson.ToSharedRef())); - } - } - } - - Result->SetArrayField(TEXT("inputs"), Inputs); - Result->SetArrayField(TEXT("outputs"), Outputs); - Result->SetArrayField(TEXT("expressions"), ExpressionList); - - // If the function has an editor graph, serialize it - UEdGraph* FuncGraph = MF->MaterialGraph; - if (FuncGraph) - { - TSharedPtr GraphJson = MCPUtils::SerializeGraph(FuncGraph); - if (GraphJson.IsValid()) - { - Result->SetObjectField(TEXT("graph"), GraphJson); - } - } - } -}; - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -UCLASS() -class UMCPHandler_CompileMaterial : public UObject, public IMCPHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Description="Material name or package path")) - FString Material; - - virtual FString GetDescription() const override - { - return TEXT("Force recompile a material and check for compilation errors."); - } - - virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override - { - // Load material - MCPAssets Assets; - if (!Assets.Exact(Material).Errors(Result).ENone().ETwo().Load()) return; - UMaterial* MaterialObj = Assets.Object(); - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Validating material '%s'"), *MaterialObj->GetName()); - - // Force recompile by triggering PreEditChange/PostEditChange - MaterialObj->PreEditChange(nullptr); - MaterialObj->PostEditChange(); - - // Collect compilation errors - TArray> ErrorArray; - bool bValid = true; - - // Check for compilation errors via FMaterialResource on current platform - FMaterialResource* Resource = MaterialObj->GetMaterialResource(GMaxRHIFeatureLevel); - if (Resource) - { - const TArray& CompileErrors = Resource->GetCompileErrors(); - for (const FString& Err : CompileErrors) - { - bValid = false; - ErrorArray.Add(MakeShared(Err)); - } - } - - // Count expressions and connections - auto Expressions = MaterialObj->GetExpressions(); - int32 ExprCount = Expressions.Num(); - int32 ConnectionCount = 0; - if (MaterialObj->MaterialGraph) - { - for (UEdGraphNode* Node : MaterialObj->MaterialGraph->Nodes) - { - if (!Node) continue; - for (UEdGraphPin* Pin : Node->Pins) - { - if (Pin && Pin->Direction == EGPD_Output) - { - ConnectionCount += Pin->LinkedTo.Num(); - } - } - } - } - - Result->SetBoolField(TEXT("valid"), bValid); - Result->SetStringField(TEXT("material"), MaterialObj->GetName()); - Result->SetStringField(TEXT("materialPath"), MaterialObj->GetPathName()); - Result->SetNumberField(TEXT("expressionCount"), ExprCount); - Result->SetNumberField(TEXT("connectionCount"), ConnectionCount); - Result->SetArrayField(TEXT("errors"), ErrorArray); - Result->SetNumberField(TEXT("errorCount"), ErrorArray.Num()); - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Material '%s' validation %s (%d errors)"), - *MaterialObj->GetName(), bValid ? TEXT("passed") : TEXT("failed"), ErrorArray.Num()); - } -}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/MCPHandlers_Mutation.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/MCPHandlers_Mutation.h deleted file mode 100644 index 5fd22d6b..00000000 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/MCPHandlers_Mutation.h +++ /dev/null @@ -1,1417 +0,0 @@ -#pragma once - -#include "CoreMinimal.h" -#include "MCPHandler.h" -#include "MCPAssetFinder.h" -#include "MCPServer.h" -#include "MCPUtils.h" -#include "Engine/Blueprint.h" -#include "Materials/Material.h" -#include "Materials/MaterialInstanceConstant.h" -#include "Materials/MaterialFunction.h" -#include "Engine/World.h" -#include "Engine/LevelScriptBlueprint.h" -#include "EdGraph/EdGraph.h" -#include "EdGraph/EdGraphNode.h" -#include "EdGraph/EdGraphPin.h" -#include "EdGraphSchema_K2.h" -#include "K2Node.h" -#include "K2Node_CallFunction.h" -#include "K2Node_Event.h" -#include "K2Node_CustomEvent.h" -#include "K2Node_FunctionEntry.h" -#include "K2Node_EditablePinBase.h" -#include "K2Node_VariableGet.h" -#include "K2Node_VariableSet.h" -#include "K2Node_BreakStruct.h" -#include "K2Node_MakeStruct.h" -#include "K2Node_DynamicCast.h" -#include "K2Node_CallParentFunction.h" -#include "K2Node_IfThenElse.h" -#include "K2Node_ExecutionSequence.h" -#include "K2Node_MacroInstance.h" -#include "K2Node_SpawnActorFromClass.h" -#include "K2Node_Select.h" -#include "K2Node_Knot.h" -#include "EdGraphNode_Comment.h" -#include "GameFramework/Actor.h" -#include "Kismet2/BlueprintEditorUtils.h" -#include "Kismet2/KismetEditorUtilities.h" -#include "Serialization/JsonReader.h" -#include "Serialization/JsonWriter.h" -#include "Serialization/JsonSerializer.h" -#include "UObject/SavePackage.h" -#include "UObject/UObjectIterator.h" -#include "Misc/PackageName.h" -#include "AssetRegistry/AssetRegistryModule.h" -#include "AssetRegistry/IAssetRegistry.h" -#include "AssetToolsModule.h" -#include "IAssetTools.h" -#include "BlueprintNodeSpawner.h" -#include "MCPHandlers_Mutation.generated.h" - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -USTRUCT() -struct FMoveNodeEntry -{ - GENERATED_BODY() - - UPROPERTY() - FString Node; - - UPROPERTY() - int32 X = 0; - - UPROPERTY() - int32 Y = 0; -}; - - -UCLASS() -class UMCPHandler_SetNodePositions : public UObject, public IMCPHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Description="Blueprint name or package path")) - FString Blueprint; - - UPROPERTY(meta=(Description="Array of {nodeId, x, y} objects")) - FMCPJsonArray Nodes; - - virtual FString GetDescription() const override - { - return TEXT("Reposition one or more nodes in a Blueprint graph."); - } - - virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override - { - - MCPAssets Assets; - if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; - UBlueprint* BP = Assets.Object(); - - TArray> Results; - int32 SuccessCount = 0; - - for (const TSharedPtr& NodeVal : Nodes.Array) - { - TSharedRef EntryResult = MakeShared(); - Results.Add(MakeShared(EntryResult)); - - FMoveNodeEntry Entry; - if (!MCPUtils::PopulateFromJson(FMoveNodeEntry::StaticStruct(), &Entry, NodeVal, &*EntryResult)) continue; - - UEdGraphNode* Node = MCPUtils::FindNodeByGuid(BP, Entry.Node); - if (!Node) - { - EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Node '%s' not found"), *Entry.Node)); - continue; - } - - int32 OldX = Node->NodePosX; - int32 OldY = Node->NodePosY; - Node->NodePosX = Entry.X; - Node->NodePosY = Entry.Y; - EntryResult->SetNumberField(TEXT("oldX"), OldX); - EntryResult->SetNumberField(TEXT("oldY"), OldY); - EntryResult->SetNumberField(TEXT("newX"), Node->NodePosX); - EntryResult->SetNumberField(TEXT("newY"), Node->NodePosY); - SuccessCount++; - } - - FBlueprintEditorUtils::MarkBlueprintAsModified(BP); - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: MoveNode — %d/%d succeeded"), - SuccessCount, Nodes.Array.Num()); - - Result->SetNumberField(TEXT("movedCount"), SuccessCount); - Result->SetNumberField(TEXT("totalRequested"), Nodes.Array.Num()); - Result->SetArrayField(TEXT("results"), Results); - } -}; - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -UCLASS() -class UMCPHandler_DuplicateNodesInGraph : public UObject, public IMCPHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Description="Blueprint name or package path")) - FString Blueprint; - - UPROPERTY(meta=(Description="Graph name")) - FString Graph; - - UPROPERTY(meta=(Description="Array of node GUIDs to duplicate")) - FMCPJsonArray Nodes; - - UPROPERTY(meta=(Optional, Description="X offset for duplicated nodes")) - int32 OffsetX = 50; - - UPROPERTY(meta=(Optional, Description="Y offset for duplicated nodes")) - int32 OffsetY = 50; - - virtual FString GetDescription() const override - { - return TEXT("Duplicate one or more nodes in a Blueprint graph. " - "Creates copies offset from the originals with new GUIDs. " - "Connections are not preserved on the duplicates."); - } - - virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override - { - - MCPAssets Assets; - if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; - UBlueprint* BP = Assets.Object(); - - // Find the target graph - FString DecodedGraphName = MCPUtils::UrlDecode(Graph); - TArray MatchingGraphs = MCPUtils::AllGraphsNamed(BP, DecodedGraphName); - if (MatchingGraphs.Num() == 0) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Graph '%s' not found"), *DecodedGraphName)); - } - UEdGraph* TargetGraph = MatchingGraphs[0]; - - if (Nodes.Array.Num() == 0) - { - return MCPUtils::MakeErrorJson(Result, TEXT("nodeIds array is empty")); - } - - // Find all source nodes - TArray SourceNodes; - TArray NotFound; - - for (const TSharedPtr& IdVal : Nodes.Array) - { - FString Node = IdVal->AsString(); - UEdGraphNode* FoundNode = MCPUtils::FindNodeByGuid(BP, Node); - if (FoundNode) - { - if (FoundNode->GetGraph() == TargetGraph) - { - SourceNodes.Add(FoundNode); - } - else - { - NotFound.Add(FString::Printf(TEXT("%s (in different graph)"), *Node)); - } - } - else - { - NotFound.Add(Node); - } - } - - if (SourceNodes.Num() == 0) - { - return MCPUtils::MakeErrorJson(Result, TEXT("No valid nodes found to duplicate")); - } - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Duplicating %d node(s) in graph '%s' of '%s'"), - SourceNodes.Num(), *DecodedGraphName, *Blueprint); - - // Duplicate each node - TArray> DuplicatedNodes; - TMap OldToNewGuidMap; - - for (UEdGraphNode* SourceNode : SourceNodes) - { - UEdGraphNode* NewNode = DuplicateObject(SourceNode, TargetGraph); - if (!NewNode) - { - TSharedRef Entry = MakeShared(); - Entry->SetStringField(TEXT("sourceNodeId"), SourceNode->NodeGuid.ToString()); - Entry->SetStringField(TEXT("error"), TEXT("DuplicateObject failed")); - DuplicatedNodes.Add(MakeShared(Entry)); - continue; - } - - NewNode->CreateNewGuid(); - OldToNewGuidMap.Add(SourceNode->NodeGuid, NewNode->NodeGuid); - - NewNode->NodePosX += OffsetX; - NewNode->NodePosY += OffsetY; - - for (UEdGraphPin* Pin : NewNode->Pins) - { - if (Pin) - { - Pin->LinkedTo.Empty(); - } - } - - TargetGraph->AddNode(NewNode, false, false); - - TSharedRef Entry = MakeShared(); - Entry->SetStringField(TEXT("sourceNodeId"), SourceNode->NodeGuid.ToString()); - Entry->SetStringField(TEXT("newNodeId"), NewNode->NodeGuid.ToString()); - Entry->SetNumberField(TEXT("posX"), NewNode->NodePosX); - Entry->SetNumberField(TEXT("posY"), NewNode->NodePosY); - Entry->SetStringField(TEXT("nodeClass"), NewNode->GetClass()->GetName()); - Entry->SetStringField(TEXT("nodeTitle"), NewNode->GetNodeTitle(ENodeTitleType::FullTitle).ToString()); - DuplicatedNodes.Add(MakeShared(Entry)); - } - - FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP); - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Duplicated %d node(s)"), - DuplicatedNodes.Num()); - - Result->SetNumberField(TEXT("duplicatedCount"), DuplicatedNodes.Num()); - Result->SetArrayField(TEXT("nodes"), DuplicatedNodes); - - if (NotFound.Num() > 0) - { - TArray> NotFoundArr; - for (const FString& NF : NotFound) - { - NotFoundArr.Add(MakeShared(NF)); - } - Result->SetArrayField(TEXT("notFound"), NotFoundArr); - } - } -}; - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -USTRUCT() -struct FSpawnNodeEntry -{ - GENERATED_BODY() - - UPROPERTY() - FString ActionName; - - UPROPERTY() - int32 PosX = 0; - - UPROPERTY() - int32 PosY = 0; -}; - - -UCLASS() -class UMCPHandler_SpawnNodesInGraph : public UObject, public IMCPHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Description="Blueprint name or package path")) - FString Blueprint; - - UPROPERTY(meta=(Description="Graph name (e.g. 'EventGraph')")) - FString Graph; - - UPROPERTY(meta=(Description="Array of {actionName, posX, posY} objects. Use search_node_types to find action names.")) - FMCPJsonArray Nodes; - - virtual FString GetDescription() const override - { - return TEXT("Create nodes in a Blueprint graph using the editor's action database. " - "Can create ANY node type that appears in the editor's right-click menu, including custom K2 nodes. " - "Use search_node_types first to find the exact action name."); - } - - virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override - { - - // Load Blueprint - MCPAssets Assets; - if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; - UBlueprint* BP = Assets.Object(); - - // Find the target graph - FString DecodedGraphName = MCPUtils::UrlDecode(Graph); - TArray MatchingGraphs = MCPUtils::AllGraphsNamed(BP, DecodedGraphName); - if (MatchingGraphs.Num() == 0) - { - TArray> GraphNames; - for (UEdGraph* G : MCPUtils::AllGraphs(BP)) - { - GraphNames.Add(MakeShared(G->GetName())); - } - MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Graph '%s' not found"), *DecodedGraphName)); - Result->SetArrayField(TEXT("availableGraphs"), GraphNames); - return; - } - UEdGraph* TargetGraph = MatchingGraphs[0]; - - TArray> Results; - int32 SuccessCount = 0; - - for (const TSharedPtr& NodeVal : Nodes.Array) - { - TSharedRef EntryResult = MakeShared(); - Results.Add(MakeShared(EntryResult)); - - FSpawnNodeEntry Entry; - if (!MCPUtils::PopulateFromJson(FSpawnNodeEntry::StaticStruct(), &Entry, NodeVal, &*EntryResult)) continue; - - // Find the spawner by exact full name - TArray Matches = MCPUtils::SearchNodeSpawners(Entry.ActionName, 0, /*ExactMatch=*/true, TargetGraph); - if (Matches.Num() == 0) - { - EntryResult->SetStringField(TEXT("error"), FString::Printf( - TEXT("No action found matching '%s'. Use search_node_types to find available actions."), - *Entry.ActionName)); - continue; - } - if (Matches.Num() > 1) - { - EntryResult->SetStringField(TEXT("error"), FString::Printf( - TEXT("Ambiguous: %d spawners match '%s'. Cannot determine which one to use."), - Matches.Num(), *Entry.ActionName)); - continue; - } - UBlueprintNodeSpawner* Spawner = Matches[0]; - - // Invoke the spawner - FVector2D Location(Entry.PosX, Entry.PosY); - IBlueprintNodeBinder::FBindingSet Bindings; - UEdGraphNode* NewNode = Spawner->Invoke(TargetGraph, Bindings, Location); - - if (!NewNode) - { - EntryResult->SetStringField(TEXT("error"), TEXT("Spawner Invoke() returned null — node creation failed.")); - continue; - } - - // Ensure valid GUID - if (!NewNode->NodeGuid.IsValid()) - { - NewNode->CreateNewGuid(); - } - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Spawned node '%s' (class %s) via action '%s' in graph '%s' of '%s'"), - *NewNode->NodeGuid.ToString(), - *NewNode->GetClass()->GetName(), - *Entry.ActionName, - *DecodedGraphName, - *Blueprint); - - // Serialize result - TSharedPtr NodeState = MCPUtils::SerializeNode(NewNode); - - EntryResult->SetStringField(TEXT("nodeId"), NewNode->NodeGuid.ToString()); - EntryResult->SetStringField(TEXT("nodeClass"), NewNode->GetClass()->GetName()); - EntryResult->SetStringField(TEXT("nodeTitle"), NewNode->GetNodeTitle(ENodeTitleType::ListView).ToString()); - if (NodeState.IsValid()) - { - EntryResult->SetObjectField(TEXT("node"), NodeState); - } - SuccessCount++; - } - - FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP); - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: SpawnNode — %d/%d succeeded in graph '%s' of '%s'"), - SuccessCount, Nodes.Array.Num(), *DecodedGraphName, *Blueprint); - - Result->SetNumberField(TEXT("successCount"), SuccessCount); - Result->SetNumberField(TEXT("totalCount"), Nodes.Array.Num()); - Result->SetArrayField(TEXT("results"), Results); - } -}; - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -UCLASS() -class UMCPHandler_GetNodeComment : public UObject, public IMCPHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Description="Blueprint name or package path")) - FString Blueprint; - - UPROPERTY(meta=(Description="Node GUID")) - FString Node; - - virtual FString GetDescription() const override - { - return TEXT("Get the comment text and bubble visibility of a node."); - } - - virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override - { - - MCPAssets Assets; - if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; - UBlueprint* BP = Assets.Object(); - - UEdGraphNode* FoundNode = MCPUtils::FindNodeByGuid(BP, Node); - if (!FoundNode) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found"), *Node)); - } - - Result->SetStringField(TEXT("comment"), FoundNode->NodeComment); - Result->SetBoolField(TEXT("commentBubbleVisible"), FoundNode->bCommentBubbleVisible); - } -}; - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -UCLASS() -class UMCPHandler_SetNodeComment : public UObject, public IMCPHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Description="Blueprint name or package path")) - FString Blueprint; - - UPROPERTY(meta=(Description="Node GUID")) - FString Node; - - UPROPERTY(meta=(Description="Comment text to set")) - FString Comment; - - virtual FString GetDescription() const override - { - return TEXT("Set a node's comment text. Makes the comment bubble visible if non-empty."); - } - - virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override - { - - MCPAssets Assets; - if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; - UBlueprint* BP = Assets.Object(); - - UEdGraphNode* FoundNode = MCPUtils::FindNodeByGuid(BP, Node); - if (!FoundNode) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found"), *Node)); - } - - FString OldComment = FoundNode->NodeComment; - FoundNode->NodeComment = Comment; - - // Make the comment bubble visible if setting a non-empty comment - if (!Comment.IsEmpty()) - { - FoundNode->bCommentBubbleVisible = true; - FoundNode->bCommentBubblePinned = true; - } - - FBlueprintEditorUtils::MarkBlueprintAsModified(BP); - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Set comment on node '%s' in '%s'"), - *Node, *Blueprint); - - Result->SetStringField(TEXT("oldComment"), OldComment); - } -}; - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -UCLASS() -class UMCPHandler_DeleteNodeFromGraph : public UObject, public IMCPHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Description="Blueprint name or package path")) - FString Blueprint; - - UPROPERTY(meta=(Description="Node GUID")) - FString Node; - - virtual FString GetDescription() const override - { - return TEXT("Delete a node from a Blueprint graph. " - "Cannot delete entry nodes (FunctionEntry, Event, CustomEvent)."); - } - - virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override - { - - MCPAssets Assets; - if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; - UBlueprint* BP = Assets.Object(); - - UEdGraph* Graph = nullptr; - UEdGraphNode* FoundNode = MCPUtils::FindNodeByGuid(BP, Node, &Graph); - if (!FoundNode) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found"), *Node)); - } - if (!Graph) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Graph not found for node '%s'"), *Node)); - } - - FString NodeClass = FoundNode->GetClass()->GetName(); - FString NodeTitle = FoundNode->GetNodeTitle(ENodeTitleType::FullTitle).ToString(); - FString GraphName = Graph->GetName(); - - // Protect root/entry nodes — deleting these leaves the graph in an invalid - // state with no root node, causing compiler errors that can't be fixed - // without recreating the entire function/event. - if (Cast(FoundNode)) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf( - TEXT("Cannot delete FunctionEntry node '%s' in graph '%s'. ") - TEXT("This is the root node of the function — removing it would leave an empty, uncompilable graph. ") - TEXT("To remove the entire function, delete it from the Blueprint editor."), - *NodeTitle, *GraphName)); - } - if (Cast(FoundNode)) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf( - TEXT("Cannot delete event entry node '%s' in graph '%s'. ") - TEXT("This is the root node of the event handler — removing it would leave an empty, uncompilable graph."), - *NodeTitle, *GraphName)); - } - if (Cast(FoundNode)) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf( - TEXT("Cannot delete CustomEvent entry node '%s' in graph '%s'. ") - TEXT("This is the root node of the custom event — removing it would leave an empty, uncompilable graph."), - *NodeTitle, *GraphName)); - } - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Deleting node '%s' (%s) from graph '%s' in '%s'"), - *Node, *NodeTitle, *GraphName, *Blueprint); - - FoundNode->BreakAllNodeLinks(); - Graph->RemoveNode(FoundNode); - - FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP); - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Node deleted")); - - Result->SetStringField(TEXT("nodeClass"), NodeClass); - Result->SetStringField(TEXT("nodeTitle"), NodeTitle); - Result->SetStringField(TEXT("graph"), GraphName); - } -}; - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -UCLASS() -class UMCPHandler_ReplaceFunctionCallsInBlueprint : public UObject, public IMCPHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Description="Blueprint name or package path")) - FString Blueprint; - - UPROPERTY(meta=(Description="Old class name to match")) - FString OldClass; - - UPROPERTY(meta=(Description="New class name to redirect to")) - FString NewClass; - - UPROPERTY(meta=(Optional, Description="If true, report what would change without modifying")) - bool DryRun = false; - - virtual FString GetDescription() const override - { - return TEXT("Redirect function call nodes from one class to another. " - "Supports dry-run to preview impact before applying."); - } - - virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override - { - - // Load Blueprint - MCPAssets Assets; - if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; - UBlueprint* BP = Assets.Object(); - - // Find the new class — try several search strategies - UClass* NewClassPtr = nullptr; - - // Try finding the class across all loaded modules - NewClassPtr = FindFirstObject(*NewClass); - - // Try with U prefix stripped/added - if (!NewClassPtr && NewClass.StartsWith(TEXT("U"))) - { - NewClassPtr = FindFirstObject(*NewClass.Mid(1)); - } - if (!NewClassPtr && !NewClass.StartsWith(TEXT("U"))) - { - NewClassPtr = FindFirstObject(*FString::Printf(TEXT("U%s"), *NewClass)); - } - - // Broader search across all modules - if (!NewClassPtr) - { - for (TObjectIterator It; It; ++It) - { - if (It->GetName() == NewClass || It->GetName() == FString::Printf(TEXT("U%s"), *NewClass)) - { - NewClassPtr = *It; - break; - } - } - } - - if (!NewClassPtr) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Could not find class '%s'"), *NewClass)); - } - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: %s function calls in '%s': %s -> %s (%s)"), - DryRun ? TEXT("[DRY RUN] Analyzing replacement of") : TEXT("Replacing"), - *Blueprint, *OldClass, *NewClass, *NewClassPtr->GetPathName()); - - // Find all CallFunction nodes - int32 ReplacedCount = 0; - TArray> BrokenConnections; - - for (UK2Node_CallFunction* CallNode : MCPUtils::AllNodes(BP)) - { - UClass* ParentClass = CallNode->FunctionReference.GetMemberParentClass(); - if (!ParentClass) - { - continue; - } - - // Match by class name (with or without U prefix, and _C suffix for BP classes) - FString ParentName = ParentClass->GetName(); - bool bMatch = (ParentName == OldClass) || - (ParentName == FString::Printf(TEXT("%s_C"), *OldClass)) || - (ParentName == FString::Printf(TEXT("U%s"), *OldClass)) || - (OldClass.StartsWith(TEXT("U")) && (ParentName == OldClass.Mid(1))) || - (OldClass.EndsWith(TEXT("_C")) && (ParentName == OldClass.LeftChop(2))); - - if (!bMatch) - { - continue; - } - - FName FuncName = CallNode->FunctionReference.GetMemberName(); - - // Find the matching function in the new class - UFunction* NewFunc = NewClassPtr->FindFunctionByName(FuncName); - if (!NewFunc) - { - UE_LOG(LogTemp, Warning, TEXT("BlueprintMCP: Function '%s' not found in '%s', skipping node"), - *FuncName.ToString(), *NewClass); - - TSharedRef Warning = MakeShared(); - Warning->SetStringField(TEXT("type"), TEXT("functionNotFound")); - Warning->SetStringField(TEXT("functionName"), FuncName.ToString()); - Warning->SetStringField(TEXT("nodeId"), CallNode->NodeGuid.ToString()); - BrokenConnections.Add(MakeShared(Warning)); - continue; - } - - if (DryRun) - { - // In dry run mode: report what would be affected without modifying - ReplacedCount++; - - // Check which pins have connections that might break - for (UEdGraphPin* Pin : CallNode->Pins) - { - if (!Pin || Pin->LinkedTo.Num() == 0) continue; - - // Check if the new function has a matching parameter - bool bPinExistsInNew = false; - for (TFieldIterator PropIt(NewFunc); PropIt; ++PropIt) - { - if (PropIt->GetFName() == Pin->PinName || - Pin->PinName == UEdGraphSchema_K2::PN_Execute || - Pin->PinName == UEdGraphSchema_K2::PN_Then || - Pin->PinName == UEdGraphSchema_K2::PN_Self || - Pin->PinName == UEdGraphSchema_K2::PN_ReturnValue) - { - bPinExistsInNew = true; - break; - } - } - - if (!bPinExistsInNew) - { - for (UEdGraphPin* Linked : Pin->LinkedTo) - { - if (Linked && Linked->GetOwningNode()) - { - TSharedRef AtRisk = MakeShared(); - AtRisk->SetStringField(TEXT("type"), TEXT("connectionAtRisk")); - AtRisk->SetStringField(TEXT("functionName"), FuncName.ToString()); - AtRisk->SetStringField(TEXT("nodeId"), CallNode->NodeGuid.ToString()); - AtRisk->SetStringField(TEXT("pinName"), Pin->PinName.ToString()); - AtRisk->SetStringField(TEXT("connectedToNode"), Linked->GetOwningNode()->NodeGuid.ToString()); - AtRisk->SetStringField(TEXT("connectedToPin"), Linked->PinName.ToString()); - BrokenConnections.Add(MakeShared(AtRisk)); - } - } - } - } - } - else - { - // Record existing pin connections before replacement - TMap>> OldPinConnections; - for (UEdGraphPin* Pin : CallNode->Pins) - { - if (Pin->LinkedTo.Num() > 0) - { - TArray> Links; - for (UEdGraphPin* Linked : Pin->LinkedTo) - { - if (Linked && Linked->GetOwningNode()) - { - Links.Add(TPair( - Linked->GetOwningNode()->NodeGuid.ToString(), - Linked->PinName.ToString())); - } - } - OldPinConnections.Add(Pin->PinName.ToString(), Links); - } - } - - // Replace the function reference - CallNode->SetFromFunction(NewFunc); - ReplacedCount++; - - // Check which connections survived - for (auto& Pair : OldPinConnections) - { - const FString& PinName = Pair.Key; - const TArray>& OldLinks = Pair.Value; - - UEdGraphPin* NewPin = CallNode->FindPin(FName(*PinName)); - for (auto& Link : OldLinks) - { - bool bStillConnected = false; - if (NewPin) - { - for (UEdGraphPin* L : NewPin->LinkedTo) - { - if (L && L->GetOwningNode() && - L->GetOwningNode()->NodeGuid.ToString() == Link.Key && - L->PinName.ToString() == Link.Value) - { - bStillConnected = true; - break; - } - } - } - if (!bStillConnected) - { - TSharedRef Broken = MakeShared(); - Broken->SetStringField(TEXT("type"), TEXT("connectionLost")); - Broken->SetStringField(TEXT("functionName"), FuncName.ToString()); - Broken->SetStringField(TEXT("nodeId"), CallNode->NodeGuid.ToString()); - Broken->SetStringField(TEXT("pinName"), PinName); - Broken->SetStringField(TEXT("wasConnectedToNode"), Link.Key); - Broken->SetStringField(TEXT("wasConnectedToPin"), Link.Value); - BrokenConnections.Add(MakeShared(Broken)); - } - } - } - } - } - - if (DryRun) - { - Result->SetNumberField(TEXT("wouldReplaceCount"), ReplacedCount); - Result->SetNumberField(TEXT("connectionsAtRisk"), BrokenConnections.Num()); - Result->SetArrayField(TEXT("connectionsAtRisk"), BrokenConnections); - return; - } - - if (ReplacedCount > 0) - { - FBlueprintEditorUtils::MarkBlueprintAsModified(BP); - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Replaced %d function call(s)"), ReplacedCount); - - Result->SetNumberField(TEXT("replacedCount"), ReplacedCount); - Result->SetNumberField(TEXT("brokenConnectionCount"), BrokenConnections.Num()); - Result->SetArrayField(TEXT("brokenConnections"), BrokenConnections); - return; - } - - Result->SetNumberField(TEXT("replacedCount"), 0); - Result->SetStringField(TEXT("message"), FString::Printf( - TEXT("No function call nodes found targeting class '%s'"), *OldClass)); - } -}; - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -UCLASS() -class UMCPHandler_RefreshAllNodesInGraph : public UObject, public IMCPHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Description="Blueprint name or package path")) - FString Blueprint; - - virtual FString GetDescription() const override - { - return TEXT("Refresh all nodes in a Blueprint, removing orphaned pins. " - "Reports compiler warnings and errors."); - } - - virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override - { - - // Load Blueprint - MCPAssets Assets; - if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; - UBlueprint* BP = Assets.Object(); - - // Count graphs and nodes before refresh - int32 GraphCount = MCPUtils::AllGraphs(BP).Num(); - int32 NodeCount = MCPUtils::AllNodes(BP).Num(); - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Refreshing all nodes in '%s' (%d graphs, %d nodes)"), - *Blueprint, GraphCount, NodeCount); - - // Refresh all nodes - FBlueprintEditorUtils::RefreshAllNodes(BP); - - // Remove orphaned pins from all nodes - int32 OrphanedPinsRemoved = 0; - for (UEdGraphNode* Node : MCPUtils::AllNodes(BP)) - { - for (int32 i = Node->Pins.Num() - 1; i >= 0; --i) - { - UEdGraphPin* Pin = Node->Pins[i]; - if (Pin && Pin->bOrphanedPin) - { - Pin->BreakAllPinLinks(); - Node->Pins.RemoveAt(i); - OrphanedPinsRemoved++; - } - } - } - - if (OrphanedPinsRemoved > 0) - { - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Removed %d orphaned pins"), OrphanedPinsRemoved); - } - - // Mark as modified and recompile after orphan removal - FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP); - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: RefreshAllNodes complete")); - - // Collect compiler warnings and errors from the blueprint status - TArray> WarningsArr; - TArray> ErrorsArr; - - if (BP->Status == BS_Error) - { - ErrorsArr.Add(MakeShared(TEXT("Blueprint has compiler errors after refresh"))); - } - - // Check each graph for nodes with error/warning status - for (UEdGraphNode* Node : MCPUtils::AllNodes(BP)) - { - if (Node->bHasCompilerMessage) - { - FString NodeTitle = Node->GetNodeTitle(ENodeTitleType::FullTitle).ToString(); - FString NodeMsg = FString::Printf(TEXT("[%s] %s: %s"), - *Node->GetGraph()->GetName(), *NodeTitle, *Node->ErrorMsg); - if (Node->ErrorType == EMessageSeverity::Error) - { - ErrorsArr.Add(MakeShared(NodeMsg)); - } - else - { - WarningsArr.Add(MakeShared(NodeMsg)); - } - } - } - - Result->SetNumberField(TEXT("graphCount"), GraphCount); - Result->SetNumberField(TEXT("nodeCount"), NodeCount); - Result->SetNumberField(TEXT("orphanedPinsRemoved"), OrphanedPinsRemoved); - Result->SetArrayField(TEXT("warnings"), WarningsArr); - Result->SetArrayField(TEXT("errors"), ErrorsArr); - } -}; - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -UCLASS() -class UMCPHandler_ChangeStructNodeType : public UObject, public IMCPHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Description="Blueprint name or package path")) - FString Blueprint; - - UPROPERTY(meta=(Description="Node GUID of the BreakStruct or MakeStruct node")) - FString Node; - - UPROPERTY(meta=(Description="New struct type name (e.g. 'FVector', 'Vector')")) - FString NewType; - - virtual FString GetDescription() const override - { - return TEXT("Change the struct type on a BreakStruct or MakeStruct node. " - "Attempts to reconnect matching pins after the type change."); - } - - virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override - { - - // Load Blueprint - MCPAssets Assets; - if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; - UBlueprint* BP = Assets.Object(); - - // Find node - UEdGraph* Graph = nullptr; - UEdGraphNode* FoundNode = MCPUtils::FindNodeByGuid(BP, Node, &Graph); - if (!FoundNode) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found"), *Node)); - } - - // Determine what kind of struct node this is - UK2Node_BreakStruct* BreakNode = Cast(FoundNode); - UK2Node_MakeStruct* MakeNode = Cast(FoundNode); - - if (!BreakNode && !MakeNode) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' is not a BreakStruct or MakeStruct node (class: %s)"), - *Node, *FoundNode->GetClass()->GetName())); - } - - // Find the new struct type - FString SearchName = NewType; - if (SearchName.StartsWith(TEXT("F"))) - { - SearchName = SearchName.Mid(1); - } - - UScriptStruct* NewStruct = FindFirstObject(*SearchName); - if (!NewStruct) - { - // Try with full name including F prefix - NewStruct = FindFirstObject(*NewType); - } - if (!NewStruct) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Struct type '%s' not found"), *NewType)); - } - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Changing struct node '%s' to type '%s'"), - *Node, *NewStruct->GetName()); - - // Helper: extract property base name from a BreakStruct pin name - auto ExtractPropertyBaseName = [](const FString& PinName) -> FString - { - // Find the last underscore before 32 hex chars (GUID) - int32 LastUnderscore; - if (PinName.FindLastChar(TEXT('_'), LastUnderscore) && (LastUnderscore > 0)) - { - FString Suffix = PinName.Mid(LastUnderscore + 1); - if (Suffix.Len() == 32) - { - FString WithoutGuid = PinName.Left(LastUnderscore); - // Strip _Index - int32 SecondUnderscore; - if (WithoutGuid.FindLastChar(TEXT('_'), SecondUnderscore) && (SecondUnderscore > 0)) - { - FString IndexStr = WithoutGuid.Mid(SecondUnderscore + 1); - if (IndexStr.IsNumeric()) - { - return WithoutGuid.Left(SecondUnderscore); - } - } - } - } - return PinName; - }; - - // Remember existing connections keyed by property base name - struct FPinConnection - { - EEdGraphPinDirection Direction; - TArray LinkedPins; - }; - TMap ConnectionsByBaseName; - - for (UEdGraphPin* Pin : FoundNode->Pins) - { - if (!Pin || Pin->LinkedTo.Num() == 0) continue; - if (Pin->PinType.PinCategory == UEdGraphSchema_K2::PC_Exec) continue; - - FString BaseName = ExtractPropertyBaseName(Pin->PinName.ToString()); - FPinConnection& Conn = ConnectionsByBaseName.FindOrAdd(BaseName); - Conn.Direction = Pin->Direction; - Conn.LinkedPins = Pin->LinkedTo; - } - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Saved %d pin connections to reconnect"), ConnectionsByBaseName.Num()); - - // Change the struct type and reconstruct - if (BreakNode) - { - BreakNode->StructType = NewStruct; - } - else if (MakeNode) - { - MakeNode->StructType = NewStruct; - } - - // Break all existing links before reconstruction - FoundNode->BreakAllNodeLinks(); - - // Reconnect pins by matching property base names - const UEdGraphSchema* Schema = Graph->GetSchema(); - if (!Schema) - { - return MCPUtils::MakeErrorJson(Result, TEXT("Graph schema not found")); - } - - // Reconstruct to rebuild pins for the new struct type (use schema version for MinimalAPI compat) - Schema->ReconstructNode(*FoundNode); - - int32 Reconnected = 0; - int32 Failed = 0; - TArray> ReconnectDetails; - - for (auto& Pair : ConnectionsByBaseName) - { - const FString& BaseName = Pair.Key; - const FPinConnection& OldConn = Pair.Value; - - // Find matching new pin - UEdGraphPin* NewPin = nullptr; - for (UEdGraphPin* Pin : FoundNode->Pins) - { - if (!Pin || Pin->Direction != OldConn.Direction) continue; - FString NewBaseName = ExtractPropertyBaseName(Pin->PinName.ToString()); - if (NewBaseName.Equals(BaseName, ESearchCase::IgnoreCase)) - { - NewPin = Pin; - break; - } - } - - // Also try matching the struct input/output pin (single struct pin) - if (!NewPin) - { - for (UEdGraphPin* Pin : FoundNode->Pins) - { - if (!Pin || Pin->Direction != OldConn.Direction) continue; - if ((Pin->PinType.PinCategory == UEdGraphSchema_K2::PC_Struct) && - (Pin->PinType.PinSubCategoryObject == NewStruct)) - { - NewPin = Pin; - break; - } - } - } - - if (NewPin) - { - for (UEdGraphPin* Target : OldConn.LinkedPins) - { - bool bOK = Schema->TryCreateConnection(NewPin, Target); - if (bOK) - { - Reconnected++; - } - else - { - Failed++; - } - - TSharedPtr Detail = MakeShared(); - Detail->SetStringField(TEXT("property"), BaseName); - Detail->SetBoolField(TEXT("connected"), bOK); - ReconnectDetails.Add(MakeShared(Detail)); - } - } - else - { - Failed += OldConn.LinkedPins.Num(); - TSharedPtr Detail = MakeShared(); - Detail->SetStringField(TEXT("property"), BaseName); - Detail->SetBoolField(TEXT("connected"), false); - Detail->SetStringField(TEXT("reason"), TEXT("No matching pin found on new struct")); - ReconnectDetails.Add(MakeShared(Detail)); - } - } - - FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP); - - // Return updated node state - TSharedPtr UpdatedNodeState = MCPUtils::SerializeNode(FoundNode); - - Result->SetStringField(TEXT("newStructType"), NewStruct->GetName()); - Result->SetStringField(TEXT("nodeClass"), FoundNode->GetClass()->GetName()); - Result->SetNumberField(TEXT("reconnected"), Reconnected); - Result->SetNumberField(TEXT("failed"), Failed); - Result->SetArrayField(TEXT("reconnectDetails"), ReconnectDetails); - if (UpdatedNodeState.IsValid()) - { - Result->SetObjectField(TEXT("updatedNode"), UpdatedNodeState); - } - } -}; - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -UCLASS() -class UMCPHandler_SetClassDefaultValue : public UObject, public IMCPHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Description="Blueprint name or package path")) - FString Blueprint; - - UPROPERTY(meta=(Description="Property name on the Class Default Object")) - FString Property; - - UPROPERTY(meta=(Description="New value (parsed according to property type)")) - FString Value; - - virtual FString GetDescription() const override - { - return TEXT("Set a default property value on a Blueprint's Class Default Object. " - "Handles class references, object references, and simple types."); - } - - virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override - { - - // Load Blueprint - MCPAssets Assets; - if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; - UBlueprint* BP = Assets.Object(); - - if (!BP->GeneratedClass) - { - return MCPUtils::MakeErrorJson(Result, TEXT("Blueprint has no GeneratedClass")); - } - - UObject* CDO = BP->GeneratedClass->GetDefaultObject(); - if (!CDO) - { - return MCPUtils::MakeErrorJson(Result, TEXT("Could not get Class Default Object")); - } - - FProperty* Prop = BP->GeneratedClass->FindPropertyByName(*Property); - if (!Prop) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Property '%s' not found on '%s'"), *Property, *Blueprint)); - } - - FString OldValue; - Prop->ExportTextItem_Direct(OldValue, Prop->ContainerPtrToValuePtr(CDO), nullptr, CDO, PPF_None); - - bool bSuccess = false; - FString ActualNewValue; - - // Handle class/soft-class properties (TSubclassOf, TSoftClassPtr) - FClassProperty* ClassProp = CastField(Prop); - FSoftClassProperty* SoftClassProp = CastField(Prop); - - if (ClassProp || SoftClassProp) - { - // Resolve the value to a UClass* - UClass* ResolvedClass = nullptr; - - // Try as a C++ class name first - for (TObjectIterator It; It; ++It) - { - if (It->GetName() == Value || It->GetName() == Value + TEXT("_C")) - { - ResolvedClass = *It; - break; - } - } - - // Try loading as a Blueprint asset - if (!ResolvedClass) - { - MCPAssets ValueAssets; - if (!ValueAssets.Exact(Value).AllContent().Errors(Result).ETwo().Load()) return; - if (!ValueAssets.Objects().IsEmpty()) - { - if (ValueAssets.Object()->GeneratedClass) - ResolvedClass = ValueAssets.Object()->GeneratedClass; - } - } - - if (!ResolvedClass) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Could not resolve '%s' to a class"), *Value)); - } - - // Validate meta class compatibility - if (ClassProp) - { - UClass* MetaClass = ClassProp->MetaClass; - if (MetaClass && !ResolvedClass->IsChildOf(MetaClass)) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf( - TEXT("'%s' is not a subclass of '%s' (required by property '%s')"), - *ResolvedClass->GetName(), *MetaClass->GetName(), *Property)); - } - ClassProp->SetPropertyValue_InContainer(CDO, ResolvedClass); - } - else - { - FSoftObjectPtr SoftPtr(ResolvedClass); - SoftClassProp->SetPropertyValue_InContainer(CDO, SoftPtr); - } - ActualNewValue = ResolvedClass->GetName(); - bSuccess = true; - } - // Handle object properties (TObjectPtr, UObject*) - else if (FObjectProperty* ObjProp = CastField(Prop)) - { - // Try finding an existing object/asset by name - UObject* ResolvedObj = nullptr; - - // Try loading as a Blueprint asset - MCPAssets ValueAssets; - if (!ValueAssets.Exact(Value).AllContent().Errors(Result).ENone().ETwo().Load()) return; - if (ValueAssets.Object()->GeneratedClass) - ResolvedObj = ValueAssets.Object()->GeneratedClass->GetDefaultObject(); - - ObjProp->SetPropertyValue_InContainer(CDO, ResolvedObj); - ActualNewValue = ResolvedObj->GetName(); - bSuccess = true; - } - // Handle simple types via ImportText - else - { - const TCHAR* ImportResult = Prop->ImportText_Direct(*Value, Prop->ContainerPtrToValuePtr(CDO), CDO, PPF_None); - if (ImportResult) - { - Prop->ExportTextItem_Direct(ActualNewValue, Prop->ContainerPtrToValuePtr(CDO), nullptr, CDO, PPF_None); - bSuccess = true; - } - else - { - return MCPUtils::MakeErrorJson(Result, FString::Printf( - TEXT("Failed to set property '%s' to '%s' — value could not be parsed for type '%s'"), - *Property, *Value, *Prop->GetCPPType())); - } - } - - if (!bSuccess) - { - return MCPUtils::MakeErrorJson(Result, TEXT("Failed to set property value")); - } - - // Mark modified and save - CDO->MarkPackageDirty(); - BP->Modify(); - - FKismetEditorUtilities::CompileBlueprint(BP); - bool bSaved = MCPUtils::SaveBlueprintPackage(BP); - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Set '%s.%s' from '%s' to '%s' (saved: %s)"), - *Blueprint, *Property, *OldValue, *ActualNewValue, bSaved ? TEXT("true") : TEXT("false")); - - Result->SetStringField(TEXT("oldValue"), OldValue); - Result->SetStringField(TEXT("newValue"), ActualNewValue); - Result->SetStringField(TEXT("propertyType"), Prop->GetCPPType()); - Result->SetBoolField(TEXT("saved"), bSaved); - } -}; - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -UCLASS() -class UMCPHandler_SearchSpawnableNodeTypes : public UObject, public IMCPHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Description="Search query string")) - FString Query; - - UPROPERTY(meta=(Optional, Description="Maximum number of results (default 50, max 500)")) - int32 MaxResults = 50; - - UPROPERTY(meta=(Optional, Description="Blueprint name or path. If specified with graph, only returns nodes compatible with that graph.")) - FString Blueprint; - - UPROPERTY(meta=(Optional, Description="Graph name to filter by compatibility. Requires blueprint.")) - FString Graph; - - virtual FString GetDescription() const override - { - return TEXT("Search the Blueprint action database for node spawners matching a query. " - "Returns full action names for use with spawn_node. " - "Optionally filter by blueprint+graph to only show compatible node types."); - } - - virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override - { - int32 ClampedMax = FMath::Clamp(MaxResults, 1, 500); - - // Optionally resolve a graph to filter by compatibility - UEdGraph* GraphFilter = nullptr; - if (!Blueprint.IsEmpty() && !Graph.IsEmpty()) - { - MCPAssets BPAssets; - if (!BPAssets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; - UBlueprint* BP = BPAssets.Object(); - - FString DecodedGraphName = MCPUtils::UrlDecode(Graph); - TArray MatchingGraphs = MCPUtils::AllGraphsNamed(BP, DecodedGraphName); - if (MatchingGraphs.Num() > 0) - { - GraphFilter = MatchingGraphs[0]; - } - if (!GraphFilter) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Graph '%s' not found"), *DecodedGraphName)); - } - } - - TArray Spawners = MCPUtils::SearchNodeSpawners(Query, ClampedMax, /*ExactMatch=*/false, GraphFilter); - - TArray> ResultArray; - for (UBlueprintNodeSpawner* Spawner : Spawners) - { - ResultArray.Add(MakeShared(MCPUtils::NodeSpawnerFullName(Spawner))); - } - - Result->SetNumberField(TEXT("count"), ResultArray.Num()); - Result->SetArrayField(TEXT("results"), ResultArray); - } -}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/MCPHandlers_Params.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/MCPHandlers_Params.h deleted file mode 100644 index f9ca9bad..00000000 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/MCPHandlers_Params.h +++ /dev/null @@ -1,490 +0,0 @@ -#pragma once - -#include "CoreMinimal.h" -#include "MCPHandler.h" -#include "MCPAssetFinder.h" -#include "MCPUtils.h" -#include "Engine/Blueprint.h" -#include "EdGraph/EdGraph.h" -#include "EdGraph/EdGraphPin.h" -#include "K2Node_FunctionEntry.h" -#include "K2Node_CustomEvent.h" -#include "K2Node_EditablePinBase.h" -#include "Kismet2/BlueprintEditorUtils.h" -#include "MCPHandlers_Params.generated.h" - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -UCLASS() -class UMCPHandler_ChangeFunctionParameterType : public UObject, public IMCPHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Description="Blueprint name or package path")) - FString Blueprint; - - UPROPERTY(meta=(Description="Name of the function or custom event")) - FString FunctionName; - - UPROPERTY(meta=(Description="Name of the parameter to change")) - FString ParamName; - - UPROPERTY(meta=(Description="New type for the parameter (e.g. 'Float', 'Vector', 'MyStruct')")) - FString NewType; - - UPROPERTY(meta=(Optional, Description="If true, analyze impact without making changes")) - bool DryRun = false; - - virtual FString GetDescription() const override - { - return TEXT("Change the type of an existing parameter on a function or custom event in a Blueprint."); - } - - virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override - { - MCPAssets Assets; - if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; - UBlueprint* BP = Assets.Object(); - - // Resolve the new type using the shared resolver (supports primitives, structs, enums, and object references) - FEdGraphPinType NewPinType; - if (!MCPUtils::ResolveTypeFromString(NewType, NewPinType, Result)) - return; - - // Find the entry node: K2Node_FunctionEntry in a function graph, - // or K2Node_CustomEvent in any graph - UK2Node_EditablePinBase* EntryNode = nullptr; - FString FoundNodeType; - - // Strategy 1: Look for a K2Node_FunctionEntry in a function graph matching the name - for (UK2Node_FunctionEntry* FuncEntry : MCPUtils::AllNodes(BP)) - { - if (FuncEntry->GetGraph()->GetName().Equals(FunctionName, ESearchCase::IgnoreCase)) - { - EntryNode = FuncEntry; - FoundNodeType = TEXT("FunctionEntry"); - break; - } - } - - // Strategy 2: Search for a K2Node_CustomEvent with matching CustomFunctionName - if (!EntryNode) - { - for (UK2Node_CustomEvent* CustomEvent : MCPUtils::AllNodes(BP)) - { - if (CustomEvent->CustomFunctionName.ToString().Equals(FunctionName, ESearchCase::IgnoreCase)) - { - EntryNode = CustomEvent; - FoundNodeType = TEXT("CustomEvent"); - break; - } - } - } - - if (!EntryNode) - { - // List available functions/events for debugging - TArray> Available; - for (UK2Node_FunctionEntry* FE : MCPUtils::AllNodes(BP)) - { - Available.Add(MakeShared( - FString::Printf(TEXT("function:%s"), *FE->GetGraph()->GetName()))); - } - for (UK2Node_CustomEvent* CE : MCPUtils::AllNodes(BP)) - { - Available.Add(MakeShared( - FString::Printf(TEXT("event:%s"), *CE->CustomFunctionName.ToString()))); - } - - MCPUtils::MakeErrorJson(Result, FString::Printf( - TEXT("Function or custom event '%s' not found in Blueprint '%s'"), - *FunctionName, *Blueprint)); - Result->SetArrayField(TEXT("availableFunctionsAndEvents"), Available); - return; - } - - // Find the UserDefinedPin matching paramName - bool bPinFound = false; - for (TSharedPtr& PinInfo : EntryNode->UserDefinedPins) - { - if (PinInfo.IsValid() && PinInfo->PinName.ToString().Equals(ParamName, ESearchCase::IgnoreCase)) - { - EntryNode->PreEditChange(nullptr); - PinInfo->PinType = NewPinType; - EntryNode->PostEditChange(); - bPinFound = true; - break; - } - } - - if (!bPinFound) - { - // List available params for debugging - TArray> ParamNames; - for (const TSharedPtr& PinInfo : EntryNode->UserDefinedPins) - { - if (PinInfo.IsValid()) - { - ParamNames.Add(MakeShared(PinInfo->PinName.ToString())); - } - } - - MCPUtils::MakeErrorJson(Result, FString::Printf( - TEXT("Parameter '%s' not found in %s '%s'"), - *ParamName, *FoundNodeType, *FunctionName)); - Result->SetArrayField(TEXT("availableParams"), ParamNames); - return; - } - - // Check for dry run - if (DryRun) - { - // Analyze what would change: report connected pins that may disconnect - TArray> AffectedPins; - for (UEdGraphPin* Pin : EntryNode->Pins) - { - if (Pin && Pin->PinName.ToString().Equals(ParamName, ESearchCase::IgnoreCase) && Pin->LinkedTo.Num() > 0) - { - for (UEdGraphPin* Linked : Pin->LinkedTo) - { - if (Linked && Linked->GetOwningNode()) - { - TSharedRef AffPin = MakeShared(); - AffPin->SetStringField(TEXT("pinName"), Pin->PinName.ToString()); - AffPin->SetStringField(TEXT("connectedToNode"), Linked->GetOwningNode()->NodeGuid.ToString()); - AffPin->SetStringField(TEXT("connectedToPin"), Linked->PinName.ToString()); - AffPin->SetStringField(TEXT("currentType"), Pin->PinType.PinCategory.ToString()); - if (Pin->PinType.PinSubCategoryObject.IsValid()) - AffPin->SetStringField(TEXT("currentSubtype"), Pin->PinType.PinSubCategoryObject->GetName()); - AffectedPins.Add(MakeShared(AffPin)); - } - } - } - } - - Result->SetBoolField(TEXT("dryRun"), true); - Result->SetStringField(TEXT("nodeType"), FoundNodeType); - Result->SetStringField(TEXT("nodeId"), EntryNode->NodeGuid.ToString()); - Result->SetNumberField(TEXT("connectionsAtRisk"), AffectedPins.Num()); - Result->SetArrayField(TEXT("affectedPins"), AffectedPins); - return; - } - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Changing param '%s' in %s '%s' of '%s' to %s"), - *ParamName, *FoundNodeType, *FunctionName, *Blueprint, *NewType); - - // Reconstruct the node to update output pins with the new type (use schema for MinimalAPI compat) - if (UEdGraph* OwningGraph = EntryNode->GetGraph()) - { - if (const UEdGraphSchema* Schema = OwningGraph->GetSchema()) - { - Schema->ReconstructNode(*EntryNode); - } - } - - // Save - bool bSaved = MCPUtils::SaveBlueprintPackage(BP); - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Parameter type changed, save %s"), - bSaved ? TEXT("succeeded") : TEXT("failed")); - - // Serialize the updated entry node state - TSharedPtr UpdatedNodeState = MCPUtils::SerializeNode(EntryNode); - - Result->SetStringField(TEXT("nodeType"), FoundNodeType); - Result->SetStringField(TEXT("nodeId"), EntryNode->NodeGuid.ToString()); - Result->SetBoolField(TEXT("saved"), bSaved); - if (UpdatedNodeState.IsValid()) - { - Result->SetObjectField(TEXT("updatedNode"), UpdatedNodeState); - } - } -}; - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -UCLASS() -class UMCPHandler_RemoveFunctionParameter : public UObject, public IMCPHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Description="Blueprint name or package path")) - FString Blueprint; - - UPROPERTY(meta=(Description="Name of the function or custom event")) - FString FunctionName; - - UPROPERTY(meta=(Description="Name of the parameter to remove")) - FString ParamName; - - virtual FString GetDescription() const override - { - return TEXT("Remove a parameter from a function or custom event in a Blueprint."); - } - - virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override - { - MCPAssets Assets; - if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; - UBlueprint* BP = Assets.Object(); - - // Find the entry node - UK2Node_EditablePinBase* EntryNode = nullptr; - FString FoundNodeType; - - // Strategy 1: Look for a K2Node_FunctionEntry in a function graph matching the name - for (UK2Node_FunctionEntry* FuncEntry : MCPUtils::AllNodes(BP)) - { - if (FuncEntry->GetGraph()->GetName().Equals(FunctionName, ESearchCase::IgnoreCase)) - { - EntryNode = FuncEntry; - FoundNodeType = TEXT("FunctionEntry"); - break; - } - } - - // Strategy 2: Search for a K2Node_CustomEvent with matching CustomFunctionName - if (!EntryNode) - { - for (UK2Node_CustomEvent* CustomEvent : MCPUtils::AllNodes(BP)) - { - if (CustomEvent->CustomFunctionName.ToString().Equals(FunctionName, ESearchCase::IgnoreCase)) - { - EntryNode = CustomEvent; - FoundNodeType = TEXT("CustomEvent"); - break; - } - } - } - - if (!EntryNode) - { - // List available functions/events for debugging - TArray> Available; - for (UK2Node_FunctionEntry* FE : MCPUtils::AllNodes(BP)) - { - Available.Add(MakeShared( - FString::Printf(TEXT("function:%s"), *FE->GetGraph()->GetName()))); - } - for (UK2Node_CustomEvent* CE : MCPUtils::AllNodes(BP)) - { - Available.Add(MakeShared( - FString::Printf(TEXT("event:%s"), *CE->CustomFunctionName.ToString()))); - } - - MCPUtils::MakeErrorJson(Result, FString::Printf( - TEXT("Function or custom event '%s' not found in Blueprint '%s'"), - *FunctionName, *Blueprint)); - Result->SetArrayField(TEXT("availableFunctionsAndEvents"), Available); - return; - } - - // Find and remove the UserDefinedPin matching paramName - int32 RemovedIndex = INDEX_NONE; - for (int32 i = 0; i < EntryNode->UserDefinedPins.Num(); ++i) - { - if (EntryNode->UserDefinedPins[i].IsValid() && - EntryNode->UserDefinedPins[i]->PinName.ToString().Equals(ParamName, ESearchCase::IgnoreCase)) - { - RemovedIndex = i; - break; - } - } - - if (RemovedIndex == INDEX_NONE) - { - // List available params for debugging - TArray> ParamNames; - for (const TSharedPtr& PinInfo : EntryNode->UserDefinedPins) - { - if (PinInfo.IsValid()) - { - ParamNames.Add(MakeShared(PinInfo->PinName.ToString())); - } - } - - MCPUtils::MakeErrorJson(Result, FString::Printf( - TEXT("Parameter '%s' not found in %s '%s'"), - *ParamName, *FoundNodeType, *FunctionName)); - Result->SetArrayField(TEXT("availableParams"), ParamNames); - return; - } - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Removing param '%s' from %s '%s' in '%s'"), - *ParamName, *FoundNodeType, *FunctionName, *Blueprint); - - // Remove the pin - EntryNode->UserDefinedPins.RemoveAt(RemovedIndex); - - // Reconstruct the node to update output pins (use schema for MinimalAPI compat) - if (UEdGraph* OwningGraph = EntryNode->GetGraph()) - { - if (const UEdGraphSchema* Schema = OwningGraph->GetSchema()) - { - Schema->ReconstructNode(*EntryNode); - } - } - - // Save - bool bSaved = MCPUtils::SaveBlueprintPackage(BP); - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Parameter removed, save %s"), - bSaved ? TEXT("succeeded") : TEXT("failed")); - - Result->SetStringField(TEXT("nodeType"), FoundNodeType); - Result->SetStringField(TEXT("nodeId"), EntryNode->NodeGuid.ToString()); - Result->SetBoolField(TEXT("saved"), bSaved); - } -}; - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -UCLASS() -class UMCPHandler_AddFunctionParameter : public UObject, public IMCPHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Description="Blueprint name or package path")) - FString Blueprint; - - UPROPERTY(meta=(Description="Name of the function, custom event, or event dispatcher")) - FString FunctionName; - - UPROPERTY(meta=(Description="Name for the new parameter")) - FString ParamName; - - UPROPERTY(meta=(Description="Type for the new parameter (e.g. 'Float', 'Vector', 'MyStruct')")) - FString ParamType; - - virtual FString GetDescription() const override - { - return TEXT("Add a new parameter to a function, custom event, or event dispatcher in a Blueprint."); - } - - virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override - { - MCPAssets Assets; - if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; - UBlueprint* BP = Assets.Object(); - - // Resolve param type - FEdGraphPinType PinType; - if (!MCPUtils::ResolveTypeFromString(ParamType, PinType, Result)) - return; - - // Find the entry node using 3 strategies - UK2Node_EditablePinBase* EntryNode = nullptr; - FString NodeType; - - FName FuncFName(*FunctionName); - - // Strategy 1: K2Node_FunctionEntry in function graphs - for (UK2Node_FunctionEntry* FE : MCPUtils::AllNodes(BP)) - { - UEdGraph* FEGraph = FE->GetGraph(); - if (!FEGraph->GetName().Equals(FunctionName, ESearchCase::IgnoreCase)) continue; - // Skip delegate signature graphs (handled in Strategy 3) - if (BP->DelegateSignatureGraphs.Contains(FEGraph)) continue; - - EntryNode = FE; - NodeType = TEXT("FunctionEntry"); - break; - } - - // Strategy 2: K2Node_CustomEvent with matching CustomFunctionName - if (!EntryNode) - { - for (UK2Node_CustomEvent* CE : MCPUtils::AllNodes(BP)) - { - if (CE->CustomFunctionName.ToString().Equals(FunctionName, ESearchCase::IgnoreCase)) - { - EntryNode = CE; - NodeType = TEXT("CustomEvent"); - break; - } - } - } - - // Strategy 3: K2Node_FunctionEntry in DelegateSignatureGraphs - if (!EntryNode) - { - for (UK2Node_FunctionEntry* FE : MCPUtils::AllNodes(BP)) - { - UEdGraph* FEGraph = FE->GetGraph(); - if (!FEGraph->GetName().Equals(FunctionName, ESearchCase::IgnoreCase)) continue; - if (!BP->DelegateSignatureGraphs.Contains(FEGraph)) continue; - - EntryNode = FE; - NodeType = TEXT("EventDispatcher"); - break; - } - } - - if (!EntryNode) - { - // Build a helpful error listing available functions, events, and dispatchers - TArray> AvailFuncs; - - for (UEdGraph* Graph : BP->FunctionGraphs) - { - if (Graph) AvailFuncs.Add(MakeShared(Graph->GetName())); - } - - // Custom events - for (UK2Node_CustomEvent* CE : MCPUtils::AllNodes(BP)) - { - AvailFuncs.Add(MakeShared( - FString::Printf(TEXT("%s (custom event)"), *CE->CustomFunctionName.ToString()))); - } - - // Dispatchers - TSet DelegateNames; - FBlueprintEditorUtils::GetDelegateNameList(BP, DelegateNames); - for (const FName& DN : DelegateNames) - { - AvailFuncs.Add(MakeShared( - FString::Printf(TEXT("%s (event dispatcher)"), *DN.ToString()))); - } - - MCPUtils::MakeErrorJson(Result, FString::Printf( - TEXT("Function, custom event, or event dispatcher '%s' not found in Blueprint '%s'"), - *FunctionName, *Blueprint)); - Result->SetArrayField(TEXT("availableFunctions"), AvailFuncs); - return; - } - - // Check for duplicate parameter name - for (const TSharedPtr& Existing : EntryNode->UserDefinedPins) - { - if (Existing.IsValid() && Existing->PinName.ToString().Equals(ParamName, ESearchCase::IgnoreCase)) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf( - TEXT("Parameter '%s' already exists on '%s'"), *ParamName, *FunctionName)); - } - } - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Adding parameter '%s' (type=%s) to %s '%s' in Blueprint '%s'"), - *ParamName, *ParamType, *NodeType, *FunctionName, *Blueprint); - - // Add the parameter pin (EGPD_Output on entry = input to callers) - EntryNode->CreateUserDefinedPin(FName(*ParamName), PinType, EGPD_Output); - - FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP); - bool bSaved = MCPUtils::SaveBlueprintPackage(BP); - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Added parameter '%s' to '%s' in '%s' (saved: %s)"), - *ParamName, *FunctionName, *Blueprint, bSaved ? TEXT("true") : TEXT("false")); - - Result->SetStringField(TEXT("nodeType"), NodeType); - Result->SetBoolField(TEXT("saved"), bSaved); - } -}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/MCPHandlers_PinMutation.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/MCPHandlers_PinMutation.h deleted file mode 100644 index cf49e8d1..00000000 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/MCPHandlers_PinMutation.h +++ /dev/null @@ -1,382 +0,0 @@ -#pragma once - -#include "CoreMinimal.h" -#include "MCPHandler.h" -#include "MCPAssetFinder.h" -#include "MCPUtils.h" -#include "Engine/Blueprint.h" -#include "Engine/World.h" -#include "EdGraph/EdGraph.h" -#include "EdGraph/EdGraphNode.h" -#include "EdGraph/EdGraphPin.h" -#include "Kismet2/BlueprintEditorUtils.h" -#include "MCPHandlers_PinMutation.generated.h" - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -USTRUCT() -struct FSetPinDefaultEntry -{ - GENERATED_BODY() - - UPROPERTY() - FString Blueprint; - - UPROPERTY() - FString Node; - - UPROPERTY() - FString PinName; - - UPROPERTY() - FString Value; -}; - - -UCLASS() -class UMCPHandler_SetPinDefaultValues : public UObject, public IMCPHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Description="Array of {blueprint, node, pinName, value} objects")) - FMCPJsonArray Pins; - - virtual FString GetDescription() const override - { - return TEXT("Set the default value of input pins on nodes."); - } - - virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override - { - - TArray> Results; - int32 SuccessCount = 0; - TSet ModifiedNodes; - TSet ModifiedBlueprints; - - for (const TSharedPtr& PinVal : Pins.Array) - { - TSharedRef EntryResult = MakeShared(); - Results.Add(MakeShared(EntryResult)); - - FSetPinDefaultEntry Entry; - if (!MCPUtils::PopulateFromJson(FSetPinDefaultEntry::StaticStruct(), &Entry, PinVal, &*EntryResult)) continue; - - MCPAssets Assets; - if (!Assets.Scan().Scan().Exact(Entry.Blueprint).Errors(&*EntryResult).ENone().ETwo().Load()) - continue; - UBlueprint* BP = Assets.Object(); - - UEdGraph* Graph = nullptr; - UEdGraphNode* Node = MCPUtils::FindNodeByGuid(BP, Entry.Node, &Graph); - if (!Node) - { - EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Node '%s' not found"), *Entry.Node)); - continue; - } - - UEdGraphPin* Pin = Node->FindPin(FName(*Entry.PinName)); - if (!Pin) - { - EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Pin '%s' not found on node '%s'"), *Entry.PinName, *Entry.Node)); - continue; - } - - if (Pin->Direction != EGPD_Input) - { - EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Pin '%s' is an output pin"), *Entry.PinName)); - continue; - } - - const UEdGraphSchema* Schema = Graph->GetSchema(); - if (Schema) - { - FString ValidationError = Schema->IsPinDefaultValid(Pin, Entry.Value, nullptr, FText::GetEmpty()); - if (!ValidationError.IsEmpty()) - { - EntryResult->SetStringField(TEXT("error"), FString::Printf( - TEXT("Invalid value for pin '%s': %s"), *Entry.PinName, *ValidationError)); - continue; - } - } - - FString OldValue = Pin->DefaultValue; - Pin->DefaultValue = Entry.Value; - - EntryResult->SetStringField(TEXT("oldValue"), OldValue); - SuccessCount++; - ModifiedNodes.Add(Node); - ModifiedBlueprints.Add(BP); - } - - for (UEdGraphNode* Node : ModifiedNodes) - { - Node->ReconstructNode(); - } - - for (UBlueprint* BP : ModifiedBlueprints) - { - FBlueprintEditorUtils::MarkBlueprintAsModified(BP); - } - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: SetPinDefault — %d/%d succeeded"), - SuccessCount, Pins.Array.Num()); - - Result->SetNumberField(TEXT("successCount"), SuccessCount); - Result->SetNumberField(TEXT("totalCount"), Pins.Array.Num()); - Result->SetArrayField(TEXT("results"), Results); - } -}; - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -USTRUCT() -struct FConnectPinsEntry -{ - GENERATED_BODY() - - UPROPERTY() - FString SourceNode; - - UPROPERTY() - FString SourcePinName; - - UPROPERTY() - FString TargetNode; - - UPROPERTY() - FString TargetPinName; -}; - - -UCLASS() -class UMCPHandler_ConnectBlueprintPins : public UObject, public IMCPHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Description="Blueprint name or package path")) - FString Blueprint; - - UPROPERTY(meta=(Description="Array of {sourceNode, sourcePinName, targetNode, targetPinName} objects")) - FMCPJsonArray Connections; - - virtual FString GetDescription() const override - { - return TEXT("Connect pins between nodes in a Blueprint graph."); - } - - virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override - { - - MCPAssets Assets; - if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; - UBlueprint* BP = Assets.Object(); - - TArray> Results; - int32 SuccessCount = 0; - - for (const TSharedPtr& ConnVal : Connections.Array) - { - TSharedRef EntryResult = MakeShared(); - Results.Add(MakeShared(EntryResult)); - - FConnectPinsEntry Entry; - if (!MCPUtils::PopulateFromJson(FConnectPinsEntry::StaticStruct(), &Entry, ConnVal, &*EntryResult)) continue; - - UEdGraph* SourceGraph = nullptr; - UEdGraphNode* SourceNode = MCPUtils::FindNodeByGuid(BP, Entry.SourceNode, &SourceGraph); - if (!SourceNode) - { - EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Source node '%s' not found"), *Entry.SourceNode)); - continue; - } - - UEdGraphNode* TargetNode = MCPUtils::FindNodeByGuid(BP, Entry.TargetNode); - if (!TargetNode) - { - EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Target node '%s' not found"), *Entry.TargetNode)); - continue; - } - - UEdGraphPin* SourcePin = SourceNode->FindPin(FName(*Entry.SourcePinName)); - if (!SourcePin) - { - EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Source pin '%s' not found on node '%s'"), *Entry.SourcePinName, *Entry.SourceNode)); - continue; - } - - UEdGraphPin* TargetPin = TargetNode->FindPin(FName(*Entry.TargetPinName)); - if (!TargetPin) - { - EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Target pin '%s' not found on node '%s'"), *Entry.TargetPinName, *Entry.TargetNode)); - continue; - } - - const UEdGraphSchema* Schema = SourceGraph->GetSchema(); - if (!Schema) - { - EntryResult->SetStringField(TEXT("error"), TEXT("Graph schema not found")); - continue; - } - - bool bConnected = Schema->TryCreateConnection(SourcePin, TargetPin); - if (!bConnected) - { - EntryResult->SetStringField(TEXT("error"), FString::Printf( - TEXT("Cannot connect %s (%s) to %s (%s) — types are incompatible"), - *Entry.SourcePinName, *SourcePin->PinType.PinCategory.ToString(), - *Entry.TargetPinName, *TargetPin->PinType.PinCategory.ToString())); - continue; - } - - SuccessCount++; - } - - if (SuccessCount > 0) - { - FBlueprintEditorUtils::MarkBlueprintAsModified(BP); - } - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: ConnectPins — %d/%d succeeded in '%s'"), - SuccessCount, Connections.Array.Num(), *Blueprint); - Result->SetNumberField(TEXT("successCount"), SuccessCount); - Result->SetNumberField(TEXT("totalCount"), Connections.Array.Num()); - Result->SetArrayField(TEXT("results"), Results); - } -}; - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -USTRUCT() -struct FDisconnectPinEntry -{ - GENERATED_BODY() - - UPROPERTY() - FString Node; - - UPROPERTY() - FString PinName; - - UPROPERTY(meta=(Optional)) - FString TargetNode; - - UPROPERTY(meta=(Optional)) - FString TargetPinName; -}; - - -UCLASS() -class UMCPHandler_DisconnectBlueprintPins : public UObject, public IMCPHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Description="Blueprint name or package path")) - FString Blueprint; - - UPROPERTY(meta=(Description="Array of {node, pinName, targetNode?, targetPinName?} objects. If target is omitted, all connections on the pin are broken.")) - FMCPJsonArray Disconnections; - - virtual FString GetDescription() const override - { - return TEXT("Disconnect pins in a Blueprint graph. " - "Can disconnect a specific link or all links on a pin."); - } - - virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override - { - - MCPAssets Assets; - if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; - UBlueprint* BP = Assets.Object(); - - TArray> Results; - int32 SuccessCount = 0; - int32 TotalDisconnected = 0; - - for (const TSharedPtr& DiscVal : Disconnections.Array) - { - TSharedRef EntryResult = MakeShared(); - Results.Add(MakeShared(EntryResult)); - - FDisconnectPinEntry Entry; - if (!MCPUtils::PopulateFromJson(FDisconnectPinEntry::StaticStruct(), &Entry, DiscVal, &*EntryResult)) continue; - - UEdGraphNode* Node = MCPUtils::FindNodeByGuid(BP, Entry.Node); - if (!Node) - { - EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Node '%s' not found"), *Entry.Node)); - continue; - } - - UEdGraphPin* Pin = Node->FindPin(FName(*Entry.PinName)); - if (!Pin) - { - EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Pin '%s' not found on node '%s'"), *Entry.PinName, *Entry.Node)); - continue; - } - - int32 DisconnectedCount = 0; - - if (!Entry.TargetNode.IsEmpty() && !Entry.TargetPinName.IsEmpty()) - { - UEdGraphNode* TargetNode = MCPUtils::FindNodeByGuid(BP, Entry.TargetNode); - if (!TargetNode) - { - EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Target node '%s' not found"), *Entry.TargetNode)); - continue; - } - - UEdGraphPin* TargetPin = TargetNode->FindPin(FName(*Entry.TargetPinName)); - if (!TargetPin) - { - EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Target pin '%s' not found on node '%s'"), *Entry.TargetPinName, *Entry.TargetNode)); - continue; - } - - if (!Pin->LinkedTo.Contains(TargetPin)) - { - EntryResult->SetStringField(TEXT("error"), TEXT("The specified pins are not connected to each other")); - continue; - } - - Pin->BreakLinkTo(TargetPin); - DisconnectedCount = 1; - } - else - { - DisconnectedCount = Pin->LinkedTo.Num(); - if (DisconnectedCount > 0) - { - Pin->BreakAllPinLinks(true); - } - } - - EntryResult->SetNumberField(TEXT("disconnectedCount"), DisconnectedCount); - SuccessCount++; - TotalDisconnected += DisconnectedCount; - } - - if (TotalDisconnected > 0) - { - FBlueprintEditorUtils::MarkBlueprintAsModified(BP); - } - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: DisconnectPin — %d/%d succeeded, %d links broken in '%s'"), - SuccessCount, Disconnections.Array.Num(), TotalDisconnected, *Blueprint); - - Result->SetNumberField(TEXT("successCount"), SuccessCount); - Result->SetNumberField(TEXT("totalCount"), Disconnections.Array.Num()); - Result->SetNumberField(TEXT("totalDisconnected"), TotalDisconnected); - Result->SetArrayField(TEXT("results"), Results); - } -}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/MCPHandlers_Read.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/MCPHandlers_Read.h deleted file mode 100644 index 56186fe6..00000000 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/MCPHandlers_Read.h +++ /dev/null @@ -1,669 +0,0 @@ -#pragma once - -#include "CoreMinimal.h" -#include "MCPHandler.h" -#include "MCPAssetFinder.h" -#include "MCPUtils.h" -#include "Engine/Blueprint.h" -#include "Engine/World.h" -#include "Engine/Level.h" -#include "Engine/LevelScriptBlueprint.h" -#include "EdGraph/EdGraph.h" -#include "EdGraph/EdGraphNode.h" -#include "K2Node_CallFunction.h" -#include "K2Node_Event.h" -#include "K2Node_CustomEvent.h" -#include "K2Node_VariableGet.h" -#include "K2Node_VariableSet.h" -#include "K2Node_BreakStruct.h" -#include "K2Node_MakeStruct.h" -#include "K2Node_FunctionEntry.h" -#include "K2Node_EditablePinBase.h" -#include "AssetRegistry/IAssetRegistry.h" -#include "MCPHandlers_Read.generated.h" - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -UCLASS() -class UMCPHandler_ListBlueprintAssets : public UObject, public IMCPHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Optional, Description="Substring filter for blueprint name or path")) - FString Filter; - - UPROPERTY(meta=(Optional, Description="Filter by parent class name (substring match)")) - FString ParentClass; - - UPROPERTY(meta=(Optional, Description="Include regular blueprints (default true)")) - bool IncludeRegular = true; - - UPROPERTY(meta=(Optional, Description="Include regular blueprints (default true)")) - bool IncludeLevel = true; - - virtual FString GetDescription() const override - { - return TEXT("List all Blueprint assets in the project, with optional filtering by name, parent class, or type."); - } - - virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override - { - MCPAssets Assets; - Assets.NoScans().Substring(Filter).Limit(500); - if (IncludeRegular) Assets.Scan(); - if (IncludeLevel) Assets.Scan(); - Assets.Info(); - for (const FAssetData& Asset : Assets.AllData()) - { - // Extract parent class name - FString ParentClassName; - if (Asset.AssetClassPath == UWorld::StaticClass()->GetClassPathName()) - { - ParentClassName = TEXT("LevelScriptActor"); - } - else - { - Asset.GetTagValue(FName(TEXT("ParentClass")), ParentClassName); - int32 DotIndex; - if (ParentClassName.FindLastChar('.', DotIndex)) - { - ParentClassName = ParentClassName.Mid(DotIndex + 1); - } - ParentClassName.RemoveFromEnd(TEXT("'")); - } - - // Apply parent class filter - if (!ParentClass.IsEmpty()) - { - if (!ParentClassName.Equals(ParentClass, ESearchCase::IgnoreCase)) - { - continue; - } - } - - Result.Appendf(TEXT("%30s %s\n"), *ParentClassName, *Asset.PackageName.ToString()); - } - } -}; - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -UCLASS() -class UMCPHandler_DumpBlueprint : public UObject, public IMCPHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Description="Blueprint name or package path")) - FString Blueprint; - - virtual FString GetDescription() const override - { - return TEXT("Load and serialize a Blueprint, returning its full structure including graphs, variables, and components."); - } - - virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override - { - MCPAssets Assets; - if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; - - TSharedRef Tmp = MCPUtils::SerializeBlueprint(Assets.Object()); - MCPUtils::CopyJsonFields(&*Tmp, Result); - } -}; - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -UCLASS() -class UMCPHandler_DumpBlueprintGraph : public UObject, public IMCPHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Description="Blueprint name or package path")) - FString Blueprint; - - UPROPERTY(meta=(Description="Graph name to dump")) - FString Graph; - - virtual FString GetDescription() const override - { - return TEXT("Dump the detailed node/pin structure of a specific graph within a Blueprint."); - } - - virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override - { - // URL-decode graph name to handle spaces encoded as '+' or '%20' - FString DecodedGraphName = MCPUtils::UrlDecode(Graph); - - MCPAssets Assets; - if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; - UBlueprint* BP = Assets.Object(); - - TArray AllGraphs = MCPUtils::AllGraphs(BP); - - for (UEdGraph* GraphObj : AllGraphs) - { - if (GraphObj->GetName().Equals(DecodedGraphName, ESearchCase::IgnoreCase)) - { - TSharedPtr GraphJson = MCPUtils::SerializeGraph(GraphObj); - if (GraphJson.IsValid()) - { - MCPUtils::CopyJsonFields(GraphJson.Get(), Result); - return; - } - } - } - - // Not found — list available graphs - TArray> GraphNames; - for (UEdGraph* GraphObj : AllGraphs) - { - GraphNames.Add(MakeShared(GraphObj->GetName())); - } - MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Graph '%s' not found"), *DecodedGraphName)); - Result->SetArrayField(TEXT("availableGraphs"), GraphNames); - } -}; - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -UCLASS() -class UMCPHandler_SearchWithinBlueprints : public UObject, public IMCPHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Description="Search query string to match against node titles, function names, event names, and variable names")) - FString Query; - - UPROPERTY(meta=(Optional, Description="Filter results to blueprints whose path contains this substring")) - FString Path; - - UPROPERTY(meta=(Optional, Description="Maximum number of results to return (default 50, max 200)")) - int32 MaxResults = 0; - - virtual FString GetDescription() const override - { - return TEXT("Search across all Blueprint graphs for nodes matching a query string."); - } - - virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override - { - int32 EffectiveMaxResults = (MaxResults > 0) ? FMath::Clamp(MaxResults, 1, 200) : 50; - - // Build a combined list of all searchable blueprints (regular + level) - auto SearchBlueprint = [&](const FString& AssetName, const FString& AssetPath, UBlueprint* BP, TArray>& OutResults) - { - for (UEdGraphNode* Node : MCPUtils::AllNodes(BP)) - { - if (OutResults.Num() >= EffectiveMaxResults) break; - - FString Title = Node->GetNodeTitle(ENodeTitleType::FullTitle).ToString(); - - FString FuncName, EventName, VarName; - if (auto* CF = Cast(Node)) - { - FuncName = CF->FunctionReference.GetMemberName().ToString(); - } - else if (auto* Ev = Cast(Node)) - { - EventName = Ev->EventReference.GetMemberName().ToString(); - } - else if (auto* CE = Cast(Node)) - { - EventName = CE->CustomFunctionName.ToString(); - } - else if (auto* VG = Cast(Node)) - { - VarName = VG->GetVarName().ToString(); - } - else if (auto* VS = Cast(Node)) - { - VarName = VS->GetVarName().ToString(); - } - - bool bMatch = Title.Contains(Query, ESearchCase::IgnoreCase) || - (!FuncName.IsEmpty() && FuncName.Contains(Query, ESearchCase::IgnoreCase)) || - (!EventName.IsEmpty() && EventName.Contains(Query, ESearchCase::IgnoreCase)) || - (!VarName.IsEmpty() && VarName.Contains(Query, ESearchCase::IgnoreCase)); - - if (bMatch) - { - TSharedRef R = MakeShared(); - R->SetStringField(TEXT("blueprint"), AssetName); - R->SetStringField(TEXT("blueprintPath"), AssetPath); - R->SetStringField(TEXT("graph"), Node->GetGraph()->GetName()); - R->SetStringField(TEXT("nodeTitle"), Title); - R->SetStringField(TEXT("nodeClass"), Node->GetClass()->GetName()); - if (!FuncName.IsEmpty()) R->SetStringField(TEXT("functionName"), FuncName); - if (!EventName.IsEmpty()) R->SetStringField(TEXT("eventName"), EventName); - if (!VarName.IsEmpty()) R->SetStringField(TEXT("variableName"), VarName); - OutResults.Add(MakeShared(R)); - } - } - }; - - MCPAssets AllBlueprints; - AllBlueprints.Info(); - MCPAssets AllWorlds; - AllWorlds.Info(); - - TArray> Results; - for (const FAssetData& Asset : AllBlueprints.AllData()) - { - if (Results.Num() >= EffectiveMaxResults) break; - - FString AssetPath = Asset.PackageName.ToString(); - if (!Path.IsEmpty() && !AssetPath.Contains(Path, ESearchCase::IgnoreCase)) - { - continue; - } - - UBlueprint* BP = Cast(const_cast(Asset).GetAsset()); - if (!BP) continue; - - SearchBlueprint(Asset.AssetName.ToString(), AssetPath, BP, Results); - } - - // Also search level blueprints - for (const FAssetData& MapAsset : AllWorlds.AllData()) - { - if (Results.Num() >= EffectiveMaxResults) break; - - FString AssetPath = MapAsset.PackageName.ToString(); - if (!Path.IsEmpty() && !AssetPath.Contains(Path, ESearchCase::IgnoreCase)) - { - continue; - } - - UWorld* World = Cast(MapAsset.GetAsset()); - if (!World || !World->PersistentLevel) continue; - ULevelScriptBlueprint* LevelBP = World->PersistentLevel->GetLevelScriptBlueprint(false); - if (!LevelBP) continue; - - int32 BeforeCount = Results.Num(); - SearchBlueprint(MapAsset.AssetName.ToString(), AssetPath, LevelBP, Results); - // Tag newly-added entries as level blueprint results - for (int32 i = BeforeCount; i < Results.Num(); ++i) - { - Results[i]->AsObject()->SetBoolField(TEXT("isLevelBlueprint"), true); - } - } - - Result->SetNumberField(TEXT("resultCount"), Results.Num()); - Result->SetArrayField(TEXT("results"), Results); - } -}; - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -// ============================================================ -// HandleTestSave — load a Blueprint and save it unmodified (diagnostic) -// ============================================================ - -UCLASS() -class UMCPHandler_TestSaveBlueprintPackage : public UObject, public IMCPHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Description="Blueprint name or package path")) - FString Blueprint; - - virtual FString GetDescription() const override - { - return TEXT("Load a Blueprint and save it unmodified as a diagnostic test."); - } - - virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override - { - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: test-save requested for '%s'"), *Blueprint); - - MCPAssets Assets; - if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; - UBlueprint* BP = Assets.Object(); - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: test-save — loaded '%s', GeneratedClass=%s"), - *BP->GetName(), - BP->GeneratedClass ? *BP->GeneratedClass->GetName() : TEXT("null")); - - // Attempt save with NO modifications - bool bSaved = MCPUtils::SaveBlueprintPackage(BP); - - Result->SetStringField(TEXT("packagePath"), BP->GetPackage()->GetName()); - Result->SetBoolField(TEXT("hasGeneratedClass"), BP->GeneratedClass != nullptr); - Result->SetBoolField(TEXT("saved"), bSaved); - } -}; - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -// ============================================================ -// HandleFindReferences — find all Blueprints referencing an asset -// ============================================================ - -UCLASS() -class UMCPHandler_FindAssetReferences : public UObject, public IMCPHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Description="Asset package path to find references for")) - FString AssetPath; - - virtual FString GetDescription() const override - { - return TEXT("Find all assets that reference a given asset, categorized into Blueprint and non-Blueprint referencers."); - } - - virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override - { - IAssetRegistry& Registry = *IAssetRegistry::Get(); - - TArray Referencers; - Registry.GetReferencers(FName(*AssetPath), Referencers); - - // Build set of known Blueprint package names for filtering - MCPAssets AllBlueprints; - AllBlueprints.Info(); - TSet BlueprintPackages; - for (const FAssetData& Asset : AllBlueprints.AllData()) - { - BlueprintPackages.Add(Asset.PackageName.ToString()); - } - - TArray> BPRefs; - TArray> OtherRefs; - for (const FName& Ref : Referencers) - { - FString RefStr = Ref.ToString(); - if (BlueprintPackages.Contains(RefStr)) - { - BPRefs.Add(MakeShared(RefStr)); - } - else - { - OtherRefs.Add(MakeShared(RefStr)); - } - } - - Result->SetNumberField(TEXT("totalReferencers"), Referencers.Num()); - Result->SetNumberField(TEXT("blueprintReferencerCount"), BPRefs.Num()); - Result->SetArrayField(TEXT("blueprintReferencers"), BPRefs); - Result->SetNumberField(TEXT("otherReferencerCount"), OtherRefs.Num()); - Result->SetArrayField(TEXT("otherReferencers"), OtherRefs); - } -}; - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -// ============================================================ -// HandleSearchByType — find all usages of a type across blueprints -// ============================================================ - -UCLASS() -class UMCPHandler_SearchTypeUsageInBlueprints : public UObject, public IMCPHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Description="Type name to search for (e.g. 'FVector', 'MyStruct'). F/E/U prefix is stripped for matching.")) - FString TypeName; - - UPROPERTY(meta=(Optional, Description="Filter to blueprints whose name or path contains this substring")) - FString Filter; - - UPROPERTY(meta=(Optional, Description="Maximum number of results to return (default 200, max 500)")) - int32 MaxResults = 0; - - virtual FString GetDescription() const override - { - return TEXT("Search all Blueprints for usages of a specific type in variables, function parameters, struct nodes, and pin connections."); - } - - virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override - { - FString DecodedTypeName = MCPUtils::UrlDecode(TypeName); - FString FilterStr = Filter.IsEmpty() ? FString() : MCPUtils::UrlDecode(Filter); - - int32 EffectiveMaxResults = (MaxResults > 0) ? FMath::Clamp(MaxResults, 1, 500) : 200; - - // Strip F/E/U prefix for comparison - FString TypeNameNoPrefix = DecodedTypeName; - if (TypeNameNoPrefix.StartsWith(TEXT("F")) || TypeNameNoPrefix.StartsWith(TEXT("E")) || TypeNameNoPrefix.StartsWith(TEXT("U"))) - { - TypeNameNoPrefix = TypeNameNoPrefix.Mid(1); - } - - auto MatchesType = [&DecodedTypeName, &TypeNameNoPrefix](const FString& TestType) -> bool - { - return TestType.Equals(DecodedTypeName, ESearchCase::IgnoreCase) || - TestType.Equals(TypeNameNoPrefix, ESearchCase::IgnoreCase); - }; - - TArray> Results; - - // Lambda that searches a single Blueprint for type usages - auto SearchOneBlueprint = [&](const FString& BPName, const FString& BPPath, UBlueprint* BP, bool bIsLevel) - { - // Check variables - for (const FBPVariableDescription& Var : BP->NewVariables) - { - if (Results.Num() >= EffectiveMaxResults) break; - - FString VarSubtype; - if (Var.VarType.PinSubCategoryObject.IsValid()) - { - VarSubtype = Var.VarType.PinSubCategoryObject->GetName(); - } - - if (MatchesType(VarSubtype) || MatchesType(Var.VarType.PinCategory.ToString())) - { - TSharedRef R = MakeShared(); - R->SetStringField(TEXT("blueprint"), BPName); - R->SetStringField(TEXT("blueprintPath"), BPPath); - R->SetStringField(TEXT("usage"), TEXT("variable")); - R->SetStringField(TEXT("location"), Var.VarName.ToString()); - R->SetStringField(TEXT("currentType"), Var.VarType.PinCategory.ToString()); - if (!VarSubtype.IsEmpty()) - R->SetStringField(TEXT("currentSubtype"), VarSubtype); - if (bIsLevel) - R->SetBoolField(TEXT("isLevelBlueprint"), true); - Results.Add(MakeShared(R)); - } - } - - // Check graphs for function/event params, struct nodes, and pin connections - for (UEdGraphNode* Node : MCPUtils::AllNodes(BP)) - { - if (Results.Num() >= EffectiveMaxResults) break; - - // Check FunctionEntry/CustomEvent parameters - if (auto* FuncEntry = Cast(Node)) - { - for (const TSharedPtr& PinInfo : FuncEntry->UserDefinedPins) - { - if (!PinInfo.IsValid()) continue; - FString ParamSubtype; - if (PinInfo->PinType.PinSubCategoryObject.IsValid()) - ParamSubtype = PinInfo->PinType.PinSubCategoryObject->GetName(); - - if (MatchesType(ParamSubtype) || MatchesType(PinInfo->PinType.PinCategory.ToString())) - { - TSharedRef R = MakeShared(); - R->SetStringField(TEXT("blueprint"), BPName); - R->SetStringField(TEXT("blueprintPath"), BPPath); - R->SetStringField(TEXT("usage"), TEXT("functionParameter")); - R->SetStringField(TEXT("location"), FString::Printf(TEXT("%s.%s"), - *Node->GetGraph()->GetName(), *PinInfo->PinName.ToString())); - R->SetStringField(TEXT("nodeId"), Node->NodeGuid.ToString()); - R->SetStringField(TEXT("currentType"), PinInfo->PinType.PinCategory.ToString()); - if (!ParamSubtype.IsEmpty()) - R->SetStringField(TEXT("currentSubtype"), ParamSubtype); - if (bIsLevel) - R->SetBoolField(TEXT("isLevelBlueprint"), true); - Results.Add(MakeShared(R)); - } - } - } - else if (auto* CustomEvent = Cast(Node)) - { - for (const TSharedPtr& PinInfo : CustomEvent->UserDefinedPins) - { - if (!PinInfo.IsValid()) continue; - FString ParamSubtype; - if (PinInfo->PinType.PinSubCategoryObject.IsValid()) - ParamSubtype = PinInfo->PinType.PinSubCategoryObject->GetName(); - - if (MatchesType(ParamSubtype) || MatchesType(PinInfo->PinType.PinCategory.ToString())) - { - TSharedRef R = MakeShared(); - R->SetStringField(TEXT("blueprint"), BPName); - R->SetStringField(TEXT("blueprintPath"), BPPath); - R->SetStringField(TEXT("usage"), TEXT("eventParameter")); - R->SetStringField(TEXT("location"), FString::Printf(TEXT("%s.%s"), - *CustomEvent->CustomFunctionName.ToString(), *PinInfo->PinName.ToString())); - R->SetStringField(TEXT("nodeId"), Node->NodeGuid.ToString()); - R->SetStringField(TEXT("currentType"), PinInfo->PinType.PinCategory.ToString()); - if (!ParamSubtype.IsEmpty()) - R->SetStringField(TEXT("currentSubtype"), ParamSubtype); - if (bIsLevel) - R->SetBoolField(TEXT("isLevelBlueprint"), true); - Results.Add(MakeShared(R)); - } - } - } - // Check Break/Make struct nodes - else if (auto* BreakNode = Cast(Node)) - { - if (BreakNode->StructType && MatchesType(BreakNode->StructType->GetName())) - { - TSharedRef R = MakeShared(); - R->SetStringField(TEXT("blueprint"), BPName); - R->SetStringField(TEXT("blueprintPath"), BPPath); - R->SetStringField(TEXT("usage"), TEXT("breakStruct")); - R->SetStringField(TEXT("location"), Node->GetGraph()->GetName()); - R->SetStringField(TEXT("nodeId"), Node->NodeGuid.ToString()); - R->SetStringField(TEXT("structType"), BreakNode->StructType->GetName()); - if (bIsLevel) - R->SetBoolField(TEXT("isLevelBlueprint"), true); - Results.Add(MakeShared(R)); - } - } - else if (auto* MakeNode = Cast(Node)) - { - if (MakeNode->StructType && MatchesType(MakeNode->StructType->GetName())) - { - TSharedRef R = MakeShared(); - R->SetStringField(TEXT("blueprint"), BPName); - R->SetStringField(TEXT("blueprintPath"), BPPath); - R->SetStringField(TEXT("usage"), TEXT("makeStruct")); - R->SetStringField(TEXT("location"), Node->GetGraph()->GetName()); - R->SetStringField(TEXT("nodeId"), Node->NodeGuid.ToString()); - R->SetStringField(TEXT("structType"), MakeNode->StructType->GetName()); - if (bIsLevel) - R->SetBoolField(TEXT("isLevelBlueprint"), true); - Results.Add(MakeShared(R)); - } - } - - // Check pin connections carrying the type - for (UEdGraphPin* Pin : Node->Pins) - { - if (!Pin || Pin->bHidden || Results.Num() >= EffectiveMaxResults) continue; - - FString PinSubtype; - if (Pin->PinType.PinSubCategoryObject.IsValid()) - PinSubtype = Pin->PinType.PinSubCategoryObject->GetName(); - - if ((Pin->LinkedTo.Num() > 0) && - (MatchesType(PinSubtype) || MatchesType(Pin->PinType.PinCategory.ToString()))) - { - TSharedRef R = MakeShared(); - R->SetStringField(TEXT("blueprint"), BPName); - R->SetStringField(TEXT("blueprintPath"), BPPath); - R->SetStringField(TEXT("usage"), TEXT("pinConnection")); - R->SetStringField(TEXT("location"), FString::Printf(TEXT("%s.%s"), - *Node->GetNodeTitle(ENodeTitleType::FullTitle).ToString(), - *Pin->PinName.ToString())); - R->SetStringField(TEXT("nodeId"), Node->NodeGuid.ToString()); - R->SetStringField(TEXT("graph"), Node->GetGraph()->GetName()); - R->SetStringField(TEXT("pinType"), Pin->PinType.PinCategory.ToString()); - if (!PinSubtype.IsEmpty()) - R->SetStringField(TEXT("pinSubtype"), PinSubtype); - R->SetNumberField(TEXT("connectionCount"), Pin->LinkedTo.Num()); - if (bIsLevel) - R->SetBoolField(TEXT("isLevelBlueprint"), true); - Results.Add(MakeShared(R)); - } - } - } - }; - - MCPAssets AllBlueprints; - AllBlueprints.Info(); - MCPAssets AllWorlds; - AllWorlds.Info(); - - // Search regular blueprints - for (const FAssetData& Asset : AllBlueprints.AllData()) - { - if (Results.Num() >= EffectiveMaxResults) break; - - FString AssetPath = Asset.PackageName.ToString(); - FString BPName = Asset.AssetName.ToString(); - - if (!FilterStr.IsEmpty() && !BPName.Contains(FilterStr, ESearchCase::IgnoreCase) && - !AssetPath.Contains(FilterStr, ESearchCase::IgnoreCase)) - { - continue; - } - - UBlueprint* BP = Cast(const_cast(Asset).GetAsset()); - if (!BP) continue; - - SearchOneBlueprint(BPName, AssetPath, BP, false); - } - - // Search level blueprints from maps - for (const FAssetData& MapAsset : AllWorlds.AllData()) - { - if (Results.Num() >= EffectiveMaxResults) break; - - FString AssetPath = MapAsset.PackageName.ToString(); - FString MapName = MapAsset.AssetName.ToString(); - - if (!FilterStr.IsEmpty() && !MapName.Contains(FilterStr, ESearchCase::IgnoreCase) && - !AssetPath.Contains(FilterStr, ESearchCase::IgnoreCase)) - { - continue; - } - - UWorld* World = Cast(MapAsset.GetAsset()); - if (!World || !World->PersistentLevel) continue; - ULevelScriptBlueprint* LevelBP = World->PersistentLevel->GetLevelScriptBlueprint(false); - if (!LevelBP) continue; - - SearchOneBlueprint(MapName, AssetPath, LevelBP, true); - } - - Result->SetNumberField(TEXT("resultCount"), Results.Num()); - Result->SetArrayField(TEXT("results"), Results); - } -}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/MCPHandlers_StateMachine.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/MCPHandlers_StateMachine.h deleted file mode 100644 index ca6dc035..00000000 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/MCPHandlers_StateMachine.h +++ /dev/null @@ -1,665 +0,0 @@ -#pragma once - -#include "CoreMinimal.h" -#include "MCPHandler.h" -#include "MCPAssetFinder.h" -#include "MCPUtils.h" -#include "EdGraph/EdGraph.h" -#include "EdGraph/EdGraphNode.h" -#include "EdGraph/EdGraphPin.h" -#include "Kismet2/KismetEditorUtilities.h" -#include "Animation/AnimBlueprint.h" -#include "Animation/AnimSequence.h" -#include "Animation/BlendSpace.h" -#include "AnimGraphNode_SequencePlayer.h" -#include "AnimGraphNode_BlendSpacePlayer.h" -#include "AnimStateNode.h" -#include "AnimStateTransitionNode.h" -#include "AnimationStateMachineGraph.h" -#include "K2Node_VariableGet.h" -#include "MCPHandlers_StateMachine.generated.h" - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -UCLASS() -class UMCPHandler_AddAnimStateToMachine : public UObject, public IMCPHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Description="Animation Blueprint name or package path")) - FString Blueprint; - - UPROPERTY(meta=(Description="State machine graph name")) - FString Graph; - - UPROPERTY(meta=(Description="Name for the new state")) - FString StateName; - - UPROPERTY(meta=(Optional, Description="X position of the new state node")) - int32 PosX = 200; - - UPROPERTY(meta=(Optional, Description="Y position of the new state node")) - int32 PosY = 0; - - UPROPERTY(meta=(Optional, Description="Animation asset name to assign to the state")) - FString AnimationAsset; - - virtual FString GetDescription() const override - { - return TEXT("Add a new state to an animation state machine graph. " - "Optionally assign an animation asset to the state."); - } - - virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override - { - MCPAssets Assets; - if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; - UAnimationStateMachineGraph* SMGraph = MCPUtils::FindStateMachineGraph(Assets.Object(), Graph); - if (!SMGraph) { MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("State machine graph '%s' not found in '%s'"), *Graph, *Blueprint)); return; } - UAnimBlueprint* AnimBP = Assets.Object(); - - // Check for duplicate state name - if (MCPUtils::FindStateByName(SMGraph, StateName, nullptr)) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("State '%s' already exists in graph '%s'"), *StateName, *Graph)); - } - - // Create the state node - UAnimStateNode* NewState = NewObject(SMGraph); - NewState->CreateNewGuid(); - NewState->NodePosX = PosX; - NewState->NodePosY = PosY; - - // Set the state name via the bound graph - NewState->PostPlacedNewNode(); - NewState->AllocateDefaultPins(); - - // Rename the bound graph to set the state name - if (NewState->GetBoundGraph()) - { - NewState->GetBoundGraph()->Rename(*StateName, nullptr); - } - - SMGraph->AddNode(NewState, false, false); - NewState->SetFlags(RF_Transactional); - - // Optionally set animation asset - if (!AnimationAsset.IsEmpty() && NewState->GetBoundGraph()) - { - // Try to find the animation asset and create a sequence player in the state's inner graph - MCPAssets AnimAssets; - UAnimSequence* AnimSeq = AnimAssets.Exact(AnimationAsset).ENone().ETwo().Load() ? AnimAssets.Object() : nullptr; - - if (AnimSeq) - { - UAnimGraphNode_SequencePlayer* SeqNode = NewObject(NewState->GetBoundGraph()); - SeqNode->CreateNewGuid(); - SeqNode->PostPlacedNewNode(); - SeqNode->AllocateDefaultPins(); - SeqNode->SetAnimationAsset(AnimSeq); - SeqNode->NodePosX = 0; - SeqNode->NodePosY = 0; - NewState->GetBoundGraph()->AddNode(SeqNode, false, false); - } - } - - // Compile and save - FKismetEditorUtilities::CompileBlueprint(AnimBP); - bool bSaved = MCPUtils::SaveBlueprintPackage(AnimBP); - - Result->SetStringField(TEXT("nodeId"), NewState->NodeGuid.ToString()); - Result->SetBoolField(TEXT("saved"), bSaved); - } -}; - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -UCLASS() -class UMCPHandler_RemoveAnimStateFromMachine : public UObject, public IMCPHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Description="Animation Blueprint name or package path")) - FString Blueprint; - - UPROPERTY(meta=(Description="State machine graph name")) - FString Graph; - - UPROPERTY(meta=(Description="Name of the state to remove")) - FString StateName; - - virtual FString GetDescription() const override - { - return TEXT("Remove a state and its connected transitions from an animation state machine graph."); - } - - virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override - { - MCPAssets Assets; - if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; - UAnimationStateMachineGraph* SMGraph = MCPUtils::FindStateMachineGraph(Assets.Object(), Graph); - if (!SMGraph) { MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("State machine graph '%s' not found in '%s'"), *Graph, *Blueprint)); return; } - UAnimBlueprint* AnimBP = Assets.Object(); - - UAnimStateNode* StateNode = MCPUtils::FindStateByName(SMGraph, StateName, Result); - if (!StateNode) return; - - // Collect and remove transitions connected to this state - TArray TransitionsToRemove; - for (UEdGraphNode* Node : SMGraph->Nodes) - { - if (UAnimStateTransitionNode* TransNode = Cast(Node)) - { - if ((TransNode->GetPreviousState() == StateNode) || (TransNode->GetNextState() == StateNode)) - { - TransitionsToRemove.Add(TransNode); - } - } - } - - int32 RemovedTransitions = TransitionsToRemove.Num(); - for (UAnimStateTransitionNode* Trans : TransitionsToRemove) - { - Trans->BreakAllNodeLinks(); - SMGraph->RemoveNode(Trans); - } - - // Remove the state - StateNode->BreakAllNodeLinks(); - SMGraph->RemoveNode(StateNode); - - // Compile and save - FKismetEditorUtilities::CompileBlueprint(AnimBP); - bool bSaved = MCPUtils::SaveBlueprintPackage(AnimBP); - - Result->SetNumberField(TEXT("removedTransitions"), RemovedTransitions); - Result->SetBoolField(TEXT("saved"), bSaved); - } -}; - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -UCLASS() -class UMCPHandler_AddAnimStateTransition : public UObject, public IMCPHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Description="Animation Blueprint name or package path")) - FString Blueprint; - - UPROPERTY(meta=(Description="State machine graph name")) - FString Graph; - - UPROPERTY(meta=(Description="Name of the source state")) - FString FromState; - - UPROPERTY(meta=(Description="Name of the target state")) - FString ToState; - - UPROPERTY(meta=(Optional, Description="Crossfade duration in seconds")) - float CrossfadeDuration = 0.0f; - - UPROPERTY(meta=(Optional, Description="Transition priority order")) - int32 Priority = 0; - - UPROPERTY(meta=(Optional, Description="Whether the transition is bidirectional")) - bool BBidirectional = false; - - virtual FString GetDescription() const override - { - return TEXT("Add a transition between two states in an animation state machine graph."); - } - - virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override - { - MCPAssets Assets; - if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; - UAnimationStateMachineGraph* SMGraph = MCPUtils::FindStateMachineGraph(Assets.Object(), Graph); - if (!SMGraph) { MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("State machine graph '%s' not found in '%s'"), *Graph, *Blueprint)); return; } - UAnimBlueprint* AnimBP = Assets.Object(); - - UAnimStateNode* FromStateNode = MCPUtils::FindStateByName(SMGraph, FromState, Result); - if (!FromStateNode) return; - - UAnimStateNode* ToStateNode = MCPUtils::FindStateByName(SMGraph, ToState, Result); - if (!ToStateNode) return; - - // Create transition node - UAnimStateTransitionNode* TransNode = NewObject(SMGraph); - TransNode->CreateNewGuid(); - TransNode->PostPlacedNewNode(); - TransNode->AllocateDefaultPins(); - - // Position between the two states - TransNode->NodePosX = (FromStateNode->NodePosX + ToStateNode->NodePosX) / 2; - TransNode->NodePosY = (FromStateNode->NodePosY + ToStateNode->NodePosY) / 2; - - SMGraph->AddNode(TransNode, false, false); - TransNode->SetFlags(RF_Transactional); - - // Connect: FromState output -> Transition input, Transition output -> ToState input - TransNode->CreateConnections(FromStateNode, ToStateNode); - - // Set optional properties - if (Json->HasField(TEXT("crossfadeDuration"))) - { - TransNode->CrossfadeDuration = CrossfadeDuration; - } - if (Json->HasField(TEXT("priority"))) - { - TransNode->PriorityOrder = Priority; - } - if (Json->HasField(TEXT("bBidirectional"))) - { - TransNode->Bidirectional = BBidirectional; - } - - // Compile and save - FKismetEditorUtilities::CompileBlueprint(AnimBP); - bool bSaved = MCPUtils::SaveBlueprintPackage(AnimBP); - - Result->SetStringField(TEXT("nodeId"), TransNode->NodeGuid.ToString()); - Result->SetBoolField(TEXT("saved"), bSaved); - } -}; - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -UCLASS() -class UMCPHandler_SetAnimTransitionRule : public UObject, public IMCPHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Description="Animation Blueprint name or package path")) - FString Blueprint; - - UPROPERTY(meta=(Description="State machine graph name")) - FString Graph; - - UPROPERTY(meta=(Description="Name of the source state")) - FString FromState; - - UPROPERTY(meta=(Description="Name of the target state")) - FString ToState; - - UPROPERTY(meta=(Optional, Description="Crossfade duration in seconds")) - float CrossfadeDuration = 0.0f; - - UPROPERTY(meta=(Optional, Description="Blend mode (as integer enum value)")) - int32 BlendMode = 0; - - UPROPERTY(meta=(Optional, Description="Transition priority order")) - int32 PriorityOrder = 0; - - UPROPERTY(meta=(Optional, Description="Logic type (as integer enum value)")) - int32 LogicType = 0; - - UPROPERTY(meta=(Optional, Description="Whether the transition is bidirectional")) - bool BBidirectional = false; - - virtual FString GetDescription() const override - { - return TEXT("Update properties on an existing transition between two states in an animation state machine."); - } - - virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override - { - MCPAssets Assets; - if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; - UAnimationStateMachineGraph* SMGraph = MCPUtils::FindStateMachineGraph(Assets.Object(), Graph); - if (!SMGraph) { MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("State machine graph '%s' not found in '%s'"), *Graph, *Blueprint)); return; } - UAnimBlueprint* AnimBP = Assets.Object(); - - UAnimStateTransitionNode* TransNode = MCPUtils::FindTransition(SMGraph, FromState, ToState); - if (!TransNode) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf( - TEXT("Transition from '%s' to '%s' not found in graph '%s'"), - *FromState, *ToState, *Graph)); - } - - // Update properties - int32 ChangedCount = 0; - TransNode->PreEditChange(nullptr); - - if (Json->HasField(TEXT("crossfadeDuration"))) - { - TransNode->CrossfadeDuration = CrossfadeDuration; - ChangedCount++; - } - if (Json->HasField(TEXT("blendMode"))) - { - TransNode->BlendMode = (EAlphaBlendOption)BlendMode; - ChangedCount++; - } - if (Json->HasField(TEXT("priorityOrder"))) - { - TransNode->PriorityOrder = PriorityOrder; - ChangedCount++; - } - if (Json->HasField(TEXT("logicType"))) - { - TransNode->LogicType = (ETransitionLogicType::Type)LogicType; - ChangedCount++; - } - if (Json->HasField(TEXT("bBidirectional"))) - { - TransNode->Bidirectional = BBidirectional; - ChangedCount++; - } - - if (ChangedCount == 0) - { - return MCPUtils::MakeErrorJson(Result, TEXT("No properties to update. Provide at least one of: crossfadeDuration, blendMode, priorityOrder, logicType, bBidirectional")); - } - TransNode->PostEditChange(); - - // Compile and save - FKismetEditorUtilities::CompileBlueprint(AnimBP); - bool bSaved = MCPUtils::SaveBlueprintPackage(AnimBP); - - Result->SetNumberField(TEXT("propertiesChanged"), ChangedCount); - Result->SetBoolField(TEXT("saved"), bSaved); - } -}; - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -UCLASS() -class UMCPHandler_SetAnimStateAnimation : public UObject, public IMCPHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Description="Animation Blueprint name or package path")) - FString Blueprint; - - UPROPERTY(meta=(Description="State machine graph name")) - FString Graph; - - UPROPERTY(meta=(Description="Name of the state to modify")) - FString StateName; - - UPROPERTY(meta=(Description="Animation asset name to assign")) - FString AnimationAsset; - - virtual FString GetDescription() const override - { - return TEXT("Set or replace the animation sequence played by a state in an animation state machine."); - } - - virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override - { - MCPAssets Assets; - if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; - UAnimationStateMachineGraph* SMGraph = MCPUtils::FindStateMachineGraph(Assets.Object(), Graph); - if (!SMGraph) { MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("State machine graph '%s' not found in '%s'"), *Graph, *Blueprint)); return; } - UAnimBlueprint* AnimBP = Assets.Object(); - - UAnimStateNode* StateNode = MCPUtils::FindStateByName(SMGraph, StateName, Result); - if (!StateNode) return; - - UEdGraph* InnerGraph = StateNode->GetBoundGraph(); - if (!InnerGraph) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("State '%s' has no bound graph"), *StateName)); - } - - // Find the animation asset - MCPAssets AnimAssets; - if (!AnimAssets.Exact(AnimationAsset).Errors(Result).ENone().ETwo().Load()) return; - UAnimSequence* AnimSeq = AnimAssets.Object(); - - // Find existing SequencePlayer or create one - UAnimGraphNode_SequencePlayer* SeqNode = nullptr; - for (UEdGraphNode* Node : InnerGraph->Nodes) - { - SeqNode = Cast(Node); - if (SeqNode) break; - } - - bool bCreatedNew = false; - if (!SeqNode) - { - SeqNode = NewObject(InnerGraph); - SeqNode->CreateNewGuid(); - SeqNode->PostPlacedNewNode(); - SeqNode->AllocateDefaultPins(); - SeqNode->NodePosX = 0; - SeqNode->NodePosY = 0; - InnerGraph->AddNode(SeqNode, false, false); - bCreatedNew = true; - } - - SeqNode->SetAnimationAsset(AnimSeq); - - // Compile and save - FKismetEditorUtilities::CompileBlueprint(AnimBP); - bool bSaved = MCPUtils::SaveBlueprintPackage(AnimBP); - - Result->SetBoolField(TEXT("createdNewNode"), bCreatedNew); - Result->SetBoolField(TEXT("saved"), bSaved); - } -}; - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -UCLASS() -class UMCPHandler_SetAnimStateBlendSpace : public UObject, public IMCPHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Description="Animation Blueprint name or package path")) - FString Blueprint; - - UPROPERTY(meta=(Description="State machine graph name")) - FString Graph; - - UPROPERTY(meta=(Description="Name of the state to modify")) - FString StateName; - - UPROPERTY(meta=(Description="Blend Space asset name or path")) - FString BlendSpace; - - UPROPERTY(meta=(Optional, Description="Blueprint variable name to wire to the X axis input")) - FString XVariable; - - UPROPERTY(meta=(Optional, Description="Blueprint variable name to wire to the Y axis input")) - FString YVariable; - - virtual FString GetDescription() const override - { - return TEXT("Place a BlendSpacePlayer in a state's inner graph, connect it to the output pose, " - "and optionally wire blueprint variables to the X and Y axis inputs."); - } - - virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override - { - MCPAssets Assets; - if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; - UAnimationStateMachineGraph* SMGraph = MCPUtils::FindStateMachineGraph(Assets.Object(), Graph); - if (!SMGraph) { MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("State machine graph '%s' not found in '%s'"), *Graph, *Blueprint)); return; } - UAnimBlueprint* AnimBP = Assets.Object(); - - UAnimStateNode* StateNode = MCPUtils::FindStateByName(SMGraph, StateName, Result); - if (!StateNode) return; - - UEdGraph* InnerGraph = StateNode->GetBoundGraph(); - if (!InnerGraph) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("State '%s' has no bound graph"), *StateName)); - } - - // Find the blend space asset - MCPAssets BlendSpaceAssets; - if (!BlendSpaceAssets.Exact(BlendSpace).Errors(Result).ENone().ETwo().Load()) return; - UBlendSpace* BlendSpaceAsset = BlendSpaceAssets.Object(); - - // Find existing BlendSpacePlayer or create one - UAnimGraphNode_BlendSpacePlayer* BSNode = nullptr; - for (UEdGraphNode* Node : InnerGraph->Nodes) - { - BSNode = Cast(Node); - if (BSNode) break; - } - - if (!BSNode) - { - BSNode = NewObject(InnerGraph); - BSNode->CreateNewGuid(); - BSNode->PostPlacedNewNode(); - BSNode->AllocateDefaultPins(); - BSNode->NodePosX = 0; - BSNode->NodePosY = 0; - InnerGraph->AddNode(BSNode, false, false); - } - - BSNode->SetAnimationAsset(BlendSpaceAsset); - - // Connect BlendSpacePlayer output to the Output Animation Pose node - { - // Find the AnimGraphNode_Root (Output Pose) in the inner graph - UEdGraphNode* ResultNode = nullptr; - for (UEdGraphNode* Node : InnerGraph->Nodes) - { - if (Node->GetClass()->GetName().Contains(TEXT("AnimGraphNode_Root")) || - Node->GetClass()->GetName().Contains(TEXT("AnimGraphNode_StateResult"))) - { - ResultNode = Node; - break; - } - } - - if (ResultNode) - { - // Find output pose pin on BlendSpacePlayer and input pose pin on result node - UEdGraphPin* BSOutputPin = nullptr; - for (UEdGraphPin* Pin : BSNode->Pins) - { - if (Pin && (Pin->Direction == EGPD_Output) && (Pin->PinType.PinCategory == UEdGraphSchema_K2::PC_Struct)) - { - BSOutputPin = Pin; - break; - } - } - - UEdGraphPin* ResultInputPin = nullptr; - for (UEdGraphPin* Pin : ResultNode->Pins) - { - if (Pin && (Pin->Direction == EGPD_Input) && (Pin->PinType.PinCategory == UEdGraphSchema_K2::PC_Struct)) - { - ResultInputPin = Pin; - break; - } - } - - if (BSOutputPin && ResultInputPin) - { - // Break existing connections on the result input - ResultInputPin->BreakAllPinLinks(); - const UEdGraphSchema* Schema = InnerGraph->GetSchema(); - if (Schema) - { - Schema->TryCreateConnection(BSOutputPin, ResultInputPin); - } - } - } - } - - // Wire X and Y variables if provided - auto WireVariable = [&](const FString& VarName, const FString& PinName) -> bool - { - if (VarName.IsEmpty()) return false; - - // Verify the variable exists in the blueprint - FName VarFName(*VarName); - bool bVarFound = false; - for (FBPVariableDescription& Var : AnimBP->NewVariables) - { - if (Var.VarName == VarFName) - { - bVarFound = true; - break; - } - } - if (!bVarFound) - { - // Also check parent class properties - if (UClass* GenClass = AnimBP->SkeletonGeneratedClass) - { - if (FProperty* Prop = GenClass->FindPropertyByName(VarFName)) - { - bVarFound = true; - } - } - } - if (!bVarFound) - { - UE_LOG(LogTemp, Warning, TEXT("BlueprintMCP: Variable '%s' not found in '%s', skipping wire"), - *VarName, *Blueprint); - return false; - } - - // Create a VariableGet node - UK2Node_VariableGet* GetNode = NewObject(InnerGraph); - GetNode->VariableReference.SetSelfMember(VarFName); - GetNode->NodePosX = BSNode->NodePosX - 250; - GetNode->NodePosY = BSNode->NodePosY; - InnerGraph->AddNode(GetNode, false, false); - GetNode->AllocateDefaultPins(); - - // Find the variable output pin - UEdGraphPin* VarOutPin = nullptr; - for (UEdGraphPin* Pin : GetNode->Pins) - { - if (Pin && (Pin->Direction == EGPD_Output) && (Pin->PinName == VarFName)) - { - VarOutPin = Pin; - break; - } - } - - // Find the target pin on the BlendSpacePlayer - UEdGraphPin* TargetPin = BSNode->FindPin(FName(*PinName)); - - if (VarOutPin && TargetPin) - { - const UEdGraphSchema* Schema = InnerGraph->GetSchema(); - if (Schema) - { - Schema->TryCreateConnection(VarOutPin, TargetPin); - return true; - } - } - - return false; - }; - - WireVariable(XVariable, TEXT("X")); - WireVariable(YVariable, TEXT("Y")); - - // Compile and save - FKismetEditorUtilities::CompileBlueprint(AnimBP); - bool bSaved = MCPUtils::SaveBlueprintPackage(AnimBP); - - Result->SetStringField(TEXT("nodeId"), BSNode->NodeGuid.ToString()); - Result->SetBoolField(TEXT("saved"), bSaved); - } -}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/MCPHandlers_UserTypes.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/MCPHandlers_UserTypes.h deleted file mode 100644 index 56978006..00000000 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/MCPHandlers_UserTypes.h +++ /dev/null @@ -1,350 +0,0 @@ -#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" - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -USTRUCT() -struct FStructPropertyEntry -{ - GENERATED_BODY() - - UPROPERTY() - FString Name; - - UPROPERTY() - FString Type; -}; - -UCLASS() -class UMCPHandler_CreateStructAsset : 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) - { - FStructPropertyEntry Entry; - if (!MCPUtils::PopulateFromJson(FStructPropertyEntry::StaticStruct(), &Entry, PropVal, Result)) return; - if (Entry.Name.IsEmpty() || Entry.Type.IsEmpty()) continue; - - FEdGraphPinType PinType; - FString TypeError; - if (!MCPUtils::ResolveTypeFromString(Entry.Type, PinType, TypeError)) - { - UE_LOG(LogTemp, Warning, TEXT("BlueprintMCP: Could not resolve type '%s' for property '%s': %s"), *Entry.Type, *Entry.Name, *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, Entry.Name); - } - PropsAdded++; - } - } - - // Save - bool bSaved = MCPUtils::SaveGenericPackage(NewStruct); - - Result->SetStringField(TEXT("assetName"), AssetName); - Result->SetNumberField(TEXT("propertiesAdded"), PropsAdded); - Result->SetBoolField(TEXT("saved"), bSaved); - } -}; - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -UCLASS() -class UMCPHandler_CreateEnumAsset : 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->SetStringField(TEXT("assetName"), AssetName); - Result->SetNumberField(TEXT("valueCount"), EnumValues.Num()); - Result->SetBoolField(TEXT("saved"), bSaved); - } -}; - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -UCLASS() -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 - MCPAssets Assets; - if (!Assets.Exact(AssetPath).Errors(Result).ENone().ETwo().Load()) return; - UUserDefinedStruct* Struct = Assets.Object(); - - // Resolve type - FEdGraphPinType PinType; - if (!MCPUtils::ResolveTypeFromString(Type, PinType, Result)) - return; - - // 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("saved"), bSaved); - } -}; - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -UCLASS() -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 - MCPAssets Assets; - if (!Assets.Exact(AssetPath).Errors(Result).ENone().ETwo().Load()) return; - UUserDefinedStruct* Struct = Assets.Object(); - - // 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("saved"), bSaved); - } -}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/MCPHandlers_Variables.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/MCPHandlers_Variables.h deleted file mode 100644 index adba1d6c..00000000 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/MCPHandlers_Variables.h +++ /dev/null @@ -1,576 +0,0 @@ -#pragma once - -#include "CoreMinimal.h" -#include "MCPHandler.h" -#include "MCPAssetFinder.h" -#include "MCPUtils.h" -#include "Engine/Blueprint.h" -#include "EdGraph/EdGraph.h" -#include "EdGraph/EdGraphPin.h" -#include "K2Node_VariableGet.h" -#include "K2Node_VariableSet.h" -#include "Kismet2/BlueprintEditorUtils.h" -#include "MCPHandlers_Variables.generated.h" - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -UCLASS() -class UMCPHandler_ChangeBlueprintVariableType : public UObject, public IMCPHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Description="Blueprint name or package path")) - FString Blueprint; - - UPROPERTY(meta=(Description="Name of the variable to change")) - FString Variable; - - UPROPERTY(meta=(Description="New type name for the variable")) - FString NewType; - - UPROPERTY(meta=(Optional, Description="Type category: object, softobject, class, softclass, interface, struct, enum")) - FString TypeCategory; - - UPROPERTY(meta=(Optional, Description="If true, analyze the change without applying it")) - bool DryRun = false; - - virtual FString GetDescription() const override - { - return TEXT("Change the type of a Blueprint member variable. " - "Supports dry-run mode to preview affected nodes before committing."); - } - - virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override - { - MCPAssets Assets; - if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; - UBlueprint* BP = Assets.Object(); - - // Verify variable exists - bool bVarFound = false; - for (const FBPVariableDescription& Var : BP->NewVariables) - { - if (Var.VarName.ToString() == Variable) - { - bVarFound = true; - break; - } - } - if (!bVarFound) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Variable '%s' not found in Blueprint '%s'"), *Variable, *Blueprint)); - } - - // Build the new pin type using shared resolver - FEdGraphPinType NewPinType; - FString ResolveInput = NewType; - - // If typeCategory is an object reference variant, use colon syntax for the resolver - if (TypeCategory == TEXT("object") || TypeCategory == TEXT("softobject") || - TypeCategory == TEXT("class") || TypeCategory == TEXT("softclass") || - TypeCategory == TEXT("interface")) - { - ResolveInput = TypeCategory + TEXT(":") + NewType; - } - - if (!MCPUtils::ResolveTypeFromString(ResolveInput, NewPinType, Result)) - return; - - // Derive typeCategory from the resolved pin type for the response - FString ResolvedTypeCategory = TypeCategory; - if (ResolvedTypeCategory.IsEmpty()) - { - if (NewPinType.PinCategory == UEdGraphSchema_K2::PC_Struct) - ResolvedTypeCategory = TEXT("struct"); - else if (NewPinType.PinCategory == UEdGraphSchema_K2::PC_Enum || NewPinType.PinCategory == UEdGraphSchema_K2::PC_Byte) - ResolvedTypeCategory = TEXT("enum"); - else if (NewPinType.PinCategory == UEdGraphSchema_K2::PC_Object) - ResolvedTypeCategory = TEXT("object"); - else if (NewPinType.PinCategory == UEdGraphSchema_K2::PC_SoftObject) - ResolvedTypeCategory = TEXT("softobject"); - else if (NewPinType.PinCategory == UEdGraphSchema_K2::PC_Class) - ResolvedTypeCategory = TEXT("class"); - else if (NewPinType.PinCategory == UEdGraphSchema_K2::PC_SoftClass) - ResolvedTypeCategory = TEXT("softclass"); - else if (NewPinType.PinCategory == UEdGraphSchema_K2::PC_Interface) - ResolvedTypeCategory = TEXT("interface"); - else - ResolvedTypeCategory = NewPinType.PinCategory.ToString(); - } - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: %s variable '%s' in '%s' to %s (%s)"), - DryRun ? TEXT("[DRY RUN] Analyzing change of") : TEXT("Changing"), - *Variable, *Blueprint, *NewType, *ResolvedTypeCategory); - - // Analyze affected nodes (get/set nodes for this variable) - TArray> AffectedNodes; - for (UK2Node_VariableGet* VG : MCPUtils::AllNodes(BP)) - { - if (VG->GetVarName().ToString() != Variable) continue; - TSharedRef AffNode = MakeShared(); - AffNode->SetStringField(TEXT("nodeId"), VG->NodeGuid.ToString()); - AffNode->SetStringField(TEXT("nodeType"), TEXT("VariableGet")); - AffNode->SetStringField(TEXT("graph"), VG->GetGraph()->GetName()); - TArray> AffPins; - for (UEdGraphPin* Pin : VG->Pins) - { - if (Pin && (Pin->LinkedTo.Num() > 0) && (Pin->Direction == EGPD_Output)) - { - AffPins.Add(MakeShared( - FString::Printf(TEXT("%s (connected to %d pin(s))"), - *Pin->PinName.ToString(), Pin->LinkedTo.Num()))); - } - } - AffNode->SetArrayField(TEXT("affectedPins"), AffPins); - AffectedNodes.Add(MakeShared(AffNode)); - } - for (UK2Node_VariableSet* VS : MCPUtils::AllNodes(BP)) - { - if (VS->GetVarName().ToString() != Variable) continue; - TSharedRef AffNode = MakeShared(); - AffNode->SetStringField(TEXT("nodeId"), VS->NodeGuid.ToString()); - AffNode->SetStringField(TEXT("nodeType"), TEXT("VariableSet")); - AffNode->SetStringField(TEXT("graph"), VS->GetGraph()->GetName()); - TArray> AffPins; - for (UEdGraphPin* Pin : VS->Pins) - { - if (Pin && Pin->LinkedTo.Num() > 0) - { - AffPins.Add(MakeShared( - FString::Printf(TEXT("%s (connected to %d pin(s))"), - *Pin->PinName.ToString(), Pin->LinkedTo.Num()))); - } - } - AffNode->SetArrayField(TEXT("affectedPins"), AffPins); - AffectedNodes.Add(MakeShared(AffNode)); - } - - if (DryRun) - { - Result->SetBoolField(TEXT("dryRun"), true); - Result->SetStringField(TEXT("typeCategory"), ResolvedTypeCategory); - Result->SetNumberField(TEXT("affectedNodeCount"), AffectedNodes.Num()); - Result->SetArrayField(TEXT("affectedNodes"), AffectedNodes); - return; - } - - // Directly modify the variable type in the description array. - BP->PreEditChange(nullptr); - for (FBPVariableDescription& Var : BP->NewVariables) - { - if (Var.VarName == FName(*Variable)) - { - Var.VarType = NewPinType; - break; - } - } - BP->PostEditChange(); - - // Save - bool bSaved = MCPUtils::SaveBlueprintPackage(BP); - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Variable type changed, save %s"), - bSaved ? TEXT("succeeded") : TEXT("failed")); - - // Return updated variable state - TSharedRef UpdatedVar = MakeShared(); - for (const FBPVariableDescription& Var : BP->NewVariables) - { - if (Var.VarName == FName(*Variable)) - { - UpdatedVar->SetStringField(TEXT("name"), Var.VarName.ToString()); - UpdatedVar->SetStringField(TEXT("type"), Var.VarType.PinCategory.ToString()); - if (Var.VarType.PinSubCategoryObject.IsValid()) - UpdatedVar->SetStringField(TEXT("subtype"), Var.VarType.PinSubCategoryObject->GetName()); - UpdatedVar->SetBoolField(TEXT("isArray"), Var.VarType.IsArray()); - break; - } - } - - Result->SetStringField(TEXT("typeCategory"), ResolvedTypeCategory); - Result->SetBoolField(TEXT("saved"), bSaved); - Result->SetObjectField(TEXT("updatedVariable"), UpdatedVar); - Result->SetArrayField(TEXT("affectedNodes"), AffectedNodes); - } -}; - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -UCLASS() -class UMCPHandler_AddBlueprintVariable : public UObject, public IMCPHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Description="Blueprint name or package path")) - FString Blueprint; - - UPROPERTY(meta=(Description="Name of the new variable")) - FString VariableName; - - UPROPERTY(meta=(Description="Type of the new variable")) - FString VariableType; - - UPROPERTY(meta=(Optional, Description="Category to assign the variable to")) - FString Category; - - UPROPERTY(meta=(Optional, Description="If true, make the variable an array")) - bool IsArray = false; - - UPROPERTY(meta=(Optional, Description="Default value for the variable")) - FString DefaultValue; - - virtual FString GetDescription() const override - { - return TEXT("Add a new member variable to a Blueprint."); - } - - virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override - { - MCPAssets Assets; - if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; - UBlueprint* BP = Assets.Object(); - - // Check for duplicate variable name - FName VarFName(*VariableName); - for (const FBPVariableDescription& Var : BP->NewVariables) - { - if (Var.VarName == VarFName) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf( - TEXT("Variable '%s' already exists in Blueprint '%s'"), *VariableName, *Blueprint)); - } - } - - // Resolve the type using the shared helper - FEdGraphPinType PinType; - if (!MCPUtils::ResolveTypeFromString(VariableType, PinType, Result)) - return; - - // Set container type for arrays - if (IsArray) - { - PinType.ContainerType = EPinContainerType::Array; - } - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Adding variable '%s' (type=%s, array=%s) to Blueprint '%s'"), - *VariableName, *VariableType, IsArray ? TEXT("true") : TEXT("false"), *Blueprint); - - // Add the variable using the editor utility function - bool bSuccess = FBlueprintEditorUtils::AddMemberVariable(BP, VarFName, PinType, DefaultValue); - if (!bSuccess) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf( - TEXT("FBlueprintEditorUtils::AddMemberVariable failed for '%s'"), *VariableName)); - } - - // Set category if provided - if (!Category.IsEmpty()) - { - FBlueprintEditorUtils::SetBlueprintVariableCategory(BP, VarFName, nullptr, FText::FromString(Category)); - } - - FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP); - bool bSaved = MCPUtils::SaveBlueprintPackage(BP); - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Added variable '%s' to '%s' (saved: %s)"), - *VariableName, *Blueprint, bSaved ? TEXT("true") : TEXT("false")); - - Result->SetBoolField(TEXT("saved"), bSaved); - } -}; - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -UCLASS() -class UMCPHandler_RemoveBlueprintVariable : public UObject, public IMCPHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Description="Blueprint name or package path")) - FString Blueprint; - - UPROPERTY(meta=(Description="Name of the variable to remove")) - FString VariableName; - - virtual FString GetDescription() const override - { - return TEXT("Remove a member variable from a Blueprint."); - } - - virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override - { - MCPAssets Assets; - if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; - UBlueprint* BP = Assets.Object(); - - // Find variable by name (case-insensitive) - FName VarFName(*VariableName); - bool bVarFound = false; - for (const FBPVariableDescription& Var : BP->NewVariables) - { - if (Var.VarName.ToString().Equals(VariableName, ESearchCase::IgnoreCase)) - { - VarFName = Var.VarName; // Use the exact name found - bVarFound = true; - break; - } - } - - if (!bVarFound) - { - // Build available variables list for helpful error message - TArray> AvailVars; - for (const FBPVariableDescription& Var : BP->NewVariables) - { - AvailVars.Add(MakeShared(Var.VarName.ToString())); - } - - MCPUtils::MakeErrorJson(Result, FString::Printf( - TEXT("Variable '%s' not found in Blueprint '%s'"), *VariableName, *Blueprint)); - Result->SetArrayField(TEXT("availableVariables"), AvailVars); - return; - } - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Removing variable '%s' from Blueprint '%s'"), - *VariableName, *Blueprint); - - // Use the editor utility to remove the variable (also cleans up Get/Set nodes) - FBlueprintEditorUtils::RemoveMemberVariable(BP, VarFName); - - FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP); - bool bSaved = MCPUtils::SaveBlueprintPackage(BP); - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Removed variable '%s' from '%s' (saved: %s)"), - *VariableName, *Blueprint, bSaved ? TEXT("true") : TEXT("false")); - - Result->SetBoolField(TEXT("saved"), bSaved); - } -}; - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -UCLASS() -class UMCPHandler_SetBlueprintVariableMetadata : public UObject, public IMCPHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Description="Blueprint name or package path")) - FString Blueprint; - - UPROPERTY(meta=(Description="Name of the variable to modify")) - FString Variable; - - UPROPERTY(meta=(Optional, Description="Category to assign the variable to")) - FString Category; - - UPROPERTY(meta=(Optional, Description="Tooltip text for the variable")) - FString Tooltip; - - UPROPERTY(meta=(Optional, Description="Replication mode: none, replicated, or repNotify")) - FString Replication; - - UPROPERTY(meta=(Optional, Description="If true, expose this variable on spawn")) - bool ExposeOnSpawn = false; - - UPROPERTY(meta=(Optional, Description="If true, mark the variable as private")) - bool IsPrivate = false; - - UPROPERTY(meta=(Optional, Description="Editability mode: editAnywhere, editDefaultsOnly, editInstanceOnly, or none")) - FString Editability; - - virtual FString GetDescription() const override - { - return TEXT("Set variable metadata properties such as category, tooltip, " - "replication, editability, and visibility flags."); - } - - virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override - { - MCPAssets Assets; - if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; - UBlueprint* BP = Assets.Object(); - - // Find the variable - FName VarFName(*Variable); - FBPVariableDescription* VarDesc = nullptr; - for (FBPVariableDescription& Var : BP->NewVariables) - { - if (Var.VarName == VarFName) - { - VarDesc = &Var; - break; - } - } - - if (!VarDesc) - { - TArray> AvailableVars; - for (const FBPVariableDescription& Var : BP->NewVariables) - { - AvailableVars.Add(MakeShared(Var.VarName.ToString())); - } - MCPUtils::MakeErrorJson(Result, FString::Printf( - TEXT("Variable '%s' not found in Blueprint '%s'"), *Variable, *Blueprint)); - Result->SetArrayField(TEXT("availableVariables"), AvailableVars); - return; - } - - TArray> Changes; - - // Category - if (Json->HasField(TEXT("category"))) - { - FString OldCategory = VarDesc->Category.ToString(); - VarDesc->Category = FText::FromString(Category); - FBlueprintEditorUtils::SetBlueprintVariableCategory(BP, VarFName, nullptr, FText::FromString(Category)); - - TSharedRef Change = MakeShared(); - Change->SetStringField(TEXT("field"), TEXT("category")); - Change->SetStringField(TEXT("oldValue"), OldCategory); - Change->SetStringField(TEXT("newValue"), Category); - Changes.Add(MakeShared(Change)); - } - - // Tooltip - if (Json->HasField(TEXT("tooltip"))) - { - FString OldTooltip; - FBlueprintEditorUtils::GetBlueprintVariableMetaData(BP, VarFName, nullptr, TEXT("tooltip"), OldTooltip); - FBlueprintEditorUtils::SetBlueprintVariableMetaData(BP, VarFName, nullptr, TEXT("tooltip"), Tooltip); - - TSharedRef Change = MakeShared(); - Change->SetStringField(TEXT("field"), TEXT("tooltip")); - Change->SetStringField(TEXT("oldValue"), OldTooltip); - Change->SetStringField(TEXT("newValue"), Tooltip); - Changes.Add(MakeShared(Change)); - } - - // Replication - if (Json->HasField(TEXT("replication"))) - { - uint64 OldFlags = VarDesc->PropertyFlags; - - if (Replication == TEXT("none")) - { - VarDesc->PropertyFlags &= ~CPF_Net; - VarDesc->PropertyFlags &= ~CPF_RepNotify; - VarDesc->RepNotifyFunc = NAME_None; - } - else if (Replication == TEXT("replicated")) - { - VarDesc->PropertyFlags |= CPF_Net; - VarDesc->PropertyFlags &= ~CPF_RepNotify; - VarDesc->RepNotifyFunc = NAME_None; - } - else if (Replication == TEXT("repNotify")) - { - VarDesc->PropertyFlags |= CPF_Net | CPF_RepNotify; - // Auto-generate RepNotify function name - VarDesc->RepNotifyFunc = FName(*FString::Printf(TEXT("OnRep_%s"), *Variable)); - } - else - { - return MCPUtils::MakeErrorJson(Result, FString::Printf( - TEXT("Invalid replication value '%s'. Valid: none, replicated, repNotify"), *Replication)); - } - - TSharedRef Change = MakeShared(); - Change->SetStringField(TEXT("field"), TEXT("replication")); - Change->SetStringField(TEXT("newValue"), Replication); - Changes.Add(MakeShared(Change)); - } - - // ExposeOnSpawn - if (Json->HasField(TEXT("exposeOnSpawn"))) - { - bool bOld = (VarDesc->PropertyFlags & CPF_ExposeOnSpawn) != 0; - if (ExposeOnSpawn) - VarDesc->PropertyFlags |= CPF_ExposeOnSpawn; - else - VarDesc->PropertyFlags &= ~CPF_ExposeOnSpawn; - - TSharedRef Change = MakeShared(); - Change->SetStringField(TEXT("field"), TEXT("exposeOnSpawn")); - Change->SetStringField(TEXT("oldValue"), bOld ? TEXT("true") : TEXT("false")); - Change->SetStringField(TEXT("newValue"), ExposeOnSpawn ? TEXT("true") : TEXT("false")); - Changes.Add(MakeShared(Change)); - } - - // isPrivate - if (Json->HasField(TEXT("isPrivate"))) - { - bool bOld = (VarDesc->PropertyFlags & CPF_DisableEditOnInstance) != 0; - // In UE5, "private" for Blueprint variables is represented via metadata - FBlueprintEditorUtils::SetBlueprintVariableMetaData(BP, VarFName, nullptr, - TEXT("BlueprintPrivate"), IsPrivate ? TEXT("true") : TEXT("false")); - - TSharedRef Change = MakeShared(); - Change->SetStringField(TEXT("field"), TEXT("isPrivate")); - Change->SetStringField(TEXT("oldValue"), bOld ? TEXT("true") : TEXT("false")); - Change->SetStringField(TEXT("newValue"), IsPrivate ? TEXT("true") : TEXT("false")); - Changes.Add(MakeShared(Change)); - } - - // Editability (EditAnywhere, EditDefaultsOnly, EditInstanceOnly) - if (Json->HasField(TEXT("editability"))) - { - // Clear all edit flags first - VarDesc->PropertyFlags &= ~(CPF_Edit | CPF_DisableEditOnInstance | CPF_DisableEditOnTemplate); - - if (Editability == TEXT("editAnywhere")) - { - VarDesc->PropertyFlags |= CPF_Edit; - } - else if (Editability == TEXT("editDefaultsOnly")) - { - VarDesc->PropertyFlags |= CPF_Edit | CPF_DisableEditOnInstance; - } - else if (Editability == TEXT("editInstanceOnly")) - { - VarDesc->PropertyFlags |= CPF_Edit | CPF_DisableEditOnTemplate; - } - else if (Editability == TEXT("none")) - { - // All edit flags already cleared - } - else - { - return MCPUtils::MakeErrorJson(Result, FString::Printf( - TEXT("Invalid editability value '%s'. Valid: editAnywhere, editDefaultsOnly, editInstanceOnly, none"), - *Editability)); - } - - TSharedRef Change = MakeShared(); - Change->SetStringField(TEXT("field"), TEXT("editability")); - Change->SetStringField(TEXT("newValue"), Editability); - Changes.Add(MakeShared(Change)); - } - - if (Changes.Num() == 0) - { - return MCPUtils::MakeErrorJson(Result, TEXT("No metadata fields specified. Provide at least one of: category, tooltip, replication, exposeOnSpawn, isPrivate, editability")); - } - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: SetVariableMetadata on '%s.%s' — %d field(s) changed"), - *Blueprint, *Variable, Changes.Num()); - - BP->PreEditChange(nullptr); - BP->PostEditChange(); - FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP); - bool bSaved = MCPUtils::SaveBlueprintPackage(BP); - - Result->SetArrayField(TEXT("changes"), Changes); - Result->SetBoolField(TEXT("saved"), bSaved); - } -}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_AddAnimStateToMachine.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_AddAnimStateToMachine.h new file mode 100644 index 00000000..1ea767f9 --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_AddAnimStateToMachine.h @@ -0,0 +1,117 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPAssetFinder.h" +#include "MCPUtils.h" +#include "EdGraph/EdGraph.h" +#include "EdGraph/EdGraphNode.h" +#include "EdGraph/EdGraphPin.h" +#include "Kismet2/KismetEditorUtilities.h" +#include "Animation/AnimBlueprint.h" +#include "Animation/AnimSequence.h" +#include "Animation/BlendSpace.h" +#include "AnimGraphNode_SequencePlayer.h" +#include "AnimGraphNode_BlendSpacePlayer.h" +#include "AnimStateNode.h" +#include "AnimStateTransitionNode.h" +#include "AnimationStateMachineGraph.h" +#include "K2Node_VariableGet.h" +#include "UMCPHandler_AddAnimStateToMachine.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +UCLASS() +class UMCPHandler_AddAnimStateToMachine : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Animation Blueprint name or package path")) + FString Blueprint; + + UPROPERTY(meta=(Description="State machine graph name")) + FString Graph; + + UPROPERTY(meta=(Description="Name for the new state")) + FString StateName; + + UPROPERTY(meta=(Optional, Description="X position of the new state node")) + int32 PosX = 200; + + UPROPERTY(meta=(Optional, Description="Y position of the new state node")) + int32 PosY = 0; + + UPROPERTY(meta=(Optional, Description="Animation asset name to assign to the state")) + FString AnimationAsset; + + virtual FString GetDescription() const override + { + return TEXT("Add a new state to an animation state machine graph. " + "Optionally assign an animation asset to the state."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override + { + MCPAssets Assets; + if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; + UAnimationStateMachineGraph* SMGraph = MCPUtils::FindStateMachineGraph(Assets.Object(), Graph); + if (!SMGraph) { MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("State machine graph '%s' not found in '%s'"), *Graph, *Blueprint)); return; } + UAnimBlueprint* AnimBP = Assets.Object(); + + // Check for duplicate state name + if (MCPUtils::FindStateByName(SMGraph, StateName, nullptr)) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("State '%s' already exists in graph '%s'"), *StateName, *Graph)); + } + + // Create the state node + UAnimStateNode* NewState = NewObject(SMGraph); + NewState->CreateNewGuid(); + NewState->NodePosX = PosX; + NewState->NodePosY = PosY; + + // Set the state name via the bound graph + NewState->PostPlacedNewNode(); + NewState->AllocateDefaultPins(); + + // Rename the bound graph to set the state name + if (NewState->GetBoundGraph()) + { + NewState->GetBoundGraph()->Rename(*StateName, nullptr); + } + + SMGraph->AddNode(NewState, false, false); + NewState->SetFlags(RF_Transactional); + + // Optionally set animation asset + if (!AnimationAsset.IsEmpty() && NewState->GetBoundGraph()) + { + // Try to find the animation asset and create a sequence player in the state's inner graph + MCPAssets AnimAssets; + UAnimSequence* AnimSeq = AnimAssets.Exact(AnimationAsset).ENone().ETwo().Load() ? AnimAssets.Object() : nullptr; + + if (AnimSeq) + { + UAnimGraphNode_SequencePlayer* SeqNode = NewObject(NewState->GetBoundGraph()); + SeqNode->CreateNewGuid(); + SeqNode->PostPlacedNewNode(); + SeqNode->AllocateDefaultPins(); + SeqNode->SetAnimationAsset(AnimSeq); + SeqNode->NodePosX = 0; + SeqNode->NodePosY = 0; + NewState->GetBoundGraph()->AddNode(SeqNode, false, false); + } + } + + // Compile and save + FKismetEditorUtilities::CompileBlueprint(AnimBP); + bool bSaved = MCPUtils::SaveBlueprintPackage(AnimBP); + + Result->SetStringField(TEXT("nodeId"), NewState->NodeGuid.ToString()); + Result->SetBoolField(TEXT("saved"), bSaved); + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_AddAnimStateTransition.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_AddAnimStateTransition.h new file mode 100644 index 00000000..d698516e --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_AddAnimStateTransition.h @@ -0,0 +1,110 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPAssetFinder.h" +#include "MCPUtils.h" +#include "EdGraph/EdGraph.h" +#include "EdGraph/EdGraphNode.h" +#include "EdGraph/EdGraphPin.h" +#include "Kismet2/KismetEditorUtilities.h" +#include "Animation/AnimBlueprint.h" +#include "Animation/AnimSequence.h" +#include "Animation/BlendSpace.h" +#include "AnimGraphNode_SequencePlayer.h" +#include "AnimGraphNode_BlendSpacePlayer.h" +#include "AnimStateNode.h" +#include "AnimStateTransitionNode.h" +#include "AnimationStateMachineGraph.h" +#include "K2Node_VariableGet.h" +#include "UMCPHandler_AddAnimStateTransition.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +UCLASS() +class UMCPHandler_AddAnimStateTransition : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Animation Blueprint name or package path")) + FString Blueprint; + + UPROPERTY(meta=(Description="State machine graph name")) + FString Graph; + + UPROPERTY(meta=(Description="Name of the source state")) + FString FromState; + + UPROPERTY(meta=(Description="Name of the target state")) + FString ToState; + + UPROPERTY(meta=(Optional, Description="Crossfade duration in seconds")) + float CrossfadeDuration = 0.0f; + + UPROPERTY(meta=(Optional, Description="Transition priority order")) + int32 Priority = 0; + + UPROPERTY(meta=(Optional, Description="Whether the transition is bidirectional")) + bool BBidirectional = false; + + virtual FString GetDescription() const override + { + return TEXT("Add a transition between two states in an animation state machine graph."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override + { + MCPAssets Assets; + if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; + UAnimationStateMachineGraph* SMGraph = MCPUtils::FindStateMachineGraph(Assets.Object(), Graph); + if (!SMGraph) { MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("State machine graph '%s' not found in '%s'"), *Graph, *Blueprint)); return; } + UAnimBlueprint* AnimBP = Assets.Object(); + + UAnimStateNode* FromStateNode = MCPUtils::FindStateByName(SMGraph, FromState, Result); + if (!FromStateNode) return; + + UAnimStateNode* ToStateNode = MCPUtils::FindStateByName(SMGraph, ToState, Result); + if (!ToStateNode) return; + + // Create transition node + UAnimStateTransitionNode* TransNode = NewObject(SMGraph); + TransNode->CreateNewGuid(); + TransNode->PostPlacedNewNode(); + TransNode->AllocateDefaultPins(); + + // Position between the two states + TransNode->NodePosX = (FromStateNode->NodePosX + ToStateNode->NodePosX) / 2; + TransNode->NodePosY = (FromStateNode->NodePosY + ToStateNode->NodePosY) / 2; + + SMGraph->AddNode(TransNode, false, false); + TransNode->SetFlags(RF_Transactional); + + // Connect: FromState output -> Transition input, Transition output -> ToState input + TransNode->CreateConnections(FromStateNode, ToStateNode); + + // Set optional properties + if (Json->HasField(TEXT("crossfadeDuration"))) + { + TransNode->CrossfadeDuration = CrossfadeDuration; + } + if (Json->HasField(TEXT("priority"))) + { + TransNode->PriorityOrder = Priority; + } + if (Json->HasField(TEXT("bBidirectional"))) + { + TransNode->Bidirectional = BBidirectional; + } + + // Compile and save + FKismetEditorUtilities::CompileBlueprint(AnimBP); + bool bSaved = MCPUtils::SaveBlueprintPackage(AnimBP); + + Result->SetStringField(TEXT("nodeId"), TransNode->NodeGuid.ToString()); + Result->SetBoolField(TEXT("saved"), bSaved); + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_AddBlueprintComponent.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_AddBlueprintComponent.h new file mode 100644 index 00000000..58488a88 --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_AddBlueprintComponent.h @@ -0,0 +1,179 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPAssetFinder.h" +#include "MCPUtils.h" +#include "Engine/Blueprint.h" +#include "Engine/SimpleConstructionScript.h" +#include "Engine/SCS_Node.h" +#include "Components/ActorComponent.h" +#include "Kismet2/BlueprintEditorUtils.h" +#include "UObject/UObjectIterator.h" +#include "UMCPHandler_AddBlueprintComponent.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +UCLASS() +class UMCPHandler_AddBlueprintComponent : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Blueprint name or package path")) + FString Blueprint; + + UPROPERTY(meta=(Description="Component class name (e.g. StaticMeshComponent, SceneComponent)")) + FString ComponentClass; + + UPROPERTY(meta=(Description="Component name for the new component")) + FString Component; + + UPROPERTY(meta=(Optional, Description="Name of the parent component to attach to")) + FString ParentComponent; + + virtual FString GetDescription() const override + { + return TEXT("Add a component to a Blueprint's SimpleConstructionScript. " + "Optionally attach it to an existing parent component."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override + { + MCPAssets Assets; + if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; + UBlueprint* BP = Assets.Object(); + + USimpleConstructionScript* SCS = BP->SimpleConstructionScript; + if (!SCS) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf( + TEXT("Blueprint '%s' does not have a SimpleConstructionScript (not an Actor Blueprint)"), + *Blueprint)); + } + + // Check for duplicate component names + const TArray& ExistingNodes = SCS->GetAllNodes(); + for (USCS_Node* Existing : ExistingNodes) + { + if (Existing && Existing->GetVariableName().ToString().Equals(Component, ESearchCase::IgnoreCase)) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf( + TEXT("A component named '%s' already exists in Blueprint '%s'"), + *Component, *Blueprint)); + } + } + + // Resolve the component class by name + // Try multiple name variants: exact name, with U prefix, without U prefix + UClass* ComponentClassObj = nullptr; + + TArray NamesToTry; + NamesToTry.Add(ComponentClass); + if (!ComponentClass.StartsWith(TEXT("U"))) + { + NamesToTry.Add(FString::Printf(TEXT("U%s"), *ComponentClass)); + } + else + { + // Also try without U prefix + NamesToTry.Add(ComponentClass.Mid(1)); + } + + for (TObjectIterator It; It; ++It) + { + if (!It->IsChildOf(UActorComponent::StaticClass())) + { + continue; + } + + FString ClassName = It->GetName(); + for (const FString& NameToTry : NamesToTry) + { + if (ClassName.Equals(NameToTry, ESearchCase::IgnoreCase)) + { + ComponentClassObj = *It; + break; + } + } + + if (ComponentClassObj) + { + break; + } + } + + if (!ComponentClassObj) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf( + TEXT("Component class '%s' not found or is not a subclass of UActorComponent. " + "Common classes: StaticMeshComponent, SkeletalMeshComponent, AudioComponent, " + "SceneComponent, BoxCollisionComponent, SphereCollisionComponent, CapsuleComponent, " + "ArrowComponent, ChildActorComponent, SpotLightComponent, PointLightComponent, " + "WidgetComponent, BillboardComponent"), + *ComponentClass)); + } + + // If parent component specified, find its SCS node + USCS_Node* ParentSCSNode = nullptr; + if (!ParentComponent.IsEmpty()) + { + for (USCS_Node* Node : ExistingNodes) + { + if (Node && Node->GetVariableName().ToString().Equals(ParentComponent, ESearchCase::IgnoreCase)) + { + ParentSCSNode = Node; + break; + } + } + + if (!ParentSCSNode) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf( + TEXT("Parent component '%s' not found in Blueprint '%s'"), + *ParentComponent, *Blueprint)); + } + } + + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Adding component '%s' (%s) to Blueprint '%s'"), + *Component, *ComponentClassObj->GetName(), *Blueprint); + + // Create the SCS node + USCS_Node* NewNode = SCS->CreateNode(ComponentClassObj, FName(*Component)); + if (!NewNode) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf( + TEXT("Failed to create SCS node for component '%s' with class '%s'"), + *Component, *ComponentClassObj->GetName())); + } + + // Add to the hierarchy + if (ParentSCSNode) + { + ParentSCSNode->AddChildNode(NewNode); + } + else + { + SCS->AddNode(NewNode); + } + + FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP); + bool bSaved = MCPUtils::SaveBlueprintPackage(BP); + + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Added component '%s' (%s) to '%s' (parent: %s, saved: %s)"), + *Component, *ComponentClassObj->GetName(), *Blueprint, + ParentSCSNode ? *ParentComponent : TEXT("(root)"), + bSaved ? TEXT("true") : TEXT("false")); + + Result->SetStringField(TEXT("component"), NewNode->GetVariableName().ToString()); + Result->SetStringField(TEXT("componentClass"), ComponentClassObj->GetName()); + if (ParentSCSNode) + { + Result->SetStringField(TEXT("parentComponent"), ParentSCSNode->GetVariableName().ToString()); + } + Result->SetBoolField(TEXT("saved"), bSaved); + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_AddBlueprintInterface.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_AddBlueprintInterface.h new file mode 100644 index 00000000..01253a1b --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_AddBlueprintInterface.h @@ -0,0 +1,152 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPAssetFinder.h" +#include "MCPServer.h" +#include "MCPUtils.h" +#include "Engine/Blueprint.h" +#include "EdGraph/EdGraph.h" +#include "Kismet2/BlueprintEditorUtils.h" +#include "UObject/UObjectIterator.h" +#include "UMCPHandler_AddBlueprintInterface.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +UCLASS() +class UMCPHandler_AddBlueprintInterface : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Blueprint name or package path")) + FString Blueprint; + + UPROPERTY(meta=(Description="Interface name (e.g. 'BPI_MyInterface') or native UInterface class name")) + FString InterfaceName; + + virtual FString GetDescription() const override + { + return TEXT("Add a Blueprint Interface implementation to a Blueprint. " + "Creates stub function graphs for each interface function."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override + { + + MCPAssets Assets; + if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; + UBlueprint* BP = Assets.Object(); + + // Resolve the interface class + UClass* InterfaceClass = nullptr; + + // Strategy 1: Search loaded UInterface classes by name + for (TObjectIterator It; It; ++It) + { + if (!It->IsChildOf(UInterface::StaticClass())) + { + continue; + } + + FString ClassName = It->GetName(); + // Match by class name (e.g. "BPI_Foo_C") or by trimmed name (e.g. "BPI_Foo") + if (ClassName.Equals(InterfaceName, ESearchCase::IgnoreCase)) + { + InterfaceClass = *It; + break; + } + // Strip the generated "_C" suffix for comparison + FString TrimmedName = ClassName; + if (TrimmedName.EndsWith(TEXT("_C"))) + { + TrimmedName = TrimmedName.LeftChop(2); + } + if (TrimmedName.Equals(InterfaceName, ESearchCase::IgnoreCase)) + { + InterfaceClass = *It; + break; + } + } + + // Strategy 2: Try loading as a Blueprint Interface asset + if (!InterfaceClass) + { + MCPAssets IfaceAssets; + if (!IfaceAssets.Exact(InterfaceName).AllContent().Errors(Result).ETwo().Load()) return; + if (!IfaceAssets.Objects().IsEmpty()) + { + UClass* GenClass = IfaceAssets.Object()->GeneratedClass; + if (GenClass && GenClass->IsChildOf(UInterface::StaticClass())) + InterfaceClass = GenClass; + } + } + + if (!InterfaceClass) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf( + TEXT("Interface '%s' not found. Provide a Blueprint Interface asset name (e.g. 'BPI_MyInterface') or a native UInterface class name."), + *InterfaceName)); + } + + // Check for duplicates + for (const FBPInterfaceDescription& IfaceDesc : BP->ImplementedInterfaces) + { + if (IfaceDesc.Interface == InterfaceClass) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf( + TEXT("Interface '%s' is already implemented by Blueprint '%s'"), + *InterfaceName, *Blueprint)); + } + } + + FTopLevelAssetPath InterfacePath = InterfaceClass->GetClassPathName(); + + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Adding interface '%s' to Blueprint '%s'"), + *InterfaceClass->GetName(), *Blueprint); + + bool bAdded = FBlueprintEditorUtils::ImplementNewInterface(BP, InterfacePath); + if (!bAdded) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf( + TEXT("FBlueprintEditorUtils::ImplementNewInterface failed for interface '%s' on Blueprint '%s'"), + *InterfaceName, *Blueprint)); + } + + // Collect stub function graph names from the newly added interface entry + TArray AddedFunctions; + for (const FBPInterfaceDescription& IfaceDesc : BP->ImplementedInterfaces) + { + if (IfaceDesc.Interface == InterfaceClass) + { + for (const UEdGraph* Graph : IfaceDesc.Graphs) + { + if (Graph) + { + AddedFunctions.Add(Graph->GetName()); + } + } + break; + } + } + + FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP); + + + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Added interface '%s' to '%s' (%d function stubs)"), + *InterfaceClass->GetName(), *Blueprint, AddedFunctions.Num()); + + Result->SetStringField(TEXT("interfaceName"), InterfaceClass->GetName()); + Result->SetStringField(TEXT("interfacePath"), InterfaceClass->GetPathName()); + + TArray> FuncArr; + for (const FString& FuncName : AddedFunctions) + { + FuncArr.Add(MakeShared(FuncName)); + } + Result->SetArrayField(TEXT("functionGraphsAdded"), FuncArr); + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_AddBlueprintVariable.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_AddBlueprintVariable.h new file mode 100644 index 00000000..d0729b79 --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_AddBlueprintVariable.h @@ -0,0 +1,102 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPAssetFinder.h" +#include "MCPUtils.h" +#include "Engine/Blueprint.h" +#include "EdGraph/EdGraph.h" +#include "EdGraph/EdGraphPin.h" +#include "K2Node_VariableGet.h" +#include "K2Node_VariableSet.h" +#include "Kismet2/BlueprintEditorUtils.h" +#include "UMCPHandler_AddBlueprintVariable.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +UCLASS() +class UMCPHandler_AddBlueprintVariable : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Blueprint name or package path")) + FString Blueprint; + + UPROPERTY(meta=(Description="Name of the new variable")) + FString VariableName; + + UPROPERTY(meta=(Description="Type of the new variable")) + FString VariableType; + + UPROPERTY(meta=(Optional, Description="Category to assign the variable to")) + FString Category; + + UPROPERTY(meta=(Optional, Description="If true, make the variable an array")) + bool IsArray = false; + + UPROPERTY(meta=(Optional, Description="Default value for the variable")) + FString DefaultValue; + + virtual FString GetDescription() const override + { + return TEXT("Add a new member variable to a Blueprint."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override + { + MCPAssets Assets; + if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; + UBlueprint* BP = Assets.Object(); + + // Check for duplicate variable name + FName VarFName(*VariableName); + for (const FBPVariableDescription& Var : BP->NewVariables) + { + if (Var.VarName == VarFName) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf( + TEXT("Variable '%s' already exists in Blueprint '%s'"), *VariableName, *Blueprint)); + } + } + + // Resolve the type using the shared helper + FEdGraphPinType PinType; + if (!MCPUtils::ResolveTypeFromString(VariableType, PinType, Result)) + return; + + // Set container type for arrays + if (IsArray) + { + PinType.ContainerType = EPinContainerType::Array; + } + + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Adding variable '%s' (type=%s, array=%s) to Blueprint '%s'"), + *VariableName, *VariableType, IsArray ? TEXT("true") : TEXT("false"), *Blueprint); + + // Add the variable using the editor utility function + bool bSuccess = FBlueprintEditorUtils::AddMemberVariable(BP, VarFName, PinType, DefaultValue); + if (!bSuccess) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf( + TEXT("FBlueprintEditorUtils::AddMemberVariable failed for '%s'"), *VariableName)); + } + + // Set category if provided + if (!Category.IsEmpty()) + { + FBlueprintEditorUtils::SetBlueprintVariableCategory(BP, VarFName, nullptr, FText::FromString(Category)); + } + + FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP); + bool bSaved = MCPUtils::SaveBlueprintPackage(BP); + + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Added variable '%s' to '%s' (saved: %s)"), + *VariableName, *Blueprint, bSaved ? TEXT("true") : TEXT("false")); + + Result->SetBoolField(TEXT("saved"), bSaved); + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/MCPHandlers_Dispatchers.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_AddEventDispatcher.h similarity index 68% rename from Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/MCPHandlers_Dispatchers.h rename to Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_AddEventDispatcher.h index e138fca6..44cb25cb 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/MCPHandlers_Dispatchers.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_AddEventDispatcher.h @@ -10,7 +10,8 @@ #include "K2Node_FunctionEntry.h" #include "K2Node_EditablePinBase.h" #include "Kismet2/BlueprintEditorUtils.h" -#include "MCPHandlers_Dispatchers.generated.h" +#include "UMCPHandler_AddEventDispatcher.generated.h" + // --------------------------------------------------------------------------- // --------------------------------------------------------------------------- @@ -156,75 +157,3 @@ public: Result->SetBoolField(TEXT("saved"), bSaved); } }; - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -UCLASS() -class UMCPHandler_ListEventDispatchers : public UObject, public IMCPHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Description="Blueprint name or package path")) - FString Blueprint; - - virtual FString GetDescription() const override - { - return TEXT("List all event dispatchers on a Blueprint, including their parameter signatures."); - } - - virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override - { - MCPAssets Assets; - if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; - UBlueprint* BP = Assets.Object(); - - TSet DelegateNameSet; - FBlueprintEditorUtils::GetDelegateNameList(BP, DelegateNameSet); - - TArray> DispatchersArr; - - for (const FName& DelegateName : DelegateNameSet) - { - TSharedRef DispObj = MakeShared(); - DispObj->SetStringField(TEXT("name"), DelegateName.ToString()); - - // Get parameter info from the signature graph - TArray> ParamsArr; - - UEdGraph* SigGraph = FBlueprintEditorUtils::GetDelegateSignatureGraphByName(BP, DelegateName); - if (SigGraph) - { - for (UK2Node_FunctionEntry* FE : MCPUtils::AllNodes(SigGraph)) - { - for (const TSharedPtr& PinInfo : FE->UserDefinedPins) - { - if (!PinInfo.IsValid()) continue; - - TSharedRef ParamObj = MakeShared(); - ParamObj->SetStringField(TEXT("name"), PinInfo->PinName.ToString()); - - // Build a human-readable type name from the pin type - FString TypeStr = PinInfo->PinType.PinCategory.ToString(); - if (PinInfo->PinType.PinSubCategoryObject.IsValid()) - { - TypeStr = PinInfo->PinType.PinSubCategoryObject->GetName(); - } - ParamObj->SetStringField(TEXT("type"), TypeStr); - - ParamsArr.Add(MakeShared(ParamObj)); - } - break; // only need the first entry node - } - } - - DispObj->SetArrayField(TEXT("parameters"), ParamsArr); - DispatchersArr.Add(MakeShared(DispObj)); - } - - Result->SetNumberField(TEXT("count"), DispatchersArr.Num()); - Result->SetArrayField(TEXT("dispatchers"), DispatchersArr); - } -}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_AddFunctionParameter.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_AddFunctionParameter.h new file mode 100644 index 00000000..56845e2b --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_AddFunctionParameter.h @@ -0,0 +1,161 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPAssetFinder.h" +#include "MCPUtils.h" +#include "Engine/Blueprint.h" +#include "EdGraph/EdGraph.h" +#include "EdGraph/EdGraphPin.h" +#include "K2Node_FunctionEntry.h" +#include "K2Node_CustomEvent.h" +#include "K2Node_EditablePinBase.h" +#include "Kismet2/BlueprintEditorUtils.h" +#include "UMCPHandler_AddFunctionParameter.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +UCLASS() +class UMCPHandler_AddFunctionParameter : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Blueprint name or package path")) + FString Blueprint; + + UPROPERTY(meta=(Description="Name of the function, custom event, or event dispatcher")) + FString FunctionName; + + UPROPERTY(meta=(Description="Name for the new parameter")) + FString ParamName; + + UPROPERTY(meta=(Description="Type for the new parameter (e.g. 'Float', 'Vector', 'MyStruct')")) + FString ParamType; + + virtual FString GetDescription() const override + { + return TEXT("Add a new parameter to a function, custom event, or event dispatcher in a Blueprint."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override + { + MCPAssets Assets; + if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; + UBlueprint* BP = Assets.Object(); + + // Resolve param type + FEdGraphPinType PinType; + if (!MCPUtils::ResolveTypeFromString(ParamType, PinType, Result)) + return; + + // Find the entry node using 3 strategies + UK2Node_EditablePinBase* EntryNode = nullptr; + FString NodeType; + + FName FuncFName(*FunctionName); + + // Strategy 1: K2Node_FunctionEntry in function graphs + for (UK2Node_FunctionEntry* FE : MCPUtils::AllNodes(BP)) + { + UEdGraph* FEGraph = FE->GetGraph(); + if (!FEGraph->GetName().Equals(FunctionName, ESearchCase::IgnoreCase)) continue; + // Skip delegate signature graphs (handled in Strategy 3) + if (BP->DelegateSignatureGraphs.Contains(FEGraph)) continue; + + EntryNode = FE; + NodeType = TEXT("FunctionEntry"); + break; + } + + // Strategy 2: K2Node_CustomEvent with matching CustomFunctionName + if (!EntryNode) + { + for (UK2Node_CustomEvent* CE : MCPUtils::AllNodes(BP)) + { + if (CE->CustomFunctionName.ToString().Equals(FunctionName, ESearchCase::IgnoreCase)) + { + EntryNode = CE; + NodeType = TEXT("CustomEvent"); + break; + } + } + } + + // Strategy 3: K2Node_FunctionEntry in DelegateSignatureGraphs + if (!EntryNode) + { + for (UK2Node_FunctionEntry* FE : MCPUtils::AllNodes(BP)) + { + UEdGraph* FEGraph = FE->GetGraph(); + if (!FEGraph->GetName().Equals(FunctionName, ESearchCase::IgnoreCase)) continue; + if (!BP->DelegateSignatureGraphs.Contains(FEGraph)) continue; + + EntryNode = FE; + NodeType = TEXT("EventDispatcher"); + break; + } + } + + if (!EntryNode) + { + // Build a helpful error listing available functions, events, and dispatchers + TArray> AvailFuncs; + + for (UEdGraph* Graph : BP->FunctionGraphs) + { + if (Graph) AvailFuncs.Add(MakeShared(Graph->GetName())); + } + + // Custom events + for (UK2Node_CustomEvent* CE : MCPUtils::AllNodes(BP)) + { + AvailFuncs.Add(MakeShared( + FString::Printf(TEXT("%s (custom event)"), *CE->CustomFunctionName.ToString()))); + } + + // Dispatchers + TSet DelegateNames; + FBlueprintEditorUtils::GetDelegateNameList(BP, DelegateNames); + for (const FName& DN : DelegateNames) + { + AvailFuncs.Add(MakeShared( + FString::Printf(TEXT("%s (event dispatcher)"), *DN.ToString()))); + } + + MCPUtils::MakeErrorJson(Result, FString::Printf( + TEXT("Function, custom event, or event dispatcher '%s' not found in Blueprint '%s'"), + *FunctionName, *Blueprint)); + Result->SetArrayField(TEXT("availableFunctions"), AvailFuncs); + return; + } + + // Check for duplicate parameter name + for (const TSharedPtr& Existing : EntryNode->UserDefinedPins) + { + if (Existing.IsValid() && Existing->PinName.ToString().Equals(ParamName, ESearchCase::IgnoreCase)) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf( + TEXT("Parameter '%s' already exists on '%s'"), *ParamName, *FunctionName)); + } + } + + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Adding parameter '%s' (type=%s) to %s '%s' in Blueprint '%s'"), + *ParamName, *ParamType, *NodeType, *FunctionName, *Blueprint); + + // Add the parameter pin (EGPD_Output on entry = input to callers) + EntryNode->CreateUserDefinedPin(FName(*ParamName), PinType, EGPD_Output); + + FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP); + bool bSaved = MCPUtils::SaveBlueprintPackage(BP); + + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Added parameter '%s' to '%s' in '%s' (saved: %s)"), + *ParamName, *FunctionName, *Blueprint, bSaved ? TEXT("true") : TEXT("false")); + + Result->SetStringField(TEXT("nodeType"), NodeType); + Result->SetBoolField(TEXT("saved"), bSaved); + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_AddMaterialExpression.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_AddMaterialExpression.h new file mode 100644 index 00000000..ec6cd2fa --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_AddMaterialExpression.h @@ -0,0 +1,239 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPAssetFinder.h" +#include "MCPUtils.h" +#include "Materials/Material.h" +#include "MaterialDomain.h" +#include "Materials/MaterialInstanceConstant.h" +#include "Materials/MaterialFunction.h" +#include "Materials/MaterialExpression.h" +#include "Materials/MaterialExpressionScalarParameter.h" +#include "Materials/MaterialExpressionVectorParameter.h" +#include "Materials/MaterialExpressionTextureObjectParameter.h" +#include "Materials/MaterialExpressionTextureSampleParameter2D.h" +#include "Materials/MaterialExpressionStaticSwitchParameter.h" +#include "Materials/MaterialExpressionConstant.h" +#include "Materials/MaterialExpressionConstant3Vector.h" +#include "Materials/MaterialExpressionConstant4Vector.h" +#include "Materials/MaterialExpressionTextureSample.h" +#include "Materials/MaterialExpressionTextureCoordinate.h" +#include "Materials/MaterialExpressionComponentMask.h" +#include "Materials/MaterialExpressionCustom.h" +#include "Materials/MaterialExpressionFunctionInput.h" +#include "Materials/MaterialExpressionFunctionOutput.h" +#include "Materials/MaterialExpressionMaterialFunctionCall.h" +#include "MaterialGraph/MaterialGraph.h" +#include "MaterialGraph/MaterialGraphNode.h" +#include "MaterialGraph/MaterialGraphSchema.h" +#include "Factories/MaterialFactoryNew.h" +#include "Factories/MaterialFunctionFactoryNew.h" +#include "AssetToolsModule.h" +#include "IAssetTools.h" +#include "AssetRegistry/AssetRegistryModule.h" +#include "EdGraph/EdGraph.h" +#include "EdGraph/EdGraphNode.h" +#include "Serialization/JsonReader.h" +#include "Serialization/JsonWriter.h" +#include "Serialization/JsonSerializer.h" +#include "Misc/Guid.h" +#include "Misc/FileHelper.h" +#include "Misc/Paths.h" +#include "UObject/SavePackage.h" +#include "UObject/UObjectIterator.h" +#include "Kismet2/BlueprintEditorUtils.h" +#include "UMCPHandler_AddMaterialExpression.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +UCLASS() +class UMCPHandler_AddMaterialExpression : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Optional, Description="Material name or package path (specify this or materialFunction, not both)")) + FString Material; + + UPROPERTY(meta=(Optional, Description="Material function name or package path (specify this or material, not both)")) + FString MaterialFunction; + + UPROPERTY(meta=(Description="Expression class name without 'MaterialExpression' prefix (e.g. 'Constant', 'ScalarParameter', 'Add', 'Multiply', 'Lerp')")) + FString ExpressionClass; + + UPROPERTY(meta=(Optional, Description="X position in the material graph editor")) + int32 PosX = 0; + + UPROPERTY(meta=(Optional, Description="Y position in the material graph editor")) + int32 PosY = 0; + + UPROPERTY(meta=(Optional, Description="If true, preview the change without applying it")) + bool DryRun = false; + + virtual FString GetDescription() const override + { + return TEXT("Add a new expression node to a material or material function graph."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override + { + if (Material.IsEmpty() && MaterialFunction.IsEmpty()) + { + return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: 'material' or 'materialFunction'")); + } + + // Map string class name to UClass via dynamic lookup + UClass* ExprClass = nullptr; + + // Convenience aliases for backward compatibility + static TMap Aliases = { + {TEXT("Lerp"), TEXT("LinearInterpolate")}, + }; + + FString LookupName = ExpressionClass; + if (const FString* Alias = Aliases.Find(ExpressionClass)) + { + LookupName = *Alias; + } + + // Dynamic lookup: find UMaterialExpression via UClass iteration + FString FullClassName = FString::Printf(TEXT("MaterialExpression%s"), *LookupName); + for (TObjectIterator It; It; ++It) + { + if (It->GetName() == FullClassName && It->IsChildOf(UMaterialExpression::StaticClass())) + { + ExprClass = *It; + break; + } + } + + if (!ExprClass) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf( + TEXT("Unknown expression class '%s'. Use the UMaterialExpression subclass name without the 'MaterialExpression' prefix " + "(e.g. 'Constant', 'ScalarParameter', 'Add', 'Multiply', 'Lerp', 'Subtract', 'Fresnel', 'Comment', etc.)"), + *ExpressionClass)); + } + if (ExprClass->HasAnyClassFlags(CLASS_Abstract)) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf( + TEXT("Expression class '%s' is abstract and cannot be instantiated."), *ExpressionClass)); + } + + // Load material or material function + UMaterial* MaterialObj = nullptr; + UMaterialFunction* MatFunc = nullptr; + UObject* Owner = nullptr; + FString AssetDisplayName; + + if (!MaterialFunction.IsEmpty()) + { + if (!Material.IsEmpty()) + { + return MCPUtils::MakeErrorJson(Result, TEXT("Specify either 'material' or 'materialFunction', not both")); + } + MCPAssets MFAssets; + if (!MFAssets.Exact(MaterialFunction).Errors(Result).ENone().ETwo().Load()) return; + MatFunc = MFAssets.Object(); + Owner = MatFunc; + AssetDisplayName = MatFunc->GetName(); + } + else + { + MCPAssets MatAssets; + if (!MatAssets.Exact(Material).Errors(Result).ENone().ETwo().Load()) return; + MaterialObj = MatAssets.Object(); + Owner = MaterialObj; + AssetDisplayName = MaterialObj->GetName(); + } + + if (DryRun) + { + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: [DRY RUN] Would add expression '%s' to '%s' at (%d, %d)"), + *ExpressionClass, *AssetDisplayName, PosX, PosY); + + Result->SetBoolField(TEXT("dryRun"), true); + Result->SetStringField(TEXT("material"), AssetDisplayName); + return; + } + + // Ensure the MaterialGraph exists (commandlet mode doesn't auto-create it) + if (MaterialObj) MCPUtils::EnsureMaterialGraph(MaterialObj); + + // Create, register, and PostEditChange the expression — all inside an SEH wrapper because + // some classes (e.g. UMaterialExpressionParameter) lack CLASS_Abstract but crash during + // PostEditChange. The SEH wrapper cleans up the bad expression on crash. + UMaterialExpression* NewExpr = nullptr; +#if PLATFORM_WINDOWS + int32 CreateResult = TryAddMaterialExpressionSEH(Owner, ExprClass, MaterialObj, MatFunc, PosX, PosY, &NewExpr); + if (CreateResult != 0 || !NewExpr) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf( + TEXT("Expression class '%s' cannot be instantiated (may be abstract or have internal errors)."), + *ExpressionClass)); + } +#else + NewExpr = NewObject(Owner, ExprClass); + if (!NewExpr) + { + return MCPUtils::MakeErrorJson(Result, TEXT("Failed to create material expression object")); + } + NewExpr->MaterialExpressionEditorX = PosX; + NewExpr->MaterialExpressionEditorY = PosY; + if (MaterialObj) + { + MaterialObj->GetExpressionCollection().AddExpression(NewExpr); + if (MaterialObj->MaterialGraph) + { + MaterialObj->MaterialGraph->RebuildGraph(); + } + MaterialObj->PreEditChange(nullptr); + MaterialObj->PostEditChange(); + MaterialObj->MarkPackageDirty(); + } + else if (MatFunc) + { + MatFunc->GetExpressionCollection().AddExpression(NewExpr); + MatFunc->PreEditChange(nullptr); + MatFunc->PostEditChange(); + MatFunc->MarkPackageDirty(); + } +#endif + + // Save + bool bSaved = MaterialObj ? MCPUtils::SaveMaterialPackage(MaterialObj) : MCPUtils::SaveGenericPackage(MatFunc); + + // Find the node GUID from the material graph (only for materials) + FString NodeGuid; + if (MaterialObj && MaterialObj->MaterialGraph) + { + for (UEdGraphNode* Node : MaterialObj->MaterialGraph->Nodes) + { + UMaterialGraphNode* MatNode = Cast(Node); + if (MatNode && MatNode->MaterialExpression == NewExpr) + { + NodeGuid = Node->NodeGuid.ToString(); + break; + } + } + } + + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Added expression '%s' to '%s' (nodeId: %s, saved: %s)"), + *ExpressionClass, *AssetDisplayName, *NodeGuid, bSaved ? TEXT("true") : TEXT("false")); + + // Serialize the expression details + TSharedPtr ExprDetails = MCPUtils::SerializeMaterialExpression(NewExpr); + + Result->SetStringField(TEXT("material"), AssetDisplayName); + Result->SetStringField(TEXT("nodeId"), NodeGuid); + if (ExprDetails.IsValid()) + { + Result->SetObjectField(TEXT("expression"), ExprDetails); + } + Result->SetBoolField(TEXT("saved"), bSaved); + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_AddStructField.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_AddStructField.h new file mode 100644 index 00000000..b7c3e413 --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_AddStructField.h @@ -0,0 +1,85 @@ +#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 "UMCPHandler_AddStructField.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +UCLASS() +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 + MCPAssets Assets; + if (!Assets.Exact(AssetPath).Errors(Result).ENone().ETwo().Load()) return; + UUserDefinedStruct* Struct = Assets.Object(); + + // Resolve type + FEdGraphPinType PinType; + if (!MCPUtils::ResolveTypeFromString(Type, PinType, Result)) + return; + + // 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("saved"), bSaved); + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_BackupAsset.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_BackupAsset.h new file mode 100644 index 00000000..6c53478a --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_BackupAsset.h @@ -0,0 +1,56 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPAssetFinder.h" +#include "MCPUtils.h" +#include "Engine/Blueprint.h" +#include "Kismet2/BlueprintEditorUtils.h" +#include "UObject/SavePackage.h" +#include "Misc/PackageName.h" +#include "AssetRegistry/AssetRegistryModule.h" +#include "AssetRegistry/IAssetRegistry.h" +#include "AssetToolsModule.h" +#include "IAssetTools.h" +#include "FileHelpers.h" +#include "UMCPHandler_BackupAsset.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +UCLASS() +class UMCPHandler_BackupAsset : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Full package path of the asset (e.g. /Game/Widgets/WB_Hotkeys)")) + FString AssetPath; + + virtual FString GetDescription() const override + { + return TEXT("Copy an asset's .uasset file to a .uasset.bak backup."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override + { + FString Filename = FPaths::ConvertRelativePathToFull( + FPackageName::LongPackageNameToFilename(AssetPath, FPackageName::GetAssetPackageExtension())); + + if (!IFileManager::Get().FileExists(*Filename)) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Asset file not found: %s"), *Filename)); + } + + FString BackupFilename = Filename + TEXT(".bak"); + uint32 CopyResult = IFileManager::Get().Copy(*BackupFilename, *Filename, true); + if (CopyResult != COPY_OK) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Failed to back up %s"), *Filename)); + } + + Result->SetStringField(TEXT("backupFile"), BackupFilename); + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_ChangeBlueprintVariableType.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_ChangeBlueprintVariableType.h new file mode 100644 index 00000000..a9f23daa --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_ChangeBlueprintVariableType.h @@ -0,0 +1,198 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPAssetFinder.h" +#include "MCPUtils.h" +#include "Engine/Blueprint.h" +#include "EdGraph/EdGraph.h" +#include "EdGraph/EdGraphPin.h" +#include "K2Node_VariableGet.h" +#include "K2Node_VariableSet.h" +#include "Kismet2/BlueprintEditorUtils.h" +#include "UMCPHandler_ChangeBlueprintVariableType.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +UCLASS() +class UMCPHandler_ChangeBlueprintVariableType : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Blueprint name or package path")) + FString Blueprint; + + UPROPERTY(meta=(Description="Name of the variable to change")) + FString Variable; + + UPROPERTY(meta=(Description="New type name for the variable")) + FString NewType; + + UPROPERTY(meta=(Optional, Description="Type category: object, softobject, class, softclass, interface, struct, enum")) + FString TypeCategory; + + UPROPERTY(meta=(Optional, Description="If true, analyze the change without applying it")) + bool DryRun = false; + + virtual FString GetDescription() const override + { + return TEXT("Change the type of a Blueprint member variable. " + "Supports dry-run mode to preview affected nodes before committing."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override + { + MCPAssets Assets; + if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; + UBlueprint* BP = Assets.Object(); + + // Verify variable exists + bool bVarFound = false; + for (const FBPVariableDescription& Var : BP->NewVariables) + { + if (Var.VarName.ToString() == Variable) + { + bVarFound = true; + break; + } + } + if (!bVarFound) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Variable '%s' not found in Blueprint '%s'"), *Variable, *Blueprint)); + } + + // Build the new pin type using shared resolver + FEdGraphPinType NewPinType; + FString ResolveInput = NewType; + + // If typeCategory is an object reference variant, use colon syntax for the resolver + if (TypeCategory == TEXT("object") || TypeCategory == TEXT("softobject") || + TypeCategory == TEXT("class") || TypeCategory == TEXT("softclass") || + TypeCategory == TEXT("interface")) + { + ResolveInput = TypeCategory + TEXT(":") + NewType; + } + + if (!MCPUtils::ResolveTypeFromString(ResolveInput, NewPinType, Result)) + return; + + // Derive typeCategory from the resolved pin type for the response + FString ResolvedTypeCategory = TypeCategory; + if (ResolvedTypeCategory.IsEmpty()) + { + if (NewPinType.PinCategory == UEdGraphSchema_K2::PC_Struct) + ResolvedTypeCategory = TEXT("struct"); + else if (NewPinType.PinCategory == UEdGraphSchema_K2::PC_Enum || NewPinType.PinCategory == UEdGraphSchema_K2::PC_Byte) + ResolvedTypeCategory = TEXT("enum"); + else if (NewPinType.PinCategory == UEdGraphSchema_K2::PC_Object) + ResolvedTypeCategory = TEXT("object"); + else if (NewPinType.PinCategory == UEdGraphSchema_K2::PC_SoftObject) + ResolvedTypeCategory = TEXT("softobject"); + else if (NewPinType.PinCategory == UEdGraphSchema_K2::PC_Class) + ResolvedTypeCategory = TEXT("class"); + else if (NewPinType.PinCategory == UEdGraphSchema_K2::PC_SoftClass) + ResolvedTypeCategory = TEXT("softclass"); + else if (NewPinType.PinCategory == UEdGraphSchema_K2::PC_Interface) + ResolvedTypeCategory = TEXT("interface"); + else + ResolvedTypeCategory = NewPinType.PinCategory.ToString(); + } + + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: %s variable '%s' in '%s' to %s (%s)"), + DryRun ? TEXT("[DRY RUN] Analyzing change of") : TEXT("Changing"), + *Variable, *Blueprint, *NewType, *ResolvedTypeCategory); + + // Analyze affected nodes (get/set nodes for this variable) + TArray> AffectedNodes; + for (UK2Node_VariableGet* VG : MCPUtils::AllNodes(BP)) + { + if (VG->GetVarName().ToString() != Variable) continue; + TSharedRef AffNode = MakeShared(); + AffNode->SetStringField(TEXT("nodeId"), VG->NodeGuid.ToString()); + AffNode->SetStringField(TEXT("nodeType"), TEXT("VariableGet")); + AffNode->SetStringField(TEXT("graph"), VG->GetGraph()->GetName()); + TArray> AffPins; + for (UEdGraphPin* Pin : VG->Pins) + { + if (Pin && (Pin->LinkedTo.Num() > 0) && (Pin->Direction == EGPD_Output)) + { + AffPins.Add(MakeShared( + FString::Printf(TEXT("%s (connected to %d pin(s))"), + *Pin->PinName.ToString(), Pin->LinkedTo.Num()))); + } + } + AffNode->SetArrayField(TEXT("affectedPins"), AffPins); + AffectedNodes.Add(MakeShared(AffNode)); + } + for (UK2Node_VariableSet* VS : MCPUtils::AllNodes(BP)) + { + if (VS->GetVarName().ToString() != Variable) continue; + TSharedRef AffNode = MakeShared(); + AffNode->SetStringField(TEXT("nodeId"), VS->NodeGuid.ToString()); + AffNode->SetStringField(TEXT("nodeType"), TEXT("VariableSet")); + AffNode->SetStringField(TEXT("graph"), VS->GetGraph()->GetName()); + TArray> AffPins; + for (UEdGraphPin* Pin : VS->Pins) + { + if (Pin && Pin->LinkedTo.Num() > 0) + { + AffPins.Add(MakeShared( + FString::Printf(TEXT("%s (connected to %d pin(s))"), + *Pin->PinName.ToString(), Pin->LinkedTo.Num()))); + } + } + AffNode->SetArrayField(TEXT("affectedPins"), AffPins); + AffectedNodes.Add(MakeShared(AffNode)); + } + + if (DryRun) + { + Result->SetBoolField(TEXT("dryRun"), true); + Result->SetStringField(TEXT("typeCategory"), ResolvedTypeCategory); + Result->SetNumberField(TEXT("affectedNodeCount"), AffectedNodes.Num()); + Result->SetArrayField(TEXT("affectedNodes"), AffectedNodes); + return; + } + + // Directly modify the variable type in the description array. + BP->PreEditChange(nullptr); + for (FBPVariableDescription& Var : BP->NewVariables) + { + if (Var.VarName == FName(*Variable)) + { + Var.VarType = NewPinType; + break; + } + } + BP->PostEditChange(); + + // Save + bool bSaved = MCPUtils::SaveBlueprintPackage(BP); + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Variable type changed, save %s"), + bSaved ? TEXT("succeeded") : TEXT("failed")); + + // Return updated variable state + TSharedRef UpdatedVar = MakeShared(); + for (const FBPVariableDescription& Var : BP->NewVariables) + { + if (Var.VarName == FName(*Variable)) + { + UpdatedVar->SetStringField(TEXT("name"), Var.VarName.ToString()); + UpdatedVar->SetStringField(TEXT("type"), Var.VarType.PinCategory.ToString()); + if (Var.VarType.PinSubCategoryObject.IsValid()) + UpdatedVar->SetStringField(TEXT("subtype"), Var.VarType.PinSubCategoryObject->GetName()); + UpdatedVar->SetBoolField(TEXT("isArray"), Var.VarType.IsArray()); + break; + } + } + + Result->SetStringField(TEXT("typeCategory"), ResolvedTypeCategory); + Result->SetBoolField(TEXT("saved"), bSaved); + Result->SetObjectField(TEXT("updatedVariable"), UpdatedVar); + Result->SetArrayField(TEXT("affectedNodes"), AffectedNodes); + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_ChangeFunctionParameterType.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_ChangeFunctionParameterType.h new file mode 100644 index 00000000..7203791d --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_ChangeFunctionParameterType.h @@ -0,0 +1,206 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPAssetFinder.h" +#include "MCPUtils.h" +#include "Engine/Blueprint.h" +#include "EdGraph/EdGraph.h" +#include "EdGraph/EdGraphPin.h" +#include "K2Node_FunctionEntry.h" +#include "K2Node_CustomEvent.h" +#include "K2Node_EditablePinBase.h" +#include "Kismet2/BlueprintEditorUtils.h" +#include "UMCPHandler_ChangeFunctionParameterType.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +UCLASS() +class UMCPHandler_ChangeFunctionParameterType : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Blueprint name or package path")) + FString Blueprint; + + UPROPERTY(meta=(Description="Name of the function or custom event")) + FString FunctionName; + + UPROPERTY(meta=(Description="Name of the parameter to change")) + FString ParamName; + + UPROPERTY(meta=(Description="New type for the parameter (e.g. 'Float', 'Vector', 'MyStruct')")) + FString NewType; + + UPROPERTY(meta=(Optional, Description="If true, analyze impact without making changes")) + bool DryRun = false; + + virtual FString GetDescription() const override + { + return TEXT("Change the type of an existing parameter on a function or custom event in a Blueprint."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override + { + MCPAssets Assets; + if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; + UBlueprint* BP = Assets.Object(); + + // Resolve the new type using the shared resolver (supports primitives, structs, enums, and object references) + FEdGraphPinType NewPinType; + if (!MCPUtils::ResolveTypeFromString(NewType, NewPinType, Result)) + return; + + // Find the entry node: K2Node_FunctionEntry in a function graph, + // or K2Node_CustomEvent in any graph + UK2Node_EditablePinBase* EntryNode = nullptr; + FString FoundNodeType; + + // Strategy 1: Look for a K2Node_FunctionEntry in a function graph matching the name + for (UK2Node_FunctionEntry* FuncEntry : MCPUtils::AllNodes(BP)) + { + if (FuncEntry->GetGraph()->GetName().Equals(FunctionName, ESearchCase::IgnoreCase)) + { + EntryNode = FuncEntry; + FoundNodeType = TEXT("FunctionEntry"); + break; + } + } + + // Strategy 2: Search for a K2Node_CustomEvent with matching CustomFunctionName + if (!EntryNode) + { + for (UK2Node_CustomEvent* CustomEvent : MCPUtils::AllNodes(BP)) + { + if (CustomEvent->CustomFunctionName.ToString().Equals(FunctionName, ESearchCase::IgnoreCase)) + { + EntryNode = CustomEvent; + FoundNodeType = TEXT("CustomEvent"); + break; + } + } + } + + if (!EntryNode) + { + // List available functions/events for debugging + TArray> Available; + for (UK2Node_FunctionEntry* FE : MCPUtils::AllNodes(BP)) + { + Available.Add(MakeShared( + FString::Printf(TEXT("function:%s"), *FE->GetGraph()->GetName()))); + } + for (UK2Node_CustomEvent* CE : MCPUtils::AllNodes(BP)) + { + Available.Add(MakeShared( + FString::Printf(TEXT("event:%s"), *CE->CustomFunctionName.ToString()))); + } + + MCPUtils::MakeErrorJson(Result, FString::Printf( + TEXT("Function or custom event '%s' not found in Blueprint '%s'"), + *FunctionName, *Blueprint)); + Result->SetArrayField(TEXT("availableFunctionsAndEvents"), Available); + return; + } + + // Find the UserDefinedPin matching paramName + bool bPinFound = false; + for (TSharedPtr& PinInfo : EntryNode->UserDefinedPins) + { + if (PinInfo.IsValid() && PinInfo->PinName.ToString().Equals(ParamName, ESearchCase::IgnoreCase)) + { + EntryNode->PreEditChange(nullptr); + PinInfo->PinType = NewPinType; + EntryNode->PostEditChange(); + bPinFound = true; + break; + } + } + + if (!bPinFound) + { + // List available params for debugging + TArray> ParamNames; + for (const TSharedPtr& PinInfo : EntryNode->UserDefinedPins) + { + if (PinInfo.IsValid()) + { + ParamNames.Add(MakeShared(PinInfo->PinName.ToString())); + } + } + + MCPUtils::MakeErrorJson(Result, FString::Printf( + TEXT("Parameter '%s' not found in %s '%s'"), + *ParamName, *FoundNodeType, *FunctionName)); + Result->SetArrayField(TEXT("availableParams"), ParamNames); + return; + } + + // Check for dry run + if (DryRun) + { + // Analyze what would change: report connected pins that may disconnect + TArray> AffectedPins; + for (UEdGraphPin* Pin : EntryNode->Pins) + { + if (Pin && Pin->PinName.ToString().Equals(ParamName, ESearchCase::IgnoreCase) && Pin->LinkedTo.Num() > 0) + { + for (UEdGraphPin* Linked : Pin->LinkedTo) + { + if (Linked && Linked->GetOwningNode()) + { + TSharedRef AffPin = MakeShared(); + AffPin->SetStringField(TEXT("pinName"), Pin->PinName.ToString()); + AffPin->SetStringField(TEXT("connectedToNode"), Linked->GetOwningNode()->NodeGuid.ToString()); + AffPin->SetStringField(TEXT("connectedToPin"), Linked->PinName.ToString()); + AffPin->SetStringField(TEXT("currentType"), Pin->PinType.PinCategory.ToString()); + if (Pin->PinType.PinSubCategoryObject.IsValid()) + AffPin->SetStringField(TEXT("currentSubtype"), Pin->PinType.PinSubCategoryObject->GetName()); + AffectedPins.Add(MakeShared(AffPin)); + } + } + } + } + + Result->SetBoolField(TEXT("dryRun"), true); + Result->SetStringField(TEXT("nodeType"), FoundNodeType); + Result->SetStringField(TEXT("nodeId"), EntryNode->NodeGuid.ToString()); + Result->SetNumberField(TEXT("connectionsAtRisk"), AffectedPins.Num()); + Result->SetArrayField(TEXT("affectedPins"), AffectedPins); + return; + } + + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Changing param '%s' in %s '%s' of '%s' to %s"), + *ParamName, *FoundNodeType, *FunctionName, *Blueprint, *NewType); + + // Reconstruct the node to update output pins with the new type (use schema for MinimalAPI compat) + if (UEdGraph* OwningGraph = EntryNode->GetGraph()) + { + if (const UEdGraphSchema* Schema = OwningGraph->GetSchema()) + { + Schema->ReconstructNode(*EntryNode); + } + } + + // Save + bool bSaved = MCPUtils::SaveBlueprintPackage(BP); + + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Parameter type changed, save %s"), + bSaved ? TEXT("succeeded") : TEXT("failed")); + + // Serialize the updated entry node state + TSharedPtr UpdatedNodeState = MCPUtils::SerializeNode(EntryNode); + + Result->SetStringField(TEXT("nodeType"), FoundNodeType); + Result->SetStringField(TEXT("nodeId"), EntryNode->NodeGuid.ToString()); + Result->SetBoolField(TEXT("saved"), bSaved); + if (UpdatedNodeState.IsValid()) + { + Result->SetObjectField(TEXT("updatedNode"), UpdatedNodeState); + } + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_ChangeStructNodeType.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_ChangeStructNodeType.h new file mode 100644 index 00000000..fc9a5323 --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_ChangeStructNodeType.h @@ -0,0 +1,279 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPAssetFinder.h" +#include "MCPServer.h" +#include "MCPUtils.h" +#include "Engine/Blueprint.h" +#include "Materials/Material.h" +#include "Materials/MaterialInstanceConstant.h" +#include "Materials/MaterialFunction.h" +#include "Engine/World.h" +#include "Engine/LevelScriptBlueprint.h" +#include "EdGraph/EdGraph.h" +#include "EdGraph/EdGraphNode.h" +#include "EdGraph/EdGraphPin.h" +#include "EdGraphSchema_K2.h" +#include "K2Node.h" +#include "K2Node_CallFunction.h" +#include "K2Node_Event.h" +#include "K2Node_CustomEvent.h" +#include "K2Node_FunctionEntry.h" +#include "K2Node_EditablePinBase.h" +#include "K2Node_VariableGet.h" +#include "K2Node_VariableSet.h" +#include "K2Node_BreakStruct.h" +#include "K2Node_MakeStruct.h" +#include "K2Node_DynamicCast.h" +#include "K2Node_CallParentFunction.h" +#include "K2Node_IfThenElse.h" +#include "K2Node_ExecutionSequence.h" +#include "K2Node_MacroInstance.h" +#include "K2Node_SpawnActorFromClass.h" +#include "K2Node_Select.h" +#include "K2Node_Knot.h" +#include "EdGraphNode_Comment.h" +#include "GameFramework/Actor.h" +#include "Kismet2/BlueprintEditorUtils.h" +#include "Kismet2/KismetEditorUtilities.h" +#include "Serialization/JsonReader.h" +#include "Serialization/JsonWriter.h" +#include "Serialization/JsonSerializer.h" +#include "UObject/SavePackage.h" +#include "UObject/UObjectIterator.h" +#include "Misc/PackageName.h" +#include "AssetRegistry/AssetRegistryModule.h" +#include "AssetRegistry/IAssetRegistry.h" +#include "AssetToolsModule.h" +#include "IAssetTools.h" +#include "BlueprintNodeSpawner.h" +#include "UMCPHandler_ChangeStructNodeType.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +UCLASS() +class UMCPHandler_ChangeStructNodeType : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Blueprint name or package path")) + FString Blueprint; + + UPROPERTY(meta=(Description="Node GUID of the BreakStruct or MakeStruct node")) + FString Node; + + UPROPERTY(meta=(Description="New struct type name (e.g. 'FVector', 'Vector')")) + FString NewType; + + virtual FString GetDescription() const override + { + return TEXT("Change the struct type on a BreakStruct or MakeStruct node. " + "Attempts to reconnect matching pins after the type change."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override + { + + // Load Blueprint + MCPAssets Assets; + if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; + UBlueprint* BP = Assets.Object(); + + // Find node + UEdGraph* Graph = nullptr; + UEdGraphNode* FoundNode = MCPUtils::FindNodeByGuid(BP, Node, &Graph); + if (!FoundNode) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found"), *Node)); + } + + // Determine what kind of struct node this is + UK2Node_BreakStruct* BreakNode = Cast(FoundNode); + UK2Node_MakeStruct* MakeNode = Cast(FoundNode); + + if (!BreakNode && !MakeNode) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' is not a BreakStruct or MakeStruct node (class: %s)"), + *Node, *FoundNode->GetClass()->GetName())); + } + + // Find the new struct type + FString SearchName = NewType; + if (SearchName.StartsWith(TEXT("F"))) + { + SearchName = SearchName.Mid(1); + } + + UScriptStruct* NewStruct = FindFirstObject(*SearchName); + if (!NewStruct) + { + // Try with full name including F prefix + NewStruct = FindFirstObject(*NewType); + } + if (!NewStruct) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Struct type '%s' not found"), *NewType)); + } + + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Changing struct node '%s' to type '%s'"), + *Node, *NewStruct->GetName()); + + // Helper: extract property base name from a BreakStruct pin name + auto ExtractPropertyBaseName = [](const FString& PinName) -> FString + { + // Find the last underscore before 32 hex chars (GUID) + int32 LastUnderscore; + if (PinName.FindLastChar(TEXT('_'), LastUnderscore) && (LastUnderscore > 0)) + { + FString Suffix = PinName.Mid(LastUnderscore + 1); + if (Suffix.Len() == 32) + { + FString WithoutGuid = PinName.Left(LastUnderscore); + // Strip _Index + int32 SecondUnderscore; + if (WithoutGuid.FindLastChar(TEXT('_'), SecondUnderscore) && (SecondUnderscore > 0)) + { + FString IndexStr = WithoutGuid.Mid(SecondUnderscore + 1); + if (IndexStr.IsNumeric()) + { + return WithoutGuid.Left(SecondUnderscore); + } + } + } + } + return PinName; + }; + + // Remember existing connections keyed by property base name + struct FPinConnection + { + EEdGraphPinDirection Direction; + TArray LinkedPins; + }; + TMap ConnectionsByBaseName; + + for (UEdGraphPin* Pin : FoundNode->Pins) + { + if (!Pin || Pin->LinkedTo.Num() == 0) continue; + if (Pin->PinType.PinCategory == UEdGraphSchema_K2::PC_Exec) continue; + + FString BaseName = ExtractPropertyBaseName(Pin->PinName.ToString()); + FPinConnection& Conn = ConnectionsByBaseName.FindOrAdd(BaseName); + Conn.Direction = Pin->Direction; + Conn.LinkedPins = Pin->LinkedTo; + } + + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Saved %d pin connections to reconnect"), ConnectionsByBaseName.Num()); + + // Change the struct type and reconstruct + if (BreakNode) + { + BreakNode->StructType = NewStruct; + } + else if (MakeNode) + { + MakeNode->StructType = NewStruct; + } + + // Break all existing links before reconstruction + FoundNode->BreakAllNodeLinks(); + + // Reconnect pins by matching property base names + const UEdGraphSchema* Schema = Graph->GetSchema(); + if (!Schema) + { + return MCPUtils::MakeErrorJson(Result, TEXT("Graph schema not found")); + } + + // Reconstruct to rebuild pins for the new struct type (use schema version for MinimalAPI compat) + Schema->ReconstructNode(*FoundNode); + + int32 Reconnected = 0; + int32 Failed = 0; + TArray> ReconnectDetails; + + for (auto& Pair : ConnectionsByBaseName) + { + const FString& BaseName = Pair.Key; + const FPinConnection& OldConn = Pair.Value; + + // Find matching new pin + UEdGraphPin* NewPin = nullptr; + for (UEdGraphPin* Pin : FoundNode->Pins) + { + if (!Pin || Pin->Direction != OldConn.Direction) continue; + FString NewBaseName = ExtractPropertyBaseName(Pin->PinName.ToString()); + if (NewBaseName.Equals(BaseName, ESearchCase::IgnoreCase)) + { + NewPin = Pin; + break; + } + } + + // Also try matching the struct input/output pin (single struct pin) + if (!NewPin) + { + for (UEdGraphPin* Pin : FoundNode->Pins) + { + if (!Pin || Pin->Direction != OldConn.Direction) continue; + if ((Pin->PinType.PinCategory == UEdGraphSchema_K2::PC_Struct) && + (Pin->PinType.PinSubCategoryObject == NewStruct)) + { + NewPin = Pin; + break; + } + } + } + + if (NewPin) + { + for (UEdGraphPin* Target : OldConn.LinkedPins) + { + bool bOK = Schema->TryCreateConnection(NewPin, Target); + if (bOK) + { + Reconnected++; + } + else + { + Failed++; + } + + TSharedPtr Detail = MakeShared(); + Detail->SetStringField(TEXT("property"), BaseName); + Detail->SetBoolField(TEXT("connected"), bOK); + ReconnectDetails.Add(MakeShared(Detail)); + } + } + else + { + Failed += OldConn.LinkedPins.Num(); + TSharedPtr Detail = MakeShared(); + Detail->SetStringField(TEXT("property"), BaseName); + Detail->SetBoolField(TEXT("connected"), false); + Detail->SetStringField(TEXT("reason"), TEXT("No matching pin found on new struct")); + ReconnectDetails.Add(MakeShared(Detail)); + } + } + + FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP); + + // Return updated node state + TSharedPtr UpdatedNodeState = MCPUtils::SerializeNode(FoundNode); + + Result->SetStringField(TEXT("newStructType"), NewStruct->GetName()); + Result->SetStringField(TEXT("nodeClass"), FoundNode->GetClass()->GetName()); + Result->SetNumberField(TEXT("reconnected"), Reconnected); + Result->SetNumberField(TEXT("failed"), Failed); + Result->SetArrayField(TEXT("reconnectDetails"), ReconnectDetails); + if (UpdatedNodeState.IsValid()) + { + Result->SetObjectField(TEXT("updatedNode"), UpdatedNodeState); + } + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_CheckPinConnectionCompatibility.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_CheckPinConnectionCompatibility.h new file mode 100644 index 00000000..b984fc8b --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_CheckPinConnectionCompatibility.h @@ -0,0 +1,136 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPAssetFinder.h" +#include "MCPUtils.h" +#include "Engine/Blueprint.h" +#include "EdGraph/EdGraph.h" +#include "EdGraph/EdGraphNode.h" +#include "EdGraph/EdGraphPin.h" +#include "EdGraphSchema_K2.h" +#include "UObject/UObjectIterator.h" +#include "UMCPHandler_CheckPinConnectionCompatibility.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +// ============================================================ +// HandleCheckPinCompatibility — pre-flight check for connect_pins +// ============================================================ + +UCLASS() +class UMCPHandler_CheckPinConnectionCompatibility : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Blueprint name or package path")) + FString Blueprint; + + UPROPERTY(meta=(Description="Source node (GUID)")) + FString SourceNode; + + UPROPERTY(meta=(Description="Source pin name")) + FString SourcePinName; + + UPROPERTY(meta=(Description="Target node (GUID)")) + FString TargetNode; + + UPROPERTY(meta=(Description="Target pin name")) + FString TargetPinName; + + virtual FString GetDescription() const override + { + return TEXT("Check whether two pins can be connected, and what kind of connection would result. " + "Use as a pre-flight check before connect_blueprint_pins."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override + { + MCPAssets Assets; + if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; + UBlueprint* BP = Assets.Object(); + + UEdGraph* SourceGraph = nullptr; + UEdGraphNode* FoundSourceNode = MCPUtils::FindNodeByGuid(BP, SourceNode, &SourceGraph); + if (!FoundSourceNode) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Source node '%s' not found"), *SourceNode)); + } + + UEdGraphNode* FoundTargetNode = MCPUtils::FindNodeByGuid(BP, TargetNode); + if (!FoundTargetNode) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Target node '%s' not found"), *TargetNode)); + } + + UEdGraphPin* SourcePin = FoundSourceNode->FindPin(FName(*SourcePinName)); + if (!SourcePin) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Source pin '%s' not found on node '%s'"), *SourcePinName, *SourceNode)); + } + + UEdGraphPin* TargetPin = FoundTargetNode->FindPin(FName(*TargetPinName)); + if (!TargetPin) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Target pin '%s' not found on node '%s'"), *TargetPinName, *TargetNode)); + } + + const UEdGraphSchema* Schema = SourceGraph ? SourceGraph->GetSchema() : nullptr; + if (!Schema) + { + return MCPUtils::MakeErrorJson(Result, TEXT("Graph schema not found")); + } + + // Check compatibility using the schema + const FPinConnectionResponse Response = Schema->CanCreateConnection(SourcePin, TargetPin); + + bool bCompatible = (Response.Response != ECanCreateConnectionResponse::CONNECT_RESPONSE_DISALLOW); + Result->SetBoolField(TEXT("compatible"), bCompatible); + + // Decode the response type + FString ResponseType; + switch (Response.Response) + { + case ECanCreateConnectionResponse::CONNECT_RESPONSE_MAKE: + ResponseType = TEXT("direct"); + break; + case ECanCreateConnectionResponse::CONNECT_RESPONSE_BREAK_OTHERS_A: + ResponseType = TEXT("breakSourceConnections"); + break; + case ECanCreateConnectionResponse::CONNECT_RESPONSE_BREAK_OTHERS_B: + ResponseType = TEXT("breakTargetConnections"); + break; + case ECanCreateConnectionResponse::CONNECT_RESPONSE_BREAK_OTHERS_AB: + ResponseType = TEXT("breakBothConnections"); + break; + case ECanCreateConnectionResponse::CONNECT_RESPONSE_MAKE_WITH_CONVERSION_NODE: + ResponseType = TEXT("requiresConversion"); + break; + case ECanCreateConnectionResponse::CONNECT_RESPONSE_MAKE_WITH_PROMOTION: + ResponseType = TEXT("requiresPromotion"); + break; + case ECanCreateConnectionResponse::CONNECT_RESPONSE_DISALLOW: + default: + ResponseType = TEXT("disallowed"); + break; + } + Result->SetStringField(TEXT("connectionType"), ResponseType); + + if (!Response.Message.IsEmpty()) + { + Result->SetStringField(TEXT("message"), Response.Message.ToString()); + } + + // Include pin type info for context + Result->SetStringField(TEXT("sourcePinType"), SourcePin->PinType.PinCategory.ToString()); + if (SourcePin->PinType.PinSubCategoryObject.IsValid()) + Result->SetStringField(TEXT("sourcePinSubtype"), SourcePin->PinType.PinSubCategoryObject->GetName()); + Result->SetStringField(TEXT("targetPinType"), TargetPin->PinType.PinCategory.ToString()); + if (TargetPin->PinType.PinSubCategoryObject.IsValid()) + Result->SetStringField(TEXT("targetPinSubtype"), TargetPin->PinType.PinSubCategoryObject->GetName()); + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/MCPHandlers_Validation.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_CompileBlueprint.h similarity index 99% rename from Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/MCPHandlers_Validation.h rename to Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_CompileBlueprint.h index 9786241c..df8642db 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/MCPHandlers_Validation.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_CompileBlueprint.h @@ -9,7 +9,8 @@ #include "EdGraph/EdGraphNode.h" #include "Kismet2/BlueprintEditorUtils.h" #include "Kismet2/KismetEditorUtilities.h" -#include "MCPHandlers_Validation.generated.h" +#include "UMCPHandler_CompileBlueprint.generated.h" + // --------------------------------------------------------------------------- // --------------------------------------------------------------------------- diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_CompileMaterial.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_CompileMaterial.h new file mode 100644 index 00000000..2da04bf1 --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_CompileMaterial.h @@ -0,0 +1,116 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPAssetFinder.h" +#include "MCPUtils.h" +#include "Materials/Material.h" +#include "MaterialDomain.h" +#include "Materials/MaterialInstanceConstant.h" +#include "Materials/MaterialFunction.h" +#include "Materials/MaterialExpression.h" +#include "Materials/MaterialExpressionScalarParameter.h" +#include "Materials/MaterialExpressionVectorParameter.h" +#include "Materials/MaterialExpressionTextureObjectParameter.h" +#include "Materials/MaterialExpressionTextureSampleParameter2D.h" +#include "Materials/MaterialExpressionStaticSwitchParameter.h" +#include "Materials/MaterialExpressionConstant.h" +#include "Materials/MaterialExpressionConstant3Vector.h" +#include "Materials/MaterialExpressionConstant4Vector.h" +#include "Materials/MaterialExpressionTextureSample.h" +#include "Materials/MaterialExpressionTextureCoordinate.h" +#include "Materials/MaterialExpressionComponentMask.h" +#include "Materials/MaterialExpressionCustom.h" +#include "Materials/MaterialExpressionFunctionInput.h" +#include "Materials/MaterialExpressionFunctionOutput.h" +#include "Materials/MaterialExpressionMaterialFunctionCall.h" +#include "MaterialGraph/MaterialGraph.h" +#include "MaterialGraph/MaterialGraphNode.h" +#include "MaterialGraph/MaterialGraphNode_Root.h" +#include "MaterialGraph/MaterialGraphSchema.h" +#include "Kismet2/BlueprintEditorUtils.h" +#include "AssetRegistry/AssetRegistryModule.h" +#include "AssetRegistry/IAssetRegistry.h" +#include "EdGraph/EdGraph.h" +#include "EdGraph/EdGraphNode.h" +#include "UMCPHandler_CompileMaterial.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +UCLASS() +class UMCPHandler_CompileMaterial : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Material name or package path")) + FString Material; + + virtual FString GetDescription() const override + { + return TEXT("Force recompile a material and check for compilation errors."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override + { + // Load material + MCPAssets Assets; + if (!Assets.Exact(Material).Errors(Result).ENone().ETwo().Load()) return; + UMaterial* MaterialObj = Assets.Object(); + + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Validating material '%s'"), *MaterialObj->GetName()); + + // Force recompile by triggering PreEditChange/PostEditChange + MaterialObj->PreEditChange(nullptr); + MaterialObj->PostEditChange(); + + // Collect compilation errors + TArray> ErrorArray; + bool bValid = true; + + // Check for compilation errors via FMaterialResource on current platform + FMaterialResource* Resource = MaterialObj->GetMaterialResource(GMaxRHIFeatureLevel); + if (Resource) + { + const TArray& CompileErrors = Resource->GetCompileErrors(); + for (const FString& Err : CompileErrors) + { + bValid = false; + ErrorArray.Add(MakeShared(Err)); + } + } + + // Count expressions and connections + auto Expressions = MaterialObj->GetExpressions(); + int32 ExprCount = Expressions.Num(); + int32 ConnectionCount = 0; + if (MaterialObj->MaterialGraph) + { + for (UEdGraphNode* Node : MaterialObj->MaterialGraph->Nodes) + { + if (!Node) continue; + for (UEdGraphPin* Pin : Node->Pins) + { + if (Pin && Pin->Direction == EGPD_Output) + { + ConnectionCount += Pin->LinkedTo.Num(); + } + } + } + } + + Result->SetBoolField(TEXT("valid"), bValid); + Result->SetStringField(TEXT("material"), MaterialObj->GetName()); + Result->SetStringField(TEXT("materialPath"), MaterialObj->GetPathName()); + Result->SetNumberField(TEXT("expressionCount"), ExprCount); + Result->SetNumberField(TEXT("connectionCount"), ConnectionCount); + Result->SetArrayField(TEXT("errors"), ErrorArray); + Result->SetNumberField(TEXT("errorCount"), ErrorArray.Num()); + + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Material '%s' validation %s (%d errors)"), + *MaterialObj->GetName(), bValid ? TEXT("passed") : TEXT("failed"), ErrorArray.Num()); + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_ConnectBlueprintPins.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_ConnectBlueprintPins.h new file mode 100644 index 00000000..e614f47b --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_ConnectBlueprintPins.h @@ -0,0 +1,134 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPAssetFinder.h" +#include "MCPUtils.h" +#include "Engine/Blueprint.h" +#include "Engine/World.h" +#include "EdGraph/EdGraph.h" +#include "EdGraph/EdGraphNode.h" +#include "EdGraph/EdGraphPin.h" +#include "Kismet2/BlueprintEditorUtils.h" +#include "UMCPHandler_ConnectBlueprintPins.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +USTRUCT() +struct FConnectPinsEntry +{ + GENERATED_BODY() + + UPROPERTY() + FString SourceNode; + + UPROPERTY() + FString SourcePinName; + + UPROPERTY() + FString TargetNode; + + UPROPERTY() + FString TargetPinName; +}; + + +UCLASS() +class UMCPHandler_ConnectBlueprintPins : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Blueprint name or package path")) + FString Blueprint; + + UPROPERTY(meta=(Description="Array of {sourceNode, sourcePinName, targetNode, targetPinName} objects")) + FMCPJsonArray Connections; + + virtual FString GetDescription() const override + { + return TEXT("Connect pins between nodes in a Blueprint graph."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override + { + + MCPAssets Assets; + if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; + UBlueprint* BP = Assets.Object(); + + TArray> Results; + int32 SuccessCount = 0; + + for (const TSharedPtr& ConnVal : Connections.Array) + { + TSharedRef EntryResult = MakeShared(); + Results.Add(MakeShared(EntryResult)); + + FConnectPinsEntry Entry; + if (!MCPUtils::PopulateFromJson(FConnectPinsEntry::StaticStruct(), &Entry, ConnVal, &*EntryResult)) continue; + + UEdGraph* SourceGraph = nullptr; + UEdGraphNode* SourceNode = MCPUtils::FindNodeByGuid(BP, Entry.SourceNode, &SourceGraph); + if (!SourceNode) + { + EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Source node '%s' not found"), *Entry.SourceNode)); + continue; + } + + UEdGraphNode* TargetNode = MCPUtils::FindNodeByGuid(BP, Entry.TargetNode); + if (!TargetNode) + { + EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Target node '%s' not found"), *Entry.TargetNode)); + continue; + } + + UEdGraphPin* SourcePin = SourceNode->FindPin(FName(*Entry.SourcePinName)); + if (!SourcePin) + { + EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Source pin '%s' not found on node '%s'"), *Entry.SourcePinName, *Entry.SourceNode)); + continue; + } + + UEdGraphPin* TargetPin = TargetNode->FindPin(FName(*Entry.TargetPinName)); + if (!TargetPin) + { + EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Target pin '%s' not found on node '%s'"), *Entry.TargetPinName, *Entry.TargetNode)); + continue; + } + + const UEdGraphSchema* Schema = SourceGraph->GetSchema(); + if (!Schema) + { + EntryResult->SetStringField(TEXT("error"), TEXT("Graph schema not found")); + continue; + } + + bool bConnected = Schema->TryCreateConnection(SourcePin, TargetPin); + if (!bConnected) + { + EntryResult->SetStringField(TEXT("error"), FString::Printf( + TEXT("Cannot connect %s (%s) to %s (%s) — types are incompatible"), + *Entry.SourcePinName, *SourcePin->PinType.PinCategory.ToString(), + *Entry.TargetPinName, *TargetPin->PinType.PinCategory.ToString())); + continue; + } + + SuccessCount++; + } + + if (SuccessCount > 0) + { + FBlueprintEditorUtils::MarkBlueprintAsModified(BP); + } + + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: ConnectPins — %d/%d succeeded in '%s'"), + SuccessCount, Connections.Array.Num(), *Blueprint); + Result->SetNumberField(TEXT("successCount"), SuccessCount); + Result->SetNumberField(TEXT("totalCount"), Connections.Array.Num()); + Result->SetArrayField(TEXT("results"), Results); + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_ConnectMaterialExpressionPins.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_ConnectMaterialExpressionPins.h new file mode 100644 index 00000000..c7cdd42e --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_ConnectMaterialExpressionPins.h @@ -0,0 +1,215 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPAssetFinder.h" +#include "MCPUtils.h" +#include "Materials/Material.h" +#include "MaterialDomain.h" +#include "Materials/MaterialInstanceConstant.h" +#include "Materials/MaterialFunction.h" +#include "Materials/MaterialExpression.h" +#include "Materials/MaterialExpressionScalarParameter.h" +#include "Materials/MaterialExpressionVectorParameter.h" +#include "Materials/MaterialExpressionTextureObjectParameter.h" +#include "Materials/MaterialExpressionTextureSampleParameter2D.h" +#include "Materials/MaterialExpressionStaticSwitchParameter.h" +#include "Materials/MaterialExpressionConstant.h" +#include "Materials/MaterialExpressionConstant3Vector.h" +#include "Materials/MaterialExpressionConstant4Vector.h" +#include "Materials/MaterialExpressionTextureSample.h" +#include "Materials/MaterialExpressionTextureCoordinate.h" +#include "Materials/MaterialExpressionComponentMask.h" +#include "Materials/MaterialExpressionCustom.h" +#include "Materials/MaterialExpressionFunctionInput.h" +#include "Materials/MaterialExpressionFunctionOutput.h" +#include "Materials/MaterialExpressionMaterialFunctionCall.h" +#include "MaterialGraph/MaterialGraph.h" +#include "MaterialGraph/MaterialGraphNode.h" +#include "MaterialGraph/MaterialGraphSchema.h" +#include "Factories/MaterialFactoryNew.h" +#include "Factories/MaterialFunctionFactoryNew.h" +#include "AssetToolsModule.h" +#include "IAssetTools.h" +#include "AssetRegistry/AssetRegistryModule.h" +#include "EdGraph/EdGraph.h" +#include "EdGraph/EdGraphNode.h" +#include "Serialization/JsonReader.h" +#include "Serialization/JsonWriter.h" +#include "Serialization/JsonSerializer.h" +#include "Misc/Guid.h" +#include "Misc/FileHelper.h" +#include "Misc/Paths.h" +#include "UObject/SavePackage.h" +#include "UObject/UObjectIterator.h" +#include "Kismet2/BlueprintEditorUtils.h" +#include "UMCPHandler_ConnectMaterialExpressionPins.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +UCLASS() +class UMCPHandler_ConnectMaterialExpressionPins : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Optional, Description="Material name or package path (specify this or materialFunction)")) + FString Material; + + UPROPERTY(meta=(Optional, Description="Material function name or package path (specify this or material)")) + FString MaterialFunction; + + UPROPERTY(meta=(Description="Node GUID of the source (output) node")) + FString SourceNode; + + UPROPERTY(meta=(Description="Pin name on the source node")) + FString SourcePinName; + + UPROPERTY(meta=(Description="Node GUID of the target (input) node")) + FString TargetNode; + + UPROPERTY(meta=(Description="Pin name on the target node")) + FString TargetPinName; + + UPROPERTY(meta=(Optional, Description="If true, preview the change without applying it")) + bool DryRun = false; + + virtual FString GetDescription() const override + { + return TEXT("Connect two pins in a material or material function graph."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override + { + if (Material.IsEmpty() && MaterialFunction.IsEmpty()) + { + return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: 'material' or 'materialFunction'")); + } + + // Load material or material function + UMaterial* MaterialObj = nullptr; + UMaterialFunction* MatFunc = nullptr; + FString AssetDisplayName; + + if (!MaterialFunction.IsEmpty()) + { + MCPAssets MFAssets; + if (!MFAssets.Exact(MaterialFunction).Errors(Result).ENone().ETwo().Load()) return; + MatFunc = MFAssets.Object(); + AssetDisplayName = MatFunc->GetName(); + } + else + { + MCPAssets MatAssets; + if (!MatAssets.Exact(Material).Errors(Result).ENone().ETwo().Load()) return; + MaterialObj = MatAssets.Object(); + AssetDisplayName = MaterialObj->GetName(); + } + + if (MaterialObj) MCPUtils::EnsureMaterialGraph(MaterialObj); + UEdGraph* Graph = MaterialObj ? (UEdGraph*)MaterialObj->MaterialGraph : (MatFunc ? MatFunc->MaterialGraph : nullptr); + if (!Graph) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("'%s' has no material graph"), *AssetDisplayName)); + } + + // Find source and target nodes by GUID + UEdGraphNode* SourceGraphNode = nullptr; + UEdGraphNode* TargetGraphNode = nullptr; + + for (UEdGraphNode* GraphNode : Graph->Nodes) + { + if (!GraphNode) continue; + if (GraphNode->NodeGuid.ToString() == SourceNode) + SourceGraphNode = GraphNode; + if (GraphNode->NodeGuid.ToString() == TargetNode) + TargetGraphNode = GraphNode; + if (SourceGraphNode && TargetGraphNode) + break; + } + + if (!SourceGraphNode) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Source node '%s' not found in material graph"), *SourceNode)); + } + if (!TargetGraphNode) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Target node '%s' not found in material graph"), *TargetNode)); + } + + // Find pins + UEdGraphPin* SourcePin = SourceGraphNode->FindPin(FName(*SourcePinName)); + if (!SourcePin) + { + // List available pins for debugging + TArray> PinNames; + for (UEdGraphPin* P : SourceGraphNode->Pins) + { + if (P) PinNames.Add(MakeShared( + FString::Printf(TEXT("%s (%s)"), *P->PinName.ToString(), + P->Direction == EGPD_Input ? TEXT("Input") : TEXT("Output")))); + } + MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Source pin '%s' not found on node '%s'"), + *SourcePinName, *SourceNode)); + Result->SetArrayField(TEXT("availablePins"), PinNames); + return; + } + + UEdGraphPin* TargetPin = TargetGraphNode->FindPin(FName(*TargetPinName)); + if (!TargetPin) + { + TArray> PinNames; + for (UEdGraphPin* P : TargetGraphNode->Pins) + { + if (P) PinNames.Add(MakeShared( + FString::Printf(TEXT("%s (%s)"), *P->PinName.ToString(), + P->Direction == EGPD_Input ? TEXT("Input") : TEXT("Output")))); + } + MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Target pin '%s' not found on node '%s'"), + *TargetPinName, *TargetNode)); + Result->SetArrayField(TEXT("availablePins"), PinNames); + return; + } + + if (DryRun) + { + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: [DRY RUN] Would connect %s.%s -> %s.%s in '%s'"), + *SourceNode, *SourcePinName, *TargetNode, *TargetPinName, *AssetDisplayName); + + Result->SetBoolField(TEXT("dryRun"), true); + Result->SetStringField(TEXT("material"), AssetDisplayName); + return; + } + + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Connecting %s.%s -> %s.%s in '%s'"), + *SourceNode, *SourcePinName, *TargetNode, *TargetPinName, *AssetDisplayName); + + // Try to connect via the schema + const UEdGraphSchema* Schema = Graph->GetSchema(); + if (!Schema) + { + return MCPUtils::MakeErrorJson(Result, TEXT("Material graph schema not found")); + } + + bool bConnected = Schema->TryCreateConnection(SourcePin, TargetPin); + + Result->SetStringField(TEXT("material"), AssetDisplayName); + + if (!bConnected) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf( + TEXT("Cannot connect %s.%s to %s.%s — types may be incompatible"), + *SourceNode, *SourcePinName, *TargetNode, *TargetPinName)); + } + + // Save + UObject* Asset = MaterialObj ? (UObject*)MaterialObj : (UObject*)MatFunc; + Asset->PreEditChange(nullptr); + Asset->PostEditChange(); + bool bSaved = MaterialObj ? MCPUtils::SaveMaterialPackage(MaterialObj) : MCPUtils::SaveGenericPackage(MatFunc); + Result->SetBoolField(TEXT("saved"), bSaved); + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_CreateAnimBlueprintAsset.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_CreateAnimBlueprintAsset.h new file mode 100644 index 00000000..55c7d105 --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_CreateAnimBlueprintAsset.h @@ -0,0 +1,133 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPAssetFinder.h" +#include "MCPUtils.h" +#include "Engine/Blueprint.h" +#include "EdGraph/EdGraph.h" +#include "EdGraph/EdGraphNode.h" +#include "Kismet2/KismetEditorUtilities.h" +#include "Dom/JsonValue.h" +#include "Animation/AnimBlueprint.h" +#include "Animation/AnimBlueprintGeneratedClass.h" +#include "Animation/Skeleton.h" +#include "AnimGraphNode_Base.h" +#include "Animation/AnimSequence.h" +#include "Animation/BlendSpace.h" +#include "UMCPHandler_CreateAnimBlueprintAsset.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +UCLASS() +class UMCPHandler_CreateAnimBlueprintAsset : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Name for the new Animation Blueprint asset")) + FString Name; + + UPROPERTY(meta=(Description="Package path where the asset will be created (must start with /Game)")) + FString PackagePath; + + UPROPERTY(meta=(Description="Name or path of the skeleton asset to use")) + FString Skeleton; + + UPROPERTY(meta=(Optional, Description="Parent class name (default: AnimInstance)")) + FString ParentClass; + + virtual FString GetDescription() const override + { + return TEXT("Create a new Animation Blueprint asset with a specified skeleton."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override + { + + if (Name.IsEmpty() || PackagePath.IsEmpty() || Skeleton.IsEmpty()) + { + return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: name, packagePath, skeleton")); + } + + if (!PackagePath.StartsWith(TEXT("/Game"))) + { + return MCPUtils::MakeErrorJson(Result, TEXT("packagePath must start with '/Game'")); + } + + // Check if asset already exists + FString FullAssetPath = PackagePath / Name; + MCPAssets ExistCheck; + if (!ExistCheck.Exact(Name).Errors(Result).EAny().Info()) return; + + // Resolve skeleton + MCPAssets SkeletonAssets; + if (!SkeletonAssets.Exact(Skeleton).Errors(Result).ENone().ETwo().Load()) return; + USkeleton* SkeletonObj = SkeletonAssets.Object(); + + // Resolve parent class (default: UAnimInstance) + UClass* ParentClassObj = UAnimInstance::StaticClass(); + if (!ParentClass.IsEmpty() && ParentClass != TEXT("AnimInstance")) + { + for (TObjectIterator It; It; ++It) + { + if (It->GetName() == ParentClass && It->IsChildOf(UAnimInstance::StaticClass())) + { + ParentClassObj = *It; + break; + } + } + } + + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Creating AnimBlueprint '%s' in '%s' with skeleton '%s'"), + *Name, *PackagePath, *SkeletonObj->GetName()); + + // Create the package + FString FullPackagePath = PackagePath / Name; + UPackage* Package = CreatePackage(*FullPackagePath); + if (!Package) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Failed to create package at '%s'"), *FullPackagePath)); + } + + // Create the Animation Blueprint + UAnimBlueprint* NewAnimBP = CastChecked( + FKismetEditorUtilities::CreateBlueprint( + ParentClassObj, + Package, + FName(*Name), + BPTYPE_Normal, + UAnimBlueprint::StaticClass(), + UAnimBlueprintGeneratedClass::StaticClass() + )); + + if (!NewAnimBP) + { + return MCPUtils::MakeErrorJson(Result, TEXT("FKismetEditorUtilities::CreateBlueprint returned null for AnimBlueprint")); + } + + // Set target skeleton + NewAnimBP->TargetSkeleton = SkeletonObj; + + // Compile + FKismetEditorUtilities::CompileBlueprint(NewAnimBP); + + // Save + bool bSaved = MCPUtils::SaveBlueprintPackage(NewAnimBP); + + + TArray> GraphNames = MCPUtils::AllGraphNamesJson(NewAnimBP); + + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Created AnimBlueprint '%s' with %d graphs (saved: %s)"), + *Name, GraphNames.Num(), bSaved ? TEXT("true") : TEXT("false")); + + Result->SetStringField(TEXT("assetPath"), FullAssetPath); + Result->SetStringField(TEXT("targetSkeleton"), SkeletonObj->GetName()); + Result->SetStringField(TEXT("parentClass"), ParentClassObj->GetName()); + Result->SetBoolField(TEXT("saved"), bSaved); + Result->SetArrayField(TEXT("graphs"), GraphNames); + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_CreateBlendSpaceAsset.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_CreateBlendSpaceAsset.h new file mode 100644 index 00000000..8742e830 --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_CreateBlendSpaceAsset.h @@ -0,0 +1,100 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPAssetFinder.h" +#include "MCPUtils.h" +#include "Engine/Blueprint.h" +#include "EdGraph/EdGraph.h" +#include "EdGraph/EdGraphNode.h" +#include "Kismet2/KismetEditorUtilities.h" +#include "Dom/JsonValue.h" +#include "Animation/AnimBlueprint.h" +#include "Animation/AnimBlueprintGeneratedClass.h" +#include "Animation/Skeleton.h" +#include "AnimGraphNode_Base.h" +#include "Animation/AnimSequence.h" +#include "Animation/BlendSpace.h" +#include "UMCPHandler_CreateBlendSpaceAsset.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +UCLASS() +class UMCPHandler_CreateBlendSpaceAsset : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Name for the new Blend Space asset")) + FString Name; + + UPROPERTY(meta=(Description="Package path where the asset will be created (must start with /Game)")) + FString PackagePath; + + UPROPERTY(meta=(Description="Name or path of the skeleton asset to use")) + FString Skeleton; + + virtual FString GetDescription() const override + { + return TEXT("Create a new 2D Blend Space asset with a specified skeleton."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override + { + + if (Name.IsEmpty() || PackagePath.IsEmpty() || Skeleton.IsEmpty()) + { + return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: name, packagePath, skeleton")); + } + + if (!PackagePath.StartsWith(TEXT("/Game"))) + { + return MCPUtils::MakeErrorJson(Result, TEXT("packagePath must start with '/Game'")); + } + + // Check if asset already exists + FString FullAssetPath = PackagePath / Name; + MCPAssets ExistCheck; + if (!ExistCheck.Exact(Name).Errors(Result).EAny().Info()) return; + + // Resolve skeleton + MCPAssets SkeletonAssets; + if (!SkeletonAssets.Exact(Skeleton).Errors(Result).ENone().ETwo().Load()) return; + USkeleton* SkeletonObj = SkeletonAssets.Object(); + + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Creating Blend Space '%s' in '%s' with skeleton '%s'"), + *Name, *PackagePath, *SkeletonObj->GetName()); + + // Create the package + FString FullPackagePath = PackagePath / Name; + UPackage* Package = CreatePackage(*FullPackagePath); + if (!Package) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Failed to create package at '%s'"), *FullPackagePath)); + } + + // Create the Blend Space + UBlendSpace* NewBS = NewObject(Package, FName(*Name), RF_Public | RF_Standalone); + if (!NewBS) + { + return MCPUtils::MakeErrorJson(Result, TEXT("Failed to create Blend Space object")); + } + + // Set skeleton + NewBS->SetSkeleton(SkeletonObj); + + // Mark dirty and save + NewBS->MarkPackageDirty(); + bool bSaved = MCPUtils::SaveGenericPackage(NewBS); + + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Created Blend Space '%s' (saved: %s)"), + *Name, bSaved ? TEXT("true") : TEXT("false")); + + Result->SetStringField(TEXT("assetPath"), FullAssetPath); + Result->SetStringField(TEXT("skeleton"), SkeletonObj->GetName()); + Result->SetBoolField(TEXT("saved"), bSaved); + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_CreateBlueprintAsset.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_CreateBlueprintAsset.h new file mode 100644 index 00000000..5a0b42e4 --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_CreateBlueprintAsset.h @@ -0,0 +1,146 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPAssetFinder.h" +#include "MCPUtils.h" +#include "Engine/Blueprint.h" +#include "EdGraph/EdGraph.h" +#include "EdGraph/EdGraphNode.h" +#include "EdGraphSchema_K2.h" +#include "K2Node_CustomEvent.h" +#include "Kismet2/BlueprintEditorUtils.h" +#include "Kismet2/KismetEditorUtilities.h" +#include "UObject/UObjectIterator.h" +#include "UMCPHandler_CreateBlueprintAsset.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +UCLASS() +class UMCPHandler_CreateBlueprintAsset : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="New Blueprint asset name")) + FString Blueprint; + + UPROPERTY(meta=(Description="Package path where the asset will be created (must start with /Game)")) + FString PackagePath; + + UPROPERTY(meta=(Description="Parent class name (C++ class name or Blueprint name)")) + FString ParentClass; + + UPROPERTY(meta=(Optional, Description="Blueprint type: Normal, Interface, FunctionLibrary, or MacroLibrary (default: Normal)")) + FString BlueprintType; + + virtual FString GetDescription() const override + { + return TEXT("Create a new Blueprint asset with a specified parent class and type."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override + { + // Validate packagePath starts with /Game + if (!PackagePath.StartsWith(TEXT("/Game"))) + { + return MCPUtils::MakeErrorJson(Result, TEXT("packagePath must start with '/Game'")); + } + + // Check if asset already exists + FString FullAssetPath = PackagePath / Blueprint; + MCPAssets ExistCheck; + if (!ExistCheck.Exact(Blueprint).Errors(Result).EAny().Info()) return; + + // Resolve parent class — try C++ class first, then Blueprint + UClass* ParentClassObj = nullptr; + + for (TObjectIterator It; It; ++It) + { + if (It->GetName() == ParentClass) + { + ParentClassObj = *It; + break; + } + } + + if (!ParentClassObj) + { + MCPAssets ParentAssets; + if (!ParentAssets.Exact(ParentClass).AllContent().Errors(Result).ETwo().Load()) return; + if (!ParentAssets.Objects().IsEmpty()) + { + if (ParentAssets.Object()->GeneratedClass) + ParentClassObj = ParentAssets.Object()->GeneratedClass; + } + } + + if (!ParentClassObj) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf( + TEXT("Could not find parent class '%s'. Provide a C++ class name (e.g. 'Actor', 'Pawn') or Blueprint name."), + *ParentClass)); + } + + // Map blueprintType string to EBlueprintType + EBlueprintType BlueprintTypeEnum = BPTYPE_Normal; + if (!BlueprintType.IsEmpty()) + { + if (!MCPUtils::StringToEnum(BlueprintType, BlueprintTypeEnum, Result, TEXT("BPTYPE_"))) return; + } + + // For Interface type, parent must be UInterface + if ((BlueprintTypeEnum == BPTYPE_Interface) && !ParentClassObj->IsChildOf(UInterface::StaticClass())) + { + // Use the engine's standard BlueprintInterface parent + ParentClassObj = UInterface::StaticClass(); + } + + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Creating Blueprint '%s' in '%s' with parent '%s' (type=%s)"), + *Blueprint, *PackagePath, *ParentClassObj->GetName(), *BlueprintType); + + // Create the package + FString FullPackagePath = PackagePath / Blueprint; + UPackage* Package = CreatePackage(*FullPackagePath); + if (!Package) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Failed to create package at '%s'"), *FullPackagePath)); + } + + // Create the Blueprint + UBlueprint* NewBP = FKismetEditorUtilities::CreateBlueprint( + ParentClassObj, + Package, + FName(*Blueprint), + BlueprintTypeEnum, + UBlueprint::StaticClass(), + UBlueprintGeneratedClass::StaticClass() + ); + + if (!NewBP) + { + return MCPUtils::MakeErrorJson(Result, TEXT("FKismetEditorUtilities::CreateBlueprint returned null")); + } + + // Compile + FKismetEditorUtilities::CompileBlueprint(NewBP); + + // Save + bool bSaved = MCPUtils::SaveBlueprintPackage(NewBP); + + + // Collect graph names + TArray> GraphNames = MCPUtils::AllGraphNamesJson(NewBP); + + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Created Blueprint '%s' with %d graphs (saved: %s)"), + *Blueprint, GraphNames.Num(), bSaved ? TEXT("true") : TEXT("false")); + + Result->SetStringField(TEXT("assetPath"), FullAssetPath); + Result->SetStringField(TEXT("parentClass"), ParentClassObj->GetName()); + Result->SetBoolField(TEXT("saved"), bSaved); + Result->SetArrayField(TEXT("graphs"), GraphNames); + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_CreateBlueprintGraph.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_CreateBlueprintGraph.h new file mode 100644 index 00000000..ebb9e021 --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_CreateBlueprintGraph.h @@ -0,0 +1,136 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPAssetFinder.h" +#include "MCPUtils.h" +#include "Engine/Blueprint.h" +#include "EdGraph/EdGraph.h" +#include "EdGraph/EdGraphNode.h" +#include "EdGraphSchema_K2.h" +#include "K2Node_CustomEvent.h" +#include "Kismet2/BlueprintEditorUtils.h" +#include "Kismet2/KismetEditorUtilities.h" +#include "UObject/UObjectIterator.h" +#include "UMCPHandler_CreateBlueprintGraph.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +UCLASS() +class UMCPHandler_CreateBlueprintGraph : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Blueprint name or package path")) + FString Blueprint; + + UPROPERTY(meta=(Description="Name for the new graph")) + FString Graph; + + UPROPERTY(meta=(Description="Type of graph: function, macro, or customEvent")) + FString GraphType; + + virtual FString GetDescription() const override + { + return TEXT("Create a new function, macro, or custom event graph in a Blueprint."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override + { + if (GraphType != TEXT("function") && GraphType != TEXT("macro") && GraphType != TEXT("customEvent")) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf( + TEXT("Invalid graphType '%s'. Valid values: function, macro, customEvent"), *GraphType)); + } + + // Load Blueprint + MCPAssets Assets; + if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; + UBlueprint* BP = Assets.Object(); + + // Check graph name uniqueness + if (!MCPUtils::AllGraphsNamed(BP, Graph).IsEmpty()) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf( + TEXT("A graph named '%s' already exists in Blueprint '%s'"), *Graph, *Blueprint)); + } + + // Also check for existing custom events with the same name + if (GraphType == TEXT("customEvent")) + { + for (UK2Node_CustomEvent* CE : MCPUtils::AllNodes(BP)) + { + if (CE->CustomFunctionName == FName(*Graph)) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf( + TEXT("A custom event named '%s' already exists in Blueprint '%s'"), *Graph, *Blueprint)); + } + } + } + + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Creating %s graph '%s' in Blueprint '%s'"), + *GraphType, *Graph, *Blueprint); + + FString CreatedNodeId; + + if (GraphType == TEXT("function")) + { + UEdGraph* NewGraph = FBlueprintEditorUtils::CreateNewGraph(BP, FName(*Graph), + UEdGraph::StaticClass(), UEdGraphSchema_K2::StaticClass()); + if (!NewGraph) + { + return MCPUtils::MakeErrorJson(Result, TEXT("Failed to create function graph")); + } + FBlueprintEditorUtils::AddFunctionGraph(BP, NewGraph, /*bIsUserCreated=*/true, /*SignatureFromObject=*/static_cast(nullptr)); + } + else if (GraphType == TEXT("macro")) + { + UEdGraph* NewGraph = FBlueprintEditorUtils::CreateNewGraph(BP, FName(*Graph), + UEdGraph::StaticClass(), UEdGraphSchema_K2::StaticClass()); + if (!NewGraph) + { + return MCPUtils::MakeErrorJson(Result, TEXT("Failed to create macro graph")); + } + FBlueprintEditorUtils::AddMacroGraph(BP, NewGraph, /*bIsUserCreated=*/true, /*SignatureFromClass=*/nullptr); + } + else // customEvent + { + // Find the EventGraph (first UbergraphPage) + UEdGraph* EventGraph = nullptr; + if (BP->UbergraphPages.Num() > 0) + { + EventGraph = BP->UbergraphPages[0]; + } + if (!EventGraph) + { + return MCPUtils::MakeErrorJson(Result, TEXT("Blueprint has no EventGraph to add a custom event to")); + } + + // Create a custom event node in the EventGraph + UK2Node_CustomEvent* NewEvent = NewObject(EventGraph); + NewEvent->CustomFunctionName = FName(*Graph); + NewEvent->bIsEditable = true; + EventGraph->AddNode(NewEvent, /*bFromUI=*/false, /*bSelectNewNode=*/false); + NewEvent->CreateNewGuid(); + NewEvent->PostPlacedNewNode(); + NewEvent->AllocateDefaultPins(); + CreatedNodeId = NewEvent->NodeGuid.ToString(); + } + + FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP); + bool bSaved = MCPUtils::SaveBlueprintPackage(BP); + + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Created %s graph '%s' in '%s' (saved: %s)"), + *GraphType, *Graph, *Blueprint, bSaved ? TEXT("true") : TEXT("false")); + + Result->SetBoolField(TEXT("saved"), bSaved); + if (!CreatedNodeId.IsEmpty()) + { + Result->SetStringField(TEXT("nodeId"), CreatedNodeId); + } + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_CreateEnumAsset.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_CreateEnumAsset.h new file mode 100644 index 00000000..93b4fde7 --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_CreateEnumAsset.h @@ -0,0 +1,97 @@ +#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 "UMCPHandler_CreateEnumAsset.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +UCLASS() +class UMCPHandler_CreateEnumAsset : 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->SetStringField(TEXT("assetName"), AssetName); + Result->SetNumberField(TEXT("valueCount"), EnumValues.Num()); + Result->SetBoolField(TEXT("saved"), bSaved); + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_CreateMaterialAsset.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_CreateMaterialAsset.h new file mode 100644 index 00000000..e21c3a70 --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_CreateMaterialAsset.h @@ -0,0 +1,175 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPAssetFinder.h" +#include "MCPUtils.h" +#include "Materials/Material.h" +#include "MaterialDomain.h" +#include "Materials/MaterialInstanceConstant.h" +#include "Materials/MaterialFunction.h" +#include "Materials/MaterialExpression.h" +#include "Materials/MaterialExpressionScalarParameter.h" +#include "Materials/MaterialExpressionVectorParameter.h" +#include "Materials/MaterialExpressionTextureObjectParameter.h" +#include "Materials/MaterialExpressionTextureSampleParameter2D.h" +#include "Materials/MaterialExpressionStaticSwitchParameter.h" +#include "Materials/MaterialExpressionConstant.h" +#include "Materials/MaterialExpressionConstant3Vector.h" +#include "Materials/MaterialExpressionConstant4Vector.h" +#include "Materials/MaterialExpressionTextureSample.h" +#include "Materials/MaterialExpressionTextureCoordinate.h" +#include "Materials/MaterialExpressionComponentMask.h" +#include "Materials/MaterialExpressionCustom.h" +#include "Materials/MaterialExpressionFunctionInput.h" +#include "Materials/MaterialExpressionFunctionOutput.h" +#include "Materials/MaterialExpressionMaterialFunctionCall.h" +#include "MaterialGraph/MaterialGraph.h" +#include "MaterialGraph/MaterialGraphNode.h" +#include "MaterialGraph/MaterialGraphSchema.h" +#include "Factories/MaterialFactoryNew.h" +#include "Factories/MaterialFunctionFactoryNew.h" +#include "AssetToolsModule.h" +#include "IAssetTools.h" +#include "AssetRegistry/AssetRegistryModule.h" +#include "EdGraph/EdGraph.h" +#include "EdGraph/EdGraphNode.h" +#include "Serialization/JsonReader.h" +#include "Serialization/JsonWriter.h" +#include "Serialization/JsonSerializer.h" +#include "Misc/Guid.h" +#include "Misc/FileHelper.h" +#include "Misc/Paths.h" +#include "UObject/SavePackage.h" +#include "UObject/UObjectIterator.h" +#include "Kismet2/BlueprintEditorUtils.h" +#include "UMCPHandler_CreateMaterialAsset.generated.h" + + +// SEH wrapper defined in BlueprintMCPServer.cpp — catches crashes from abstract/invalid expression classes. +// Wraps the entire creation + registration + PostEditChange flow so that if the expression crashes +// (e.g. UMaterialExpressionParameter), it cleans up and returns -1 instead of terminating the process. +#if PLATFORM_WINDOWS +extern int32 TryAddMaterialExpressionSEH( + UObject* Owner, UClass* ExprClass, UMaterial* Material, UMaterialFunction* MatFunc, + int32 PosX, int32 PosY, UMaterialExpression** OutExpr); +#endif + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +UCLASS() +class UMCPHandler_CreateMaterialAsset : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Name for the new material asset")) + FString Name; + + UPROPERTY(meta=(Description="Package path where the asset will be created (must start with /Game)")) + FString PackagePath; + + UPROPERTY(meta=(Optional, Description="Material domain: Surface, DeferredDecal, LightFunction, Volume, PostProcess, UI")) + FString Domain; + + UPROPERTY(meta=(Optional, Description="Blend mode: Opaque, Masked, Translucent, Additive, Modulate")) + FString BlendMode; + + UPROPERTY(meta=(Optional, Description="Whether the material is two-sided")) + bool TwoSided = false; + + virtual FString GetDescription() const override + { + return TEXT("Create a new UMaterial asset with optional domain, blend mode, and two-sided settings."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override + { + if (!PackagePath.StartsWith(TEXT("/Game"))) + { + return MCPUtils::MakeErrorJson(Result, TEXT("packagePath must start with '/Game'")); + } + + // Check if asset already exists + MCPAssets ExistCheck; + if (!ExistCheck.Exact(Name).Errors(Result).EAny().Info()) return; + + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Creating Material '%s' in '%s'"), *Name, *PackagePath); + + // Create via IAssetTools + factory + IAssetTools& AssetTools = FModuleManager::LoadModuleChecked("AssetTools").Get(); + UMaterialFactoryNew* Factory = NewObject(); + UObject* NewAsset = AssetTools.CreateAsset(Name, PackagePath, UMaterial::StaticClass(), Factory); + + if (!NewAsset) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Failed to create Material '%s' in '%s'"), *Name, *PackagePath)); + } + + UMaterial* MaterialObj = Cast(NewAsset); + if (!MaterialObj) + { + return MCPUtils::MakeErrorJson(Result, TEXT("Created asset is not a UMaterial")); + } + + // Apply optional properties + bool bHasTwoSided = Json->HasField(TEXT("twoSided")); + + MaterialObj->PreEditChange(nullptr); + + // Parse domain + if (!Domain.IsEmpty()) + { + if (Domain == TEXT("Surface")) + MaterialObj->MaterialDomain = MD_Surface; + else if (Domain == TEXT("DeferredDecal")) + MaterialObj->MaterialDomain = MD_DeferredDecal; + else if (Domain == TEXT("LightFunction")) + MaterialObj->MaterialDomain = MD_LightFunction; + else if (Domain == TEXT("Volume")) + MaterialObj->MaterialDomain = MD_Volume; + else if (Domain == TEXT("PostProcess")) + MaterialObj->MaterialDomain = MD_PostProcess; + else if (Domain == TEXT("UI")) + MaterialObj->MaterialDomain = MD_UI; + } + + // Parse blend mode + if (!BlendMode.IsEmpty()) + { + if (BlendMode == TEXT("Opaque")) + MaterialObj->BlendMode = BLEND_Opaque; + else if (BlendMode == TEXT("Masked")) + MaterialObj->BlendMode = BLEND_Masked; + else if (BlendMode == TEXT("Translucent")) + MaterialObj->BlendMode = BLEND_Translucent; + else if (BlendMode == TEXT("Additive")) + MaterialObj->BlendMode = BLEND_Additive; + else if (BlendMode == TEXT("Modulate")) + MaterialObj->BlendMode = BLEND_Modulate; + } + + if (bHasTwoSided) + { + MaterialObj->TwoSided = TwoSided; + } + + MaterialObj->PostEditChange(); + + // Save + bool bSaved = MCPUtils::SaveMaterialPackage(MaterialObj); + + + + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Created Material '%s' (saved: %s)"), + *Name, bSaved ? TEXT("true") : TEXT("false")); + + Result->SetStringField(TEXT("path"), MaterialObj->GetPathName()); + Result->SetStringField(TEXT("domain"), MCPUtils::EnumToString(MaterialObj->MaterialDomain, TEXT("MD_"))); + Result->SetStringField(TEXT("blendMode"), MCPUtils::EnumToString(MaterialObj->BlendMode, TEXT("BLEND_"))); + Result->SetBoolField(TEXT("twoSided"), MaterialObj->TwoSided != 0); + Result->SetBoolField(TEXT("saved"), bSaved); + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_CreateMaterialFunctionAsset.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_CreateMaterialFunctionAsset.h new file mode 100644 index 00000000..a059dd54 --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_CreateMaterialFunctionAsset.h @@ -0,0 +1,118 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPAssetFinder.h" +#include "MCPUtils.h" +#include "Materials/Material.h" +#include "MaterialDomain.h" +#include "Materials/MaterialInstanceConstant.h" +#include "Materials/MaterialFunction.h" +#include "Materials/MaterialExpression.h" +#include "Materials/MaterialExpressionScalarParameter.h" +#include "Materials/MaterialExpressionVectorParameter.h" +#include "Materials/MaterialExpressionTextureObjectParameter.h" +#include "Materials/MaterialExpressionTextureSampleParameter2D.h" +#include "Materials/MaterialExpressionStaticSwitchParameter.h" +#include "Materials/MaterialExpressionConstant.h" +#include "Materials/MaterialExpressionConstant3Vector.h" +#include "Materials/MaterialExpressionConstant4Vector.h" +#include "Materials/MaterialExpressionTextureSample.h" +#include "Materials/MaterialExpressionTextureCoordinate.h" +#include "Materials/MaterialExpressionComponentMask.h" +#include "Materials/MaterialExpressionCustom.h" +#include "Materials/MaterialExpressionFunctionInput.h" +#include "Materials/MaterialExpressionFunctionOutput.h" +#include "Materials/MaterialExpressionMaterialFunctionCall.h" +#include "MaterialGraph/MaterialGraph.h" +#include "MaterialGraph/MaterialGraphNode.h" +#include "MaterialGraph/MaterialGraphSchema.h" +#include "Factories/MaterialFactoryNew.h" +#include "Factories/MaterialFunctionFactoryNew.h" +#include "AssetToolsModule.h" +#include "IAssetTools.h" +#include "AssetRegistry/AssetRegistryModule.h" +#include "EdGraph/EdGraph.h" +#include "EdGraph/EdGraphNode.h" +#include "Serialization/JsonReader.h" +#include "Serialization/JsonWriter.h" +#include "Serialization/JsonSerializer.h" +#include "Misc/Guid.h" +#include "Misc/FileHelper.h" +#include "Misc/Paths.h" +#include "UObject/SavePackage.h" +#include "UObject/UObjectIterator.h" +#include "Kismet2/BlueprintEditorUtils.h" +#include "UMCPHandler_CreateMaterialFunctionAsset.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +UCLASS() +class UMCPHandler_CreateMaterialFunctionAsset : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Name for the new material function asset")) + FString Name; + + UPROPERTY(meta=(Description="Package path where the asset will be created (must start with /Game)")) + FString PackagePath; + + UPROPERTY(meta=(Optional, Description="Description for the material function")) + FString Description; + + virtual FString GetDescription() const override + { + return TEXT("Create a new UMaterialFunction asset with an optional description."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override + { + if (!PackagePath.StartsWith(TEXT("/Game"))) + { + return MCPUtils::MakeErrorJson(Result, TEXT("packagePath must start with '/Game'")); + } + + // Check if asset already exists + MCPAssets ExistCheck; + if (!ExistCheck.Exact(Name).Errors(Result).EAny().Info()) return; + + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Creating Material Function '%s' in '%s'"), *Name, *PackagePath); + + // Create via IAssetTools + factory + IAssetTools& AssetTools = FModuleManager::LoadModuleChecked("AssetTools").Get(); + UMaterialFunctionFactoryNew* Factory = NewObject(); + UObject* NewAsset = AssetTools.CreateAsset(Name, PackagePath, UMaterialFunction::StaticClass(), Factory); + + if (!NewAsset) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Failed to create Material Function '%s' in '%s'"), *Name, *PackagePath)); + } + + UMaterialFunction* MF = Cast(NewAsset); + if (!MF) + { + return MCPUtils::MakeErrorJson(Result, TEXT("Created asset is not a UMaterialFunction")); + } + + // Set optional description + if (!Description.IsEmpty()) + { + MF->Description = Description; + } + + // Save + bool bSaved = MCPUtils::SaveGenericPackage(MF); + + + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Created Material Function '%s' (saved: %s)"), + *Name, bSaved ? TEXT("true") : TEXT("false")); + + Result->SetStringField(TEXT("path"), MF->GetPathName()); + Result->SetBoolField(TEXT("saved"), bSaved); + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_CreateMaterialInstanceAsset.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_CreateMaterialInstanceAsset.h new file mode 100644 index 00000000..79761817 --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_CreateMaterialInstanceAsset.h @@ -0,0 +1,125 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPAssetFinder.h" +#include "MCPUtils.h" +#include "Materials/Material.h" +#include "Materials/MaterialInterface.h" +#include "Materials/MaterialInstanceConstant.h" +#include "Materials/MaterialExpressionScalarParameter.h" +#include "Materials/MaterialExpressionVectorParameter.h" +#include "Materials/MaterialExpressionTextureSampleParameter2D.h" +#include "Materials/MaterialExpressionStaticSwitchParameter.h" +#include "Factories/MaterialInstanceConstantFactoryNew.h" +#include "AssetToolsModule.h" +#include "IAssetTools.h" +#include "Engine/Texture.h" +#include "UMCPHandler_CreateMaterialInstanceAsset.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +UCLASS() +class UMCPHandler_CreateMaterialInstanceAsset : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Name for the new Material Instance asset")) + FString Name; + + UPROPERTY(meta=(Description="Package path where the asset will be created (must start with /Game)")) + FString PackagePath; + + UPROPERTY(meta=(Description="Parent material name or path (Material or Material Instance)")) + FString ParentMaterial; + + virtual FString GetDescription() const override + { + return TEXT("Create a new Material Instance Constant asset with a specified parent material."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override + { + // Validate packagePath starts with /Game + if (!PackagePath.StartsWith(TEXT("/Game"))) + { + return MCPUtils::MakeErrorJson(Result, TEXT("packagePath must start with '/Game'")); + } + + // Check if asset already exists + { + MCPAssets ExistCheck; + if (!ExistCheck.Exact(Name).Errors(Result).EAny().Info()) return; + } + + // Load parent material — try as Material first, then as Material Instance + UMaterialInterface* ParentMaterialObj = nullptr; + { + MCPAssets MatAssets; + if (MatAssets.Exact(ParentMaterial).ETwo().Load() && !MatAssets.Objects().IsEmpty()) + { + ParentMaterialObj = MatAssets.Object(); + } + else + { + MCPAssets MIAssets; + if (MIAssets.Exact(ParentMaterial).ETwo().Load() && !MIAssets.Objects().IsEmpty()) + { + ParentMaterialObj = MIAssets.Object(); + } + } + } + + if (!ParentMaterialObj) + { + // Also try LoadObject as a fallback with the raw path + ParentMaterialObj = LoadObject(nullptr, *ParentMaterial); + } + + if (!ParentMaterialObj) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf( + TEXT("Parent material '%s' not found. Provide a Material or Material Instance name/path."), + *ParentMaterial)); + } + + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Creating Material Instance '%s' in '%s' with parent '%s'"), + *Name, *PackagePath, *ParentMaterialObj->GetName()); + + // Create via factory + AssetTools + IAssetTools& AssetTools = FModuleManager::LoadModuleChecked("AssetTools").Get(); + UMaterialInstanceConstantFactoryNew* Factory = NewObject(); + + UObject* NewAsset = AssetTools.CreateAsset(Name, PackagePath, UMaterialInstanceConstant::StaticClass(), Factory); + if (!NewAsset) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Failed to create Material Instance asset '%s' in '%s'"), *Name, *PackagePath)); + } + + UMaterialInstanceConstant* MI = Cast(NewAsset); + if (!MI) + { + return MCPUtils::MakeErrorJson(Result, TEXT("Created asset is not a UMaterialInstanceConstant")); + } + + // Set parent + MI->PreEditChange(nullptr); + MI->Parent = ParentMaterialObj; + MI->PostEditChange(); + + // Save + bool bSaved = MCPUtils::SaveGenericPackage(MI); + + + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Created Material Instance '%s' with parent '%s' (saved: %s)"), + *Name, *ParentMaterialObj->GetName(), bSaved ? TEXT("true") : TEXT("false")); + + Result->SetStringField(TEXT("path"), MI->GetPathName()); + Result->SetStringField(TEXT("parent"), ParentMaterialObj->GetPathName()); + Result->SetBoolField(TEXT("saved"), bSaved); + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_CreateStructAsset.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_CreateStructAsset.h new file mode 100644 index 00000000..bdda34fd --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_CreateStructAsset.h @@ -0,0 +1,140 @@ +#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 "UMCPHandler_CreateStructAsset.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +USTRUCT() +struct FStructPropertyEntry +{ + GENERATED_BODY() + + UPROPERTY() + FString Name; + + UPROPERTY() + FString Type; +}; + +UCLASS() +class UMCPHandler_CreateStructAsset : 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) + { + FStructPropertyEntry Entry; + if (!MCPUtils::PopulateFromJson(FStructPropertyEntry::StaticStruct(), &Entry, PropVal, Result)) return; + if (Entry.Name.IsEmpty() || Entry.Type.IsEmpty()) continue; + + FEdGraphPinType PinType; + FString TypeError; + if (!MCPUtils::ResolveTypeFromString(Entry.Type, PinType, TypeError)) + { + UE_LOG(LogTemp, Warning, TEXT("BlueprintMCP: Could not resolve type '%s' for property '%s': %s"), *Entry.Type, *Entry.Name, *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, Entry.Name); + } + PropsAdded++; + } + } + + // Save + bool bSaved = MCPUtils::SaveGenericPackage(NewStruct); + + Result->SetStringField(TEXT("assetName"), AssetName); + Result->SetNumberField(TEXT("propertiesAdded"), PropsAdded); + Result->SetBoolField(TEXT("saved"), bSaved); + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_DeleteAsset.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_DeleteAsset.h new file mode 100644 index 00000000..8c181b23 --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_DeleteAsset.h @@ -0,0 +1,167 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPAssetFinder.h" +#include "MCPUtils.h" +#include "Engine/Blueprint.h" +#include "Kismet2/BlueprintEditorUtils.h" +#include "UObject/SavePackage.h" +#include "Misc/PackageName.h" +#include "AssetRegistry/AssetRegistryModule.h" +#include "AssetRegistry/IAssetRegistry.h" +#include "AssetToolsModule.h" +#include "IAssetTools.h" +#include "FileHelpers.h" +#include "UMCPHandler_DeleteAsset.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +UCLASS() +class UMCPHandler_DeleteAsset : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Package path of the asset to delete")) + FString AssetPath; + + UPROPERTY(meta=(Optional, Description="If true, skip reference check and force delete")) + bool Force = false; + + virtual FString GetDescription() const override + { + return TEXT("Delete a .uasset after verifying no references. " + "Use force=true to skip the reference check."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override + { + + // Check if asset file exists on disk + FString PackageFilename = FPackageName::LongPackageNameToFilename( + AssetPath, FPackageName::GetAssetPackageExtension()); + PackageFilename = FPaths::ConvertRelativePathToFull(PackageFilename); + + if (!IFileManager::Get().FileExists(*PackageFilename)) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Asset file not found on disk: %s"), *PackageFilename)); + } + + // Check references + IAssetRegistry& Registry = *IAssetRegistry::Get(); + TArray Referencers; + Registry.GetReferencers(FName(*AssetPath), Referencers); + + // Filter out self-references + Referencers.RemoveAll([this](const FName& Ref) { + return Ref.ToString() == AssetPath; + }); + + if ((Referencers.Num() > 0) && !Force) + { + // Classify references as "live" (loaded in memory) vs "stale" (only on disk) + TArray> LiveRefs; + TArray> StaleRefs; + for (const FName& Ref : Referencers) + { + FString RefStr = Ref.ToString(); + UPackage* RefPackage = FindPackage(nullptr, *RefStr); + if (RefPackage) + { + LiveRefs.Add(MakeShared(RefStr)); + } + else + { + StaleRefs.Add(MakeShared(RefStr)); + } + } + + MCPUtils::MakeErrorJson(Result, TEXT("Asset is still referenced. Remove all references first.")); + Result->SetStringField(TEXT("assetPath"), AssetPath); + Result->SetNumberField(TEXT("referencerCount"), Referencers.Num()); + Result->SetNumberField(TEXT("liveReferencerCount"), LiveRefs.Num()); + Result->SetArrayField(TEXT("liveReferencers"), LiveRefs); + Result->SetNumberField(TEXT("staleReferencerCount"), StaleRefs.Num()); + Result->SetArrayField(TEXT("staleReferencers"), StaleRefs); + Result->SetStringField(TEXT("suggestion"), + StaleRefs.Num() > 0 + ? TEXT("Some references may be stale. Consider force=true to skip the reference check, or use change_variable_type to migrate references first.") + : TEXT("All references are live. Migrate with change_variable_type or replace_function_calls before deleting.")); + return; + } + + // Force delete: unload the package from memory first + TArray> RefWarnings; + if (Force) + { + // Collect reference warnings when force-deleting with existing references + for (const FName& Ref : Referencers) + { + RefWarnings.Add(MakeShared( + FString::Printf(TEXT("Warning: '%s' still references this asset"), *Ref.ToString()))); + } + + UPackage* Package = FindPackage(nullptr, *AssetPath); + if (Package) + { + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Force-unloading package '%s' from memory"), *AssetPath); + + // Collect all objects in this package + TArray ObjectsInPackage; + GetObjectsWithPackage(Package, ObjectsInPackage); + + // Clear flags and remove from root to allow GC + for (UObject* Obj : ObjectsInPackage) + { + if (Obj) + { + Obj->ClearFlags(RF_Standalone | RF_Public); + Obj->RemoveFromRoot(); + } + } + Package->ClearFlags(RF_Standalone | RF_Public); + Package->RemoveFromRoot(); + + // Reset loaders to release file handles + ResetLoaders(Package); + // Force garbage collection to free the objects + CollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS); + } + } + + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Deleting asset '%s' (%s)%s"), + *AssetPath, *PackageFilename, Force ? TEXT(" [FORCE]") : TEXT("")); + + // Delete the file on disk + bool bDeleted = IFileManager::Get().Delete(*PackageFilename, false, true); + + if (bDeleted) + { + // Trigger an asset registry rescan so it notices the deletion + TArray PathsToScan; + int32 LastSlash; + if (AssetPath.FindLastChar(TEXT('/'), LastSlash)) + { + PathsToScan.Add(AssetPath.Left(LastSlash)); + } + if (PathsToScan.Num() > 0) + { + Registry.ScanPathsSynchronous(PathsToScan, true); + } + } + + Result->SetStringField(TEXT("filename"), PackageFilename); + if (!bDeleted) + { + MCPUtils::MakeErrorJson(Result, TEXT("Failed to delete file from disk")); + } + if (RefWarnings.Num() > 0) + { + Result->SetArrayField(TEXT("warnings"), RefWarnings); + } + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_DeleteBlueprintGraph.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_DeleteBlueprintGraph.h new file mode 100644 index 00000000..1b959b73 --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_DeleteBlueprintGraph.h @@ -0,0 +1,105 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPAssetFinder.h" +#include "MCPUtils.h" +#include "Engine/Blueprint.h" +#include "EdGraph/EdGraph.h" +#include "EdGraph/EdGraphNode.h" +#include "EdGraphSchema_K2.h" +#include "K2Node_CustomEvent.h" +#include "Kismet2/BlueprintEditorUtils.h" +#include "Kismet2/KismetEditorUtilities.h" +#include "UObject/UObjectIterator.h" +#include "UMCPHandler_DeleteBlueprintGraph.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +UCLASS() +class UMCPHandler_DeleteBlueprintGraph : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Blueprint name or package path")) + FString Blueprint; + + UPROPERTY(meta=(Description="Name of the graph to delete")) + FString Graph; + + virtual FString GetDescription() const override + { + return TEXT("Delete a function or macro graph from a Blueprint. Cannot delete EventGraph pages."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override + { + MCPAssets Assets; + if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; + UBlueprint* BP = Assets.Object(); + + // Find the graph + UEdGraph* TargetGraph = nullptr; + FString GraphType; + + for (UEdGraph* CandidateGraph : BP->FunctionGraphs) + { + if (CandidateGraph && CandidateGraph->GetName().Equals(Graph, ESearchCase::IgnoreCase)) + { + TargetGraph = CandidateGraph; + GraphType = TEXT("function"); + break; + } + } + if (!TargetGraph) + { + for (UEdGraph* CandidateGraph : BP->MacroGraphs) + { + if (CandidateGraph && CandidateGraph->GetName().Equals(Graph, ESearchCase::IgnoreCase)) + { + TargetGraph = CandidateGraph; + GraphType = TEXT("macro"); + break; + } + } + } + + // Check if it's an UbergraphPage (EventGraph) — disallow deletion + if (!TargetGraph) + { + for (UEdGraph* CandidateGraph : BP->UbergraphPages) + { + if (CandidateGraph && CandidateGraph->GetName().Equals(Graph, ESearchCase::IgnoreCase)) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf( + TEXT("Cannot delete UbergraphPage '%s'. EventGraph and other Ubergraph pages cannot be deleted."), + *Graph)); + } + } + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Graph '%s' not found in Blueprint '%s'"), *Graph, *Blueprint)); + } + + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Deleting %s graph '%s' from Blueprint '%s'"), + *GraphType, *Graph, *Blueprint); + + // Count nodes for reporting + int32 NodeCount = TargetGraph->Nodes.Num(); + + // Remove the graph + FBlueprintEditorUtils::RemoveGraph(BP, TargetGraph, EGraphRemoveFlags::Default); + + FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP); + bool bSaved = MCPUtils::SaveBlueprintPackage(BP); + + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Deleted graph '%s' (%d nodes), save %s"), + *Graph, NodeCount, bSaved ? TEXT("true") : TEXT("false")); + + Result->SetStringField(TEXT("graphType"), GraphType); + Result->SetNumberField(TEXT("nodeCount"), NodeCount); + Result->SetBoolField(TEXT("saved"), bSaved); + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_DeleteMaterialExpression.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_DeleteMaterialExpression.h new file mode 100644 index 00000000..d8285c96 --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_DeleteMaterialExpression.h @@ -0,0 +1,180 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPAssetFinder.h" +#include "MCPUtils.h" +#include "Materials/Material.h" +#include "MaterialDomain.h" +#include "Materials/MaterialInstanceConstant.h" +#include "Materials/MaterialFunction.h" +#include "Materials/MaterialExpression.h" +#include "Materials/MaterialExpressionScalarParameter.h" +#include "Materials/MaterialExpressionVectorParameter.h" +#include "Materials/MaterialExpressionTextureObjectParameter.h" +#include "Materials/MaterialExpressionTextureSampleParameter2D.h" +#include "Materials/MaterialExpressionStaticSwitchParameter.h" +#include "Materials/MaterialExpressionConstant.h" +#include "Materials/MaterialExpressionConstant3Vector.h" +#include "Materials/MaterialExpressionConstant4Vector.h" +#include "Materials/MaterialExpressionTextureSample.h" +#include "Materials/MaterialExpressionTextureCoordinate.h" +#include "Materials/MaterialExpressionComponentMask.h" +#include "Materials/MaterialExpressionCustom.h" +#include "Materials/MaterialExpressionFunctionInput.h" +#include "Materials/MaterialExpressionFunctionOutput.h" +#include "Materials/MaterialExpressionMaterialFunctionCall.h" +#include "MaterialGraph/MaterialGraph.h" +#include "MaterialGraph/MaterialGraphNode.h" +#include "MaterialGraph/MaterialGraphSchema.h" +#include "Factories/MaterialFactoryNew.h" +#include "Factories/MaterialFunctionFactoryNew.h" +#include "AssetToolsModule.h" +#include "IAssetTools.h" +#include "AssetRegistry/AssetRegistryModule.h" +#include "EdGraph/EdGraph.h" +#include "EdGraph/EdGraphNode.h" +#include "Serialization/JsonReader.h" +#include "Serialization/JsonWriter.h" +#include "Serialization/JsonSerializer.h" +#include "Misc/Guid.h" +#include "Misc/FileHelper.h" +#include "Misc/Paths.h" +#include "UObject/SavePackage.h" +#include "UObject/UObjectIterator.h" +#include "Kismet2/BlueprintEditorUtils.h" +#include "UMCPHandler_DeleteMaterialExpression.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +UCLASS() +class UMCPHandler_DeleteMaterialExpression : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Optional, Description="Material name or package path (specify this or materialFunction)")) + FString Material; + + UPROPERTY(meta=(Optional, Description="Material function name or package path (specify this or material)")) + FString MaterialFunction; + + UPROPERTY(meta=(Description="Node GUID of the expression to delete")) + FString Node; + + UPROPERTY(meta=(Optional, Description="If true, preview the change without applying it")) + bool DryRun = false; + + virtual FString GetDescription() const override + { + return TEXT("Remove an expression node from a material or material function graph."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override + { + if (Material.IsEmpty() && MaterialFunction.IsEmpty()) + { + return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: 'material' or 'materialFunction'")); + } + + // Load material or material function + UMaterial* MaterialObj = nullptr; + UMaterialFunction* MatFunc = nullptr; + FString AssetDisplayName; + + if (!MaterialFunction.IsEmpty()) + { + MCPAssets MFAssets; + if (!MFAssets.Exact(MaterialFunction).Errors(Result).ENone().ETwo().Load()) return; + MatFunc = MFAssets.Object(); + AssetDisplayName = MatFunc->GetName(); + } + else + { + MCPAssets MatAssets; + if (!MatAssets.Exact(Material).Errors(Result).ENone().ETwo().Load()) return; + MaterialObj = MatAssets.Object(); + AssetDisplayName = MaterialObj->GetName(); + } + + // For materials, we need the graph to find nodes by GUID + if (MaterialObj) MCPUtils::EnsureMaterialGraph(MaterialObj); + UEdGraph* Graph = MaterialObj ? (UEdGraph*)MaterialObj->MaterialGraph : (MatFunc ? MatFunc->MaterialGraph : nullptr); + if (!Graph) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("'%s' has no material graph"), *AssetDisplayName)); + } + + // Find the node by GUID + UMaterialGraphNode* TargetMatNode = nullptr; + for (UEdGraphNode* GraphNode : Graph->Nodes) + { + if (!GraphNode) continue; + if (GraphNode->NodeGuid.ToString() == Node) + { + TargetMatNode = Cast(GraphNode); + break; + } + } + + if (!TargetMatNode) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found in material graph"), *Node)); + } + + if (!TargetMatNode->MaterialExpression) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' has no associated material expression"), *Node)); + } + + // Capture info before deletion + FString DeletedNodeTitle = TargetMatNode->GetNodeTitle(ENodeTitleType::FullTitle).ToString(); + FString DeletedExprClass = TargetMatNode->MaterialExpression->GetClass()->GetName(); + + if (DryRun) + { + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: [DRY RUN] Would delete expression '%s' (nodeId: %s) from '%s'"), + *DeletedExprClass, *Node, *AssetDisplayName); + + Result->SetBoolField(TEXT("dryRun"), true); + Result->SetStringField(TEXT("material"), AssetDisplayName); + Result->SetStringField(TEXT("deletedNodeTitle"), DeletedNodeTitle); + Result->SetStringField(TEXT("deletedExpressionClass"), DeletedExprClass); + return; + } + + // Remove the expression + UMaterialExpression* ExprToRemove = TargetMatNode->MaterialExpression; + if (MaterialObj) + { + MaterialObj->GetExpressionCollection().RemoveExpression(ExprToRemove); + } + else + { + MatFunc->GetExpressionCollection().RemoveExpression(ExprToRemove); + } + ExprToRemove->MarkAsGarbage(); + + // Rebuild graph + Graph->NotifyGraphChanged(); + + UObject* Asset = MaterialObj ? (UObject*)MaterialObj : (UObject*)MatFunc; + Asset->PreEditChange(nullptr); + Asset->PostEditChange(); + Asset->MarkPackageDirty(); + + // Save + bool bSaved = MaterialObj ? MCPUtils::SaveMaterialPackage(MaterialObj) : MCPUtils::SaveGenericPackage(MatFunc); + + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Deleted expression '%s' (nodeId: %s) from '%s' (saved: %s)"), + *DeletedExprClass, *Node, *AssetDisplayName, bSaved ? TEXT("true") : TEXT("false")); + + Result->SetStringField(TEXT("material"), AssetDisplayName); + Result->SetStringField(TEXT("deletedNodeTitle"), DeletedNodeTitle); + Result->SetStringField(TEXT("deletedExpressionClass"), DeletedExprClass); + Result->SetBoolField(TEXT("saved"), bSaved); + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_DeleteNodeFromGraph.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_DeleteNodeFromGraph.h new file mode 100644 index 00000000..8ba83248 --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_DeleteNodeFromGraph.h @@ -0,0 +1,138 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPAssetFinder.h" +#include "MCPServer.h" +#include "MCPUtils.h" +#include "Engine/Blueprint.h" +#include "Materials/Material.h" +#include "Materials/MaterialInstanceConstant.h" +#include "Materials/MaterialFunction.h" +#include "Engine/World.h" +#include "Engine/LevelScriptBlueprint.h" +#include "EdGraph/EdGraph.h" +#include "EdGraph/EdGraphNode.h" +#include "EdGraph/EdGraphPin.h" +#include "EdGraphSchema_K2.h" +#include "K2Node.h" +#include "K2Node_CallFunction.h" +#include "K2Node_Event.h" +#include "K2Node_CustomEvent.h" +#include "K2Node_FunctionEntry.h" +#include "K2Node_EditablePinBase.h" +#include "K2Node_VariableGet.h" +#include "K2Node_VariableSet.h" +#include "K2Node_BreakStruct.h" +#include "K2Node_MakeStruct.h" +#include "K2Node_DynamicCast.h" +#include "K2Node_CallParentFunction.h" +#include "K2Node_IfThenElse.h" +#include "K2Node_ExecutionSequence.h" +#include "K2Node_MacroInstance.h" +#include "K2Node_SpawnActorFromClass.h" +#include "K2Node_Select.h" +#include "K2Node_Knot.h" +#include "EdGraphNode_Comment.h" +#include "GameFramework/Actor.h" +#include "Kismet2/BlueprintEditorUtils.h" +#include "Kismet2/KismetEditorUtilities.h" +#include "Serialization/JsonReader.h" +#include "Serialization/JsonWriter.h" +#include "Serialization/JsonSerializer.h" +#include "UObject/SavePackage.h" +#include "UObject/UObjectIterator.h" +#include "Misc/PackageName.h" +#include "AssetRegistry/AssetRegistryModule.h" +#include "AssetRegistry/IAssetRegistry.h" +#include "AssetToolsModule.h" +#include "IAssetTools.h" +#include "BlueprintNodeSpawner.h" +#include "UMCPHandler_DeleteNodeFromGraph.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +UCLASS() +class UMCPHandler_DeleteNodeFromGraph : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Blueprint name or package path")) + FString Blueprint; + + UPROPERTY(meta=(Description="Node GUID")) + FString Node; + + virtual FString GetDescription() const override + { + return TEXT("Delete a node from a Blueprint graph. " + "Cannot delete entry nodes (FunctionEntry, Event, CustomEvent)."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override + { + + MCPAssets Assets; + if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; + UBlueprint* BP = Assets.Object(); + + UEdGraph* Graph = nullptr; + UEdGraphNode* FoundNode = MCPUtils::FindNodeByGuid(BP, Node, &Graph); + if (!FoundNode) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found"), *Node)); + } + if (!Graph) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Graph not found for node '%s'"), *Node)); + } + + FString NodeClass = FoundNode->GetClass()->GetName(); + FString NodeTitle = FoundNode->GetNodeTitle(ENodeTitleType::FullTitle).ToString(); + FString GraphName = Graph->GetName(); + + // Protect root/entry nodes — deleting these leaves the graph in an invalid + // state with no root node, causing compiler errors that can't be fixed + // without recreating the entire function/event. + if (Cast(FoundNode)) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf( + TEXT("Cannot delete FunctionEntry node '%s' in graph '%s'. ") + TEXT("This is the root node of the function — removing it would leave an empty, uncompilable graph. ") + TEXT("To remove the entire function, delete it from the Blueprint editor."), + *NodeTitle, *GraphName)); + } + if (Cast(FoundNode)) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf( + TEXT("Cannot delete event entry node '%s' in graph '%s'. ") + TEXT("This is the root node of the event handler — removing it would leave an empty, uncompilable graph."), + *NodeTitle, *GraphName)); + } + if (Cast(FoundNode)) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf( + TEXT("Cannot delete CustomEvent entry node '%s' in graph '%s'. ") + TEXT("This is the root node of the custom event — removing it would leave an empty, uncompilable graph."), + *NodeTitle, *GraphName)); + } + + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Deleting node '%s' (%s) from graph '%s' in '%s'"), + *Node, *NodeTitle, *GraphName, *Blueprint); + + FoundNode->BreakAllNodeLinks(); + Graph->RemoveNode(FoundNode); + + FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP); + + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Node deleted")); + + Result->SetStringField(TEXT("nodeClass"), NodeClass); + Result->SetStringField(TEXT("nodeTitle"), NodeTitle); + Result->SetStringField(TEXT("graph"), GraphName); + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_DescribeMaterialInEnglish.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_DescribeMaterialInEnglish.h new file mode 100644 index 00000000..bab423c5 --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_DescribeMaterialInEnglish.h @@ -0,0 +1,245 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPAssetFinder.h" +#include "MCPUtils.h" +#include "Materials/Material.h" +#include "MaterialDomain.h" +#include "Materials/MaterialInstanceConstant.h" +#include "Materials/MaterialFunction.h" +#include "Materials/MaterialExpression.h" +#include "Materials/MaterialExpressionScalarParameter.h" +#include "Materials/MaterialExpressionVectorParameter.h" +#include "Materials/MaterialExpressionTextureObjectParameter.h" +#include "Materials/MaterialExpressionTextureSampleParameter2D.h" +#include "Materials/MaterialExpressionStaticSwitchParameter.h" +#include "Materials/MaterialExpressionConstant.h" +#include "Materials/MaterialExpressionConstant3Vector.h" +#include "Materials/MaterialExpressionConstant4Vector.h" +#include "Materials/MaterialExpressionTextureSample.h" +#include "Materials/MaterialExpressionTextureCoordinate.h" +#include "Materials/MaterialExpressionComponentMask.h" +#include "Materials/MaterialExpressionCustom.h" +#include "Materials/MaterialExpressionFunctionInput.h" +#include "Materials/MaterialExpressionFunctionOutput.h" +#include "Materials/MaterialExpressionMaterialFunctionCall.h" +#include "MaterialGraph/MaterialGraph.h" +#include "MaterialGraph/MaterialGraphNode.h" +#include "MaterialGraph/MaterialGraphNode_Root.h" +#include "MaterialGraph/MaterialGraphSchema.h" +#include "Kismet2/BlueprintEditorUtils.h" +#include "AssetRegistry/AssetRegistryModule.h" +#include "AssetRegistry/IAssetRegistry.h" +#include "EdGraph/EdGraph.h" +#include "EdGraph/EdGraphNode.h" +#include "UMCPHandler_DescribeMaterialInEnglish.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +UCLASS() +class UMCPHandler_DescribeMaterialInEnglish : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Material name or package path")) + FString Material; + + virtual FString GetDescription() const override + { + return TEXT("Generate a human-readable description of a material by tracing its expression graph from the root node inputs."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override + { + MCPAssets Assets; + if (!Assets.Exact(Material).Errors(Result).ENone().ETwo().Load()) return; + UMaterial* MaterialObj = Assets.Object(); + + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: DescribeMaterial — '%s'"), *MaterialObj->GetName()); + + // Ensure material graph is built + if (!MaterialObj->MaterialGraph) + { + MaterialObj->MaterialGraph = CastChecked( + FBlueprintEditorUtils::CreateNewGraph(MaterialObj, NAME_None, UMaterialGraph::StaticClass(), UMaterialGraphSchema::StaticClass())); + MaterialObj->MaterialGraph->Material = MaterialObj; + MaterialObj->MaterialGraph->RebuildGraph(); + } + + if (!MaterialObj->MaterialGraph) + { + return MCPUtils::MakeErrorJson(Result, TEXT("Could not build MaterialGraph for this material")); + } + + // Recursive helper: trace backwards from a pin and build a description string + TFunction TracePin = [&TracePin](UEdGraphPin* Pin, int32 Depth) -> FString + { + if (!Pin || Depth > 10) + return TEXT("(unknown)"); + + // If no connections, report as unconnected + if (Pin->LinkedTo.Num() == 0) + { + if (!Pin->DefaultValue.IsEmpty()) + return FString::Printf(TEXT("(default: %s)"), *Pin->DefaultValue); + return TEXT("(unconnected)"); + } + + TArray Sources; + for (UEdGraphPin* LinkedPin : Pin->LinkedTo) + { + if (!LinkedPin || !LinkedPin->GetOwningNode()) continue; + + UEdGraphNode* SourceNode = LinkedPin->GetOwningNode(); + FString NodeDesc; + + // Check if this is a material graph node + if (UMaterialGraphNode* MatNode = Cast(SourceNode)) + { + UMaterialExpression* Expr = MatNode->MaterialExpression; + if (!Expr) + { + NodeDesc = TEXT("(null expression)"); + } + else if (auto* SP = Cast(Expr)) + { + NodeDesc = FString::Printf(TEXT("ScalarParam \"%s\" (default: %.4f)"), *SP->ParameterName.ToString(), SP->DefaultValue); + } + else if (auto* VP = Cast(Expr)) + { + NodeDesc = FString::Printf(TEXT("VectorParam \"%s\" (default: R=%.2f G=%.2f B=%.2f A=%.2f)"), + *VP->ParameterName.ToString(), VP->DefaultValue.R, VP->DefaultValue.G, VP->DefaultValue.B, VP->DefaultValue.A); + } + else if (auto* TP = Cast(Expr)) + { + FString TexName = TP->Texture ? TP->Texture->GetName() : TEXT("None"); + NodeDesc = FString::Printf(TEXT("TextureParam \"%s\" (%s)"), *TP->ParameterName.ToString(), *TexName); + } + else if (auto* SSP = Cast(Expr)) + { + NodeDesc = FString::Printf(TEXT("StaticSwitchParam \"%s\" (default: %s)"), + *SSP->ParameterName.ToString(), SSP->DefaultValue ? TEXT("true") : TEXT("false")); + } + else if (auto* SC = Cast(Expr)) + { + NodeDesc = FString::Printf(TEXT("Constant(%.4f)"), SC->R); + } + else if (auto* C3 = Cast(Expr)) + { + NodeDesc = FString::Printf(TEXT("Constant3(R=%.2f G=%.2f B=%.2f)"), C3->Constant.R, C3->Constant.G, C3->Constant.B); + } + else if (auto* C4 = Cast(Expr)) + { + NodeDesc = FString::Printf(TEXT("Constant4(R=%.2f G=%.2f B=%.2f A=%.2f)"), C4->Constant.R, C4->Constant.G, C4->Constant.B, C4->Constant.A); + } + else if (auto* TS = Cast(Expr)) + { + FString TexName = TS->Texture ? TS->Texture->GetName() : TEXT("None"); + NodeDesc = FString::Printf(TEXT("TextureSample(%s)"), *TexName); + } + else if (auto* MFC = Cast(Expr)) + { + FString FuncName = MFC->MaterialFunction ? MFC->MaterialFunction->GetName() : TEXT("None"); + NodeDesc = FString::Printf(TEXT("FunctionCall(%s)"), *FuncName); + } + else + { + NodeDesc = Expr->GetClass()->GetName(); + } + + // If the source node has input pins with connections, recurse + TArray InputDescs; + for (UEdGraphPin* InputPin : SourceNode->Pins) + { + if (!InputPin || InputPin->Direction != EGPD_Input || InputPin->LinkedTo.Num() == 0) continue; + FString InputDesc = TracePin(InputPin, Depth + 1); + InputDescs.Add(InputDesc); + } + + if (InputDescs.Num() > 0) + { + NodeDesc += TEXT(" <- (") + FString::Join(InputDescs, TEXT(", ")) + TEXT(")"); + } + } + else + { + // Non-material node (e.g., root, comment), just use title + NodeDesc = SourceNode->GetNodeTitle(ENodeTitleType::FullTitle).ToString(); + } + + Sources.Add(NodeDesc); + } + + if (Sources.Num() == 1) + return Sources[0]; + + return TEXT("(") + FString::Join(Sources, TEXT(", ")) + TEXT(")"); + }; + + // Find root node and trace each input + TArray> InputDescriptions; + + UMaterialGraphNode_Root* RootNode = nullptr; + for (UEdGraphNode* Node : MaterialObj->MaterialGraph->Nodes) + { + RootNode = Cast(Node); + if (RootNode) break; + } + + if (!RootNode) + { + return MCPUtils::MakeErrorJson(Result, TEXT("Could not find root node in material graph")); + } + + for (UEdGraphPin* Pin : RootNode->Pins) + { + if (!Pin || Pin->Direction != EGPD_Input) continue; + + FString PinName = Pin->PinName.ToString(); + FString Description; + + if (Pin->LinkedTo.Num() == 0) + { + Description = TEXT("(unconnected)"); + } + else + { + Description = TracePin(Pin, 0); + } + + TSharedRef InputObj = MakeShared(); + InputObj->SetStringField(TEXT("input"), PinName); + InputObj->SetStringField(TEXT("chain"), Description); + InputObj->SetBoolField(TEXT("connected"), Pin->LinkedTo.Num() > 0); + InputDescriptions.Add(MakeShared(InputObj)); + } + + Result->SetStringField(TEXT("material"), MaterialObj->GetName()); + Result->SetStringField(TEXT("materialPath"), MaterialObj->GetPathName()); + Result->SetArrayField(TEXT("inputs"), InputDescriptions); + + // Also include a compact text description + FString TextDesc; + for (const TSharedPtr& Val : InputDescriptions) + { + TSharedPtr Obj = Val->AsObject(); + if (!Obj.IsValid()) continue; + FString InputName = Obj->GetStringField(TEXT("input")); + FString Chain = Obj->GetStringField(TEXT("chain")); + bool bConnected = Obj->GetBoolField(TEXT("connected")); + if (bConnected) + { + TextDesc += FString::Printf(TEXT("%s <- %s\n"), *InputName, *Chain); + } + } + if (!TextDesc.IsEmpty()) + { + Result->SetStringField(TEXT("description"), TextDesc); + } + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/MCPHandlers_DiffBlueprints.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_DiffTwoBlueprints.h similarity index 99% rename from Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/MCPHandlers_DiffBlueprints.h rename to Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_DiffTwoBlueprints.h index 9b146523..59031f66 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/MCPHandlers_DiffBlueprints.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_DiffTwoBlueprints.h @@ -9,7 +9,8 @@ #include "EdGraph/EdGraph.h" #include "EdGraph/EdGraphNode.h" #include "EdGraph/EdGraphPin.h" -#include "MCPHandlers_DiffBlueprints.generated.h" +#include "UMCPHandler_DiffTwoBlueprints.generated.h" + // --------------------------------------------------------------------------- // --------------------------------------------------------------------------- diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_DisconnectBlueprintPins.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_DisconnectBlueprintPins.h new file mode 100644 index 00000000..c71ca83e --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_DisconnectBlueprintPins.h @@ -0,0 +1,144 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPAssetFinder.h" +#include "MCPUtils.h" +#include "Engine/Blueprint.h" +#include "Engine/World.h" +#include "EdGraph/EdGraph.h" +#include "EdGraph/EdGraphNode.h" +#include "EdGraph/EdGraphPin.h" +#include "Kismet2/BlueprintEditorUtils.h" +#include "UMCPHandler_DisconnectBlueprintPins.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +USTRUCT() +struct FDisconnectPinEntry +{ + GENERATED_BODY() + + UPROPERTY() + FString Node; + + UPROPERTY() + FString PinName; + + UPROPERTY(meta=(Optional)) + FString TargetNode; + + UPROPERTY(meta=(Optional)) + FString TargetPinName; +}; + + +UCLASS() +class UMCPHandler_DisconnectBlueprintPins : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Blueprint name or package path")) + FString Blueprint; + + UPROPERTY(meta=(Description="Array of {node, pinName, targetNode?, targetPinName?} objects. If target is omitted, all connections on the pin are broken.")) + FMCPJsonArray Disconnections; + + virtual FString GetDescription() const override + { + return TEXT("Disconnect pins in a Blueprint graph. " + "Can disconnect a specific link or all links on a pin."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override + { + + MCPAssets Assets; + if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; + UBlueprint* BP = Assets.Object(); + + TArray> Results; + int32 SuccessCount = 0; + int32 TotalDisconnected = 0; + + for (const TSharedPtr& DiscVal : Disconnections.Array) + { + TSharedRef EntryResult = MakeShared(); + Results.Add(MakeShared(EntryResult)); + + FDisconnectPinEntry Entry; + if (!MCPUtils::PopulateFromJson(FDisconnectPinEntry::StaticStruct(), &Entry, DiscVal, &*EntryResult)) continue; + + UEdGraphNode* Node = MCPUtils::FindNodeByGuid(BP, Entry.Node); + if (!Node) + { + EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Node '%s' not found"), *Entry.Node)); + continue; + } + + UEdGraphPin* Pin = Node->FindPin(FName(*Entry.PinName)); + if (!Pin) + { + EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Pin '%s' not found on node '%s'"), *Entry.PinName, *Entry.Node)); + continue; + } + + int32 DisconnectedCount = 0; + + if (!Entry.TargetNode.IsEmpty() && !Entry.TargetPinName.IsEmpty()) + { + UEdGraphNode* TargetNode = MCPUtils::FindNodeByGuid(BP, Entry.TargetNode); + if (!TargetNode) + { + EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Target node '%s' not found"), *Entry.TargetNode)); + continue; + } + + UEdGraphPin* TargetPin = TargetNode->FindPin(FName(*Entry.TargetPinName)); + if (!TargetPin) + { + EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Target pin '%s' not found on node '%s'"), *Entry.TargetPinName, *Entry.TargetNode)); + continue; + } + + if (!Pin->LinkedTo.Contains(TargetPin)) + { + EntryResult->SetStringField(TEXT("error"), TEXT("The specified pins are not connected to each other")); + continue; + } + + Pin->BreakLinkTo(TargetPin); + DisconnectedCount = 1; + } + else + { + DisconnectedCount = Pin->LinkedTo.Num(); + if (DisconnectedCount > 0) + { + Pin->BreakAllPinLinks(true); + } + } + + EntryResult->SetNumberField(TEXT("disconnectedCount"), DisconnectedCount); + SuccessCount++; + TotalDisconnected += DisconnectedCount; + } + + if (TotalDisconnected > 0) + { + FBlueprintEditorUtils::MarkBlueprintAsModified(BP); + } + + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: DisconnectPin — %d/%d succeeded, %d links broken in '%s'"), + SuccessCount, Disconnections.Array.Num(), TotalDisconnected, *Blueprint); + + Result->SetNumberField(TEXT("successCount"), SuccessCount); + Result->SetNumberField(TEXT("totalCount"), Disconnections.Array.Num()); + Result->SetNumberField(TEXT("totalDisconnected"), TotalDisconnected); + Result->SetArrayField(TEXT("results"), Results); + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_DisconnectMaterialExpressionPin.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_DisconnectMaterialExpressionPin.h new file mode 100644 index 00000000..1fdc01ba --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_DisconnectMaterialExpressionPin.h @@ -0,0 +1,177 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPAssetFinder.h" +#include "MCPUtils.h" +#include "Materials/Material.h" +#include "MaterialDomain.h" +#include "Materials/MaterialInstanceConstant.h" +#include "Materials/MaterialFunction.h" +#include "Materials/MaterialExpression.h" +#include "Materials/MaterialExpressionScalarParameter.h" +#include "Materials/MaterialExpressionVectorParameter.h" +#include "Materials/MaterialExpressionTextureObjectParameter.h" +#include "Materials/MaterialExpressionTextureSampleParameter2D.h" +#include "Materials/MaterialExpressionStaticSwitchParameter.h" +#include "Materials/MaterialExpressionConstant.h" +#include "Materials/MaterialExpressionConstant3Vector.h" +#include "Materials/MaterialExpressionConstant4Vector.h" +#include "Materials/MaterialExpressionTextureSample.h" +#include "Materials/MaterialExpressionTextureCoordinate.h" +#include "Materials/MaterialExpressionComponentMask.h" +#include "Materials/MaterialExpressionCustom.h" +#include "Materials/MaterialExpressionFunctionInput.h" +#include "Materials/MaterialExpressionFunctionOutput.h" +#include "Materials/MaterialExpressionMaterialFunctionCall.h" +#include "MaterialGraph/MaterialGraph.h" +#include "MaterialGraph/MaterialGraphNode.h" +#include "MaterialGraph/MaterialGraphSchema.h" +#include "Factories/MaterialFactoryNew.h" +#include "Factories/MaterialFunctionFactoryNew.h" +#include "AssetToolsModule.h" +#include "IAssetTools.h" +#include "AssetRegistry/AssetRegistryModule.h" +#include "EdGraph/EdGraph.h" +#include "EdGraph/EdGraphNode.h" +#include "Serialization/JsonReader.h" +#include "Serialization/JsonWriter.h" +#include "Serialization/JsonSerializer.h" +#include "Misc/Guid.h" +#include "Misc/FileHelper.h" +#include "Misc/Paths.h" +#include "UObject/SavePackage.h" +#include "UObject/UObjectIterator.h" +#include "Kismet2/BlueprintEditorUtils.h" +#include "UMCPHandler_DisconnectMaterialExpressionPin.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +UCLASS() +class UMCPHandler_DisconnectMaterialExpressionPin : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Optional, Description="Material name or package path (specify this or materialFunction)")) + FString Material; + + UPROPERTY(meta=(Optional, Description="Material function name or package path (specify this or material)")) + FString MaterialFunction; + + UPROPERTY(meta=(Description="Node GUID of the node whose pin to disconnect")) + FString Node; + + UPROPERTY(meta=(Description="Pin name to disconnect")) + FString PinName; + + UPROPERTY(meta=(Optional, Description="If true, preview the change without applying it")) + bool DryRun = false; + + virtual FString GetDescription() const override + { + return TEXT("Break all connections on a specific pin in a material or material function graph."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override + { + if (Material.IsEmpty() && MaterialFunction.IsEmpty()) + { + return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: 'material' or 'materialFunction'")); + } + + // Load material or material function + UMaterial* MaterialObj = nullptr; + UMaterialFunction* MatFunc = nullptr; + FString AssetDisplayName; + + if (!MaterialFunction.IsEmpty()) + { + MCPAssets MFAssets; + if (!MFAssets.Exact(MaterialFunction).Errors(Result).ENone().ETwo().Load()) return; + MatFunc = MFAssets.Object(); + AssetDisplayName = MatFunc->GetName(); + } + else + { + MCPAssets MatAssets; + if (!MatAssets.Exact(Material).Errors(Result).ENone().ETwo().Load()) return; + MaterialObj = MatAssets.Object(); + AssetDisplayName = MaterialObj->GetName(); + } + + if (MaterialObj) MCPUtils::EnsureMaterialGraph(MaterialObj); + UEdGraph* Graph = MaterialObj ? (UEdGraph*)MaterialObj->MaterialGraph : (MatFunc ? MatFunc->MaterialGraph : nullptr); + if (!Graph) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("'%s' has no material graph"), *AssetDisplayName)); + } + + // Find node by GUID + UEdGraphNode* TargetGraphNode = nullptr; + for (UEdGraphNode* GraphNode : Graph->Nodes) + { + if (!GraphNode) continue; + if (GraphNode->NodeGuid.ToString() == Node) + { + TargetGraphNode = GraphNode; + break; + } + } + + if (!TargetGraphNode) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found in material graph"), *Node)); + } + + // Find pin + UEdGraphPin* Pin = TargetGraphNode->FindPin(FName(*PinName)); + if (!Pin) + { + TArray> PinNames; + for (UEdGraphPin* P : TargetGraphNode->Pins) + { + if (P) PinNames.Add(MakeShared( + FString::Printf(TEXT("%s (%s)"), *P->PinName.ToString(), + P->Direction == EGPD_Input ? TEXT("Input") : TEXT("Output")))); + } + MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Pin '%s' not found on node '%s'"), + *PinName, *Node)); + Result->SetArrayField(TEXT("availablePins"), PinNames); + return; + } + + int32 BrokenCount = Pin->LinkedTo.Num(); + + if (DryRun) + { + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: [DRY RUN] Would disconnect pin '%s' on node '%s' in '%s' (%d links)"), + *PinName, *Node, *AssetDisplayName, BrokenCount); + + Result->SetBoolField(TEXT("dryRun"), true); + Result->SetStringField(TEXT("material"), AssetDisplayName); + Result->SetNumberField(TEXT("brokenLinkCount"), BrokenCount); + return; + } + + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Disconnecting pin '%s' on node '%s' in '%s' (%d links)"), + *PinName, *Node, *AssetDisplayName, BrokenCount); + + // Break all links + Pin->BreakAllPinLinks(); + + UObject* Asset = MaterialObj ? (UObject*)MaterialObj : (UObject*)MatFunc; + Asset->PreEditChange(nullptr); + Asset->PostEditChange(); + + // Save + bool bSaved = MaterialObj ? MCPUtils::SaveMaterialPackage(MaterialObj) : MCPUtils::SaveGenericPackage(MatFunc); + + Result->SetStringField(TEXT("material"), AssetDisplayName); + Result->SetNumberField(TEXT("brokenLinkCount"), BrokenCount); + Result->SetBoolField(TEXT("saved"), bSaved); + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_DumpBlueprint.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_DumpBlueprint.h new file mode 100644 index 00000000..3dcf6698 --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_DumpBlueprint.h @@ -0,0 +1,52 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPAssetFinder.h" +#include "MCPUtils.h" +#include "Engine/Blueprint.h" +#include "Engine/World.h" +#include "Engine/Level.h" +#include "Engine/LevelScriptBlueprint.h" +#include "EdGraph/EdGraph.h" +#include "EdGraph/EdGraphNode.h" +#include "K2Node_CallFunction.h" +#include "K2Node_Event.h" +#include "K2Node_CustomEvent.h" +#include "K2Node_VariableGet.h" +#include "K2Node_VariableSet.h" +#include "K2Node_BreakStruct.h" +#include "K2Node_MakeStruct.h" +#include "K2Node_FunctionEntry.h" +#include "K2Node_EditablePinBase.h" +#include "AssetRegistry/IAssetRegistry.h" +#include "UMCPHandler_DumpBlueprint.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +UCLASS() +class UMCPHandler_DumpBlueprint : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Blueprint name or package path")) + FString Blueprint; + + virtual FString GetDescription() const override + { + return TEXT("Load and serialize a Blueprint, returning its full structure including graphs, variables, and components."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override + { + MCPAssets Assets; + if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; + + TSharedRef Tmp = MCPUtils::SerializeBlueprint(Assets.Object()); + MCPUtils::CopyJsonFields(&*Tmp, Result); + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_DumpBlueprintGraph.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_DumpBlueprintGraph.h new file mode 100644 index 00000000..570fe382 --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_DumpBlueprintGraph.h @@ -0,0 +1,80 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPAssetFinder.h" +#include "MCPUtils.h" +#include "Engine/Blueprint.h" +#include "Engine/World.h" +#include "Engine/Level.h" +#include "Engine/LevelScriptBlueprint.h" +#include "EdGraph/EdGraph.h" +#include "EdGraph/EdGraphNode.h" +#include "K2Node_CallFunction.h" +#include "K2Node_Event.h" +#include "K2Node_CustomEvent.h" +#include "K2Node_VariableGet.h" +#include "K2Node_VariableSet.h" +#include "K2Node_BreakStruct.h" +#include "K2Node_MakeStruct.h" +#include "K2Node_FunctionEntry.h" +#include "K2Node_EditablePinBase.h" +#include "AssetRegistry/IAssetRegistry.h" +#include "UMCPHandler_DumpBlueprintGraph.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +UCLASS() +class UMCPHandler_DumpBlueprintGraph : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Blueprint name or package path")) + FString Blueprint; + + UPROPERTY(meta=(Description="Graph name to dump")) + FString Graph; + + virtual FString GetDescription() const override + { + return TEXT("Dump the detailed node/pin structure of a specific graph within a Blueprint."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override + { + // URL-decode graph name to handle spaces encoded as '+' or '%20' + FString DecodedGraphName = MCPUtils::UrlDecode(Graph); + + MCPAssets Assets; + if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; + UBlueprint* BP = Assets.Object(); + + TArray AllGraphs = MCPUtils::AllGraphs(BP); + + for (UEdGraph* GraphObj : AllGraphs) + { + if (GraphObj->GetName().Equals(DecodedGraphName, ESearchCase::IgnoreCase)) + { + TSharedPtr GraphJson = MCPUtils::SerializeGraph(GraphObj); + if (GraphJson.IsValid()) + { + MCPUtils::CopyJsonFields(GraphJson.Get(), Result); + return; + } + } + } + + // Not found — list available graphs + TArray> GraphNames; + for (UEdGraph* GraphObj : AllGraphs) + { + GraphNames.Add(MakeShared(GraphObj->GetName())); + } + MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Graph '%s' not found"), *DecodedGraphName)); + Result->SetArrayField(TEXT("availableGraphs"), GraphNames); + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_DumpMaterial.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_DumpMaterial.h new file mode 100644 index 00000000..c7c76feb --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_DumpMaterial.h @@ -0,0 +1,293 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPAssetFinder.h" +#include "MCPUtils.h" +#include "Materials/Material.h" +#include "MaterialDomain.h" +#include "Materials/MaterialInstanceConstant.h" +#include "Materials/MaterialFunction.h" +#include "Materials/MaterialExpression.h" +#include "Materials/MaterialExpressionScalarParameter.h" +#include "Materials/MaterialExpressionVectorParameter.h" +#include "Materials/MaterialExpressionTextureObjectParameter.h" +#include "Materials/MaterialExpressionTextureSampleParameter2D.h" +#include "Materials/MaterialExpressionStaticSwitchParameter.h" +#include "Materials/MaterialExpressionConstant.h" +#include "Materials/MaterialExpressionConstant3Vector.h" +#include "Materials/MaterialExpressionConstant4Vector.h" +#include "Materials/MaterialExpressionTextureSample.h" +#include "Materials/MaterialExpressionTextureCoordinate.h" +#include "Materials/MaterialExpressionComponentMask.h" +#include "Materials/MaterialExpressionCustom.h" +#include "Materials/MaterialExpressionFunctionInput.h" +#include "Materials/MaterialExpressionFunctionOutput.h" +#include "Materials/MaterialExpressionMaterialFunctionCall.h" +#include "MaterialGraph/MaterialGraph.h" +#include "MaterialGraph/MaterialGraphNode.h" +#include "MaterialGraph/MaterialGraphNode_Root.h" +#include "MaterialGraph/MaterialGraphSchema.h" +#include "Kismet2/BlueprintEditorUtils.h" +#include "AssetRegistry/AssetRegistryModule.h" +#include "AssetRegistry/IAssetRegistry.h" +#include "EdGraph/EdGraph.h" +#include "EdGraph/EdGraphNode.h" +#include "UMCPHandler_DumpMaterial.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +UCLASS() +class UMCPHandler_DumpMaterial : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Material or MaterialInstance name or package path")) + FString Material; + + virtual FString GetDescription() const override + { + return TEXT("Get detailed info about a material or material instance, including parameters, usage flags, and referenced textures."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override + { + FString DecodedName = MCPUtils::UrlDecode(Material); + + // Try loading as UMaterial or UMaterialInstanceConstant + MCPAssets Assets; + Assets.Scan(UMaterial::StaticClass()); + Assets.Scan(UMaterialInstanceConstant::StaticClass()); + if (!Assets.Exact(DecodedName).Errors(Result).ENone().ETwo().Load()) return; + UMaterialInterface* LoadedObj = Assets.Object(); + + if (UMaterial* MaterialObj = Cast(LoadedObj)) + { + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: GetMaterial — loaded material '%s'"), *MaterialObj->GetName()); + + Result->SetStringField(TEXT("name"), MaterialObj->GetName()); + Result->SetStringField(TEXT("path"), MaterialObj->GetPathName()); + Result->SetStringField(TEXT("type"), TEXT("Material")); + + // Material domain + FString DomainStr = TEXT("Unknown"); + if (const UEnum* DomainEnum = StaticEnum()) + { + DomainStr = DomainEnum->GetNameStringByValue((int64)MaterialObj->MaterialDomain); + } + Result->SetStringField(TEXT("domain"), DomainStr); + + // Blend mode + FString BlendModeStr = TEXT("Unknown"); + if (const UEnum* BlendEnum = StaticEnum()) + { + BlendModeStr = BlendEnum->GetNameStringByValue((int64)MaterialObj->BlendMode); + } + Result->SetStringField(TEXT("blendMode"), BlendModeStr); + + // Shading models + TArray> ShadingModels; + FMaterialShadingModelField SMField = MaterialObj->GetShadingModels(); + if (const UEnum* SMEnum = StaticEnum()) + { + for (int32 i = 0; i < SMEnum->NumEnums() - 1; ++i) + { + EMaterialShadingModel SM = (EMaterialShadingModel)SMEnum->GetValueByIndex(i); + if (SMField.HasShadingModel(SM)) + { + ShadingModels.Add(MakeShared(SMEnum->GetNameStringByIndex(i))); + } + } + } + Result->SetArrayField(TEXT("shadingModels"), ShadingModels); + + // Two-sided + Result->SetBoolField(TEXT("twoSided"), MaterialObj->IsTwoSided()); + + // Expression count + auto Expressions = MaterialObj->GetExpressions(); + Result->SetNumberField(TEXT("expressionCount"), Expressions.Num()); + + // Parameters — iterate expressions for parameter types + TArray> Parameters; + for (UMaterialExpression* Expr : Expressions) + { + if (!Expr) continue; + + TSharedRef ParamObj = MakeShared(); + bool bIsParam = false; + + if (auto* SP = Cast(Expr)) + { + bIsParam = true; + ParamObj->SetStringField(TEXT("name"), SP->ParameterName.ToString()); + ParamObj->SetStringField(TEXT("type"), TEXT("Scalar")); + ParamObj->SetStringField(TEXT("group"), SP->Group.ToString()); + ParamObj->SetNumberField(TEXT("defaultValue"), SP->DefaultValue); + } + else if (auto* VP = Cast(Expr)) + { + bIsParam = true; + ParamObj->SetStringField(TEXT("name"), VP->ParameterName.ToString()); + ParamObj->SetStringField(TEXT("type"), TEXT("Vector")); + ParamObj->SetStringField(TEXT("group"), VP->Group.ToString()); + TSharedRef DefVal = MakeShared(); + DefVal->SetNumberField(TEXT("r"), VP->DefaultValue.R); + DefVal->SetNumberField(TEXT("g"), VP->DefaultValue.G); + DefVal->SetNumberField(TEXT("b"), VP->DefaultValue.B); + DefVal->SetNumberField(TEXT("a"), VP->DefaultValue.A); + ParamObj->SetObjectField(TEXT("defaultValue"), DefVal); + } + else if (auto* TP = Cast(Expr)) + { + bIsParam = true; + ParamObj->SetStringField(TEXT("name"), TP->ParameterName.ToString()); + ParamObj->SetStringField(TEXT("type"), TEXT("Texture")); + ParamObj->SetStringField(TEXT("group"), TP->Group.ToString()); + if (TP->Texture) + ParamObj->SetStringField(TEXT("defaultValue"), TP->Texture->GetPathName()); + } + else if (auto* SSP = Cast(Expr)) + { + bIsParam = true; + ParamObj->SetStringField(TEXT("name"), SSP->ParameterName.ToString()); + ParamObj->SetStringField(TEXT("type"), TEXT("StaticSwitch")); + ParamObj->SetStringField(TEXT("group"), SSP->Group.ToString()); + ParamObj->SetBoolField(TEXT("defaultValue"), SSP->DefaultValue); + } + + if (bIsParam) + { + Parameters.Add(MakeShared(ParamObj)); + } + } + Result->SetArrayField(TEXT("parameters"), Parameters); + + // Referenced textures + TArray> ReferencedTextures; + auto RefTexObjs = MaterialObj->GetReferencedTextures(); + for (const TObjectPtr& TexObj : RefTexObjs) + { + if (TexObj) + { + ReferencedTextures.Add(MakeShared(TexObj->GetPathName())); + } + } + Result->SetArrayField(TEXT("referencedTextures"), ReferencedTextures); + + // Graph node count + int32 GraphNodeCount = 0; + if (MaterialObj->MaterialGraph) + { + GraphNodeCount = MaterialObj->MaterialGraph->Nodes.Num(); + } + Result->SetNumberField(TEXT("graphNodeCount"), GraphNodeCount); + + // Usage flags + TSharedRef UsageFlags = MakeShared(); + UsageFlags->SetBoolField(TEXT("bUsedWithSkeletalMesh"), MaterialObj->bUsedWithSkeletalMesh != 0); + UsageFlags->SetBoolField(TEXT("bUsedWithMorphTargets"), MaterialObj->bUsedWithMorphTargets != 0); + UsageFlags->SetBoolField(TEXT("bUsedWithNiagaraSprites"), MaterialObj->bUsedWithNiagaraSprites != 0); + UsageFlags->SetBoolField(TEXT("bUsedWithParticleSprites"), MaterialObj->bUsedWithParticleSprites != 0); + UsageFlags->SetBoolField(TEXT("bUsedWithStaticLighting"), MaterialObj->bUsedWithStaticLighting != 0); + Result->SetObjectField(TEXT("usageFlags"), UsageFlags); + + // Opacity mask clip value + Result->SetNumberField(TEXT("opacityMaskClipValue"), MaterialObj->OpacityMaskClipValue); + + // Additional settings + Result->SetBoolField(TEXT("ditheredLODTransition"), MaterialObj->DitheredLODTransition != 0); + Result->SetBoolField(TEXT("bAllowNegativeEmissiveColor"), MaterialObj->bAllowNegativeEmissiveColor != 0); + + // Texture sample count (simple expression scan) + int32 TextureSampleCount = 0; + for (UMaterialExpression* Expr : Expressions) + { + if (Expr && Expr->IsA()) + { + TextureSampleCount++; + } + } + Result->SetNumberField(TEXT("textureSampleCount"), TextureSampleCount); + + return; + } + + if (UMaterialInstanceConstant* MI = Cast(LoadedObj)) + { + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: GetMaterial — loaded material instance '%s'"), *MI->GetName()); + + Result->SetStringField(TEXT("name"), MI->GetName()); + Result->SetStringField(TEXT("path"), MI->GetPathName()); + Result->SetStringField(TEXT("type"), TEXT("MaterialInstance")); + + if (MI->Parent) + { + Result->SetStringField(TEXT("parent"), MI->Parent->GetName()); + Result->SetStringField(TEXT("parentPath"), MI->Parent->GetPathName()); + } + + // Overridden parameters + TArray> OverriddenParams; + + // Scalar parameters + for (const FScalarParameterValue& Param : MI->ScalarParameterValues) + { + TSharedRef PObj = MakeShared(); + PObj->SetStringField(TEXT("name"), Param.ParameterInfo.Name.ToString()); + PObj->SetStringField(TEXT("type"), TEXT("Scalar")); + PObj->SetNumberField(TEXT("value"), Param.ParameterValue); + OverriddenParams.Add(MakeShared(PObj)); + } + + // Vector parameters + for (const FVectorParameterValue& Param : MI->VectorParameterValues) + { + TSharedRef PObj = MakeShared(); + PObj->SetStringField(TEXT("name"), Param.ParameterInfo.Name.ToString()); + PObj->SetStringField(TEXT("type"), TEXT("Vector")); + TSharedRef Val = MakeShared(); + Val->SetNumberField(TEXT("r"), Param.ParameterValue.R); + Val->SetNumberField(TEXT("g"), Param.ParameterValue.G); + Val->SetNumberField(TEXT("b"), Param.ParameterValue.B); + Val->SetNumberField(TEXT("a"), Param.ParameterValue.A); + PObj->SetObjectField(TEXT("value"), Val); + OverriddenParams.Add(MakeShared(PObj)); + } + + // Texture parameters + for (const FTextureParameterValue& Param : MI->TextureParameterValues) + { + TSharedRef PObj = MakeShared(); + PObj->SetStringField(TEXT("name"), Param.ParameterInfo.Name.ToString()); + PObj->SetStringField(TEXT("type"), TEXT("Texture")); + if (Param.ParameterValue) + PObj->SetStringField(TEXT("value"), Param.ParameterValue->GetPathName()); + else + PObj->SetStringField(TEXT("value"), TEXT("None")); + OverriddenParams.Add(MakeShared(PObj)); + } + + // Static switch parameters + for (const FStaticSwitchParameter& Param : MI->GetStaticParameters().StaticSwitchParameters) + { + TSharedRef PObj = MakeShared(); + PObj->SetStringField(TEXT("name"), Param.ParameterInfo.Name.ToString()); + PObj->SetStringField(TEXT("type"), TEXT("StaticSwitch")); + PObj->SetBoolField(TEXT("value"), Param.Value); + PObj->SetBoolField(TEXT("overridden"), Param.bOverride); + OverriddenParams.Add(MakeShared(PObj)); + } + + Result->SetArrayField(TEXT("overriddenParameters"), OverriddenParams); + + return; + } + + MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Material or MaterialInstance '%s' not found. Use list_materials to see available assets."), *DecodedName)); + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_DumpMaterialExpressionGraph.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_DumpMaterialExpressionGraph.h new file mode 100644 index 00000000..6c62de29 --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_DumpMaterialExpressionGraph.h @@ -0,0 +1,95 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPAssetFinder.h" +#include "MCPUtils.h" +#include "Materials/Material.h" +#include "MaterialDomain.h" +#include "Materials/MaterialInstanceConstant.h" +#include "Materials/MaterialFunction.h" +#include "Materials/MaterialExpression.h" +#include "Materials/MaterialExpressionScalarParameter.h" +#include "Materials/MaterialExpressionVectorParameter.h" +#include "Materials/MaterialExpressionTextureObjectParameter.h" +#include "Materials/MaterialExpressionTextureSampleParameter2D.h" +#include "Materials/MaterialExpressionStaticSwitchParameter.h" +#include "Materials/MaterialExpressionConstant.h" +#include "Materials/MaterialExpressionConstant3Vector.h" +#include "Materials/MaterialExpressionConstant4Vector.h" +#include "Materials/MaterialExpressionTextureSample.h" +#include "Materials/MaterialExpressionTextureCoordinate.h" +#include "Materials/MaterialExpressionComponentMask.h" +#include "Materials/MaterialExpressionCustom.h" +#include "Materials/MaterialExpressionFunctionInput.h" +#include "Materials/MaterialExpressionFunctionOutput.h" +#include "Materials/MaterialExpressionMaterialFunctionCall.h" +#include "MaterialGraph/MaterialGraph.h" +#include "MaterialGraph/MaterialGraphNode.h" +#include "MaterialGraph/MaterialGraphNode_Root.h" +#include "MaterialGraph/MaterialGraphSchema.h" +#include "Kismet2/BlueprintEditorUtils.h" +#include "AssetRegistry/AssetRegistryModule.h" +#include "AssetRegistry/IAssetRegistry.h" +#include "EdGraph/EdGraph.h" +#include "EdGraph/EdGraphNode.h" +#include "UMCPHandler_DumpMaterialExpressionGraph.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +UCLASS() +class UMCPHandler_DumpMaterialExpressionGraph : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Material name or package path")) + FString Material; + + virtual FString GetDescription() const override + { + return TEXT("Get the serialized expression graph for a material, including all nodes and connections."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override + { + FString DecodedName = MCPUtils::UrlDecode(Material); + + MCPAssets Assets; + if (!Assets.Exact(DecodedName).Errors(Result).ENone().ETwo().Load()) return; + UMaterial* MaterialObj = Assets.Object(); + + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: GetMaterialGraph — material '%s'"), *MaterialObj->GetName()); + + // Ensure the material graph is built + if (!MaterialObj->MaterialGraph) + { + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: GetMaterialGraph — MaterialGraph is null, attempting rebuild")); + // The material graph is built lazily by the material editor; force-create it + MaterialObj->MaterialGraph = CastChecked( + FBlueprintEditorUtils::CreateNewGraph(MaterialObj, NAME_None, UMaterialGraph::StaticClass(), UMaterialGraphSchema::StaticClass())); + MaterialObj->MaterialGraph->Material = MaterialObj; + MaterialObj->MaterialGraph->RebuildGraph(); + } + + if (!MaterialObj->MaterialGraph) + { + return MCPUtils::MakeErrorJson(Result, TEXT("Could not build MaterialGraph for this material")); + } + + TSharedPtr GraphJson = MCPUtils::SerializeGraph(MaterialObj->MaterialGraph); + if (!GraphJson.IsValid()) + { + return MCPUtils::MakeErrorJson(Result, TEXT("Failed to serialize material graph")); + } + + MCPUtils::CopyJsonFields(GraphJson.Get(), Result); + + // Add material name context + Result->SetStringField(TEXT("material"), MaterialObj->GetName()); + Result->SetStringField(TEXT("materialPath"), MaterialObj->GetPathName()); + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_DumpMaterialFunction.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_DumpMaterialFunction.h new file mode 100644 index 00000000..722f6c46 --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_DumpMaterialFunction.h @@ -0,0 +1,128 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPAssetFinder.h" +#include "MCPUtils.h" +#include "Materials/Material.h" +#include "MaterialDomain.h" +#include "Materials/MaterialInstanceConstant.h" +#include "Materials/MaterialFunction.h" +#include "Materials/MaterialExpression.h" +#include "Materials/MaterialExpressionScalarParameter.h" +#include "Materials/MaterialExpressionVectorParameter.h" +#include "Materials/MaterialExpressionTextureObjectParameter.h" +#include "Materials/MaterialExpressionTextureSampleParameter2D.h" +#include "Materials/MaterialExpressionStaticSwitchParameter.h" +#include "Materials/MaterialExpressionConstant.h" +#include "Materials/MaterialExpressionConstant3Vector.h" +#include "Materials/MaterialExpressionConstant4Vector.h" +#include "Materials/MaterialExpressionTextureSample.h" +#include "Materials/MaterialExpressionTextureCoordinate.h" +#include "Materials/MaterialExpressionComponentMask.h" +#include "Materials/MaterialExpressionCustom.h" +#include "Materials/MaterialExpressionFunctionInput.h" +#include "Materials/MaterialExpressionFunctionOutput.h" +#include "Materials/MaterialExpressionMaterialFunctionCall.h" +#include "MaterialGraph/MaterialGraph.h" +#include "MaterialGraph/MaterialGraphNode.h" +#include "MaterialGraph/MaterialGraphNode_Root.h" +#include "MaterialGraph/MaterialGraphSchema.h" +#include "Kismet2/BlueprintEditorUtils.h" +#include "AssetRegistry/AssetRegistryModule.h" +#include "AssetRegistry/IAssetRegistry.h" +#include "EdGraph/EdGraph.h" +#include "EdGraph/EdGraphNode.h" +#include "UMCPHandler_DumpMaterialFunction.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +UCLASS() +class UMCPHandler_DumpMaterialFunction : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="MaterialFunction name or package path")) + FString MaterialFunction; + + virtual FString GetDescription() const override + { + return TEXT("Get detailed info about a material function, including its inputs, outputs, and expressions."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override + { + FString DecodedName = MCPUtils::UrlDecode(MaterialFunction); + + MCPAssets Assets; + if (!Assets.Exact(DecodedName).Errors(Result).ENone().ETwo().Load()) return; + UMaterialFunction* MF = Assets.Object(); + + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: GetMaterialFunction — '%s'"), *MF->GetName()); + + Result->SetStringField(TEXT("name"), MF->GetName()); + Result->SetStringField(TEXT("path"), MF->GetPathName()); + Result->SetStringField(TEXT("description"), MF->GetDescription()); + + // Expression count + auto Expressions = MF->GetExpressions(); + Result->SetNumberField(TEXT("expressionCount"), Expressions.Num()); + + // List function inputs and outputs from expressions + TArray> Inputs; + TArray> Outputs; + TArray> ExpressionList; + + { + for (UMaterialExpression* Expr : Expressions) + { + if (!Expr) continue; + + if (auto* FI = Cast(Expr)) + { + TSharedRef InputObj = MakeShared(); + InputObj->SetStringField(TEXT("name"), FI->InputName.ToString()); + InputObj->SetStringField(TEXT("type"), TEXT("FunctionInput")); + InputObj->SetNumberField(TEXT("posX"), FI->MaterialExpressionEditorX); + InputObj->SetNumberField(TEXT("posY"), FI->MaterialExpressionEditorY); + Inputs.Add(MakeShared(InputObj)); + } + else if (auto* FO = Cast(Expr)) + { + TSharedRef OutputObj = MakeShared(); + OutputObj->SetStringField(TEXT("name"), FO->OutputName.ToString()); + OutputObj->SetStringField(TEXT("type"), TEXT("FunctionOutput")); + OutputObj->SetNumberField(TEXT("posX"), FO->MaterialExpressionEditorX); + OutputObj->SetNumberField(TEXT("posY"), FO->MaterialExpressionEditorY); + Outputs.Add(MakeShared(OutputObj)); + } + + // Serialize every expression + TSharedPtr ExprJson = MCPUtils::SerializeMaterialExpression(Expr); + if (ExprJson.IsValid()) + { + ExpressionList.Add(MakeShared(ExprJson.ToSharedRef())); + } + } + } + + Result->SetArrayField(TEXT("inputs"), Inputs); + Result->SetArrayField(TEXT("outputs"), Outputs); + Result->SetArrayField(TEXT("expressions"), ExpressionList); + + // If the function has an editor graph, serialize it + UEdGraph* FuncGraph = MF->MaterialGraph; + if (FuncGraph) + { + TSharedPtr GraphJson = MCPUtils::SerializeGraph(FuncGraph); + if (GraphJson.IsValid()) + { + Result->SetObjectField(TEXT("graph"), GraphJson); + } + } + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_DumpMaterialInstanceParameters.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_DumpMaterialInstanceParameters.h new file mode 100644 index 00000000..731b8ce4 --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_DumpMaterialInstanceParameters.h @@ -0,0 +1,264 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPAssetFinder.h" +#include "MCPUtils.h" +#include "Materials/Material.h" +#include "Materials/MaterialInterface.h" +#include "Materials/MaterialInstanceConstant.h" +#include "Materials/MaterialExpressionScalarParameter.h" +#include "Materials/MaterialExpressionVectorParameter.h" +#include "Materials/MaterialExpressionTextureSampleParameter2D.h" +#include "Materials/MaterialExpressionStaticSwitchParameter.h" +#include "Factories/MaterialInstanceConstantFactoryNew.h" +#include "AssetToolsModule.h" +#include "IAssetTools.h" +#include "Engine/Texture.h" +#include "UMCPHandler_DumpMaterialInstanceParameters.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +UCLASS() +class UMCPHandler_DumpMaterialInstanceParameters : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Material Instance name or path to inspect")) + FString MaterialInstance; + + virtual FString GetDescription() const override + { + return TEXT("List all parameters on a Material Instance, including overridden and inherited parameters."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override + { + MCPAssets Assets; + if (!Assets.Exact(MaterialInstance).Errors(Result).ENone().ETwo().Load()) return; + UMaterialInstanceConstant* MI = Assets.Object(); + + Result->SetStringField(TEXT("name"), MI->GetName()); + Result->SetStringField(TEXT("path"), MI->GetPathName()); + + // Parent info + if (MI->Parent) + { + Result->SetStringField(TEXT("parent"), MI->Parent->GetPathName()); + } + + // Build parent chain + TArray> ParentChainArr; + { + UMaterialInterface* Current = MI->Parent; + while (Current) + { + TSharedRef ParentObj = MakeShared(); + ParentObj->SetStringField(TEXT("name"), Current->GetName()); + ParentObj->SetStringField(TEXT("path"), Current->GetPathName()); + ParentObj->SetStringField(TEXT("class"), Current->GetClass()->GetName()); + ParentChainArr.Add(MakeShared(ParentObj)); + + UMaterialInstanceConstant* ParentMI = Cast(Current); + if (ParentMI) + { + Current = ParentMI->Parent; + } + else + { + break; // Reached the root Material + } + } + } + Result->SetArrayField(TEXT("parentChain"), ParentChainArr); + + // Scalar parameters + TArray> ScalarArr; + for (const FScalarParameterValue& Param : MI->ScalarParameterValues) + { + TSharedRef PObj = MakeShared(); + PObj->SetStringField(TEXT("name"), Param.ParameterInfo.Name.ToString()); + PObj->SetNumberField(TEXT("value"), Param.ParameterValue); + PObj->SetBoolField(TEXT("isOverridden"), true); // Present in ScalarParameterValues means it's overridden + ScalarArr.Add(MakeShared(PObj)); + } + Result->SetArrayField(TEXT("scalarParameters"), ScalarArr); + + // Vector parameters + TArray> VectorArr; + for (const FVectorParameterValue& Param : MI->VectorParameterValues) + { + TSharedRef PObj = MakeShared(); + PObj->SetStringField(TEXT("name"), Param.ParameterInfo.Name.ToString()); + PObj->SetNumberField(TEXT("r"), Param.ParameterValue.R); + PObj->SetNumberField(TEXT("g"), Param.ParameterValue.G); + PObj->SetNumberField(TEXT("b"), Param.ParameterValue.B); + PObj->SetNumberField(TEXT("a"), Param.ParameterValue.A); + PObj->SetBoolField(TEXT("isOverridden"), true); + VectorArr.Add(MakeShared(PObj)); + } + Result->SetArrayField(TEXT("vectorParameters"), VectorArr); + + // Texture parameters + TArray> TextureArr; + for (const FTextureParameterValue& Param : MI->TextureParameterValues) + { + TSharedRef PObj = MakeShared(); + PObj->SetStringField(TEXT("name"), Param.ParameterInfo.Name.ToString()); + if (Param.ParameterValue) + { + PObj->SetStringField(TEXT("texture"), Param.ParameterValue->GetPathName()); + } + else + { + PObj->SetStringField(TEXT("texture"), TEXT("None")); + } + PObj->SetBoolField(TEXT("isOverridden"), true); + TextureArr.Add(MakeShared(PObj)); + } + Result->SetArrayField(TEXT("textureParameters"), TextureArr); + + // Static switch parameters + TArray> StaticSwitchArr; + { + FStaticParameterSet StaticParams; + MI->GetStaticParameterValues(StaticParams); + + for (const FStaticSwitchParameter& Param : StaticParams.StaticSwitchParameters) + { + TSharedRef PObj = MakeShared(); + PObj->SetStringField(TEXT("name"), Param.ParameterInfo.Name.ToString()); + PObj->SetBoolField(TEXT("value"), Param.Value); + PObj->SetBoolField(TEXT("isOverridden"), Param.bOverride); + StaticSwitchArr.Add(MakeShared(PObj)); + } + } + Result->SetArrayField(TEXT("staticSwitchParameters"), StaticSwitchArr); + + // Also report inherited parameters from the parent material for discoverability + TArray> InheritedScalarArr; + TArray> InheritedVectorArr; + TArray> InheritedTextureArr; + TArray> InheritedStaticSwitchArr; + { + UMaterial* BaseMat = MI->GetMaterial(); + if (BaseMat) + { + // Collect names of already-overridden parameters for filtering + TSet OverriddenScalars; + for (const FScalarParameterValue& P : MI->ScalarParameterValues) + { + OverriddenScalars.Add(P.ParameterInfo.Name.ToString()); + } + TSet OverriddenVectors; + for (const FVectorParameterValue& P : MI->VectorParameterValues) + { + OverriddenVectors.Add(P.ParameterInfo.Name.ToString()); + } + TSet OverriddenTextures; + for (const FTextureParameterValue& P : MI->TextureParameterValues) + { + OverriddenTextures.Add(P.ParameterInfo.Name.ToString()); + } + TSet OverriddenStaticSwitches; + { + FStaticParameterSet SP; + MI->GetStaticParameterValues(SP); + for (const FStaticSwitchParameter& P : SP.StaticSwitchParameters) + { + if (P.bOverride) + { + OverriddenStaticSwitches.Add(P.ParameterInfo.Name.ToString()); + } + } + } + + for (UMaterialExpression* Expr : BaseMat->GetExpressions()) + { + if (auto* SP = Cast(Expr)) + { + if (!OverriddenScalars.Contains(SP->ParameterName.ToString())) + { + TSharedRef PObj = MakeShared(); + PObj->SetStringField(TEXT("name"), SP->ParameterName.ToString()); + PObj->SetNumberField(TEXT("defaultValue"), SP->DefaultValue); + PObj->SetBoolField(TEXT("isOverridden"), false); + InheritedScalarArr.Add(MakeShared(PObj)); + } + } + else if (auto* VP = Cast(Expr)) + { + if (!OverriddenVectors.Contains(VP->ParameterName.ToString())) + { + TSharedRef PObj = MakeShared(); + PObj->SetStringField(TEXT("name"), VP->ParameterName.ToString()); + PObj->SetNumberField(TEXT("r"), VP->DefaultValue.R); + PObj->SetNumberField(TEXT("g"), VP->DefaultValue.G); + PObj->SetNumberField(TEXT("b"), VP->DefaultValue.B); + PObj->SetNumberField(TEXT("a"), VP->DefaultValue.A); + PObj->SetBoolField(TEXT("isOverridden"), false); + InheritedVectorArr.Add(MakeShared(PObj)); + } + } + else if (auto* TP = Cast(Expr)) + { + if (!OverriddenTextures.Contains(TP->ParameterName.ToString())) + { + TSharedRef PObj = MakeShared(); + PObj->SetStringField(TEXT("name"), TP->ParameterName.ToString()); + if (TP->Texture) + { + PObj->SetStringField(TEXT("defaultTexture"), TP->Texture->GetPathName()); + } + else + { + PObj->SetStringField(TEXT("defaultTexture"), TEXT("None")); + } + PObj->SetBoolField(TEXT("isOverridden"), false); + InheritedTextureArr.Add(MakeShared(PObj)); + } + } + else if (auto* SSP = Cast(Expr)) + { + if (!OverriddenStaticSwitches.Contains(SSP->ParameterName.ToString())) + { + TSharedRef PObj = MakeShared(); + PObj->SetStringField(TEXT("name"), SSP->ParameterName.ToString()); + PObj->SetBoolField(TEXT("defaultValue"), SSP->DefaultValue); + PObj->SetBoolField(TEXT("isOverridden"), false); + InheritedStaticSwitchArr.Add(MakeShared(PObj)); + } + } + } + } + } + + // Merge inherited (non-overridden) params into the arrays + for (const TSharedPtr& V : InheritedScalarArr) + { + ScalarArr.Add(V); + } + for (const TSharedPtr& V : InheritedVectorArr) + { + VectorArr.Add(V); + } + for (const TSharedPtr& V : InheritedTextureArr) + { + TextureArr.Add(V); + } + for (const TSharedPtr& V : InheritedStaticSwitchArr) + { + StaticSwitchArr.Add(V); + } + + // Update arrays with merged data + Result->SetArrayField(TEXT("scalarParameters"), ScalarArr); + Result->SetArrayField(TEXT("vectorParameters"), VectorArr); + Result->SetArrayField(TEXT("textureParameters"), TextureArr); + Result->SetArrayField(TEXT("staticSwitchParameters"), StaticSwitchArr); + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_DuplicateNodesInGraph.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_DuplicateNodesInGraph.h new file mode 100644 index 00000000..13369de0 --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_DuplicateNodesInGraph.h @@ -0,0 +1,200 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPAssetFinder.h" +#include "MCPServer.h" +#include "MCPUtils.h" +#include "Engine/Blueprint.h" +#include "Materials/Material.h" +#include "Materials/MaterialInstanceConstant.h" +#include "Materials/MaterialFunction.h" +#include "Engine/World.h" +#include "Engine/LevelScriptBlueprint.h" +#include "EdGraph/EdGraph.h" +#include "EdGraph/EdGraphNode.h" +#include "EdGraph/EdGraphPin.h" +#include "EdGraphSchema_K2.h" +#include "K2Node.h" +#include "K2Node_CallFunction.h" +#include "K2Node_Event.h" +#include "K2Node_CustomEvent.h" +#include "K2Node_FunctionEntry.h" +#include "K2Node_EditablePinBase.h" +#include "K2Node_VariableGet.h" +#include "K2Node_VariableSet.h" +#include "K2Node_BreakStruct.h" +#include "K2Node_MakeStruct.h" +#include "K2Node_DynamicCast.h" +#include "K2Node_CallParentFunction.h" +#include "K2Node_IfThenElse.h" +#include "K2Node_ExecutionSequence.h" +#include "K2Node_MacroInstance.h" +#include "K2Node_SpawnActorFromClass.h" +#include "K2Node_Select.h" +#include "K2Node_Knot.h" +#include "EdGraphNode_Comment.h" +#include "GameFramework/Actor.h" +#include "Kismet2/BlueprintEditorUtils.h" +#include "Kismet2/KismetEditorUtilities.h" +#include "Serialization/JsonReader.h" +#include "Serialization/JsonWriter.h" +#include "Serialization/JsonSerializer.h" +#include "UObject/SavePackage.h" +#include "UObject/UObjectIterator.h" +#include "Misc/PackageName.h" +#include "AssetRegistry/AssetRegistryModule.h" +#include "AssetRegistry/IAssetRegistry.h" +#include "AssetToolsModule.h" +#include "IAssetTools.h" +#include "BlueprintNodeSpawner.h" +#include "UMCPHandler_DuplicateNodesInGraph.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +UCLASS() +class UMCPHandler_DuplicateNodesInGraph : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Blueprint name or package path")) + FString Blueprint; + + UPROPERTY(meta=(Description="Graph name")) + FString Graph; + + UPROPERTY(meta=(Description="Array of node GUIDs to duplicate")) + FMCPJsonArray Nodes; + + UPROPERTY(meta=(Optional, Description="X offset for duplicated nodes")) + int32 OffsetX = 50; + + UPROPERTY(meta=(Optional, Description="Y offset for duplicated nodes")) + int32 OffsetY = 50; + + virtual FString GetDescription() const override + { + return TEXT("Duplicate one or more nodes in a Blueprint graph. " + "Creates copies offset from the originals with new GUIDs. " + "Connections are not preserved on the duplicates."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override + { + + MCPAssets Assets; + if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; + UBlueprint* BP = Assets.Object(); + + // Find the target graph + FString DecodedGraphName = MCPUtils::UrlDecode(Graph); + TArray MatchingGraphs = MCPUtils::AllGraphsNamed(BP, DecodedGraphName); + if (MatchingGraphs.Num() == 0) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Graph '%s' not found"), *DecodedGraphName)); + } + UEdGraph* TargetGraph = MatchingGraphs[0]; + + if (Nodes.Array.Num() == 0) + { + return MCPUtils::MakeErrorJson(Result, TEXT("nodeIds array is empty")); + } + + // Find all source nodes + TArray SourceNodes; + TArray NotFound; + + for (const TSharedPtr& IdVal : Nodes.Array) + { + FString Node = IdVal->AsString(); + UEdGraphNode* FoundNode = MCPUtils::FindNodeByGuid(BP, Node); + if (FoundNode) + { + if (FoundNode->GetGraph() == TargetGraph) + { + SourceNodes.Add(FoundNode); + } + else + { + NotFound.Add(FString::Printf(TEXT("%s (in different graph)"), *Node)); + } + } + else + { + NotFound.Add(Node); + } + } + + if (SourceNodes.Num() == 0) + { + return MCPUtils::MakeErrorJson(Result, TEXT("No valid nodes found to duplicate")); + } + + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Duplicating %d node(s) in graph '%s' of '%s'"), + SourceNodes.Num(), *DecodedGraphName, *Blueprint); + + // Duplicate each node + TArray> DuplicatedNodes; + TMap OldToNewGuidMap; + + for (UEdGraphNode* SourceNode : SourceNodes) + { + UEdGraphNode* NewNode = DuplicateObject(SourceNode, TargetGraph); + if (!NewNode) + { + TSharedRef Entry = MakeShared(); + Entry->SetStringField(TEXT("sourceNodeId"), SourceNode->NodeGuid.ToString()); + Entry->SetStringField(TEXT("error"), TEXT("DuplicateObject failed")); + DuplicatedNodes.Add(MakeShared(Entry)); + continue; + } + + NewNode->CreateNewGuid(); + OldToNewGuidMap.Add(SourceNode->NodeGuid, NewNode->NodeGuid); + + NewNode->NodePosX += OffsetX; + NewNode->NodePosY += OffsetY; + + for (UEdGraphPin* Pin : NewNode->Pins) + { + if (Pin) + { + Pin->LinkedTo.Empty(); + } + } + + TargetGraph->AddNode(NewNode, false, false); + + TSharedRef Entry = MakeShared(); + Entry->SetStringField(TEXT("sourceNodeId"), SourceNode->NodeGuid.ToString()); + Entry->SetStringField(TEXT("newNodeId"), NewNode->NodeGuid.ToString()); + Entry->SetNumberField(TEXT("posX"), NewNode->NodePosX); + Entry->SetNumberField(TEXT("posY"), NewNode->NodePosY); + Entry->SetStringField(TEXT("nodeClass"), NewNode->GetClass()->GetName()); + Entry->SetStringField(TEXT("nodeTitle"), NewNode->GetNodeTitle(ENodeTitleType::FullTitle).ToString()); + DuplicatedNodes.Add(MakeShared(Entry)); + } + + FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP); + + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Duplicated %d node(s)"), + DuplicatedNodes.Num()); + + Result->SetNumberField(TEXT("duplicatedCount"), DuplicatedNodes.Num()); + Result->SetArrayField(TEXT("nodes"), DuplicatedNodes); + + if (NotFound.Num() > 0) + { + TArray> NotFoundArr; + for (const FString& NF : NotFound) + { + NotFoundArr.Add(MakeShared(NF)); + } + Result->SetArrayField(TEXT("notFound"), NotFoundArr); + } + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_FindAssetReferences.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_FindAssetReferences.h new file mode 100644 index 00000000..edb7fc5a --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_FindAssetReferences.h @@ -0,0 +1,85 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPAssetFinder.h" +#include "MCPUtils.h" +#include "Engine/Blueprint.h" +#include "Engine/World.h" +#include "Engine/Level.h" +#include "Engine/LevelScriptBlueprint.h" +#include "EdGraph/EdGraph.h" +#include "EdGraph/EdGraphNode.h" +#include "K2Node_CallFunction.h" +#include "K2Node_Event.h" +#include "K2Node_CustomEvent.h" +#include "K2Node_VariableGet.h" +#include "K2Node_VariableSet.h" +#include "K2Node_BreakStruct.h" +#include "K2Node_MakeStruct.h" +#include "K2Node_FunctionEntry.h" +#include "K2Node_EditablePinBase.h" +#include "AssetRegistry/IAssetRegistry.h" +#include "UMCPHandler_FindAssetReferences.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +// ============================================================ +// HandleFindReferences — find all Blueprints referencing an asset +// ============================================================ + +UCLASS() +class UMCPHandler_FindAssetReferences : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Asset package path to find references for")) + FString AssetPath; + + virtual FString GetDescription() const override + { + return TEXT("Find all assets that reference a given asset, categorized into Blueprint and non-Blueprint referencers."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override + { + IAssetRegistry& Registry = *IAssetRegistry::Get(); + + TArray Referencers; + Registry.GetReferencers(FName(*AssetPath), Referencers); + + // Build set of known Blueprint package names for filtering + MCPAssets AllBlueprints; + AllBlueprints.Info(); + TSet BlueprintPackages; + for (const FAssetData& Asset : AllBlueprints.AllData()) + { + BlueprintPackages.Add(Asset.PackageName.ToString()); + } + + TArray> BPRefs; + TArray> OtherRefs; + for (const FName& Ref : Referencers) + { + FString RefStr = Ref.ToString(); + if (BlueprintPackages.Contains(RefStr)) + { + BPRefs.Add(MakeShared(RefStr)); + } + else + { + OtherRefs.Add(MakeShared(RefStr)); + } + } + + Result->SetNumberField(TEXT("totalReferencers"), Referencers.Num()); + Result->SetNumberField(TEXT("blueprintReferencerCount"), BPRefs.Num()); + Result->SetArrayField(TEXT("blueprintReferencers"), BPRefs); + Result->SetNumberField(TEXT("otherReferencerCount"), OtherRefs.Num()); + Result->SetArrayField(TEXT("otherReferencers"), OtherRefs); + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_FindMaterialReferences.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_FindMaterialReferences.h new file mode 100644 index 00000000..52c484bd --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_FindMaterialReferences.h @@ -0,0 +1,85 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPAssetFinder.h" +#include "MCPUtils.h" +#include "Materials/Material.h" +#include "MaterialDomain.h" +#include "Materials/MaterialInstanceConstant.h" +#include "Materials/MaterialFunction.h" +#include "Materials/MaterialExpression.h" +#include "Materials/MaterialExpressionScalarParameter.h" +#include "Materials/MaterialExpressionVectorParameter.h" +#include "Materials/MaterialExpressionTextureObjectParameter.h" +#include "Materials/MaterialExpressionTextureSampleParameter2D.h" +#include "Materials/MaterialExpressionStaticSwitchParameter.h" +#include "Materials/MaterialExpressionConstant.h" +#include "Materials/MaterialExpressionConstant3Vector.h" +#include "Materials/MaterialExpressionConstant4Vector.h" +#include "Materials/MaterialExpressionTextureSample.h" +#include "Materials/MaterialExpressionTextureCoordinate.h" +#include "Materials/MaterialExpressionComponentMask.h" +#include "Materials/MaterialExpressionCustom.h" +#include "Materials/MaterialExpressionFunctionInput.h" +#include "Materials/MaterialExpressionFunctionOutput.h" +#include "Materials/MaterialExpressionMaterialFunctionCall.h" +#include "MaterialGraph/MaterialGraph.h" +#include "MaterialGraph/MaterialGraphNode.h" +#include "MaterialGraph/MaterialGraphNode_Root.h" +#include "MaterialGraph/MaterialGraphSchema.h" +#include "Kismet2/BlueprintEditorUtils.h" +#include "AssetRegistry/AssetRegistryModule.h" +#include "AssetRegistry/IAssetRegistry.h" +#include "EdGraph/EdGraph.h" +#include "EdGraph/EdGraphNode.h" +#include "UMCPHandler_FindMaterialReferences.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +UCLASS() +class UMCPHandler_FindMaterialReferences : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Material or MaterialInstance name or package path")) + FString Material; + + virtual FString GetDescription() const override + { + return TEXT("Find all assets that reference a given material or material instance."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override + { + // Try to find the material's package path (search both Material and MaterialInstance) + MCPAssets Assets; + Assets.Scan().Scan(); + if (!Assets.Exact(Material).Errors(Result).ENone().ETwo().Info()) return; + FString PackagePath = Assets.OneData().PackageName.ToString(); + + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: FindMaterialReferences — '%s' (package: %s)"), *Material, *PackagePath); + + IAssetRegistry& Registry = *IAssetRegistry::Get(); + + TArray Referencers; + Registry.GetReferencers(FName(*PackagePath), Referencers); + + TArray> RefArray; + for (const FName& Ref : Referencers) + { + FString RefStr = Ref.ToString(); + // Skip self-reference + if (RefStr == PackagePath) continue; + RefArray.Add(MakeShared(RefStr)); + } + + Result->SetStringField(TEXT("packagePath"), PackagePath); + Result->SetNumberField(TEXT("totalReferencers"), RefArray.Num()); + Result->SetArrayField(TEXT("referencers"), RefArray); + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_GetNodeComment.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_GetNodeComment.h new file mode 100644 index 00000000..0aa3580d --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_GetNodeComment.h @@ -0,0 +1,91 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPAssetFinder.h" +#include "MCPServer.h" +#include "MCPUtils.h" +#include "Engine/Blueprint.h" +#include "Materials/Material.h" +#include "Materials/MaterialInstanceConstant.h" +#include "Materials/MaterialFunction.h" +#include "Engine/World.h" +#include "Engine/LevelScriptBlueprint.h" +#include "EdGraph/EdGraph.h" +#include "EdGraph/EdGraphNode.h" +#include "EdGraph/EdGraphPin.h" +#include "EdGraphSchema_K2.h" +#include "K2Node.h" +#include "K2Node_CallFunction.h" +#include "K2Node_Event.h" +#include "K2Node_CustomEvent.h" +#include "K2Node_FunctionEntry.h" +#include "K2Node_EditablePinBase.h" +#include "K2Node_VariableGet.h" +#include "K2Node_VariableSet.h" +#include "K2Node_BreakStruct.h" +#include "K2Node_MakeStruct.h" +#include "K2Node_DynamicCast.h" +#include "K2Node_CallParentFunction.h" +#include "K2Node_IfThenElse.h" +#include "K2Node_ExecutionSequence.h" +#include "K2Node_MacroInstance.h" +#include "K2Node_SpawnActorFromClass.h" +#include "K2Node_Select.h" +#include "K2Node_Knot.h" +#include "EdGraphNode_Comment.h" +#include "GameFramework/Actor.h" +#include "Kismet2/BlueprintEditorUtils.h" +#include "Kismet2/KismetEditorUtilities.h" +#include "Serialization/JsonReader.h" +#include "Serialization/JsonWriter.h" +#include "Serialization/JsonSerializer.h" +#include "UObject/SavePackage.h" +#include "UObject/UObjectIterator.h" +#include "Misc/PackageName.h" +#include "AssetRegistry/AssetRegistryModule.h" +#include "AssetRegistry/IAssetRegistry.h" +#include "AssetToolsModule.h" +#include "IAssetTools.h" +#include "BlueprintNodeSpawner.h" +#include "UMCPHandler_GetNodeComment.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +UCLASS() +class UMCPHandler_GetNodeComment : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Blueprint name or package path")) + FString Blueprint; + + UPROPERTY(meta=(Description="Node GUID")) + FString Node; + + virtual FString GetDescription() const override + { + return TEXT("Get the comment text and bubble visibility of a node."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override + { + + MCPAssets Assets; + if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; + UBlueprint* BP = Assets.Object(); + + UEdGraphNode* FoundNode = MCPUtils::FindNodeByGuid(BP, Node); + if (!FoundNode) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found"), *Node)); + } + + Result->SetStringField(TEXT("comment"), FoundNode->NodeComment); + Result->SetBoolField(TEXT("commentBubbleVisible"), FoundNode->bCommentBubbleVisible); + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_GetPinDetails.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_GetPinDetails.h new file mode 100644 index 00000000..53d6ab36 --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_GetPinDetails.h @@ -0,0 +1,127 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPAssetFinder.h" +#include "MCPUtils.h" +#include "Engine/Blueprint.h" +#include "EdGraph/EdGraph.h" +#include "EdGraph/EdGraphNode.h" +#include "EdGraph/EdGraphPin.h" +#include "EdGraphSchema_K2.h" +#include "UObject/UObjectIterator.h" +#include "UMCPHandler_GetPinDetails.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +// ============================================================ +// HandleGetPinInfo — detailed information about a specific pin +// ============================================================ + +UCLASS() +class UMCPHandler_GetPinDetails : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Blueprint name or package path")) + FString Blueprint; + + UPROPERTY(meta=(Description="Node to look up (GUID)")) + FString Node; + + UPROPERTY(meta=(Description="Pin name on the node")) + FString PinName; + + virtual FString GetDescription() const override + { + return TEXT("Get detailed information about a specific pin on a blueprint node, " + "including type, connections, and default values."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override + { + MCPAssets Assets; + if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; + UBlueprint* BP = Assets.Object(); + + UEdGraph* Graph = nullptr; + UEdGraphNode* FoundNode = MCPUtils::FindNodeByGuid(BP, Node, &Graph); + if (!FoundNode) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found"), *Node)); + } + + UEdGraphPin* Pin = FoundNode->FindPin(FName(*PinName)); + if (!Pin) + { + // List available pins + TArray> AvailPins; + for (UEdGraphPin* P : FoundNode->Pins) + { + if (P) + { + TSharedRef PinObj = MakeShared(); + PinObj->SetStringField(TEXT("name"), P->PinName.ToString()); + PinObj->SetStringField(TEXT("direction"), P->Direction == EGPD_Input ? TEXT("Input") : TEXT("Output")); + PinObj->SetStringField(TEXT("type"), P->PinType.PinCategory.ToString()); + AvailPins.Add(MakeShared(PinObj)); + } + } + MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Pin '%s' not found on node '%s'"), *PinName, *Node)); + Result->SetArrayField(TEXT("availablePins"), AvailPins); + return; + } + + Result->SetStringField(TEXT("pinName"), Pin->PinName.ToString()); + Result->SetStringField(TEXT("direction"), Pin->Direction == EGPD_Input ? TEXT("Input") : TEXT("Output")); + Result->SetStringField(TEXT("type"), Pin->PinType.PinCategory.ToString()); + + if (!Pin->PinType.PinSubCategory.IsNone()) + { + Result->SetStringField(TEXT("subCategory"), Pin->PinType.PinSubCategory.ToString()); + } + if (Pin->PinType.PinSubCategoryObject.IsValid()) + { + Result->SetStringField(TEXT("subtype"), Pin->PinType.PinSubCategoryObject->GetName()); + } + + Result->SetBoolField(TEXT("isArray"), Pin->PinType.IsArray()); + Result->SetBoolField(TEXT("isSet"), Pin->PinType.IsSet()); + Result->SetBoolField(TEXT("isMap"), Pin->PinType.IsMap()); + Result->SetBoolField(TEXT("isReference"), Pin->PinType.bIsReference); + Result->SetBoolField(TEXT("isConst"), Pin->PinType.bIsConst); + + if (!Pin->DefaultValue.IsEmpty()) + { + Result->SetStringField(TEXT("defaultValue"), Pin->DefaultValue); + } + if (!Pin->DefaultTextValue.IsEmpty()) + { + Result->SetStringField(TEXT("defaultTextValue"), Pin->DefaultTextValue.ToString()); + } + if (Pin->DefaultObject) + { + Result->SetStringField(TEXT("defaultObject"), Pin->DefaultObject->GetPathName()); + } + + // Connected pins + if (Pin->LinkedTo.Num() > 0) + { + TArray> Conns; + for (UEdGraphPin* Linked : Pin->LinkedTo) + { + if (!Linked || !Linked->GetOwningNode()) continue; + TSharedRef CJ = MakeShared(); + CJ->SetStringField(TEXT("nodeId"), Linked->GetOwningNode()->NodeGuid.ToString()); + CJ->SetStringField(TEXT("pinName"), Linked->PinName.ToString()); + CJ->SetStringField(TEXT("nodeTitle"), Linked->GetOwningNode()->GetNodeTitle(ENodeTitleType::FullTitle).ToString()); + Conns.Add(MakeShared(CJ)); + } + Result->SetArrayField(TEXT("connectedTo"), Conns); + } + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_ListAnimSlotNames.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_ListAnimSlotNames.h new file mode 100644 index 00000000..b547890c --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_ListAnimSlotNames.h @@ -0,0 +1,77 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPAssetFinder.h" +#include "MCPUtils.h" +#include "Engine/Blueprint.h" +#include "EdGraph/EdGraph.h" +#include "EdGraph/EdGraphNode.h" +#include "Kismet2/KismetEditorUtilities.h" +#include "Dom/JsonValue.h" +#include "Animation/AnimBlueprint.h" +#include "Animation/AnimBlueprintGeneratedClass.h" +#include "Animation/Skeleton.h" +#include "AnimGraphNode_Base.h" +#include "Animation/AnimSequence.h" +#include "Animation/BlendSpace.h" +#include "UMCPHandler_ListAnimSlotNames.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +UCLASS() +class UMCPHandler_ListAnimSlotNames : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Animation Blueprint name or package path")) + FString Blueprint; + + virtual FString GetDescription() const override + { + return TEXT("List all animation slot names used in an Animation Blueprint."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override + { + if (Blueprint.IsEmpty()) + { + return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: blueprint")); + } + + MCPAssets Assets; + if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; + UAnimBlueprint* AnimBP = Assets.Object(); + + // Walk all anim nodes to collect slot names + TSet SlotNames; + for (UAnimGraphNode_Base* AnimNode : MCPUtils::AllNodes(AnimBP)) + { + // Check for SlotName property via reflection + for (TFieldIterator PropIt(AnimNode->GetClass()); PropIt; ++PropIt) + { + if (PropIt->GetName().Contains(TEXT("SlotName")) || PropIt->GetName().Contains(TEXT("Slot"))) + { + FName SlotValue = PropIt->GetPropertyValue_InContainer(AnimNode); + if (!SlotValue.IsNone()) + { + SlotNames.Add(SlotValue.ToString()); + } + } + } + } + + TArray> SlotsArr; + for (const FString& Slot : SlotNames) + { + SlotsArr.Add(MakeShared(Slot)); + } + + Result->SetArrayField(TEXT("slots"), SlotsArr); + Result->SetNumberField(TEXT("count"), SlotsArr.Num()); + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_ListAnimSyncGroups.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_ListAnimSyncGroups.h new file mode 100644 index 00000000..89cd12ff --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_ListAnimSyncGroups.h @@ -0,0 +1,77 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPAssetFinder.h" +#include "MCPUtils.h" +#include "Engine/Blueprint.h" +#include "EdGraph/EdGraph.h" +#include "EdGraph/EdGraphNode.h" +#include "Kismet2/KismetEditorUtilities.h" +#include "Dom/JsonValue.h" +#include "Animation/AnimBlueprint.h" +#include "Animation/AnimBlueprintGeneratedClass.h" +#include "Animation/Skeleton.h" +#include "AnimGraphNode_Base.h" +#include "Animation/AnimSequence.h" +#include "Animation/BlendSpace.h" +#include "UMCPHandler_ListAnimSyncGroups.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +UCLASS() +class UMCPHandler_ListAnimSyncGroups : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Animation Blueprint name or package path")) + FString Blueprint; + + virtual FString GetDescription() const override + { + return TEXT("List all sync group names used in an Animation Blueprint."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override + { + if (Blueprint.IsEmpty()) + { + return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: blueprint")); + } + + MCPAssets Assets; + if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; + UAnimBlueprint* AnimBP = Assets.Object(); + + // Walk all anim nodes to collect sync group names + TSet SyncGroupNames; + for (UAnimGraphNode_Base* AnimNode : MCPUtils::AllNodes(AnimBP)) + { + // Check for SyncGroup/GroupName property via reflection + for (TFieldIterator PropIt(AnimNode->GetClass()); PropIt; ++PropIt) + { + if (PropIt->GetName().Contains(TEXT("SyncGroup")) || PropIt->GetName().Contains(TEXT("GroupName"))) + { + FName GroupValue = PropIt->GetPropertyValue_InContainer(AnimNode); + if (!GroupValue.IsNone()) + { + SyncGroupNames.Add(GroupValue.ToString()); + } + } + } + } + + TArray> GroupsArr; + for (const FString& Group : SyncGroupNames) + { + GroupsArr.Add(MakeShared(Group)); + } + + Result->SetArrayField(TEXT("syncGroups"), GroupsArr); + Result->SetNumberField(TEXT("count"), GroupsArr.Num()); + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_ListBlueprintAssets.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_ListBlueprintAssets.h new file mode 100644 index 00000000..672c40b6 --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_ListBlueprintAssets.h @@ -0,0 +1,91 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPAssetFinder.h" +#include "MCPUtils.h" +#include "Engine/Blueprint.h" +#include "Engine/World.h" +#include "Engine/Level.h" +#include "Engine/LevelScriptBlueprint.h" +#include "EdGraph/EdGraph.h" +#include "EdGraph/EdGraphNode.h" +#include "K2Node_CallFunction.h" +#include "K2Node_Event.h" +#include "K2Node_CustomEvent.h" +#include "K2Node_VariableGet.h" +#include "K2Node_VariableSet.h" +#include "K2Node_BreakStruct.h" +#include "K2Node_MakeStruct.h" +#include "K2Node_FunctionEntry.h" +#include "K2Node_EditablePinBase.h" +#include "AssetRegistry/IAssetRegistry.h" +#include "UMCPHandler_ListBlueprintAssets.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +UCLASS() +class UMCPHandler_ListBlueprintAssets : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Optional, Description="Substring filter for blueprint name or path")) + FString Filter; + + UPROPERTY(meta=(Optional, Description="Filter by parent class name (substring match)")) + FString ParentClass; + + UPROPERTY(meta=(Optional, Description="Include regular blueprints (default true)")) + bool IncludeRegular = true; + + UPROPERTY(meta=(Optional, Description="Include regular blueprints (default true)")) + bool IncludeLevel = true; + + virtual FString GetDescription() const override + { + return TEXT("List all Blueprint assets in the project, with optional filtering by name, parent class, or type."); + } + + virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override + { + MCPAssets Assets; + Assets.NoScans().Substring(Filter).Limit(500); + if (IncludeRegular) Assets.Scan(); + if (IncludeLevel) Assets.Scan(); + Assets.Info(); + for (const FAssetData& Asset : Assets.AllData()) + { + // Extract parent class name + FString ParentClassName; + if (Asset.AssetClassPath == UWorld::StaticClass()->GetClassPathName()) + { + ParentClassName = TEXT("LevelScriptActor"); + } + else + { + Asset.GetTagValue(FName(TEXT("ParentClass")), ParentClassName); + int32 DotIndex; + if (ParentClassName.FindLastChar('.', DotIndex)) + { + ParentClassName = ParentClassName.Mid(DotIndex + 1); + } + ParentClassName.RemoveFromEnd(TEXT("'")); + } + + // Apply parent class filter + if (!ParentClass.IsEmpty()) + { + if (!ParentClassName.Equals(ParentClass, ESearchCase::IgnoreCase)) + { + continue; + } + } + + Result.Appendf(TEXT("%30s %s\n"), *ParentClassName, *Asset.PackageName.ToString()); + } + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_ListBlueprintComponents.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_ListBlueprintComponents.h new file mode 100644 index 00000000..20b6f71c --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_ListBlueprintComponents.h @@ -0,0 +1,105 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPAssetFinder.h" +#include "MCPUtils.h" +#include "Engine/Blueprint.h" +#include "Engine/SimpleConstructionScript.h" +#include "Engine/SCS_Node.h" +#include "Components/ActorComponent.h" +#include "Kismet2/BlueprintEditorUtils.h" +#include "UObject/UObjectIterator.h" +#include "UMCPHandler_ListBlueprintComponents.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +UCLASS() +class UMCPHandler_ListBlueprintComponents : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Blueprint name or package path")) + FString Blueprint; + + virtual FString GetDescription() const override + { + return TEXT("List all components in a Blueprint's SimpleConstructionScript, " + "including hierarchy and scene root information."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override + { + MCPAssets Assets; + if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; + UBlueprint* BP = Assets.Object(); + + USimpleConstructionScript* SCS = BP->SimpleConstructionScript; + if (!SCS) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf( + TEXT("Blueprint '%s' does not have a SimpleConstructionScript (not an Actor Blueprint)"), + *Blueprint)); + } + + const TArray& AllNodes = SCS->GetAllNodes(); + + TArray> ComponentsArr; + for (USCS_Node* Node : AllNodes) + { + if (!Node) + { + continue; + } + + TSharedRef CompObj = MakeShared(); + CompObj->SetStringField(TEXT("name"), Node->GetVariableName().ToString()); + + if (Node->ComponentClass) + { + CompObj->SetStringField(TEXT("componentClass"), Node->ComponentClass->GetName()); + } + else + { + CompObj->SetStringField(TEXT("componentClass"), TEXT("None")); + } + + // Parent component info + USCS_Node* ParentNode = nullptr; + for (USCS_Node* Candidate : AllNodes) + { + if (Candidate && Candidate->GetChildNodes().Contains(Node)) + { + ParentNode = Candidate; + break; + } + } + + if (ParentNode) + { + CompObj->SetStringField(TEXT("parentComponent"), ParentNode->GetVariableName().ToString()); + } + + // Check if this is a default scene root (first root node with SceneComponent class) + bool bIsSceneRoot = false; + const TArray& RootNodes = SCS->GetRootNodes(); + if (RootNodes.Num() > 0 && RootNodes[0] == Node) + { + bIsSceneRoot = true; + } + CompObj->SetBoolField(TEXT("isSceneRoot"), bIsSceneRoot); + + // List child count for informational purposes + CompObj->SetNumberField(TEXT("childCount"), Node->GetChildNodes().Num()); + + ComponentsArr.Add(MakeShared(CompObj)); + } + + Result->SetNumberField(TEXT("count"), ComponentsArr.Num()); + Result->SetArrayField(TEXT("components"), ComponentsArr); + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_ListBlueprintInterfaces.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_ListBlueprintInterfaces.h new file mode 100644 index 00000000..714ba287 --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_ListBlueprintInterfaces.h @@ -0,0 +1,69 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPAssetFinder.h" +#include "MCPServer.h" +#include "MCPUtils.h" +#include "Engine/Blueprint.h" +#include "EdGraph/EdGraph.h" +#include "Kismet2/BlueprintEditorUtils.h" +#include "UObject/UObjectIterator.h" +#include "UMCPHandler_ListBlueprintInterfaces.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +UCLASS() +class UMCPHandler_ListBlueprintInterfaces : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Blueprint name or package path")) + FString Blueprint; + + virtual FString GetDescription() const override + { + return TEXT("List all Blueprint Interfaces implemented by a Blueprint, " + "including their function graphs."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override + { + + MCPAssets Assets; + if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; + UBlueprint* BP = Assets.Object(); + + TArray> InterfacesArr; + for (const FBPInterfaceDescription& IfaceDesc : BP->ImplementedInterfaces) + { + if (!IfaceDesc.Interface) + { + continue; + } + + TSharedRef IfaceObj = MakeShared(); + IfaceObj->SetStringField(TEXT("name"), IfaceDesc.Interface->GetName()); + IfaceObj->SetStringField(TEXT("classPath"), IfaceDesc.Interface->GetPathName()); + + TArray> FuncArr; + for (const UEdGraph* Graph : IfaceDesc.Graphs) + { + if (Graph) + { + FuncArr.Add(MakeShared(Graph->GetName())); + } + } + IfaceObj->SetArrayField(TEXT("functions"), FuncArr); + + InterfacesArr.Add(MakeShared(IfaceObj)); + } + + Result->SetNumberField(TEXT("count"), InterfacesArr.Num()); + Result->SetArrayField(TEXT("interfaces"), InterfacesArr); + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_ListClassFunctions.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_ListClassFunctions.h new file mode 100644 index 00000000..b63b3666 --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_ListClassFunctions.h @@ -0,0 +1,131 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPAssetFinder.h" +#include "MCPUtils.h" +#include "Engine/Blueprint.h" +#include "EdGraph/EdGraph.h" +#include "EdGraph/EdGraphNode.h" +#include "EdGraph/EdGraphPin.h" +#include "EdGraphSchema_K2.h" +#include "UObject/UObjectIterator.h" +#include "UMCPHandler_ListClassFunctions.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +// ============================================================ +// HandleListFunctions — list Blueprint-callable functions on a class +// ============================================================ + +UCLASS() +class UMCPHandler_ListClassFunctions : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Class name to list functions for")) + FString ClassName; + + UPROPERTY(meta=(Optional, Description="Substring filter for function names")) + FString Filter; + + virtual FString GetDescription() const override + { + return TEXT("List Blueprint-callable functions on a UClass, including parameter info and flags."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override + { + // Find the class + UClass* FoundClass = nullptr; + for (TObjectIterator It; It; ++It) + { + if (It->GetName() == ClassName || It->GetName() == ClassName + TEXT("_C")) + { + FoundClass = *It; + break; + } + } + if (!FoundClass) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Class '%s' not found"), *ClassName)); + } + + TArray> FuncList; + + for (TFieldIterator FuncIt(FoundClass); FuncIt; ++FuncIt) + { + UFunction* Func = *FuncIt; + if (!Func) continue; + + // Only include Blueprint-callable functions + if (!Func->HasAnyFunctionFlags(FUNC_BlueprintCallable | FUNC_BlueprintPure | FUNC_BlueprintEvent)) continue; + + FString FuncName = Func->GetName(); + + // Apply filter + if (!Filter.IsEmpty() && !FuncName.Contains(Filter, ESearchCase::IgnoreCase)) + { + continue; + } + + TSharedRef FuncObj = MakeShared(); + FuncObj->SetStringField(TEXT("name"), FuncName); + + // Determine the owning class + UClass* OwnerClass = Func->GetOwnerClass(); + if (OwnerClass) + { + FuncObj->SetStringField(TEXT("definedIn"), OwnerClass->GetName()); + } + + // Function flags + FuncObj->SetBoolField(TEXT("isPure"), Func->HasAnyFunctionFlags(FUNC_BlueprintPure)); + FuncObj->SetBoolField(TEXT("isStatic"), Func->HasAnyFunctionFlags(FUNC_Static)); + FuncObj->SetBoolField(TEXT("isEvent"), Func->HasAnyFunctionFlags(FUNC_BlueprintEvent)); + FuncObj->SetBoolField(TEXT("isConst"), Func->HasAnyFunctionFlags(FUNC_Const)); + + // Parameters + TArray> Params; + FString ReturnType; + for (TFieldIterator PropIt(Func); PropIt; ++PropIt) + { + FProperty* Prop = *PropIt; + if (!Prop) continue; + + FString PropType = Prop->GetCPPType(); + + if (Prop->HasAnyPropertyFlags(CPF_ReturnParm)) + { + ReturnType = PropType; + continue; + } + + if (Prop->HasAnyPropertyFlags(CPF_Parm)) + { + TSharedRef ParamObj = MakeShared(); + ParamObj->SetStringField(TEXT("name"), Prop->GetName()); + ParamObj->SetStringField(TEXT("type"), PropType); + ParamObj->SetBoolField(TEXT("isOutput"), Prop->HasAnyPropertyFlags(CPF_OutParm) && !Prop->HasAnyPropertyFlags(CPF_ReferenceParm)); + ParamObj->SetBoolField(TEXT("isReference"), Prop->HasAnyPropertyFlags(CPF_ReferenceParm)); + Params.Add(MakeShared(ParamObj)); + } + } + FuncObj->SetArrayField(TEXT("parameters"), Params); + if (!ReturnType.IsEmpty()) + { + FuncObj->SetStringField(TEXT("returnType"), ReturnType); + } + + FuncList.Add(MakeShared(FuncObj)); + } + + Result->SetStringField(TEXT("className"), FoundClass->GetName()); + Result->SetNumberField(TEXT("count"), FuncList.Num()); + Result->SetArrayField(TEXT("functions"), FuncList); + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_ListClassProperties.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_ListClassProperties.h new file mode 100644 index 00000000..f5437f13 --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_ListClassProperties.h @@ -0,0 +1,106 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPAssetFinder.h" +#include "MCPUtils.h" +#include "Engine/Blueprint.h" +#include "EdGraph/EdGraph.h" +#include "EdGraph/EdGraphNode.h" +#include "EdGraph/EdGraphPin.h" +#include "EdGraphSchema_K2.h" +#include "UObject/UObjectIterator.h" +#include "UMCPHandler_ListClassProperties.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +// ============================================================ +// HandleListProperties — list properties on a class +// ============================================================ + +UCLASS() +class UMCPHandler_ListClassProperties : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Class name to list properties for")) + FString ClassName; + + UPROPERTY(meta=(Optional, Description="Substring filter for property names")) + FString Filter; + + virtual FString GetDescription() const override + { + return TEXT("List properties on a UClass, including type, owning class, and property flags."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override + { + // Find the class + UClass* FoundClass = nullptr; + for (TObjectIterator It; It; ++It) + { + if (It->GetName() == ClassName || It->GetName() == ClassName + TEXT("_C")) + { + FoundClass = *It; + break; + } + } + if (!FoundClass) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Class '%s' not found"), *ClassName)); + } + + TArray> PropList; + + for (TFieldIterator PropIt(FoundClass); PropIt; ++PropIt) + { + FProperty* Prop = *PropIt; + if (!Prop) continue; + + FString PropName = Prop->GetName(); + + // Apply filter + if (!Filter.IsEmpty() && !PropName.Contains(Filter, ESearchCase::IgnoreCase)) + { + continue; + } + + TSharedRef PropObj = MakeShared(); + PropObj->SetStringField(TEXT("name"), PropName); + PropObj->SetStringField(TEXT("type"), Prop->GetCPPType()); + + // Determine the owning class + UClass* OwnerClass = Prop->GetOwnerClass(); + if (OwnerClass) + { + PropObj->SetStringField(TEXT("definedIn"), OwnerClass->GetName()); + } + + // Property flags + TArray> Flags; + if (Prop->HasAnyPropertyFlags(CPF_BlueprintVisible)) Flags.Add(MakeShared(TEXT("BlueprintVisible"))); + if (Prop->HasAnyPropertyFlags(CPF_BlueprintReadOnly)) Flags.Add(MakeShared(TEXT("BlueprintReadOnly"))); + if (Prop->HasAnyPropertyFlags(CPF_Edit)) Flags.Add(MakeShared(TEXT("EditAnywhere"))); + if (Prop->HasAnyPropertyFlags(CPF_EditConst)) Flags.Add(MakeShared(TEXT("VisibleOnly"))); + if (Prop->HasAnyPropertyFlags(CPF_Config)) Flags.Add(MakeShared(TEXT("Config"))); + if (Prop->HasAnyPropertyFlags(CPF_SaveGame)) Flags.Add(MakeShared(TEXT("SaveGame"))); + if (Prop->HasAnyPropertyFlags(CPF_Transient)) Flags.Add(MakeShared(TEXT("Transient"))); + if (Prop->HasAnyPropertyFlags(CPF_RepNotify)) Flags.Add(MakeShared(TEXT("RepNotify"))); + if (Flags.Num() > 0) + { + PropObj->SetArrayField(TEXT("flags"), Flags); + } + + PropList.Add(MakeShared(PropObj)); + } + + Result->SetStringField(TEXT("className"), FoundClass->GetName()); + Result->SetNumberField(TEXT("count"), PropList.Num()); + Result->SetArrayField(TEXT("properties"), PropList); + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_ListEventDispatchers.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_ListEventDispatchers.h new file mode 100644 index 00000000..943eae19 --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_ListEventDispatchers.h @@ -0,0 +1,86 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPAssetFinder.h" +#include "MCPUtils.h" +#include "Engine/Blueprint.h" +#include "EdGraph/EdGraph.h" +#include "EdGraph/EdGraphPin.h" +#include "K2Node_FunctionEntry.h" +#include "K2Node_EditablePinBase.h" +#include "Kismet2/BlueprintEditorUtils.h" +#include "UMCPHandler_ListEventDispatchers.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +UCLASS() +class UMCPHandler_ListEventDispatchers : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Blueprint name or package path")) + FString Blueprint; + + virtual FString GetDescription() const override + { + return TEXT("List all event dispatchers on a Blueprint, including their parameter signatures."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override + { + MCPAssets Assets; + if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; + UBlueprint* BP = Assets.Object(); + + TSet DelegateNameSet; + FBlueprintEditorUtils::GetDelegateNameList(BP, DelegateNameSet); + + TArray> DispatchersArr; + + for (const FName& DelegateName : DelegateNameSet) + { + TSharedRef DispObj = MakeShared(); + DispObj->SetStringField(TEXT("name"), DelegateName.ToString()); + + // Get parameter info from the signature graph + TArray> ParamsArr; + + UEdGraph* SigGraph = FBlueprintEditorUtils::GetDelegateSignatureGraphByName(BP, DelegateName); + if (SigGraph) + { + for (UK2Node_FunctionEntry* FE : MCPUtils::AllNodes(SigGraph)) + { + for (const TSharedPtr& PinInfo : FE->UserDefinedPins) + { + if (!PinInfo.IsValid()) continue; + + TSharedRef ParamObj = MakeShared(); + ParamObj->SetStringField(TEXT("name"), PinInfo->PinName.ToString()); + + // Build a human-readable type name from the pin type + FString TypeStr = PinInfo->PinType.PinCategory.ToString(); + if (PinInfo->PinType.PinSubCategoryObject.IsValid()) + { + TypeStr = PinInfo->PinType.PinSubCategoryObject->GetName(); + } + ParamObj->SetStringField(TEXT("type"), TypeStr); + + ParamsArr.Add(MakeShared(ParamObj)); + } + break; // only need the first entry node + } + } + + DispObj->SetArrayField(TEXT("parameters"), ParamsArr); + DispatchersArr.Add(MakeShared(DispObj)); + } + + Result->SetNumberField(TEXT("count"), DispatchersArr.Num()); + Result->SetArrayField(TEXT("dispatchers"), DispatchersArr); + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_ListMaterialAssets.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_ListMaterialAssets.h new file mode 100644 index 00000000..0a16f6d0 --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_ListMaterialAssets.h @@ -0,0 +1,85 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPAssetFinder.h" +#include "MCPUtils.h" +#include "Materials/Material.h" +#include "MaterialDomain.h" +#include "Materials/MaterialInstanceConstant.h" +#include "Materials/MaterialFunction.h" +#include "Materials/MaterialExpression.h" +#include "Materials/MaterialExpressionScalarParameter.h" +#include "Materials/MaterialExpressionVectorParameter.h" +#include "Materials/MaterialExpressionTextureObjectParameter.h" +#include "Materials/MaterialExpressionTextureSampleParameter2D.h" +#include "Materials/MaterialExpressionStaticSwitchParameter.h" +#include "Materials/MaterialExpressionConstant.h" +#include "Materials/MaterialExpressionConstant3Vector.h" +#include "Materials/MaterialExpressionConstant4Vector.h" +#include "Materials/MaterialExpressionTextureSample.h" +#include "Materials/MaterialExpressionTextureCoordinate.h" +#include "Materials/MaterialExpressionComponentMask.h" +#include "Materials/MaterialExpressionCustom.h" +#include "Materials/MaterialExpressionFunctionInput.h" +#include "Materials/MaterialExpressionFunctionOutput.h" +#include "Materials/MaterialExpressionMaterialFunctionCall.h" +#include "MaterialGraph/MaterialGraph.h" +#include "MaterialGraph/MaterialGraphNode.h" +#include "MaterialGraph/MaterialGraphNode_Root.h" +#include "MaterialGraph/MaterialGraphSchema.h" +#include "Kismet2/BlueprintEditorUtils.h" +#include "AssetRegistry/AssetRegistryModule.h" +#include "AssetRegistry/IAssetRegistry.h" +#include "EdGraph/EdGraph.h" +#include "EdGraph/EdGraphNode.h" +#include "UMCPHandler_ListMaterialAssets.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +UCLASS() +class UMCPHandler_ListMaterialAssets : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Optional, Description="Filter string to match against material name or path")) + FString Filter; + + UPROPERTY(meta=(Optional, Description="Type filter: 'all', 'material', or 'instance'")) + FString Type; + + virtual FString GetDescription() const override + { + return TEXT("List Material and MaterialInstance assets, optionally filtered by name and type."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override + { + bool bIncludeMaterials = Type.IsEmpty() || Type == TEXT("all") || Type == TEXT("material"); + bool bIncludeInstances = Type.IsEmpty() || Type == TEXT("all") || Type == TEXT("instance"); + + MCPAssets Assets; + if (bIncludeMaterials) Assets.Scan(UMaterial::StaticClass()); + if (bIncludeInstances) Assets.Scan(UMaterialInstanceConstant::StaticClass()); + Assets.Substring(Filter).NoDerived().Info(); + + TArray> Entries; + for (const FAssetData& Asset : Assets.AllData()) + { + TSharedRef Entry = MakeShared(); + Entry->SetStringField(TEXT("name"), Asset.AssetName.ToString()); + Entry->SetStringField(TEXT("path"), Asset.PackageName.ToString()); + Entry->SetStringField(TEXT("type"), + Asset.AssetClassPath.GetAssetName() == TEXT("MaterialInstanceConstant") + ? TEXT("MaterialInstance") : TEXT("Material")); + Entries.Add(MakeShared(Entry)); + } + + Result->SetNumberField(TEXT("count"), Entries.Num()); + Result->SetArrayField(TEXT("materials"), Entries); + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_ListMaterialFunctionAssets.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_ListMaterialFunctionAssets.h new file mode 100644 index 00000000..3e27e24e --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_ListMaterialFunctionAssets.h @@ -0,0 +1,74 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPAssetFinder.h" +#include "MCPUtils.h" +#include "Materials/Material.h" +#include "MaterialDomain.h" +#include "Materials/MaterialInstanceConstant.h" +#include "Materials/MaterialFunction.h" +#include "Materials/MaterialExpression.h" +#include "Materials/MaterialExpressionScalarParameter.h" +#include "Materials/MaterialExpressionVectorParameter.h" +#include "Materials/MaterialExpressionTextureObjectParameter.h" +#include "Materials/MaterialExpressionTextureSampleParameter2D.h" +#include "Materials/MaterialExpressionStaticSwitchParameter.h" +#include "Materials/MaterialExpressionConstant.h" +#include "Materials/MaterialExpressionConstant3Vector.h" +#include "Materials/MaterialExpressionConstant4Vector.h" +#include "Materials/MaterialExpressionTextureSample.h" +#include "Materials/MaterialExpressionTextureCoordinate.h" +#include "Materials/MaterialExpressionComponentMask.h" +#include "Materials/MaterialExpressionCustom.h" +#include "Materials/MaterialExpressionFunctionInput.h" +#include "Materials/MaterialExpressionFunctionOutput.h" +#include "Materials/MaterialExpressionMaterialFunctionCall.h" +#include "MaterialGraph/MaterialGraph.h" +#include "MaterialGraph/MaterialGraphNode.h" +#include "MaterialGraph/MaterialGraphNode_Root.h" +#include "MaterialGraph/MaterialGraphSchema.h" +#include "Kismet2/BlueprintEditorUtils.h" +#include "AssetRegistry/AssetRegistryModule.h" +#include "AssetRegistry/IAssetRegistry.h" +#include "EdGraph/EdGraph.h" +#include "EdGraph/EdGraphNode.h" +#include "UMCPHandler_ListMaterialFunctionAssets.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +UCLASS() +class UMCPHandler_ListMaterialFunctionAssets : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Optional, Description="Filter string to match against function name or path")) + FString Filter; + + virtual FString GetDescription() const override + { + return TEXT("List MaterialFunction assets, optionally filtered by name or path."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override + { + MCPAssets Assets; + Assets.Substring(Filter).Info(); + + TArray> Entries; + for (const FAssetData& Asset : Assets.AllData()) + { + TSharedRef Entry = MakeShared(); + Entry->SetStringField(TEXT("name"), Asset.AssetName.ToString()); + Entry->SetStringField(TEXT("path"), Asset.PackageName.ToString()); + Entries.Add(MakeShared(Entry)); + } + + Result->SetNumberField(TEXT("count"), Entries.Num()); + Result->SetArrayField(TEXT("functions"), Entries); + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_RefreshAllNodesInGraph.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_RefreshAllNodesInGraph.h new file mode 100644 index 00000000..99139534 --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_RefreshAllNodesInGraph.h @@ -0,0 +1,151 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPAssetFinder.h" +#include "MCPServer.h" +#include "MCPUtils.h" +#include "Engine/Blueprint.h" +#include "Materials/Material.h" +#include "Materials/MaterialInstanceConstant.h" +#include "Materials/MaterialFunction.h" +#include "Engine/World.h" +#include "Engine/LevelScriptBlueprint.h" +#include "EdGraph/EdGraph.h" +#include "EdGraph/EdGraphNode.h" +#include "EdGraph/EdGraphPin.h" +#include "EdGraphSchema_K2.h" +#include "K2Node.h" +#include "K2Node_CallFunction.h" +#include "K2Node_Event.h" +#include "K2Node_CustomEvent.h" +#include "K2Node_FunctionEntry.h" +#include "K2Node_EditablePinBase.h" +#include "K2Node_VariableGet.h" +#include "K2Node_VariableSet.h" +#include "K2Node_BreakStruct.h" +#include "K2Node_MakeStruct.h" +#include "K2Node_DynamicCast.h" +#include "K2Node_CallParentFunction.h" +#include "K2Node_IfThenElse.h" +#include "K2Node_ExecutionSequence.h" +#include "K2Node_MacroInstance.h" +#include "K2Node_SpawnActorFromClass.h" +#include "K2Node_Select.h" +#include "K2Node_Knot.h" +#include "EdGraphNode_Comment.h" +#include "GameFramework/Actor.h" +#include "Kismet2/BlueprintEditorUtils.h" +#include "Kismet2/KismetEditorUtilities.h" +#include "Serialization/JsonReader.h" +#include "Serialization/JsonWriter.h" +#include "Serialization/JsonSerializer.h" +#include "UObject/SavePackage.h" +#include "UObject/UObjectIterator.h" +#include "Misc/PackageName.h" +#include "AssetRegistry/AssetRegistryModule.h" +#include "AssetRegistry/IAssetRegistry.h" +#include "AssetToolsModule.h" +#include "IAssetTools.h" +#include "BlueprintNodeSpawner.h" +#include "UMCPHandler_RefreshAllNodesInGraph.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +UCLASS() +class UMCPHandler_RefreshAllNodesInGraph : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Blueprint name or package path")) + FString Blueprint; + + virtual FString GetDescription() const override + { + return TEXT("Refresh all nodes in a Blueprint, removing orphaned pins. " + "Reports compiler warnings and errors."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override + { + + // Load Blueprint + MCPAssets Assets; + if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; + UBlueprint* BP = Assets.Object(); + + // Count graphs and nodes before refresh + int32 GraphCount = MCPUtils::AllGraphs(BP).Num(); + int32 NodeCount = MCPUtils::AllNodes(BP).Num(); + + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Refreshing all nodes in '%s' (%d graphs, %d nodes)"), + *Blueprint, GraphCount, NodeCount); + + // Refresh all nodes + FBlueprintEditorUtils::RefreshAllNodes(BP); + + // Remove orphaned pins from all nodes + int32 OrphanedPinsRemoved = 0; + for (UEdGraphNode* Node : MCPUtils::AllNodes(BP)) + { + for (int32 i = Node->Pins.Num() - 1; i >= 0; --i) + { + UEdGraphPin* Pin = Node->Pins[i]; + if (Pin && Pin->bOrphanedPin) + { + Pin->BreakAllPinLinks(); + Node->Pins.RemoveAt(i); + OrphanedPinsRemoved++; + } + } + } + + if (OrphanedPinsRemoved > 0) + { + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Removed %d orphaned pins"), OrphanedPinsRemoved); + } + + // Mark as modified and recompile after orphan removal + FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP); + + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: RefreshAllNodes complete")); + + // Collect compiler warnings and errors from the blueprint status + TArray> WarningsArr; + TArray> ErrorsArr; + + if (BP->Status == BS_Error) + { + ErrorsArr.Add(MakeShared(TEXT("Blueprint has compiler errors after refresh"))); + } + + // Check each graph for nodes with error/warning status + for (UEdGraphNode* Node : MCPUtils::AllNodes(BP)) + { + if (Node->bHasCompilerMessage) + { + FString NodeTitle = Node->GetNodeTitle(ENodeTitleType::FullTitle).ToString(); + FString NodeMsg = FString::Printf(TEXT("[%s] %s: %s"), + *Node->GetGraph()->GetName(), *NodeTitle, *Node->ErrorMsg); + if (Node->ErrorType == EMessageSeverity::Error) + { + ErrorsArr.Add(MakeShared(NodeMsg)); + } + else + { + WarningsArr.Add(MakeShared(NodeMsg)); + } + } + } + + Result->SetNumberField(TEXT("graphCount"), GraphCount); + Result->SetNumberField(TEXT("nodeCount"), NodeCount); + Result->SetNumberField(TEXT("orphanedPinsRemoved"), OrphanedPinsRemoved); + Result->SetArrayField(TEXT("warnings"), WarningsArr); + Result->SetArrayField(TEXT("errors"), ErrorsArr); + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_RemoveAnimStateFromMachine.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_RemoveAnimStateFromMachine.h new file mode 100644 index 00000000..2ef69068 --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_RemoveAnimStateFromMachine.h @@ -0,0 +1,89 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPAssetFinder.h" +#include "MCPUtils.h" +#include "EdGraph/EdGraph.h" +#include "EdGraph/EdGraphNode.h" +#include "EdGraph/EdGraphPin.h" +#include "Kismet2/KismetEditorUtilities.h" +#include "Animation/AnimBlueprint.h" +#include "Animation/AnimSequence.h" +#include "Animation/BlendSpace.h" +#include "AnimGraphNode_SequencePlayer.h" +#include "AnimGraphNode_BlendSpacePlayer.h" +#include "AnimStateNode.h" +#include "AnimStateTransitionNode.h" +#include "AnimationStateMachineGraph.h" +#include "K2Node_VariableGet.h" +#include "UMCPHandler_RemoveAnimStateFromMachine.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +UCLASS() +class UMCPHandler_RemoveAnimStateFromMachine : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Animation Blueprint name or package path")) + FString Blueprint; + + UPROPERTY(meta=(Description="State machine graph name")) + FString Graph; + + UPROPERTY(meta=(Description="Name of the state to remove")) + FString StateName; + + virtual FString GetDescription() const override + { + return TEXT("Remove a state and its connected transitions from an animation state machine graph."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override + { + MCPAssets Assets; + if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; + UAnimationStateMachineGraph* SMGraph = MCPUtils::FindStateMachineGraph(Assets.Object(), Graph); + if (!SMGraph) { MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("State machine graph '%s' not found in '%s'"), *Graph, *Blueprint)); return; } + UAnimBlueprint* AnimBP = Assets.Object(); + + UAnimStateNode* StateNode = MCPUtils::FindStateByName(SMGraph, StateName, Result); + if (!StateNode) return; + + // Collect and remove transitions connected to this state + TArray TransitionsToRemove; + for (UEdGraphNode* Node : SMGraph->Nodes) + { + if (UAnimStateTransitionNode* TransNode = Cast(Node)) + { + if ((TransNode->GetPreviousState() == StateNode) || (TransNode->GetNextState() == StateNode)) + { + TransitionsToRemove.Add(TransNode); + } + } + } + + int32 RemovedTransitions = TransitionsToRemove.Num(); + for (UAnimStateTransitionNode* Trans : TransitionsToRemove) + { + Trans->BreakAllNodeLinks(); + SMGraph->RemoveNode(Trans); + } + + // Remove the state + StateNode->BreakAllNodeLinks(); + SMGraph->RemoveNode(StateNode); + + // Compile and save + FKismetEditorUtilities::CompileBlueprint(AnimBP); + bool bSaved = MCPUtils::SaveBlueprintPackage(AnimBP); + + Result->SetNumberField(TEXT("removedTransitions"), RemovedTransitions); + Result->SetBoolField(TEXT("saved"), bSaved); + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_RemoveBlueprintComponent.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_RemoveBlueprintComponent.h new file mode 100644 index 00000000..cbd457b4 --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_RemoveBlueprintComponent.h @@ -0,0 +1,106 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPAssetFinder.h" +#include "MCPUtils.h" +#include "Engine/Blueprint.h" +#include "Engine/SimpleConstructionScript.h" +#include "Engine/SCS_Node.h" +#include "Components/ActorComponent.h" +#include "Kismet2/BlueprintEditorUtils.h" +#include "UObject/UObjectIterator.h" +#include "UMCPHandler_RemoveBlueprintComponent.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +UCLASS() +class UMCPHandler_RemoveBlueprintComponent : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Blueprint name or package path")) + FString Blueprint; + + UPROPERTY(meta=(Description="Component to remove")) + FString Component; + + virtual FString GetDescription() const override + { + return TEXT("Remove a component from a Blueprint's SimpleConstructionScript."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override + { + MCPAssets Assets; + if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; + UBlueprint* BP = Assets.Object(); + + USimpleConstructionScript* SCS = BP->SimpleConstructionScript; + if (!SCS) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf( + TEXT("Blueprint '%s' does not have a SimpleConstructionScript (not an Actor Blueprint)"), + *Blueprint)); + } + + // Find the node to remove + USCS_Node* NodeToRemove = nullptr; + const TArray& AllNodes = SCS->GetAllNodes(); + for (USCS_Node* Node : AllNodes) + { + if (Node && Node->GetVariableName().ToString().Equals(Component, ESearchCase::IgnoreCase)) + { + NodeToRemove = Node; + break; + } + } + + if (!NodeToRemove) + { + // Build list of component names for the error message + TArray> CompList; + for (USCS_Node* Node : AllNodes) + { + if (Node) + { + CompList.Add(MakeShared(Node->GetVariableName().ToString())); + } + } + + MCPUtils::MakeErrorJson(Result, FString::Printf( + TEXT("Component '%s' not found in Blueprint '%s'"), + *Component, *Blueprint)); + Result->SetArrayField(TEXT("existingComponents"), CompList); + return; + } + + // Prevent removing the root scene component if it has children + const TArray& RootNodes = SCS->GetRootNodes(); + if (RootNodes.Contains(NodeToRemove) && NodeToRemove->GetChildNodes().Num() > 0) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf( + TEXT("Cannot remove component '%s' because it is a root component with %d child(ren). " + "Remove or re-parent the children first."), + *Component, NodeToRemove->GetChildNodes().Num())); + } + + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Removing component '%s' from Blueprint '%s'"), + *Component, *Blueprint); + + // Remove the node (promotes children to parent if it has any — but we've guarded root above) + SCS->RemoveNodeAndPromoteChildren(NodeToRemove); + + FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP); + bool bSaved = MCPUtils::SaveBlueprintPackage(BP); + + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Removed component '%s' from '%s' (saved: %s)"), + *Component, *Blueprint, bSaved ? TEXT("true") : TEXT("false")); + + Result->SetBoolField(TEXT("saved"), bSaved); + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_RemoveBlueprintInterface.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_RemoveBlueprintInterface.h new file mode 100644 index 00000000..50bb91bf --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_RemoveBlueprintInterface.h @@ -0,0 +1,109 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPAssetFinder.h" +#include "MCPServer.h" +#include "MCPUtils.h" +#include "Engine/Blueprint.h" +#include "EdGraph/EdGraph.h" +#include "Kismet2/BlueprintEditorUtils.h" +#include "UObject/UObjectIterator.h" +#include "UMCPHandler_RemoveBlueprintInterface.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +UCLASS() +class UMCPHandler_RemoveBlueprintInterface : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Blueprint name or package path")) + FString Blueprint; + + UPROPERTY(meta=(Description="Interface name to remove")) + FString InterfaceName; + + UPROPERTY(meta=(Optional, Description="If true, keep the function graphs as regular functions")) + bool PreserveFunctions = false; + + virtual FString GetDescription() const override + { + return TEXT("Remove a Blueprint Interface implementation from a Blueprint. " + "Optionally preserve the function graphs as regular functions."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override + { + + MCPAssets Assets; + if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; + UBlueprint* BP = Assets.Object(); + + // Find the interface in ImplementedInterfaces by name (case-insensitive) + UClass* FoundInterface = nullptr; + for (const FBPInterfaceDescription& IfaceDesc : BP->ImplementedInterfaces) + { + if (!IfaceDesc.Interface) + { + continue; + } + + FString ClassName = IfaceDesc.Interface->GetName(); + if (ClassName.Equals(InterfaceName, ESearchCase::IgnoreCase)) + { + FoundInterface = IfaceDesc.Interface; + break; + } + // Strip "_C" suffix for comparison + FString TrimmedName = ClassName; + if (TrimmedName.EndsWith(TEXT("_C"))) + { + TrimmedName = TrimmedName.LeftChop(2); + } + if (TrimmedName.Equals(InterfaceName, ESearchCase::IgnoreCase)) + { + FoundInterface = IfaceDesc.Interface; + break; + } + } + + if (!FoundInterface) + { + // Build helpful error with list of implemented interfaces + TArray> IfaceList; + for (const FBPInterfaceDescription& IfaceDesc : BP->ImplementedInterfaces) + { + if (IfaceDesc.Interface) + { + IfaceList.Add(MakeShared(IfaceDesc.Interface->GetName())); + } + } + + MCPUtils::MakeErrorJson(Result, FString::Printf( + TEXT("Interface '%s' is not implemented by Blueprint '%s'"), + *InterfaceName, *Blueprint)); + Result->SetArrayField(TEXT("implementedInterfaces"), IfaceList); + return; + } + + FTopLevelAssetPath InterfacePath = FoundInterface->GetClassPathName(); + + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Removing interface '%s' from Blueprint '%s' (preserveFunctions: %s)"), + *FoundInterface->GetName(), *Blueprint, PreserveFunctions ? TEXT("true") : TEXT("false")); + + FBlueprintEditorUtils::RemoveInterface(BP, InterfacePath, PreserveFunctions); + + FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP); + + + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Removed interface '%s' from '%s'"), + *FoundInterface->GetName(), *Blueprint); + + Result->SetStringField(TEXT("interfaceName"), FoundInterface->GetName()); + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_RemoveBlueprintVariable.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_RemoveBlueprintVariable.h new file mode 100644 index 00000000..d08597ec --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_RemoveBlueprintVariable.h @@ -0,0 +1,85 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPAssetFinder.h" +#include "MCPUtils.h" +#include "Engine/Blueprint.h" +#include "EdGraph/EdGraph.h" +#include "EdGraph/EdGraphPin.h" +#include "K2Node_VariableGet.h" +#include "K2Node_VariableSet.h" +#include "Kismet2/BlueprintEditorUtils.h" +#include "UMCPHandler_RemoveBlueprintVariable.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +UCLASS() +class UMCPHandler_RemoveBlueprintVariable : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Blueprint name or package path")) + FString Blueprint; + + UPROPERTY(meta=(Description="Name of the variable to remove")) + FString VariableName; + + virtual FString GetDescription() const override + { + return TEXT("Remove a member variable from a Blueprint."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override + { + MCPAssets Assets; + if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; + UBlueprint* BP = Assets.Object(); + + // Find variable by name (case-insensitive) + FName VarFName(*VariableName); + bool bVarFound = false; + for (const FBPVariableDescription& Var : BP->NewVariables) + { + if (Var.VarName.ToString().Equals(VariableName, ESearchCase::IgnoreCase)) + { + VarFName = Var.VarName; // Use the exact name found + bVarFound = true; + break; + } + } + + if (!bVarFound) + { + // Build available variables list for helpful error message + TArray> AvailVars; + for (const FBPVariableDescription& Var : BP->NewVariables) + { + AvailVars.Add(MakeShared(Var.VarName.ToString())); + } + + MCPUtils::MakeErrorJson(Result, FString::Printf( + TEXT("Variable '%s' not found in Blueprint '%s'"), *VariableName, *Blueprint)); + Result->SetArrayField(TEXT("availableVariables"), AvailVars); + return; + } + + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Removing variable '%s' from Blueprint '%s'"), + *VariableName, *Blueprint); + + // Use the editor utility to remove the variable (also cleans up Get/Set nodes) + FBlueprintEditorUtils::RemoveMemberVariable(BP, VarFName); + + FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP); + bool bSaved = MCPUtils::SaveBlueprintPackage(BP); + + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Removed variable '%s' from '%s' (saved: %s)"), + *VariableName, *Blueprint, bSaved ? TEXT("true") : TEXT("false")); + + Result->SetBoolField(TEXT("saved"), bSaved); + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_RemoveFunctionParameter.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_RemoveFunctionParameter.h new file mode 100644 index 00000000..386670dc --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_RemoveFunctionParameter.h @@ -0,0 +1,154 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPAssetFinder.h" +#include "MCPUtils.h" +#include "Engine/Blueprint.h" +#include "EdGraph/EdGraph.h" +#include "EdGraph/EdGraphPin.h" +#include "K2Node_FunctionEntry.h" +#include "K2Node_CustomEvent.h" +#include "K2Node_EditablePinBase.h" +#include "Kismet2/BlueprintEditorUtils.h" +#include "UMCPHandler_RemoveFunctionParameter.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +UCLASS() +class UMCPHandler_RemoveFunctionParameter : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Blueprint name or package path")) + FString Blueprint; + + UPROPERTY(meta=(Description="Name of the function or custom event")) + FString FunctionName; + + UPROPERTY(meta=(Description="Name of the parameter to remove")) + FString ParamName; + + virtual FString GetDescription() const override + { + return TEXT("Remove a parameter from a function or custom event in a Blueprint."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override + { + MCPAssets Assets; + if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; + UBlueprint* BP = Assets.Object(); + + // Find the entry node + UK2Node_EditablePinBase* EntryNode = nullptr; + FString FoundNodeType; + + // Strategy 1: Look for a K2Node_FunctionEntry in a function graph matching the name + for (UK2Node_FunctionEntry* FuncEntry : MCPUtils::AllNodes(BP)) + { + if (FuncEntry->GetGraph()->GetName().Equals(FunctionName, ESearchCase::IgnoreCase)) + { + EntryNode = FuncEntry; + FoundNodeType = TEXT("FunctionEntry"); + break; + } + } + + // Strategy 2: Search for a K2Node_CustomEvent with matching CustomFunctionName + if (!EntryNode) + { + for (UK2Node_CustomEvent* CustomEvent : MCPUtils::AllNodes(BP)) + { + if (CustomEvent->CustomFunctionName.ToString().Equals(FunctionName, ESearchCase::IgnoreCase)) + { + EntryNode = CustomEvent; + FoundNodeType = TEXT("CustomEvent"); + break; + } + } + } + + if (!EntryNode) + { + // List available functions/events for debugging + TArray> Available; + for (UK2Node_FunctionEntry* FE : MCPUtils::AllNodes(BP)) + { + Available.Add(MakeShared( + FString::Printf(TEXT("function:%s"), *FE->GetGraph()->GetName()))); + } + for (UK2Node_CustomEvent* CE : MCPUtils::AllNodes(BP)) + { + Available.Add(MakeShared( + FString::Printf(TEXT("event:%s"), *CE->CustomFunctionName.ToString()))); + } + + MCPUtils::MakeErrorJson(Result, FString::Printf( + TEXT("Function or custom event '%s' not found in Blueprint '%s'"), + *FunctionName, *Blueprint)); + Result->SetArrayField(TEXT("availableFunctionsAndEvents"), Available); + return; + } + + // Find and remove the UserDefinedPin matching paramName + int32 RemovedIndex = INDEX_NONE; + for (int32 i = 0; i < EntryNode->UserDefinedPins.Num(); ++i) + { + if (EntryNode->UserDefinedPins[i].IsValid() && + EntryNode->UserDefinedPins[i]->PinName.ToString().Equals(ParamName, ESearchCase::IgnoreCase)) + { + RemovedIndex = i; + break; + } + } + + if (RemovedIndex == INDEX_NONE) + { + // List available params for debugging + TArray> ParamNames; + for (const TSharedPtr& PinInfo : EntryNode->UserDefinedPins) + { + if (PinInfo.IsValid()) + { + ParamNames.Add(MakeShared(PinInfo->PinName.ToString())); + } + } + + MCPUtils::MakeErrorJson(Result, FString::Printf( + TEXT("Parameter '%s' not found in %s '%s'"), + *ParamName, *FoundNodeType, *FunctionName)); + Result->SetArrayField(TEXT("availableParams"), ParamNames); + return; + } + + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Removing param '%s' from %s '%s' in '%s'"), + *ParamName, *FoundNodeType, *FunctionName, *Blueprint); + + // Remove the pin + EntryNode->UserDefinedPins.RemoveAt(RemovedIndex); + + // Reconstruct the node to update output pins (use schema for MinimalAPI compat) + if (UEdGraph* OwningGraph = EntryNode->GetGraph()) + { + if (const UEdGraphSchema* Schema = OwningGraph->GetSchema()) + { + Schema->ReconstructNode(*EntryNode); + } + } + + // Save + bool bSaved = MCPUtils::SaveBlueprintPackage(BP); + + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Parameter removed, save %s"), + bSaved ? TEXT("succeeded") : TEXT("failed")); + + Result->SetStringField(TEXT("nodeType"), FoundNodeType); + Result->SetStringField(TEXT("nodeId"), EntryNode->NodeGuid.ToString()); + Result->SetBoolField(TEXT("saved"), bSaved); + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_RemoveStructField.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_RemoveStructField.h new file mode 100644 index 00000000..63ddedcf --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_RemoveStructField.h @@ -0,0 +1,86 @@ +#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 "UMCPHandler_RemoveStructField.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +UCLASS() +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 + MCPAssets Assets; + if (!Assets.Exact(AssetPath).Errors(Result).ENone().ETwo().Load()) return; + UUserDefinedStruct* Struct = Assets.Object(); + + // 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("saved"), bSaved); + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_RenameAsset.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_RenameAsset.h new file mode 100644 index 00000000..7a8e39a0 --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_RenameAsset.h @@ -0,0 +1,95 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPAssetFinder.h" +#include "MCPUtils.h" +#include "Engine/Blueprint.h" +#include "Kismet2/BlueprintEditorUtils.h" +#include "UObject/SavePackage.h" +#include "Misc/PackageName.h" +#include "AssetRegistry/AssetRegistryModule.h" +#include "AssetRegistry/IAssetRegistry.h" +#include "AssetToolsModule.h" +#include "IAssetTools.h" +#include "FileHelpers.h" +#include "UMCPHandler_RenameAsset.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +UCLASS() +class UMCPHandler_RenameAsset : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Current package path of the asset")) + FString AssetPath; + + UPROPERTY(meta=(Description="New package path or new asset name")) + FString NewPath; + + virtual FString GetDescription() const override + { + return TEXT("Rename or move an asset with reference fixup."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override + { + + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Renaming asset '%s' -> '%s'"), *AssetPath, *NewPath); + + // Use FAssetToolsModule to perform the rename with reference fixup + FAssetToolsModule& AssetToolsModule = FModuleManager::LoadModuleChecked("AssetTools"); + IAssetTools& AssetTools = AssetToolsModule.Get(); + + // Build the source/dest arrays + TArray RenameData; + + // We need to load the asset to get the object + MCPAssets Assets; + if (!Assets.Exact(AssetPath).Errors(Result).ENone().ETwo().Load()) return; + UObject* AssetObj = Assets.Object(); + + // Parse new path into package path and asset name + FString NewPackagePath, NewAssetName; + int32 LastSlash; + if (NewPath.FindLastChar(TEXT('/'), LastSlash)) + { + NewPackagePath = NewPath.Left(LastSlash); + NewAssetName = NewPath.Mid(LastSlash + 1); + } + else + { + // If no slash, assume same directory with new name + FString OldPackagePath; + if (AssetPath.FindLastChar(TEXT('/'), LastSlash)) + { + OldPackagePath = AssetPath.Left(LastSlash); + } + NewPackagePath = OldPackagePath; + NewAssetName = NewPath; + } + + FAssetRenameData RenameEntry(AssetObj, NewPackagePath, NewAssetName); + RenameData.Add(RenameEntry); + + bool bSuccess = AssetTools.RenameAssets(RenameData); + + if (bSuccess) + { + } + + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Rename %s"), bSuccess ? TEXT("succeeded") : TEXT("failed")); + + Result->SetStringField(TEXT("newPackagePath"), NewPackagePath); + Result->SetStringField(TEXT("newAssetName"), NewAssetName); + if (!bSuccess) + { + MCPUtils::MakeErrorJson(Result, TEXT("Asset rename failed. The target path may be invalid or a conflicting asset may exist.")); + } + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_RenameBlueprintGraph.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_RenameBlueprintGraph.h new file mode 100644 index 00000000..14072db5 --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_RenameBlueprintGraph.h @@ -0,0 +1,115 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPAssetFinder.h" +#include "MCPUtils.h" +#include "Engine/Blueprint.h" +#include "EdGraph/EdGraph.h" +#include "EdGraph/EdGraphNode.h" +#include "EdGraphSchema_K2.h" +#include "K2Node_CustomEvent.h" +#include "Kismet2/BlueprintEditorUtils.h" +#include "Kismet2/KismetEditorUtilities.h" +#include "UObject/UObjectIterator.h" +#include "UMCPHandler_RenameBlueprintGraph.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +UCLASS() +class UMCPHandler_RenameBlueprintGraph : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Blueprint name or package path")) + FString Blueprint; + + UPROPERTY(meta=(Description="Current name of the graph to rename")) + FString Graph; + + UPROPERTY(meta=(Description="New name for the graph")) + FString NewName; + + virtual FString GetDescription() const override + { + return TEXT("Rename a function or macro graph in a Blueprint. Cannot rename EventGraph pages."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override + { + MCPAssets Assets; + if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; + UBlueprint* BP = Assets.Object(); + + // Check if it's an UbergraphPage — disallow rename + for (UEdGraph* CandidateGraph : BP->UbergraphPages) + { + if (CandidateGraph && CandidateGraph->GetName().Equals(Graph, ESearchCase::IgnoreCase)) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf( + TEXT("Cannot rename UbergraphPage '%s'. EventGraph and other Ubergraph pages cannot be renamed."), + *Graph)); + } + } + + // Find the graph in FunctionGraphs or MacroGraphs + UEdGraph* TargetGraph = nullptr; + FString GraphType; + + for (UEdGraph* CandidateGraph : BP->FunctionGraphs) + { + if (CandidateGraph && CandidateGraph->GetName().Equals(Graph, ESearchCase::IgnoreCase)) + { + TargetGraph = CandidateGraph; + GraphType = TEXT("function"); + break; + } + } + if (!TargetGraph) + { + for (UEdGraph* CandidateGraph : BP->MacroGraphs) + { + if (CandidateGraph && CandidateGraph->GetName().Equals(Graph, ESearchCase::IgnoreCase)) + { + TargetGraph = CandidateGraph; + GraphType = TEXT("macro"); + break; + } + } + } + + if (!TargetGraph) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Graph '%s' not found in Blueprint '%s'"), *Graph, *Blueprint)); + } + + // Check for name collision + for (UEdGraph* Existing : MCPUtils::AllGraphsNamed(BP, NewName)) + { + if (Existing != TargetGraph) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf( + TEXT("A graph named '%s' already exists in Blueprint '%s'"), *NewName, *Blueprint)); + } + } + + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Renaming %s graph '%s' to '%s' in Blueprint '%s'"), + *GraphType, *Graph, *NewName, *Blueprint); + + FBlueprintEditorUtils::RenameGraph(TargetGraph, NewName); + + FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP); + bool bSaved = MCPUtils::SaveBlueprintPackage(BP); + + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Renamed graph '%s' to '%s', save %s"), + *Graph, *NewName, bSaved ? TEXT("true") : TEXT("false")); + + Result->SetStringField(TEXT("newName"), TargetGraph->GetName()); + Result->SetStringField(TEXT("graphType"), GraphType); + Result->SetBoolField(TEXT("saved"), bSaved); + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_ReparentBlueprint.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_ReparentBlueprint.h new file mode 100644 index 00000000..aecea5e4 --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_ReparentBlueprint.h @@ -0,0 +1,117 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPAssetFinder.h" +#include "MCPUtils.h" +#include "Engine/Blueprint.h" +#include "EdGraph/EdGraph.h" +#include "EdGraph/EdGraphNode.h" +#include "EdGraphSchema_K2.h" +#include "K2Node_CustomEvent.h" +#include "Kismet2/BlueprintEditorUtils.h" +#include "Kismet2/KismetEditorUtilities.h" +#include "UObject/UObjectIterator.h" +#include "UMCPHandler_ReparentBlueprint.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +UCLASS() +class UMCPHandler_ReparentBlueprint : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Blueprint name or package path")) + FString Blueprint; + + UPROPERTY(meta=(Description="Name of the new parent class (C++ class name or Blueprint name)")) + FString NewParentClass; + + virtual FString GetDescription() const override + { + return TEXT("Change a Blueprint's parent class. Accepts C++ class names or Blueprint names."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override + { + // Load Blueprint + MCPAssets Assets; + if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; + UBlueprint* BP = Assets.Object(); + + FString OldParentName = BP->ParentClass ? BP->ParentClass->GetName() : TEXT("None"); + + // Find the new parent class + // Try C++ class first (e.g. "WebUIHUD" finds /Script/ModuleName.WebUIHUD) + UClass* NewParentClassObj = nullptr; + + // Search across all packages for native classes + for (TObjectIterator It; It; ++It) + { + if (It->GetName() == NewParentClass) + { + NewParentClassObj = *It; + break; + } + } + + // If not found as C++ class, try loading as a Blueprint asset + if (!NewParentClassObj) + { + MCPAssets ParentAssets; + if (!ParentAssets.Exact(NewParentClass).AllContent().Errors(Result).ETwo().Load()) return; + if (!ParentAssets.Objects().IsEmpty()) + { + if (ParentAssets.Object()->GeneratedClass) + NewParentClassObj = ParentAssets.Object()->GeneratedClass; + } + } + + if (!NewParentClassObj) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf( + TEXT("Could not find class '%s'. Provide a C++ class name (e.g. 'WebUIHUD') or Blueprint name."), + *NewParentClass)); + } + + // Validate: new parent must be compatible + if (BP->ParentClass && !NewParentClassObj->IsChildOf(BP->ParentClass->GetSuperClass()) && + BP->ParentClass != NewParentClassObj) + { + // Just warn, don't block — the user may intentionally reparent to a sibling + UE_LOG(LogTemp, Warning, + TEXT("BlueprintMCP: Reparenting '%s' from '%s' to '%s' — classes are not in a direct hierarchy"), + *Blueprint, *OldParentName, *NewParentClassObj->GetName()); + } + + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Reparenting '%s' from '%s' to '%s'"), + *Blueprint, *OldParentName, *NewParentClassObj->GetName()); + + // Perform reparent + BP->PreEditChange(nullptr); + BP->ParentClass = NewParentClassObj; + BP->PostEditChange(); + + // Refresh all nodes to pick up new parent's functions/variables + FBlueprintEditorUtils::RefreshAllNodes(BP); + + // Compile + FKismetEditorUtilities::CompileBlueprint(BP); + + // Save + bool bSaved = MCPUtils::SaveBlueprintPackage(BP); + + FString NewParentActualName = NewParentClassObj->GetName(); + + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Reparent complete, save %s"), + bSaved ? TEXT("succeeded") : TEXT("failed")); + + Result->SetStringField(TEXT("oldParentClass"), OldParentName); + Result->SetStringField(TEXT("newParentClass"), NewParentActualName); + Result->SetBoolField(TEXT("saved"), bSaved); + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_ReparentMaterialInstance.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_ReparentMaterialInstance.h new file mode 100644 index 00000000..bbe68191 --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_ReparentMaterialInstance.h @@ -0,0 +1,133 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPAssetFinder.h" +#include "MCPUtils.h" +#include "Materials/Material.h" +#include "Materials/MaterialInterface.h" +#include "Materials/MaterialInstanceConstant.h" +#include "Materials/MaterialExpressionScalarParameter.h" +#include "Materials/MaterialExpressionVectorParameter.h" +#include "Materials/MaterialExpressionTextureSampleParameter2D.h" +#include "Materials/MaterialExpressionStaticSwitchParameter.h" +#include "Factories/MaterialInstanceConstantFactoryNew.h" +#include "AssetToolsModule.h" +#include "IAssetTools.h" +#include "Engine/Texture.h" +#include "UMCPHandler_ReparentMaterialInstance.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +UCLASS() +class UMCPHandler_ReparentMaterialInstance : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Material Instance name or path to reparent")) + FString MaterialInstance; + + UPROPERTY(meta=(Description="New parent material name or path (Material or Material Instance)")) + FString NewParent; + + UPROPERTY(meta=(Optional, Description="If true, validate without applying changes")) + bool DryRun = false; + + virtual FString GetDescription() const override + { + return TEXT("Change the parent material of a Material Instance. " + "Validates against circular parent chains."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override + { + // Load the Material Instance + MCPAssets Assets; + if (!Assets.Exact(MaterialInstance).Errors(Result).ENone().ETwo().Load()) return; + UMaterialInstanceConstant* MI = Assets.Object(); + + // Capture old parent + FString OldParentPath = MI->Parent ? MI->Parent->GetPathName() : TEXT("None"); + + // Load new parent — try as Material first, then as Material Instance + UMaterialInterface* NewParentObj = nullptr; + { + MCPAssets MatAssets; + if (MatAssets.Exact(NewParent).ETwo().Load() && !MatAssets.Objects().IsEmpty()) + { + NewParentObj = MatAssets.Object(); + } + else + { + MCPAssets MIAssets; + if (MIAssets.Exact(NewParent).ETwo().Load() && !MIAssets.Objects().IsEmpty()) + { + NewParentObj = MIAssets.Object(); + } + } + } + + if (!NewParentObj) + { + // Try LoadObject as a fallback + NewParentObj = LoadObject(nullptr, *NewParent); + } + + if (!NewParentObj) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf( + TEXT("New parent material '%s' not found. Provide a Material or Material Instance name/path."), + *NewParent)); + } + + // Prevent circular parenting — check if NewParent is this MI or has this MI in its chain + { + UMaterialInterface* Check = NewParentObj; + while (Check) + { + if (Check == MI) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf( + TEXT("Cannot reparent '%s' to '%s' — this would create a circular parent chain."), + *MaterialInstance, *NewParent)); + } + UMaterialInstanceConstant* CheckMI = Cast(Check); + if (CheckMI) + { + Check = CheckMI->Parent; + } + else + { + break; + } + } + } + + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: %s Material Instance '%s': parent '%s' -> '%s'"), + DryRun ? TEXT("[DRY RUN] Reparenting") : TEXT("Reparenting"), + *MaterialInstance, *OldParentPath, *NewParentObj->GetPathName()); + + if (!DryRun) + { + MI->PreEditChange(nullptr); + MI->Parent = NewParentObj; + MI->PostEditChange(); + + bool bSaved = MCPUtils::SaveGenericPackage(MI); + + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Reparented Material Instance '%s' (saved: %s)"), + *MaterialInstance, bSaved ? TEXT("true") : TEXT("false")); + } + + Result->SetStringField(TEXT("oldParent"), OldParentPath); + Result->SetStringField(TEXT("newParent"), NewParentObj->GetPathName()); + if (DryRun) + { + Result->SetBoolField(TEXT("dryRun"), true); + } + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_ReplaceFunctionCallsInBlueprint.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_ReplaceFunctionCallsInBlueprint.h new file mode 100644 index 00000000..7c39d1e6 --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_ReplaceFunctionCallsInBlueprint.h @@ -0,0 +1,303 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPAssetFinder.h" +#include "MCPServer.h" +#include "MCPUtils.h" +#include "Engine/Blueprint.h" +#include "Materials/Material.h" +#include "Materials/MaterialInstanceConstant.h" +#include "Materials/MaterialFunction.h" +#include "Engine/World.h" +#include "Engine/LevelScriptBlueprint.h" +#include "EdGraph/EdGraph.h" +#include "EdGraph/EdGraphNode.h" +#include "EdGraph/EdGraphPin.h" +#include "EdGraphSchema_K2.h" +#include "K2Node.h" +#include "K2Node_CallFunction.h" +#include "K2Node_Event.h" +#include "K2Node_CustomEvent.h" +#include "K2Node_FunctionEntry.h" +#include "K2Node_EditablePinBase.h" +#include "K2Node_VariableGet.h" +#include "K2Node_VariableSet.h" +#include "K2Node_BreakStruct.h" +#include "K2Node_MakeStruct.h" +#include "K2Node_DynamicCast.h" +#include "K2Node_CallParentFunction.h" +#include "K2Node_IfThenElse.h" +#include "K2Node_ExecutionSequence.h" +#include "K2Node_MacroInstance.h" +#include "K2Node_SpawnActorFromClass.h" +#include "K2Node_Select.h" +#include "K2Node_Knot.h" +#include "EdGraphNode_Comment.h" +#include "GameFramework/Actor.h" +#include "Kismet2/BlueprintEditorUtils.h" +#include "Kismet2/KismetEditorUtilities.h" +#include "Serialization/JsonReader.h" +#include "Serialization/JsonWriter.h" +#include "Serialization/JsonSerializer.h" +#include "UObject/SavePackage.h" +#include "UObject/UObjectIterator.h" +#include "Misc/PackageName.h" +#include "AssetRegistry/AssetRegistryModule.h" +#include "AssetRegistry/IAssetRegistry.h" +#include "AssetToolsModule.h" +#include "IAssetTools.h" +#include "BlueprintNodeSpawner.h" +#include "UMCPHandler_ReplaceFunctionCallsInBlueprint.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +UCLASS() +class UMCPHandler_ReplaceFunctionCallsInBlueprint : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Blueprint name or package path")) + FString Blueprint; + + UPROPERTY(meta=(Description="Old class name to match")) + FString OldClass; + + UPROPERTY(meta=(Description="New class name to redirect to")) + FString NewClass; + + UPROPERTY(meta=(Optional, Description="If true, report what would change without modifying")) + bool DryRun = false; + + virtual FString GetDescription() const override + { + return TEXT("Redirect function call nodes from one class to another. " + "Supports dry-run to preview impact before applying."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override + { + + // Load Blueprint + MCPAssets Assets; + if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; + UBlueprint* BP = Assets.Object(); + + // Find the new class — try several search strategies + UClass* NewClassPtr = nullptr; + + // Try finding the class across all loaded modules + NewClassPtr = FindFirstObject(*NewClass); + + // Try with U prefix stripped/added + if (!NewClassPtr && NewClass.StartsWith(TEXT("U"))) + { + NewClassPtr = FindFirstObject(*NewClass.Mid(1)); + } + if (!NewClassPtr && !NewClass.StartsWith(TEXT("U"))) + { + NewClassPtr = FindFirstObject(*FString::Printf(TEXT("U%s"), *NewClass)); + } + + // Broader search across all modules + if (!NewClassPtr) + { + for (TObjectIterator It; It; ++It) + { + if (It->GetName() == NewClass || It->GetName() == FString::Printf(TEXT("U%s"), *NewClass)) + { + NewClassPtr = *It; + break; + } + } + } + + if (!NewClassPtr) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Could not find class '%s'"), *NewClass)); + } + + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: %s function calls in '%s': %s -> %s (%s)"), + DryRun ? TEXT("[DRY RUN] Analyzing replacement of") : TEXT("Replacing"), + *Blueprint, *OldClass, *NewClass, *NewClassPtr->GetPathName()); + + // Find all CallFunction nodes + int32 ReplacedCount = 0; + TArray> BrokenConnections; + + for (UK2Node_CallFunction* CallNode : MCPUtils::AllNodes(BP)) + { + UClass* ParentClass = CallNode->FunctionReference.GetMemberParentClass(); + if (!ParentClass) + { + continue; + } + + // Match by class name (with or without U prefix, and _C suffix for BP classes) + FString ParentName = ParentClass->GetName(); + bool bMatch = (ParentName == OldClass) || + (ParentName == FString::Printf(TEXT("%s_C"), *OldClass)) || + (ParentName == FString::Printf(TEXT("U%s"), *OldClass)) || + (OldClass.StartsWith(TEXT("U")) && (ParentName == OldClass.Mid(1))) || + (OldClass.EndsWith(TEXT("_C")) && (ParentName == OldClass.LeftChop(2))); + + if (!bMatch) + { + continue; + } + + FName FuncName = CallNode->FunctionReference.GetMemberName(); + + // Find the matching function in the new class + UFunction* NewFunc = NewClassPtr->FindFunctionByName(FuncName); + if (!NewFunc) + { + UE_LOG(LogTemp, Warning, TEXT("BlueprintMCP: Function '%s' not found in '%s', skipping node"), + *FuncName.ToString(), *NewClass); + + TSharedRef Warning = MakeShared(); + Warning->SetStringField(TEXT("type"), TEXT("functionNotFound")); + Warning->SetStringField(TEXT("functionName"), FuncName.ToString()); + Warning->SetStringField(TEXT("nodeId"), CallNode->NodeGuid.ToString()); + BrokenConnections.Add(MakeShared(Warning)); + continue; + } + + if (DryRun) + { + // In dry run mode: report what would be affected without modifying + ReplacedCount++; + + // Check which pins have connections that might break + for (UEdGraphPin* Pin : CallNode->Pins) + { + if (!Pin || Pin->LinkedTo.Num() == 0) continue; + + // Check if the new function has a matching parameter + bool bPinExistsInNew = false; + for (TFieldIterator PropIt(NewFunc); PropIt; ++PropIt) + { + if (PropIt->GetFName() == Pin->PinName || + Pin->PinName == UEdGraphSchema_K2::PN_Execute || + Pin->PinName == UEdGraphSchema_K2::PN_Then || + Pin->PinName == UEdGraphSchema_K2::PN_Self || + Pin->PinName == UEdGraphSchema_K2::PN_ReturnValue) + { + bPinExistsInNew = true; + break; + } + } + + if (!bPinExistsInNew) + { + for (UEdGraphPin* Linked : Pin->LinkedTo) + { + if (Linked && Linked->GetOwningNode()) + { + TSharedRef AtRisk = MakeShared(); + AtRisk->SetStringField(TEXT("type"), TEXT("connectionAtRisk")); + AtRisk->SetStringField(TEXT("functionName"), FuncName.ToString()); + AtRisk->SetStringField(TEXT("nodeId"), CallNode->NodeGuid.ToString()); + AtRisk->SetStringField(TEXT("pinName"), Pin->PinName.ToString()); + AtRisk->SetStringField(TEXT("connectedToNode"), Linked->GetOwningNode()->NodeGuid.ToString()); + AtRisk->SetStringField(TEXT("connectedToPin"), Linked->PinName.ToString()); + BrokenConnections.Add(MakeShared(AtRisk)); + } + } + } + } + } + else + { + // Record existing pin connections before replacement + TMap>> OldPinConnections; + for (UEdGraphPin* Pin : CallNode->Pins) + { + if (Pin->LinkedTo.Num() > 0) + { + TArray> Links; + for (UEdGraphPin* Linked : Pin->LinkedTo) + { + if (Linked && Linked->GetOwningNode()) + { + Links.Add(TPair( + Linked->GetOwningNode()->NodeGuid.ToString(), + Linked->PinName.ToString())); + } + } + OldPinConnections.Add(Pin->PinName.ToString(), Links); + } + } + + // Replace the function reference + CallNode->SetFromFunction(NewFunc); + ReplacedCount++; + + // Check which connections survived + for (auto& Pair : OldPinConnections) + { + const FString& PinName = Pair.Key; + const TArray>& OldLinks = Pair.Value; + + UEdGraphPin* NewPin = CallNode->FindPin(FName(*PinName)); + for (auto& Link : OldLinks) + { + bool bStillConnected = false; + if (NewPin) + { + for (UEdGraphPin* L : NewPin->LinkedTo) + { + if (L && L->GetOwningNode() && + L->GetOwningNode()->NodeGuid.ToString() == Link.Key && + L->PinName.ToString() == Link.Value) + { + bStillConnected = true; + break; + } + } + } + if (!bStillConnected) + { + TSharedRef Broken = MakeShared(); + Broken->SetStringField(TEXT("type"), TEXT("connectionLost")); + Broken->SetStringField(TEXT("functionName"), FuncName.ToString()); + Broken->SetStringField(TEXT("nodeId"), CallNode->NodeGuid.ToString()); + Broken->SetStringField(TEXT("pinName"), PinName); + Broken->SetStringField(TEXT("wasConnectedToNode"), Link.Key); + Broken->SetStringField(TEXT("wasConnectedToPin"), Link.Value); + BrokenConnections.Add(MakeShared(Broken)); + } + } + } + } + } + + if (DryRun) + { + Result->SetNumberField(TEXT("wouldReplaceCount"), ReplacedCount); + Result->SetNumberField(TEXT("connectionsAtRisk"), BrokenConnections.Num()); + Result->SetArrayField(TEXT("connectionsAtRisk"), BrokenConnections); + return; + } + + if (ReplacedCount > 0) + { + FBlueprintEditorUtils::MarkBlueprintAsModified(BP); + + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Replaced %d function call(s)"), ReplacedCount); + + Result->SetNumberField(TEXT("replacedCount"), ReplacedCount); + Result->SetNumberField(TEXT("brokenConnectionCount"), BrokenConnections.Num()); + Result->SetArrayField(TEXT("brokenConnections"), BrokenConnections); + return; + } + + Result->SetNumberField(TEXT("replacedCount"), 0); + Result->SetStringField(TEXT("message"), FString::Printf( + TEXT("No function call nodes found targeting class '%s'"), *OldClass)); + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_RestoreAsset.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_RestoreAsset.h new file mode 100644 index 00000000..a1c5d7a1 --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_RestoreAsset.h @@ -0,0 +1,72 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPAssetFinder.h" +#include "MCPUtils.h" +#include "Engine/Blueprint.h" +#include "Kismet2/BlueprintEditorUtils.h" +#include "UObject/SavePackage.h" +#include "Misc/PackageName.h" +#include "AssetRegistry/AssetRegistryModule.h" +#include "AssetRegistry/IAssetRegistry.h" +#include "AssetToolsModule.h" +#include "IAssetTools.h" +#include "FileHelpers.h" +#include "UMCPHandler_RestoreAsset.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +UCLASS() +class UMCPHandler_RestoreAsset : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Full package path of the asset (e.g. /Game/Widgets/WB_Hotkeys)")) + FString AssetPath; + + virtual FString GetDescription() const override + { + return TEXT("Restore a .uasset file from its .uasset.bak backup, reloading it in the editor."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override + { + FString Filename = FPaths::ConvertRelativePathToFull( + FPackageName::LongPackageNameToFilename(AssetPath, FPackageName::GetAssetPackageExtension())); + FString BackupFilename = Filename + TEXT(".bak"); + + if (!IFileManager::Get().FileExists(*BackupFilename)) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Backup file not found: %s"), *BackupFilename)); + } + + // Release file handles if the package is loaded + UPackage* Package = FindPackage(nullptr, *AssetPath); + if (Package) + { + ResetLoaders(Package); + } + + // Copy backup over the original + uint32 CopyResult = IFileManager::Get().Copy(*Filename, *BackupFilename, true); + if (CopyResult != COPY_OK) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Failed to restore %s"), *Filename)); + } + + // Reload the package if it was loaded + if (Package) + { + bool bReloaded = false; + FText ErrorMessage; + UEditorLoadingAndSavingUtils::ReloadPackages({Package}, bReloaded, ErrorMessage, EReloadPackagesInteractionMode::AssumePositive); + } + + Result->SetStringField(TEXT("restoredFrom"), BackupFilename); + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_SearchSpawnableNodeTypes.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_SearchSpawnableNodeTypes.h new file mode 100644 index 00000000..5d29628b --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_SearchSpawnableNodeTypes.h @@ -0,0 +1,118 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPAssetFinder.h" +#include "MCPServer.h" +#include "MCPUtils.h" +#include "Engine/Blueprint.h" +#include "Materials/Material.h" +#include "Materials/MaterialInstanceConstant.h" +#include "Materials/MaterialFunction.h" +#include "Engine/World.h" +#include "Engine/LevelScriptBlueprint.h" +#include "EdGraph/EdGraph.h" +#include "EdGraph/EdGraphNode.h" +#include "EdGraph/EdGraphPin.h" +#include "EdGraphSchema_K2.h" +#include "K2Node.h" +#include "K2Node_CallFunction.h" +#include "K2Node_Event.h" +#include "K2Node_CustomEvent.h" +#include "K2Node_FunctionEntry.h" +#include "K2Node_EditablePinBase.h" +#include "K2Node_VariableGet.h" +#include "K2Node_VariableSet.h" +#include "K2Node_BreakStruct.h" +#include "K2Node_MakeStruct.h" +#include "K2Node_DynamicCast.h" +#include "K2Node_CallParentFunction.h" +#include "K2Node_IfThenElse.h" +#include "K2Node_ExecutionSequence.h" +#include "K2Node_MacroInstance.h" +#include "K2Node_SpawnActorFromClass.h" +#include "K2Node_Select.h" +#include "K2Node_Knot.h" +#include "EdGraphNode_Comment.h" +#include "GameFramework/Actor.h" +#include "Kismet2/BlueprintEditorUtils.h" +#include "Kismet2/KismetEditorUtilities.h" +#include "Serialization/JsonReader.h" +#include "Serialization/JsonWriter.h" +#include "Serialization/JsonSerializer.h" +#include "UObject/SavePackage.h" +#include "UObject/UObjectIterator.h" +#include "Misc/PackageName.h" +#include "AssetRegistry/AssetRegistryModule.h" +#include "AssetRegistry/IAssetRegistry.h" +#include "AssetToolsModule.h" +#include "IAssetTools.h" +#include "BlueprintNodeSpawner.h" +#include "UMCPHandler_SearchSpawnableNodeTypes.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +UCLASS() +class UMCPHandler_SearchSpawnableNodeTypes : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Search query string")) + FString Query; + + UPROPERTY(meta=(Optional, Description="Maximum number of results (default 50, max 500)")) + int32 MaxResults = 50; + + UPROPERTY(meta=(Optional, Description="Blueprint name or path. If specified with graph, only returns nodes compatible with that graph.")) + FString Blueprint; + + UPROPERTY(meta=(Optional, Description="Graph name to filter by compatibility. Requires blueprint.")) + FString Graph; + + virtual FString GetDescription() const override + { + return TEXT("Search the Blueprint action database for node spawners matching a query. " + "Returns full action names for use with spawn_node. " + "Optionally filter by blueprint+graph to only show compatible node types."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override + { + int32 ClampedMax = FMath::Clamp(MaxResults, 1, 500); + + // Optionally resolve a graph to filter by compatibility + UEdGraph* GraphFilter = nullptr; + if (!Blueprint.IsEmpty() && !Graph.IsEmpty()) + { + MCPAssets BPAssets; + if (!BPAssets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; + UBlueprint* BP = BPAssets.Object(); + + FString DecodedGraphName = MCPUtils::UrlDecode(Graph); + TArray MatchingGraphs = MCPUtils::AllGraphsNamed(BP, DecodedGraphName); + if (MatchingGraphs.Num() > 0) + { + GraphFilter = MatchingGraphs[0]; + } + if (!GraphFilter) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Graph '%s' not found"), *DecodedGraphName)); + } + } + + TArray Spawners = MCPUtils::SearchNodeSpawners(Query, ClampedMax, /*ExactMatch=*/false, GraphFilter); + + TArray> ResultArray; + for (UBlueprintNodeSpawner* Spawner : Spawners) + { + ResultArray.Add(MakeShared(MCPUtils::NodeSpawnerFullName(Spawner))); + } + + Result->SetNumberField(TEXT("count"), ResultArray.Num()); + Result->SetArrayField(TEXT("results"), ResultArray); + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_SearchTypeUsageInBlueprints.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_SearchTypeUsageInBlueprints.h new file mode 100644 index 00000000..75630f4b --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_SearchTypeUsageInBlueprints.h @@ -0,0 +1,283 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPAssetFinder.h" +#include "MCPUtils.h" +#include "Engine/Blueprint.h" +#include "Engine/World.h" +#include "Engine/Level.h" +#include "Engine/LevelScriptBlueprint.h" +#include "EdGraph/EdGraph.h" +#include "EdGraph/EdGraphNode.h" +#include "K2Node_CallFunction.h" +#include "K2Node_Event.h" +#include "K2Node_CustomEvent.h" +#include "K2Node_VariableGet.h" +#include "K2Node_VariableSet.h" +#include "K2Node_BreakStruct.h" +#include "K2Node_MakeStruct.h" +#include "K2Node_FunctionEntry.h" +#include "K2Node_EditablePinBase.h" +#include "AssetRegistry/IAssetRegistry.h" +#include "UMCPHandler_SearchTypeUsageInBlueprints.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +// ============================================================ +// HandleSearchByType — find all usages of a type across blueprints +// ============================================================ + +UCLASS() +class UMCPHandler_SearchTypeUsageInBlueprints : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Type name to search for (e.g. 'FVector', 'MyStruct'). F/E/U prefix is stripped for matching.")) + FString TypeName; + + UPROPERTY(meta=(Optional, Description="Filter to blueprints whose name or path contains this substring")) + FString Filter; + + UPROPERTY(meta=(Optional, Description="Maximum number of results to return (default 200, max 500)")) + int32 MaxResults = 0; + + virtual FString GetDescription() const override + { + return TEXT("Search all Blueprints for usages of a specific type in variables, function parameters, struct nodes, and pin connections."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override + { + FString DecodedTypeName = MCPUtils::UrlDecode(TypeName); + FString FilterStr = Filter.IsEmpty() ? FString() : MCPUtils::UrlDecode(Filter); + + int32 EffectiveMaxResults = (MaxResults > 0) ? FMath::Clamp(MaxResults, 1, 500) : 200; + + // Strip F/E/U prefix for comparison + FString TypeNameNoPrefix = DecodedTypeName; + if (TypeNameNoPrefix.StartsWith(TEXT("F")) || TypeNameNoPrefix.StartsWith(TEXT("E")) || TypeNameNoPrefix.StartsWith(TEXT("U"))) + { + TypeNameNoPrefix = TypeNameNoPrefix.Mid(1); + } + + auto MatchesType = [&DecodedTypeName, &TypeNameNoPrefix](const FString& TestType) -> bool + { + return TestType.Equals(DecodedTypeName, ESearchCase::IgnoreCase) || + TestType.Equals(TypeNameNoPrefix, ESearchCase::IgnoreCase); + }; + + TArray> Results; + + // Lambda that searches a single Blueprint for type usages + auto SearchOneBlueprint = [&](const FString& BPName, const FString& BPPath, UBlueprint* BP, bool bIsLevel) + { + // Check variables + for (const FBPVariableDescription& Var : BP->NewVariables) + { + if (Results.Num() >= EffectiveMaxResults) break; + + FString VarSubtype; + if (Var.VarType.PinSubCategoryObject.IsValid()) + { + VarSubtype = Var.VarType.PinSubCategoryObject->GetName(); + } + + if (MatchesType(VarSubtype) || MatchesType(Var.VarType.PinCategory.ToString())) + { + TSharedRef R = MakeShared(); + R->SetStringField(TEXT("blueprint"), BPName); + R->SetStringField(TEXT("blueprintPath"), BPPath); + R->SetStringField(TEXT("usage"), TEXT("variable")); + R->SetStringField(TEXT("location"), Var.VarName.ToString()); + R->SetStringField(TEXT("currentType"), Var.VarType.PinCategory.ToString()); + if (!VarSubtype.IsEmpty()) + R->SetStringField(TEXT("currentSubtype"), VarSubtype); + if (bIsLevel) + R->SetBoolField(TEXT("isLevelBlueprint"), true); + Results.Add(MakeShared(R)); + } + } + + // Check graphs for function/event params, struct nodes, and pin connections + for (UEdGraphNode* Node : MCPUtils::AllNodes(BP)) + { + if (Results.Num() >= EffectiveMaxResults) break; + + // Check FunctionEntry/CustomEvent parameters + if (auto* FuncEntry = Cast(Node)) + { + for (const TSharedPtr& PinInfo : FuncEntry->UserDefinedPins) + { + if (!PinInfo.IsValid()) continue; + FString ParamSubtype; + if (PinInfo->PinType.PinSubCategoryObject.IsValid()) + ParamSubtype = PinInfo->PinType.PinSubCategoryObject->GetName(); + + if (MatchesType(ParamSubtype) || MatchesType(PinInfo->PinType.PinCategory.ToString())) + { + TSharedRef R = MakeShared(); + R->SetStringField(TEXT("blueprint"), BPName); + R->SetStringField(TEXT("blueprintPath"), BPPath); + R->SetStringField(TEXT("usage"), TEXT("functionParameter")); + R->SetStringField(TEXT("location"), FString::Printf(TEXT("%s.%s"), + *Node->GetGraph()->GetName(), *PinInfo->PinName.ToString())); + R->SetStringField(TEXT("nodeId"), Node->NodeGuid.ToString()); + R->SetStringField(TEXT("currentType"), PinInfo->PinType.PinCategory.ToString()); + if (!ParamSubtype.IsEmpty()) + R->SetStringField(TEXT("currentSubtype"), ParamSubtype); + if (bIsLevel) + R->SetBoolField(TEXT("isLevelBlueprint"), true); + Results.Add(MakeShared(R)); + } + } + } + else if (auto* CustomEvent = Cast(Node)) + { + for (const TSharedPtr& PinInfo : CustomEvent->UserDefinedPins) + { + if (!PinInfo.IsValid()) continue; + FString ParamSubtype; + if (PinInfo->PinType.PinSubCategoryObject.IsValid()) + ParamSubtype = PinInfo->PinType.PinSubCategoryObject->GetName(); + + if (MatchesType(ParamSubtype) || MatchesType(PinInfo->PinType.PinCategory.ToString())) + { + TSharedRef R = MakeShared(); + R->SetStringField(TEXT("blueprint"), BPName); + R->SetStringField(TEXT("blueprintPath"), BPPath); + R->SetStringField(TEXT("usage"), TEXT("eventParameter")); + R->SetStringField(TEXT("location"), FString::Printf(TEXT("%s.%s"), + *CustomEvent->CustomFunctionName.ToString(), *PinInfo->PinName.ToString())); + R->SetStringField(TEXT("nodeId"), Node->NodeGuid.ToString()); + R->SetStringField(TEXT("currentType"), PinInfo->PinType.PinCategory.ToString()); + if (!ParamSubtype.IsEmpty()) + R->SetStringField(TEXT("currentSubtype"), ParamSubtype); + if (bIsLevel) + R->SetBoolField(TEXT("isLevelBlueprint"), true); + Results.Add(MakeShared(R)); + } + } + } + // Check Break/Make struct nodes + else if (auto* BreakNode = Cast(Node)) + { + if (BreakNode->StructType && MatchesType(BreakNode->StructType->GetName())) + { + TSharedRef R = MakeShared(); + R->SetStringField(TEXT("blueprint"), BPName); + R->SetStringField(TEXT("blueprintPath"), BPPath); + R->SetStringField(TEXT("usage"), TEXT("breakStruct")); + R->SetStringField(TEXT("location"), Node->GetGraph()->GetName()); + R->SetStringField(TEXT("nodeId"), Node->NodeGuid.ToString()); + R->SetStringField(TEXT("structType"), BreakNode->StructType->GetName()); + if (bIsLevel) + R->SetBoolField(TEXT("isLevelBlueprint"), true); + Results.Add(MakeShared(R)); + } + } + else if (auto* MakeNode = Cast(Node)) + { + if (MakeNode->StructType && MatchesType(MakeNode->StructType->GetName())) + { + TSharedRef R = MakeShared(); + R->SetStringField(TEXT("blueprint"), BPName); + R->SetStringField(TEXT("blueprintPath"), BPPath); + R->SetStringField(TEXT("usage"), TEXT("makeStruct")); + R->SetStringField(TEXT("location"), Node->GetGraph()->GetName()); + R->SetStringField(TEXT("nodeId"), Node->NodeGuid.ToString()); + R->SetStringField(TEXT("structType"), MakeNode->StructType->GetName()); + if (bIsLevel) + R->SetBoolField(TEXT("isLevelBlueprint"), true); + Results.Add(MakeShared(R)); + } + } + + // Check pin connections carrying the type + for (UEdGraphPin* Pin : Node->Pins) + { + if (!Pin || Pin->bHidden || Results.Num() >= EffectiveMaxResults) continue; + + FString PinSubtype; + if (Pin->PinType.PinSubCategoryObject.IsValid()) + PinSubtype = Pin->PinType.PinSubCategoryObject->GetName(); + + if ((Pin->LinkedTo.Num() > 0) && + (MatchesType(PinSubtype) || MatchesType(Pin->PinType.PinCategory.ToString()))) + { + TSharedRef R = MakeShared(); + R->SetStringField(TEXT("blueprint"), BPName); + R->SetStringField(TEXT("blueprintPath"), BPPath); + R->SetStringField(TEXT("usage"), TEXT("pinConnection")); + R->SetStringField(TEXT("location"), FString::Printf(TEXT("%s.%s"), + *Node->GetNodeTitle(ENodeTitleType::FullTitle).ToString(), + *Pin->PinName.ToString())); + R->SetStringField(TEXT("nodeId"), Node->NodeGuid.ToString()); + R->SetStringField(TEXT("graph"), Node->GetGraph()->GetName()); + R->SetStringField(TEXT("pinType"), Pin->PinType.PinCategory.ToString()); + if (!PinSubtype.IsEmpty()) + R->SetStringField(TEXT("pinSubtype"), PinSubtype); + R->SetNumberField(TEXT("connectionCount"), Pin->LinkedTo.Num()); + if (bIsLevel) + R->SetBoolField(TEXT("isLevelBlueprint"), true); + Results.Add(MakeShared(R)); + } + } + } + }; + + MCPAssets AllBlueprints; + AllBlueprints.Info(); + MCPAssets AllWorlds; + AllWorlds.Info(); + + // Search regular blueprints + for (const FAssetData& Asset : AllBlueprints.AllData()) + { + if (Results.Num() >= EffectiveMaxResults) break; + + FString AssetPath = Asset.PackageName.ToString(); + FString BPName = Asset.AssetName.ToString(); + + if (!FilterStr.IsEmpty() && !BPName.Contains(FilterStr, ESearchCase::IgnoreCase) && + !AssetPath.Contains(FilterStr, ESearchCase::IgnoreCase)) + { + continue; + } + + UBlueprint* BP = Cast(const_cast(Asset).GetAsset()); + if (!BP) continue; + + SearchOneBlueprint(BPName, AssetPath, BP, false); + } + + // Search level blueprints from maps + for (const FAssetData& MapAsset : AllWorlds.AllData()) + { + if (Results.Num() >= EffectiveMaxResults) break; + + FString AssetPath = MapAsset.PackageName.ToString(); + FString MapName = MapAsset.AssetName.ToString(); + + if (!FilterStr.IsEmpty() && !MapName.Contains(FilterStr, ESearchCase::IgnoreCase) && + !AssetPath.Contains(FilterStr, ESearchCase::IgnoreCase)) + { + continue; + } + + UWorld* World = Cast(MapAsset.GetAsset()); + if (!World || !World->PersistentLevel) continue; + ULevelScriptBlueprint* LevelBP = World->PersistentLevel->GetLevelScriptBlueprint(false); + if (!LevelBP) continue; + + SearchOneBlueprint(MapName, AssetPath, LevelBP, true); + } + + Result->SetNumberField(TEXT("resultCount"), Results.Num()); + Result->SetArrayField(TEXT("results"), Results); + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_SearchUnrealClasses.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_SearchUnrealClasses.h new file mode 100644 index 00000000..5a8d5d90 --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_SearchUnrealClasses.h @@ -0,0 +1,137 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPAssetFinder.h" +#include "MCPUtils.h" +#include "Engine/Blueprint.h" +#include "EdGraph/EdGraph.h" +#include "EdGraph/EdGraphNode.h" +#include "EdGraph/EdGraphPin.h" +#include "EdGraphSchema_K2.h" +#include "UObject/UObjectIterator.h" +#include "UMCPHandler_SearchUnrealClasses.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +// ============================================================ +// HandleListClasses — discover available UClasses +// ============================================================ + +UCLASS() +class UMCPHandler_SearchUnrealClasses : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Optional, Description="Substring filter for class names")) + FString Filter; + + UPROPERTY(meta=(Optional, Description="Parent class name to restrict results to subclasses")) + FString ParentClass; + + UPROPERTY(meta=(Optional, Description="Maximum number of results to return (1-500, default 100)")) + int32 Limit = 100; + + virtual FString GetDescription() const override + { + return TEXT("Search for available UClasses by name substring and/or parent class. " + "Returns class metadata including flags, parent class, and package."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override + { + if (Json->HasField(TEXT("limit"))) + { + Limit = FMath::Clamp(Limit, 1, 500); + } + + UClass* ParentClassObj = nullptr; + if (!ParentClass.IsEmpty()) + { + for (TObjectIterator It; It; ++It) + { + if (It->GetName() == ParentClass || It->GetName() == ParentClass + TEXT("_C")) + { + ParentClassObj = *It; + break; + } + } + if (!ParentClassObj) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Parent class '%s' not found"), *ParentClass)); + } + } + + TArray> ClassList; + int32 TotalMatched = 0; + + for (TObjectIterator It; It; ++It) + { + UClass* Class = *It; + if (!Class) continue; + + // Skip internal/deprecated classes + if (Class->HasAnyClassFlags(CLASS_Deprecated | CLASS_NewerVersionExists)) continue; + + // Apply parent filter + if (ParentClassObj && !Class->IsChildOf(ParentClassObj)) continue; + + FString ClassName = Class->GetName(); + + // Apply name filter + if (!Filter.IsEmpty() && !ClassName.Contains(Filter, ESearchCase::IgnoreCase)) + { + continue; + } + + TotalMatched++; + if (ClassList.Num() >= Limit) continue; // Count but don't add beyond limit + + TSharedRef ClassObj = MakeShared(); + ClassObj->SetStringField(TEXT("name"), ClassName); + ClassObj->SetStringField(TEXT("fullPath"), Class->GetPathName()); + + // Determine if it's a Blueprint-generated class + bool bIsBlueprint = Class->ClassGeneratedBy != nullptr; + ClassObj->SetBoolField(TEXT("isBlueprint"), bIsBlueprint); + + // Parent class + if (Class->GetSuperClass()) + { + ClassObj->SetStringField(TEXT("parentClass"), Class->GetSuperClass()->GetName()); + } + + // Module/package info + UPackage* Package = Class->GetOuterUPackage(); + if (Package) + { + ClassObj->SetStringField(TEXT("package"), Package->GetName()); + } + + // Flags + TArray> Flags; + if (Class->HasAnyClassFlags(CLASS_Abstract)) Flags.Add(MakeShared(TEXT("Abstract"))); + if (Class->HasAnyClassFlags(CLASS_Interface)) Flags.Add(MakeShared(TEXT("Interface"))); + if (Class->HasAnyClassFlags(CLASS_MinimalAPI)) Flags.Add(MakeShared(TEXT("MinimalAPI"))); + if (Flags.Num() > 0) + { + ClassObj->SetArrayField(TEXT("flags"), Flags); + } + + ClassList.Add(MakeShared(ClassObj)); + } + + Result->SetNumberField(TEXT("count"), ClassList.Num()); + Result->SetNumberField(TEXT("totalMatched"), TotalMatched); + if (TotalMatched > Limit) + { + Result->SetBoolField(TEXT("truncated"), true); + Result->SetNumberField(TEXT("limit"), Limit); + } + Result->SetArrayField(TEXT("classes"), ClassList); + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_SearchWithinBlueprints.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_SearchWithinBlueprints.h new file mode 100644 index 00000000..b5cfb2f0 --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_SearchWithinBlueprints.h @@ -0,0 +1,156 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPAssetFinder.h" +#include "MCPUtils.h" +#include "Engine/Blueprint.h" +#include "Engine/World.h" +#include "Engine/Level.h" +#include "Engine/LevelScriptBlueprint.h" +#include "EdGraph/EdGraph.h" +#include "EdGraph/EdGraphNode.h" +#include "K2Node_CallFunction.h" +#include "K2Node_Event.h" +#include "K2Node_CustomEvent.h" +#include "K2Node_VariableGet.h" +#include "K2Node_VariableSet.h" +#include "K2Node_BreakStruct.h" +#include "K2Node_MakeStruct.h" +#include "K2Node_FunctionEntry.h" +#include "K2Node_EditablePinBase.h" +#include "AssetRegistry/IAssetRegistry.h" +#include "UMCPHandler_SearchWithinBlueprints.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +UCLASS() +class UMCPHandler_SearchWithinBlueprints : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Search query string to match against node titles, function names, event names, and variable names")) + FString Query; + + UPROPERTY(meta=(Optional, Description="Filter results to blueprints whose path contains this substring")) + FString Path; + + UPROPERTY(meta=(Optional, Description="Maximum number of results to return (default 50, max 200)")) + int32 MaxResults = 0; + + virtual FString GetDescription() const override + { + return TEXT("Search across all Blueprint graphs for nodes matching a query string."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override + { + int32 EffectiveMaxResults = (MaxResults > 0) ? FMath::Clamp(MaxResults, 1, 200) : 50; + + // Build a combined list of all searchable blueprints (regular + level) + auto SearchBlueprint = [&](const FString& AssetName, const FString& AssetPath, UBlueprint* BP, TArray>& OutResults) + { + for (UEdGraphNode* Node : MCPUtils::AllNodes(BP)) + { + if (OutResults.Num() >= EffectiveMaxResults) break; + + FString Title = Node->GetNodeTitle(ENodeTitleType::FullTitle).ToString(); + + FString FuncName, EventName, VarName; + if (auto* CF = Cast(Node)) + { + FuncName = CF->FunctionReference.GetMemberName().ToString(); + } + else if (auto* Ev = Cast(Node)) + { + EventName = Ev->EventReference.GetMemberName().ToString(); + } + else if (auto* CE = Cast(Node)) + { + EventName = CE->CustomFunctionName.ToString(); + } + else if (auto* VG = Cast(Node)) + { + VarName = VG->GetVarName().ToString(); + } + else if (auto* VS = Cast(Node)) + { + VarName = VS->GetVarName().ToString(); + } + + bool bMatch = Title.Contains(Query, ESearchCase::IgnoreCase) || + (!FuncName.IsEmpty() && FuncName.Contains(Query, ESearchCase::IgnoreCase)) || + (!EventName.IsEmpty() && EventName.Contains(Query, ESearchCase::IgnoreCase)) || + (!VarName.IsEmpty() && VarName.Contains(Query, ESearchCase::IgnoreCase)); + + if (bMatch) + { + TSharedRef R = MakeShared(); + R->SetStringField(TEXT("blueprint"), AssetName); + R->SetStringField(TEXT("blueprintPath"), AssetPath); + R->SetStringField(TEXT("graph"), Node->GetGraph()->GetName()); + R->SetStringField(TEXT("nodeTitle"), Title); + R->SetStringField(TEXT("nodeClass"), Node->GetClass()->GetName()); + if (!FuncName.IsEmpty()) R->SetStringField(TEXT("functionName"), FuncName); + if (!EventName.IsEmpty()) R->SetStringField(TEXT("eventName"), EventName); + if (!VarName.IsEmpty()) R->SetStringField(TEXT("variableName"), VarName); + OutResults.Add(MakeShared(R)); + } + } + }; + + MCPAssets AllBlueprints; + AllBlueprints.Info(); + MCPAssets AllWorlds; + AllWorlds.Info(); + + TArray> Results; + for (const FAssetData& Asset : AllBlueprints.AllData()) + { + if (Results.Num() >= EffectiveMaxResults) break; + + FString AssetPath = Asset.PackageName.ToString(); + if (!Path.IsEmpty() && !AssetPath.Contains(Path, ESearchCase::IgnoreCase)) + { + continue; + } + + UBlueprint* BP = Cast(const_cast(Asset).GetAsset()); + if (!BP) continue; + + SearchBlueprint(Asset.AssetName.ToString(), AssetPath, BP, Results); + } + + // Also search level blueprints + for (const FAssetData& MapAsset : AllWorlds.AllData()) + { + if (Results.Num() >= EffectiveMaxResults) break; + + FString AssetPath = MapAsset.PackageName.ToString(); + if (!Path.IsEmpty() && !AssetPath.Contains(Path, ESearchCase::IgnoreCase)) + { + continue; + } + + UWorld* World = Cast(MapAsset.GetAsset()); + if (!World || !World->PersistentLevel) continue; + ULevelScriptBlueprint* LevelBP = World->PersistentLevel->GetLevelScriptBlueprint(false); + if (!LevelBP) continue; + + int32 BeforeCount = Results.Num(); + SearchBlueprint(MapAsset.AssetName.ToString(), AssetPath, LevelBP, Results); + // Tag newly-added entries as level blueprint results + for (int32 i = BeforeCount; i < Results.Num(); ++i) + { + Results[i]->AsObject()->SetBoolField(TEXT("isLevelBlueprint"), true); + } + } + + Result->SetNumberField(TEXT("resultCount"), Results.Num()); + Result->SetArrayField(TEXT("results"), Results); + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_SearchWithinMaterials.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_SearchWithinMaterials.h new file mode 100644 index 00000000..a55cd1ad --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_SearchWithinMaterials.h @@ -0,0 +1,138 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPAssetFinder.h" +#include "MCPUtils.h" +#include "Materials/Material.h" +#include "MaterialDomain.h" +#include "Materials/MaterialInstanceConstant.h" +#include "Materials/MaterialFunction.h" +#include "Materials/MaterialExpression.h" +#include "Materials/MaterialExpressionScalarParameter.h" +#include "Materials/MaterialExpressionVectorParameter.h" +#include "Materials/MaterialExpressionTextureObjectParameter.h" +#include "Materials/MaterialExpressionTextureSampleParameter2D.h" +#include "Materials/MaterialExpressionStaticSwitchParameter.h" +#include "Materials/MaterialExpressionConstant.h" +#include "Materials/MaterialExpressionConstant3Vector.h" +#include "Materials/MaterialExpressionConstant4Vector.h" +#include "Materials/MaterialExpressionTextureSample.h" +#include "Materials/MaterialExpressionTextureCoordinate.h" +#include "Materials/MaterialExpressionComponentMask.h" +#include "Materials/MaterialExpressionCustom.h" +#include "Materials/MaterialExpressionFunctionInput.h" +#include "Materials/MaterialExpressionFunctionOutput.h" +#include "Materials/MaterialExpressionMaterialFunctionCall.h" +#include "MaterialGraph/MaterialGraph.h" +#include "MaterialGraph/MaterialGraphNode.h" +#include "MaterialGraph/MaterialGraphNode_Root.h" +#include "MaterialGraph/MaterialGraphSchema.h" +#include "Kismet2/BlueprintEditorUtils.h" +#include "AssetRegistry/AssetRegistryModule.h" +#include "AssetRegistry/IAssetRegistry.h" +#include "EdGraph/EdGraph.h" +#include "EdGraph/EdGraphNode.h" +#include "UMCPHandler_SearchWithinMaterials.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +UCLASS() +class UMCPHandler_SearchWithinMaterials : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Search query string to match against material names, expression classes, and parameter names")) + FString Query; + + UPROPERTY(meta=(Optional, Description="Maximum number of results to return (default 50, max 200)")) + int32 MaxResults = 50; + + virtual FString GetDescription() const override + { + return TEXT("Search across all materials for matching material names, expression types, and parameter names."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override + { + FString DecodedQuery = MCPUtils::UrlDecode(Query); + + MaxResults = FMath::Clamp(MaxResults, 1, 200); + + TArray> Results; + + MCPAssets AllMaterials; + AllMaterials.Info(); + + for (const FAssetData& Asset : AllMaterials.AllData()) + { + if (Results.Num() >= MaxResults) break; + + FString MatName = Asset.AssetName.ToString(); + + // Check material name first + bool bNameMatch = MatName.Contains(DecodedQuery, ESearchCase::IgnoreCase); + + UMaterial* MaterialObj = Cast(const_cast(Asset).GetAsset()); + if (!MaterialObj) continue; + + auto Expressions = MaterialObj->GetExpressions(); + + if (bNameMatch) + { + // Add a match for the material itself + TSharedRef R = MakeShared(); + R->SetStringField(TEXT("material"), MatName); + R->SetStringField(TEXT("materialPath"), Asset.PackageName.ToString()); + R->SetStringField(TEXT("matchType"), TEXT("materialName")); + Results.Add(MakeShared(R)); + } + + // Search expressions + for (UMaterialExpression* Expr : Expressions) + { + if (!Expr || Results.Num() >= MaxResults) continue; + + FString ExprDesc = Expr->GetDescription(); + FString ExprClass = Expr->GetClass()->GetName(); + + // Check parameter name + FString ParamName; + if (auto* SP = Cast(Expr)) + ParamName = SP->ParameterName.ToString(); + else if (auto* VP = Cast(Expr)) + ParamName = VP->ParameterName.ToString(); + else if (auto* TP = Cast(Expr)) + ParamName = TP->ParameterName.ToString(); + else if (auto* SSP = Cast(Expr)) + ParamName = SSP->ParameterName.ToString(); + + bool bExprMatch = ExprDesc.Contains(DecodedQuery, ESearchCase::IgnoreCase) || + ExprClass.Contains(DecodedQuery, ESearchCase::IgnoreCase) || + (!ParamName.IsEmpty() && ParamName.Contains(DecodedQuery, ESearchCase::IgnoreCase)); + + if (bExprMatch) + { + TSharedRef R = MakeShared(); + R->SetStringField(TEXT("material"), MatName); + R->SetStringField(TEXT("materialPath"), Asset.PackageName.ToString()); + R->SetStringField(TEXT("matchType"), TEXT("expression")); + R->SetStringField(TEXT("expressionClass"), ExprClass); + if (!ExprDesc.IsEmpty()) + R->SetStringField(TEXT("description"), ExprDesc); + if (!ParamName.IsEmpty()) + R->SetStringField(TEXT("parameterName"), ParamName); + Results.Add(MakeShared(R)); + } + } + } + + Result->SetStringField(TEXT("query"), DecodedQuery); + Result->SetNumberField(TEXT("resultCount"), Results.Num()); + Result->SetArrayField(TEXT("results"), Results); + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_SetAnimStateAnimation.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_SetAnimStateAnimation.h new file mode 100644 index 00000000..763e3e85 --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_SetAnimStateAnimation.h @@ -0,0 +1,102 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPAssetFinder.h" +#include "MCPUtils.h" +#include "EdGraph/EdGraph.h" +#include "EdGraph/EdGraphNode.h" +#include "EdGraph/EdGraphPin.h" +#include "Kismet2/KismetEditorUtilities.h" +#include "Animation/AnimBlueprint.h" +#include "Animation/AnimSequence.h" +#include "Animation/BlendSpace.h" +#include "AnimGraphNode_SequencePlayer.h" +#include "AnimGraphNode_BlendSpacePlayer.h" +#include "AnimStateNode.h" +#include "AnimStateTransitionNode.h" +#include "AnimationStateMachineGraph.h" +#include "K2Node_VariableGet.h" +#include "UMCPHandler_SetAnimStateAnimation.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +UCLASS() +class UMCPHandler_SetAnimStateAnimation : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Animation Blueprint name or package path")) + FString Blueprint; + + UPROPERTY(meta=(Description="State machine graph name")) + FString Graph; + + UPROPERTY(meta=(Description="Name of the state to modify")) + FString StateName; + + UPROPERTY(meta=(Description="Animation asset name to assign")) + FString AnimationAsset; + + virtual FString GetDescription() const override + { + return TEXT("Set or replace the animation sequence played by a state in an animation state machine."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override + { + MCPAssets Assets; + if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; + UAnimationStateMachineGraph* SMGraph = MCPUtils::FindStateMachineGraph(Assets.Object(), Graph); + if (!SMGraph) { MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("State machine graph '%s' not found in '%s'"), *Graph, *Blueprint)); return; } + UAnimBlueprint* AnimBP = Assets.Object(); + + UAnimStateNode* StateNode = MCPUtils::FindStateByName(SMGraph, StateName, Result); + if (!StateNode) return; + + UEdGraph* InnerGraph = StateNode->GetBoundGraph(); + if (!InnerGraph) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("State '%s' has no bound graph"), *StateName)); + } + + // Find the animation asset + MCPAssets AnimAssets; + if (!AnimAssets.Exact(AnimationAsset).Errors(Result).ENone().ETwo().Load()) return; + UAnimSequence* AnimSeq = AnimAssets.Object(); + + // Find existing SequencePlayer or create one + UAnimGraphNode_SequencePlayer* SeqNode = nullptr; + for (UEdGraphNode* Node : InnerGraph->Nodes) + { + SeqNode = Cast(Node); + if (SeqNode) break; + } + + bool bCreatedNew = false; + if (!SeqNode) + { + SeqNode = NewObject(InnerGraph); + SeqNode->CreateNewGuid(); + SeqNode->PostPlacedNewNode(); + SeqNode->AllocateDefaultPins(); + SeqNode->NodePosX = 0; + SeqNode->NodePosY = 0; + InnerGraph->AddNode(SeqNode, false, false); + bCreatedNew = true; + } + + SeqNode->SetAnimationAsset(AnimSeq); + + // Compile and save + FKismetEditorUtilities::CompileBlueprint(AnimBP); + bool bSaved = MCPUtils::SaveBlueprintPackage(AnimBP); + + Result->SetBoolField(TEXT("createdNewNode"), bCreatedNew); + Result->SetBoolField(TEXT("saved"), bSaved); + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_SetAnimStateBlendSpace.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_SetAnimStateBlendSpace.h new file mode 100644 index 00000000..d216d3ea --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_SetAnimStateBlendSpace.h @@ -0,0 +1,229 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPAssetFinder.h" +#include "MCPUtils.h" +#include "EdGraph/EdGraph.h" +#include "EdGraph/EdGraphNode.h" +#include "EdGraph/EdGraphPin.h" +#include "Kismet2/KismetEditorUtilities.h" +#include "Animation/AnimBlueprint.h" +#include "Animation/AnimSequence.h" +#include "Animation/BlendSpace.h" +#include "AnimGraphNode_SequencePlayer.h" +#include "AnimGraphNode_BlendSpacePlayer.h" +#include "AnimStateNode.h" +#include "AnimStateTransitionNode.h" +#include "AnimationStateMachineGraph.h" +#include "K2Node_VariableGet.h" +#include "UMCPHandler_SetAnimStateBlendSpace.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +UCLASS() +class UMCPHandler_SetAnimStateBlendSpace : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Animation Blueprint name or package path")) + FString Blueprint; + + UPROPERTY(meta=(Description="State machine graph name")) + FString Graph; + + UPROPERTY(meta=(Description="Name of the state to modify")) + FString StateName; + + UPROPERTY(meta=(Description="Blend Space asset name or path")) + FString BlendSpace; + + UPROPERTY(meta=(Optional, Description="Blueprint variable name to wire to the X axis input")) + FString XVariable; + + UPROPERTY(meta=(Optional, Description="Blueprint variable name to wire to the Y axis input")) + FString YVariable; + + virtual FString GetDescription() const override + { + return TEXT("Place a BlendSpacePlayer in a state's inner graph, connect it to the output pose, " + "and optionally wire blueprint variables to the X and Y axis inputs."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override + { + MCPAssets Assets; + if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; + UAnimationStateMachineGraph* SMGraph = MCPUtils::FindStateMachineGraph(Assets.Object(), Graph); + if (!SMGraph) { MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("State machine graph '%s' not found in '%s'"), *Graph, *Blueprint)); return; } + UAnimBlueprint* AnimBP = Assets.Object(); + + UAnimStateNode* StateNode = MCPUtils::FindStateByName(SMGraph, StateName, Result); + if (!StateNode) return; + + UEdGraph* InnerGraph = StateNode->GetBoundGraph(); + if (!InnerGraph) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("State '%s' has no bound graph"), *StateName)); + } + + // Find the blend space asset + MCPAssets BlendSpaceAssets; + if (!BlendSpaceAssets.Exact(BlendSpace).Errors(Result).ENone().ETwo().Load()) return; + UBlendSpace* BlendSpaceAsset = BlendSpaceAssets.Object(); + + // Find existing BlendSpacePlayer or create one + UAnimGraphNode_BlendSpacePlayer* BSNode = nullptr; + for (UEdGraphNode* Node : InnerGraph->Nodes) + { + BSNode = Cast(Node); + if (BSNode) break; + } + + if (!BSNode) + { + BSNode = NewObject(InnerGraph); + BSNode->CreateNewGuid(); + BSNode->PostPlacedNewNode(); + BSNode->AllocateDefaultPins(); + BSNode->NodePosX = 0; + BSNode->NodePosY = 0; + InnerGraph->AddNode(BSNode, false, false); + } + + BSNode->SetAnimationAsset(BlendSpaceAsset); + + // Connect BlendSpacePlayer output to the Output Animation Pose node + { + // Find the AnimGraphNode_Root (Output Pose) in the inner graph + UEdGraphNode* ResultNode = nullptr; + for (UEdGraphNode* Node : InnerGraph->Nodes) + { + if (Node->GetClass()->GetName().Contains(TEXT("AnimGraphNode_Root")) || + Node->GetClass()->GetName().Contains(TEXT("AnimGraphNode_StateResult"))) + { + ResultNode = Node; + break; + } + } + + if (ResultNode) + { + // Find output pose pin on BlendSpacePlayer and input pose pin on result node + UEdGraphPin* BSOutputPin = nullptr; + for (UEdGraphPin* Pin : BSNode->Pins) + { + if (Pin && (Pin->Direction == EGPD_Output) && (Pin->PinType.PinCategory == UEdGraphSchema_K2::PC_Struct)) + { + BSOutputPin = Pin; + break; + } + } + + UEdGraphPin* ResultInputPin = nullptr; + for (UEdGraphPin* Pin : ResultNode->Pins) + { + if (Pin && (Pin->Direction == EGPD_Input) && (Pin->PinType.PinCategory == UEdGraphSchema_K2::PC_Struct)) + { + ResultInputPin = Pin; + break; + } + } + + if (BSOutputPin && ResultInputPin) + { + // Break existing connections on the result input + ResultInputPin->BreakAllPinLinks(); + const UEdGraphSchema* Schema = InnerGraph->GetSchema(); + if (Schema) + { + Schema->TryCreateConnection(BSOutputPin, ResultInputPin); + } + } + } + } + + // Wire X and Y variables if provided + auto WireVariable = [&](const FString& VarName, const FString& PinName) -> bool + { + if (VarName.IsEmpty()) return false; + + // Verify the variable exists in the blueprint + FName VarFName(*VarName); + bool bVarFound = false; + for (FBPVariableDescription& Var : AnimBP->NewVariables) + { + if (Var.VarName == VarFName) + { + bVarFound = true; + break; + } + } + if (!bVarFound) + { + // Also check parent class properties + if (UClass* GenClass = AnimBP->SkeletonGeneratedClass) + { + if (FProperty* Prop = GenClass->FindPropertyByName(VarFName)) + { + bVarFound = true; + } + } + } + if (!bVarFound) + { + UE_LOG(LogTemp, Warning, TEXT("BlueprintMCP: Variable '%s' not found in '%s', skipping wire"), + *VarName, *Blueprint); + return false; + } + + // Create a VariableGet node + UK2Node_VariableGet* GetNode = NewObject(InnerGraph); + GetNode->VariableReference.SetSelfMember(VarFName); + GetNode->NodePosX = BSNode->NodePosX - 250; + GetNode->NodePosY = BSNode->NodePosY; + InnerGraph->AddNode(GetNode, false, false); + GetNode->AllocateDefaultPins(); + + // Find the variable output pin + UEdGraphPin* VarOutPin = nullptr; + for (UEdGraphPin* Pin : GetNode->Pins) + { + if (Pin && (Pin->Direction == EGPD_Output) && (Pin->PinName == VarFName)) + { + VarOutPin = Pin; + break; + } + } + + // Find the target pin on the BlendSpacePlayer + UEdGraphPin* TargetPin = BSNode->FindPin(FName(*PinName)); + + if (VarOutPin && TargetPin) + { + const UEdGraphSchema* Schema = InnerGraph->GetSchema(); + if (Schema) + { + Schema->TryCreateConnection(VarOutPin, TargetPin); + return true; + } + } + + return false; + }; + + WireVariable(XVariable, TEXT("X")); + WireVariable(YVariable, TEXT("Y")); + + // Compile and save + FKismetEditorUtilities::CompileBlueprint(AnimBP); + bool bSaved = MCPUtils::SaveBlueprintPackage(AnimBP); + + Result->SetStringField(TEXT("nodeId"), BSNode->NodeGuid.ToString()); + Result->SetBoolField(TEXT("saved"), bSaved); + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_SetAnimTransitionRule.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_SetAnimTransitionRule.h new file mode 100644 index 00000000..a3bcadaa --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_SetAnimTransitionRule.h @@ -0,0 +1,124 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPAssetFinder.h" +#include "MCPUtils.h" +#include "EdGraph/EdGraph.h" +#include "EdGraph/EdGraphNode.h" +#include "EdGraph/EdGraphPin.h" +#include "Kismet2/KismetEditorUtilities.h" +#include "Animation/AnimBlueprint.h" +#include "Animation/AnimSequence.h" +#include "Animation/BlendSpace.h" +#include "AnimGraphNode_SequencePlayer.h" +#include "AnimGraphNode_BlendSpacePlayer.h" +#include "AnimStateNode.h" +#include "AnimStateTransitionNode.h" +#include "AnimationStateMachineGraph.h" +#include "K2Node_VariableGet.h" +#include "UMCPHandler_SetAnimTransitionRule.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +UCLASS() +class UMCPHandler_SetAnimTransitionRule : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Animation Blueprint name or package path")) + FString Blueprint; + + UPROPERTY(meta=(Description="State machine graph name")) + FString Graph; + + UPROPERTY(meta=(Description="Name of the source state")) + FString FromState; + + UPROPERTY(meta=(Description="Name of the target state")) + FString ToState; + + UPROPERTY(meta=(Optional, Description="Crossfade duration in seconds")) + float CrossfadeDuration = 0.0f; + + UPROPERTY(meta=(Optional, Description="Blend mode (as integer enum value)")) + int32 BlendMode = 0; + + UPROPERTY(meta=(Optional, Description="Transition priority order")) + int32 PriorityOrder = 0; + + UPROPERTY(meta=(Optional, Description="Logic type (as integer enum value)")) + int32 LogicType = 0; + + UPROPERTY(meta=(Optional, Description="Whether the transition is bidirectional")) + bool BBidirectional = false; + + virtual FString GetDescription() const override + { + return TEXT("Update properties on an existing transition between two states in an animation state machine."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override + { + MCPAssets Assets; + if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; + UAnimationStateMachineGraph* SMGraph = MCPUtils::FindStateMachineGraph(Assets.Object(), Graph); + if (!SMGraph) { MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("State machine graph '%s' not found in '%s'"), *Graph, *Blueprint)); return; } + UAnimBlueprint* AnimBP = Assets.Object(); + + UAnimStateTransitionNode* TransNode = MCPUtils::FindTransition(SMGraph, FromState, ToState); + if (!TransNode) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf( + TEXT("Transition from '%s' to '%s' not found in graph '%s'"), + *FromState, *ToState, *Graph)); + } + + // Update properties + int32 ChangedCount = 0; + TransNode->PreEditChange(nullptr); + + if (Json->HasField(TEXT("crossfadeDuration"))) + { + TransNode->CrossfadeDuration = CrossfadeDuration; + ChangedCount++; + } + if (Json->HasField(TEXT("blendMode"))) + { + TransNode->BlendMode = (EAlphaBlendOption)BlendMode; + ChangedCount++; + } + if (Json->HasField(TEXT("priorityOrder"))) + { + TransNode->PriorityOrder = PriorityOrder; + ChangedCount++; + } + if (Json->HasField(TEXT("logicType"))) + { + TransNode->LogicType = (ETransitionLogicType::Type)LogicType; + ChangedCount++; + } + if (Json->HasField(TEXT("bBidirectional"))) + { + TransNode->Bidirectional = BBidirectional; + ChangedCount++; + } + + if (ChangedCount == 0) + { + return MCPUtils::MakeErrorJson(Result, TEXT("No properties to update. Provide at least one of: crossfadeDuration, blendMode, priorityOrder, logicType, bBidirectional")); + } + TransNode->PostEditChange(); + + // Compile and save + FKismetEditorUtilities::CompileBlueprint(AnimBP); + bool bSaved = MCPUtils::SaveBlueprintPackage(AnimBP); + + Result->SetNumberField(TEXT("propertiesChanged"), ChangedCount); + Result->SetBoolField(TEXT("saved"), bSaved); + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_SetBlendSpaceSamplePoints.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_SetBlendSpaceSamplePoints.h new file mode 100644 index 00000000..65479768 --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_SetBlendSpaceSamplePoints.h @@ -0,0 +1,155 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPAssetFinder.h" +#include "MCPUtils.h" +#include "Engine/Blueprint.h" +#include "EdGraph/EdGraph.h" +#include "EdGraph/EdGraphNode.h" +#include "Kismet2/KismetEditorUtilities.h" +#include "Dom/JsonValue.h" +#include "Animation/AnimBlueprint.h" +#include "Animation/AnimBlueprintGeneratedClass.h" +#include "Animation/Skeleton.h" +#include "AnimGraphNode_Base.h" +#include "Animation/AnimSequence.h" +#include "Animation/BlendSpace.h" +#include "UMCPHandler_SetBlendSpaceSamplePoints.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +USTRUCT() +struct FBlendSpaceSampleEntry +{ + GENERATED_BODY() + + UPROPERTY() + FString AnimationAsset; + + UPROPERTY() + float X = 0.0f; + + UPROPERTY() + float Y = 0.0f; +}; + +UCLASS() +class UMCPHandler_SetBlendSpaceSamplePoints : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Blend Space asset name or package path")) + FString BlendSpace; + + UPROPERTY(meta=(Optional, Description="Display name for the X axis")) + FString AxisXName; + + UPROPERTY(meta=(Optional, Description="Display name for the Y axis")) + FString AxisYName; + + UPROPERTY(meta=(Optional, Description="Minimum value for X axis")) + float AxisXMin = 0.0f; + + UPROPERTY(meta=(Optional, Description="Maximum value for X axis")) + float AxisXMax = 0.0f; + + UPROPERTY(meta=(Optional, Description="Minimum value for Y axis")) + float AxisYMin = 0.0f; + + UPROPERTY(meta=(Optional, Description="Maximum value for Y axis")) + float AxisYMax = 0.0f; + + UPROPERTY(meta=(Optional, Description="Array of sample points, each with animationAsset, x, y")) + FMCPJsonArray Samples; + + virtual FString GetDescription() const override + { + return TEXT("Set axis parameters and animation sample points on a Blend Space. " + "Replaces all existing samples."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override + { + if (BlendSpace.IsEmpty()) + { + return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: blendSpace")); + } + + // Load the blend space + MCPAssets Assets; + if (!Assets.Exact(BlendSpace).Errors(Result).ENone().ETwo().Load()) return; + UBlendSpace* BS = Assets.Object(); + + // Set axis parameters + BS->PreEditChange(nullptr); + + const FBlendParameter& ParamX = BS->GetBlendParameter(0); + const FBlendParameter& ParamY = BS->GetBlendParameter(1); + + // We need to modify BlendParameters directly — use const_cast since there's no setter API + FBlendParameter& MutableParamX = const_cast(ParamX); + FBlendParameter& MutableParamY = const_cast(ParamY); + + if (!AxisXName.IsEmpty()) MutableParamX.DisplayName = AxisXName; + if (Json->HasField(TEXT("axisXMin"))) MutableParamX.Min = AxisXMin; + if (Json->HasField(TEXT("axisXMax"))) MutableParamX.Max = AxisXMax; + + if (!AxisYName.IsEmpty()) MutableParamY.DisplayName = AxisYName; + if (Json->HasField(TEXT("axisYMin"))) MutableParamY.Min = AxisYMin; + if (Json->HasField(TEXT("axisYMax"))) MutableParamY.Max = AxisYMax; + + // Clear existing samples (delete from end to start) + int32 NumExisting = BS->GetNumberOfBlendSamples(); + for (int32 i = NumExisting - 1; i >= 0; --i) + { + BS->DeleteSample(i); + } + + // Add new samples + int32 SamplesSet = 0; + + for (const TSharedPtr& SampleVal : Samples.Array) + { + FBlendSpaceSampleEntry Entry; + if (!MCPUtils::PopulateFromJson(FBlendSpaceSampleEntry::StaticStruct(), &Entry, SampleVal, Result)) return; + + UAnimSequence* AnimSeq = nullptr; + if (!Entry.AnimationAsset.IsEmpty()) + { + MCPAssets AnimAssets; + if (AnimAssets.Exact(Entry.AnimationAsset).Load()) + AnimSeq = AnimAssets.Object(); + } + + FVector SampleValue(Entry.X, Entry.Y, 0.0f); + if (AnimSeq) + { + BS->AddSample(AnimSeq, SampleValue); + } + else + { + BS->AddSample(SampleValue); + } + SamplesSet++; + } + + BS->ValidateSampleData(); + BS->PostEditChange(); + + // Save + BS->MarkPackageDirty(); + bool bSaved = MCPUtils::SaveGenericPackage(BS); + + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Set %d samples on Blend Space '%s' (saved: %s)"), + SamplesSet, *BS->GetName(), bSaved ? TEXT("true") : TEXT("false")); + + Result->SetStringField(TEXT("blendSpace"), BS->GetPathName()); + Result->SetNumberField(TEXT("samplesSet"), SamplesSet); + Result->SetBoolField(TEXT("saved"), bSaved); + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_SetBlueprintVariableMetadata.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_SetBlueprintVariableMetadata.h new file mode 100644 index 00000000..01012e5f --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_SetBlueprintVariableMetadata.h @@ -0,0 +1,234 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPAssetFinder.h" +#include "MCPUtils.h" +#include "Engine/Blueprint.h" +#include "EdGraph/EdGraph.h" +#include "EdGraph/EdGraphPin.h" +#include "K2Node_VariableGet.h" +#include "K2Node_VariableSet.h" +#include "Kismet2/BlueprintEditorUtils.h" +#include "UMCPHandler_SetBlueprintVariableMetadata.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +UCLASS() +class UMCPHandler_SetBlueprintVariableMetadata : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Blueprint name or package path")) + FString Blueprint; + + UPROPERTY(meta=(Description="Name of the variable to modify")) + FString Variable; + + UPROPERTY(meta=(Optional, Description="Category to assign the variable to")) + FString Category; + + UPROPERTY(meta=(Optional, Description="Tooltip text for the variable")) + FString Tooltip; + + UPROPERTY(meta=(Optional, Description="Replication mode: none, replicated, or repNotify")) + FString Replication; + + UPROPERTY(meta=(Optional, Description="If true, expose this variable on spawn")) + bool ExposeOnSpawn = false; + + UPROPERTY(meta=(Optional, Description="If true, mark the variable as private")) + bool IsPrivate = false; + + UPROPERTY(meta=(Optional, Description="Editability mode: editAnywhere, editDefaultsOnly, editInstanceOnly, or none")) + FString Editability; + + virtual FString GetDescription() const override + { + return TEXT("Set variable metadata properties such as category, tooltip, " + "replication, editability, and visibility flags."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override + { + MCPAssets Assets; + if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; + UBlueprint* BP = Assets.Object(); + + // Find the variable + FName VarFName(*Variable); + FBPVariableDescription* VarDesc = nullptr; + for (FBPVariableDescription& Var : BP->NewVariables) + { + if (Var.VarName == VarFName) + { + VarDesc = &Var; + break; + } + } + + if (!VarDesc) + { + TArray> AvailableVars; + for (const FBPVariableDescription& Var : BP->NewVariables) + { + AvailableVars.Add(MakeShared(Var.VarName.ToString())); + } + MCPUtils::MakeErrorJson(Result, FString::Printf( + TEXT("Variable '%s' not found in Blueprint '%s'"), *Variable, *Blueprint)); + Result->SetArrayField(TEXT("availableVariables"), AvailableVars); + return; + } + + TArray> Changes; + + // Category + if (Json->HasField(TEXT("category"))) + { + FString OldCategory = VarDesc->Category.ToString(); + VarDesc->Category = FText::FromString(Category); + FBlueprintEditorUtils::SetBlueprintVariableCategory(BP, VarFName, nullptr, FText::FromString(Category)); + + TSharedRef Change = MakeShared(); + Change->SetStringField(TEXT("field"), TEXT("category")); + Change->SetStringField(TEXT("oldValue"), OldCategory); + Change->SetStringField(TEXT("newValue"), Category); + Changes.Add(MakeShared(Change)); + } + + // Tooltip + if (Json->HasField(TEXT("tooltip"))) + { + FString OldTooltip; + FBlueprintEditorUtils::GetBlueprintVariableMetaData(BP, VarFName, nullptr, TEXT("tooltip"), OldTooltip); + FBlueprintEditorUtils::SetBlueprintVariableMetaData(BP, VarFName, nullptr, TEXT("tooltip"), Tooltip); + + TSharedRef Change = MakeShared(); + Change->SetStringField(TEXT("field"), TEXT("tooltip")); + Change->SetStringField(TEXT("oldValue"), OldTooltip); + Change->SetStringField(TEXT("newValue"), Tooltip); + Changes.Add(MakeShared(Change)); + } + + // Replication + if (Json->HasField(TEXT("replication"))) + { + uint64 OldFlags = VarDesc->PropertyFlags; + + if (Replication == TEXT("none")) + { + VarDesc->PropertyFlags &= ~CPF_Net; + VarDesc->PropertyFlags &= ~CPF_RepNotify; + VarDesc->RepNotifyFunc = NAME_None; + } + else if (Replication == TEXT("replicated")) + { + VarDesc->PropertyFlags |= CPF_Net; + VarDesc->PropertyFlags &= ~CPF_RepNotify; + VarDesc->RepNotifyFunc = NAME_None; + } + else if (Replication == TEXT("repNotify")) + { + VarDesc->PropertyFlags |= CPF_Net | CPF_RepNotify; + // Auto-generate RepNotify function name + VarDesc->RepNotifyFunc = FName(*FString::Printf(TEXT("OnRep_%s"), *Variable)); + } + else + { + return MCPUtils::MakeErrorJson(Result, FString::Printf( + TEXT("Invalid replication value '%s'. Valid: none, replicated, repNotify"), *Replication)); + } + + TSharedRef Change = MakeShared(); + Change->SetStringField(TEXT("field"), TEXT("replication")); + Change->SetStringField(TEXT("newValue"), Replication); + Changes.Add(MakeShared(Change)); + } + + // ExposeOnSpawn + if (Json->HasField(TEXT("exposeOnSpawn"))) + { + bool bOld = (VarDesc->PropertyFlags & CPF_ExposeOnSpawn) != 0; + if (ExposeOnSpawn) + VarDesc->PropertyFlags |= CPF_ExposeOnSpawn; + else + VarDesc->PropertyFlags &= ~CPF_ExposeOnSpawn; + + TSharedRef Change = MakeShared(); + Change->SetStringField(TEXT("field"), TEXT("exposeOnSpawn")); + Change->SetStringField(TEXT("oldValue"), bOld ? TEXT("true") : TEXT("false")); + Change->SetStringField(TEXT("newValue"), ExposeOnSpawn ? TEXT("true") : TEXT("false")); + Changes.Add(MakeShared(Change)); + } + + // isPrivate + if (Json->HasField(TEXT("isPrivate"))) + { + bool bOld = (VarDesc->PropertyFlags & CPF_DisableEditOnInstance) != 0; + // In UE5, "private" for Blueprint variables is represented via metadata + FBlueprintEditorUtils::SetBlueprintVariableMetaData(BP, VarFName, nullptr, + TEXT("BlueprintPrivate"), IsPrivate ? TEXT("true") : TEXT("false")); + + TSharedRef Change = MakeShared(); + Change->SetStringField(TEXT("field"), TEXT("isPrivate")); + Change->SetStringField(TEXT("oldValue"), bOld ? TEXT("true") : TEXT("false")); + Change->SetStringField(TEXT("newValue"), IsPrivate ? TEXT("true") : TEXT("false")); + Changes.Add(MakeShared(Change)); + } + + // Editability (EditAnywhere, EditDefaultsOnly, EditInstanceOnly) + if (Json->HasField(TEXT("editability"))) + { + // Clear all edit flags first + VarDesc->PropertyFlags &= ~(CPF_Edit | CPF_DisableEditOnInstance | CPF_DisableEditOnTemplate); + + if (Editability == TEXT("editAnywhere")) + { + VarDesc->PropertyFlags |= CPF_Edit; + } + else if (Editability == TEXT("editDefaultsOnly")) + { + VarDesc->PropertyFlags |= CPF_Edit | CPF_DisableEditOnInstance; + } + else if (Editability == TEXT("editInstanceOnly")) + { + VarDesc->PropertyFlags |= CPF_Edit | CPF_DisableEditOnTemplate; + } + else if (Editability == TEXT("none")) + { + // All edit flags already cleared + } + else + { + return MCPUtils::MakeErrorJson(Result, FString::Printf( + TEXT("Invalid editability value '%s'. Valid: editAnywhere, editDefaultsOnly, editInstanceOnly, none"), + *Editability)); + } + + TSharedRef Change = MakeShared(); + Change->SetStringField(TEXT("field"), TEXT("editability")); + Change->SetStringField(TEXT("newValue"), Editability); + Changes.Add(MakeShared(Change)); + } + + if (Changes.Num() == 0) + { + return MCPUtils::MakeErrorJson(Result, TEXT("No metadata fields specified. Provide at least one of: category, tooltip, replication, exposeOnSpawn, isPrivate, editability")); + } + + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: SetVariableMetadata on '%s.%s' — %d field(s) changed"), + *Blueprint, *Variable, Changes.Num()); + + BP->PreEditChange(nullptr); + BP->PostEditChange(); + FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP); + bool bSaved = MCPUtils::SaveBlueprintPackage(BP); + + Result->SetArrayField(TEXT("changes"), Changes); + Result->SetBoolField(TEXT("saved"), bSaved); + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_SetClassDefaultValue.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_SetClassDefaultValue.h new file mode 100644 index 00000000..69a4225d --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_SetClassDefaultValue.h @@ -0,0 +1,219 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPAssetFinder.h" +#include "MCPServer.h" +#include "MCPUtils.h" +#include "Engine/Blueprint.h" +#include "Materials/Material.h" +#include "Materials/MaterialInstanceConstant.h" +#include "Materials/MaterialFunction.h" +#include "Engine/World.h" +#include "Engine/LevelScriptBlueprint.h" +#include "EdGraph/EdGraph.h" +#include "EdGraph/EdGraphNode.h" +#include "EdGraph/EdGraphPin.h" +#include "EdGraphSchema_K2.h" +#include "K2Node.h" +#include "K2Node_CallFunction.h" +#include "K2Node_Event.h" +#include "K2Node_CustomEvent.h" +#include "K2Node_FunctionEntry.h" +#include "K2Node_EditablePinBase.h" +#include "K2Node_VariableGet.h" +#include "K2Node_VariableSet.h" +#include "K2Node_BreakStruct.h" +#include "K2Node_MakeStruct.h" +#include "K2Node_DynamicCast.h" +#include "K2Node_CallParentFunction.h" +#include "K2Node_IfThenElse.h" +#include "K2Node_ExecutionSequence.h" +#include "K2Node_MacroInstance.h" +#include "K2Node_SpawnActorFromClass.h" +#include "K2Node_Select.h" +#include "K2Node_Knot.h" +#include "EdGraphNode_Comment.h" +#include "GameFramework/Actor.h" +#include "Kismet2/BlueprintEditorUtils.h" +#include "Kismet2/KismetEditorUtilities.h" +#include "Serialization/JsonReader.h" +#include "Serialization/JsonWriter.h" +#include "Serialization/JsonSerializer.h" +#include "UObject/SavePackage.h" +#include "UObject/UObjectIterator.h" +#include "Misc/PackageName.h" +#include "AssetRegistry/AssetRegistryModule.h" +#include "AssetRegistry/IAssetRegistry.h" +#include "AssetToolsModule.h" +#include "IAssetTools.h" +#include "BlueprintNodeSpawner.h" +#include "UMCPHandler_SetClassDefaultValue.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +UCLASS() +class UMCPHandler_SetClassDefaultValue : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Blueprint name or package path")) + FString Blueprint; + + UPROPERTY(meta=(Description="Property name on the Class Default Object")) + FString Property; + + UPROPERTY(meta=(Description="New value (parsed according to property type)")) + FString Value; + + virtual FString GetDescription() const override + { + return TEXT("Set a default property value on a Blueprint's Class Default Object. " + "Handles class references, object references, and simple types."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override + { + + // Load Blueprint + MCPAssets Assets; + if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; + UBlueprint* BP = Assets.Object(); + + if (!BP->GeneratedClass) + { + return MCPUtils::MakeErrorJson(Result, TEXT("Blueprint has no GeneratedClass")); + } + + UObject* CDO = BP->GeneratedClass->GetDefaultObject(); + if (!CDO) + { + return MCPUtils::MakeErrorJson(Result, TEXT("Could not get Class Default Object")); + } + + FProperty* Prop = BP->GeneratedClass->FindPropertyByName(*Property); + if (!Prop) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Property '%s' not found on '%s'"), *Property, *Blueprint)); + } + + FString OldValue; + Prop->ExportTextItem_Direct(OldValue, Prop->ContainerPtrToValuePtr(CDO), nullptr, CDO, PPF_None); + + bool bSuccess = false; + FString ActualNewValue; + + // Handle class/soft-class properties (TSubclassOf, TSoftClassPtr) + FClassProperty* ClassProp = CastField(Prop); + FSoftClassProperty* SoftClassProp = CastField(Prop); + + if (ClassProp || SoftClassProp) + { + // Resolve the value to a UClass* + UClass* ResolvedClass = nullptr; + + // Try as a C++ class name first + for (TObjectIterator It; It; ++It) + { + if (It->GetName() == Value || It->GetName() == Value + TEXT("_C")) + { + ResolvedClass = *It; + break; + } + } + + // Try loading as a Blueprint asset + if (!ResolvedClass) + { + MCPAssets ValueAssets; + if (!ValueAssets.Exact(Value).AllContent().Errors(Result).ETwo().Load()) return; + if (!ValueAssets.Objects().IsEmpty()) + { + if (ValueAssets.Object()->GeneratedClass) + ResolvedClass = ValueAssets.Object()->GeneratedClass; + } + } + + if (!ResolvedClass) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Could not resolve '%s' to a class"), *Value)); + } + + // Validate meta class compatibility + if (ClassProp) + { + UClass* MetaClass = ClassProp->MetaClass; + if (MetaClass && !ResolvedClass->IsChildOf(MetaClass)) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf( + TEXT("'%s' is not a subclass of '%s' (required by property '%s')"), + *ResolvedClass->GetName(), *MetaClass->GetName(), *Property)); + } + ClassProp->SetPropertyValue_InContainer(CDO, ResolvedClass); + } + else + { + FSoftObjectPtr SoftPtr(ResolvedClass); + SoftClassProp->SetPropertyValue_InContainer(CDO, SoftPtr); + } + ActualNewValue = ResolvedClass->GetName(); + bSuccess = true; + } + // Handle object properties (TObjectPtr, UObject*) + else if (FObjectProperty* ObjProp = CastField(Prop)) + { + // Try finding an existing object/asset by name + UObject* ResolvedObj = nullptr; + + // Try loading as a Blueprint asset + MCPAssets ValueAssets; + if (!ValueAssets.Exact(Value).AllContent().Errors(Result).ENone().ETwo().Load()) return; + if (ValueAssets.Object()->GeneratedClass) + ResolvedObj = ValueAssets.Object()->GeneratedClass->GetDefaultObject(); + + ObjProp->SetPropertyValue_InContainer(CDO, ResolvedObj); + ActualNewValue = ResolvedObj->GetName(); + bSuccess = true; + } + // Handle simple types via ImportText + else + { + const TCHAR* ImportResult = Prop->ImportText_Direct(*Value, Prop->ContainerPtrToValuePtr(CDO), CDO, PPF_None); + if (ImportResult) + { + Prop->ExportTextItem_Direct(ActualNewValue, Prop->ContainerPtrToValuePtr(CDO), nullptr, CDO, PPF_None); + bSuccess = true; + } + else + { + return MCPUtils::MakeErrorJson(Result, FString::Printf( + TEXT("Failed to set property '%s' to '%s' — value could not be parsed for type '%s'"), + *Property, *Value, *Prop->GetCPPType())); + } + } + + if (!bSuccess) + { + return MCPUtils::MakeErrorJson(Result, TEXT("Failed to set property value")); + } + + // Mark modified and save + CDO->MarkPackageDirty(); + BP->Modify(); + + FKismetEditorUtilities::CompileBlueprint(BP); + bool bSaved = MCPUtils::SaveBlueprintPackage(BP); + + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Set '%s.%s' from '%s' to '%s' (saved: %s)"), + *Blueprint, *Property, *OldValue, *ActualNewValue, bSaved ? TEXT("true") : TEXT("false")); + + Result->SetStringField(TEXT("oldValue"), OldValue); + Result->SetStringField(TEXT("newValue"), ActualNewValue); + Result->SetStringField(TEXT("propertyType"), Prop->GetCPPType()); + Result->SetBoolField(TEXT("saved"), bSaved); + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_SetMaterialExpressionPosition.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_SetMaterialExpressionPosition.h new file mode 100644 index 00000000..cf46e525 --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_SetMaterialExpressionPosition.h @@ -0,0 +1,167 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPAssetFinder.h" +#include "MCPUtils.h" +#include "Materials/Material.h" +#include "MaterialDomain.h" +#include "Materials/MaterialInstanceConstant.h" +#include "Materials/MaterialFunction.h" +#include "Materials/MaterialExpression.h" +#include "Materials/MaterialExpressionScalarParameter.h" +#include "Materials/MaterialExpressionVectorParameter.h" +#include "Materials/MaterialExpressionTextureObjectParameter.h" +#include "Materials/MaterialExpressionTextureSampleParameter2D.h" +#include "Materials/MaterialExpressionStaticSwitchParameter.h" +#include "Materials/MaterialExpressionConstant.h" +#include "Materials/MaterialExpressionConstant3Vector.h" +#include "Materials/MaterialExpressionConstant4Vector.h" +#include "Materials/MaterialExpressionTextureSample.h" +#include "Materials/MaterialExpressionTextureCoordinate.h" +#include "Materials/MaterialExpressionComponentMask.h" +#include "Materials/MaterialExpressionCustom.h" +#include "Materials/MaterialExpressionFunctionInput.h" +#include "Materials/MaterialExpressionFunctionOutput.h" +#include "Materials/MaterialExpressionMaterialFunctionCall.h" +#include "MaterialGraph/MaterialGraph.h" +#include "MaterialGraph/MaterialGraphNode.h" +#include "MaterialGraph/MaterialGraphSchema.h" +#include "Factories/MaterialFactoryNew.h" +#include "Factories/MaterialFunctionFactoryNew.h" +#include "AssetToolsModule.h" +#include "IAssetTools.h" +#include "AssetRegistry/AssetRegistryModule.h" +#include "EdGraph/EdGraph.h" +#include "EdGraph/EdGraphNode.h" +#include "Serialization/JsonReader.h" +#include "Serialization/JsonWriter.h" +#include "Serialization/JsonSerializer.h" +#include "Misc/Guid.h" +#include "Misc/FileHelper.h" +#include "Misc/Paths.h" +#include "UObject/SavePackage.h" +#include "UObject/UObjectIterator.h" +#include "Kismet2/BlueprintEditorUtils.h" +#include "UMCPHandler_SetMaterialExpressionPosition.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +UCLASS() +class UMCPHandler_SetMaterialExpressionPosition : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Optional, Description="Material name or package path (specify this or materialFunction)")) + FString Material; + + UPROPERTY(meta=(Optional, Description="Material function name or package path (specify this or material)")) + FString MaterialFunction; + + UPROPERTY(meta=(Description="Node GUID of the expression to reposition")) + FString Node; + + UPROPERTY(meta=(Description="New X position")) + int32 PosX = 0; + + UPROPERTY(meta=(Description="New Y position")) + int32 PosY = 0; + + UPROPERTY(meta=(Optional, Description="If true, preview the change without applying it")) + bool DryRun = false; + + virtual FString GetDescription() const override + { + return TEXT("Reposition a material expression node in the material graph editor."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override + { + if (Material.IsEmpty() && MaterialFunction.IsEmpty()) + { + return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: 'material' or 'materialFunction'")); + } + + // Load material or material function + UMaterial* MaterialObj = nullptr; + UMaterialFunction* MatFunc = nullptr; + FString AssetDisplayName; + + if (!MaterialFunction.IsEmpty()) + { + MCPAssets MFAssets; + if (!MFAssets.Exact(MaterialFunction).Errors(Result).ENone().ETwo().Load()) return; + MatFunc = MFAssets.Object(); + AssetDisplayName = MatFunc->GetName(); + } + else + { + MCPAssets MatAssets; + if (!MatAssets.Exact(Material).Errors(Result).ENone().ETwo().Load()) return; + MaterialObj = MatAssets.Object(); + AssetDisplayName = MaterialObj->GetName(); + } + + if (MaterialObj) MCPUtils::EnsureMaterialGraph(MaterialObj); + UEdGraph* Graph = MaterialObj ? (UEdGraph*)MaterialObj->MaterialGraph : (MatFunc ? MatFunc->MaterialGraph : nullptr); + if (!Graph) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("'%s' has no material graph"), *AssetDisplayName)); + } + + // Find node by GUID + UMaterialGraphNode* TargetMatNode = nullptr; + for (UEdGraphNode* GraphNode : Graph->Nodes) + { + if (!GraphNode) continue; + if (GraphNode->NodeGuid.ToString() == Node) + { + TargetMatNode = Cast(GraphNode); + break; + } + } + + if (!TargetMatNode) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found in material graph"), *Node)); + } + + if (DryRun) + { + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: [DRY RUN] Would move node '%s' to (%d, %d) in '%s'"), + *Node, PosX, PosY, *AssetDisplayName); + + Result->SetBoolField(TEXT("dryRun"), true); + Result->SetStringField(TEXT("material"), AssetDisplayName); + return; + } + + // Set position on the graph node + TargetMatNode->NodePosX = PosX; + TargetMatNode->NodePosY = PosY; + + // Also update the underlying expression position + if (TargetMatNode->MaterialExpression) + { + TargetMatNode->MaterialExpression->MaterialExpressionEditorX = PosX; + TargetMatNode->MaterialExpression->MaterialExpressionEditorY = PosY; + } + + UObject* Asset = MaterialObj ? (UObject*)MaterialObj : (UObject*)MatFunc; + Asset->PreEditChange(nullptr); + Asset->PostEditChange(); + + // Save + bool bSaved = MaterialObj ? MCPUtils::SaveMaterialPackage(MaterialObj) : MCPUtils::SaveGenericPackage(MatFunc); + + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Moved node '%s' to (%d, %d) in '%s' (saved: %s)"), + *Node, PosX, PosY, *AssetDisplayName, bSaved ? TEXT("true") : TEXT("false")); + + Result->SetStringField(TEXT("material"), AssetDisplayName); + Result->SetBoolField(TEXT("saved"), bSaved); + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_SetMaterialExpressionProperty.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_SetMaterialExpressionProperty.h new file mode 100644 index 00000000..96d00ed2 --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_SetMaterialExpressionProperty.h @@ -0,0 +1,330 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPAssetFinder.h" +#include "MCPUtils.h" +#include "Materials/Material.h" +#include "MaterialDomain.h" +#include "Materials/MaterialInstanceConstant.h" +#include "Materials/MaterialFunction.h" +#include "Materials/MaterialExpression.h" +#include "Materials/MaterialExpressionScalarParameter.h" +#include "Materials/MaterialExpressionVectorParameter.h" +#include "Materials/MaterialExpressionTextureObjectParameter.h" +#include "Materials/MaterialExpressionTextureSampleParameter2D.h" +#include "Materials/MaterialExpressionStaticSwitchParameter.h" +#include "Materials/MaterialExpressionConstant.h" +#include "Materials/MaterialExpressionConstant3Vector.h" +#include "Materials/MaterialExpressionConstant4Vector.h" +#include "Materials/MaterialExpressionTextureSample.h" +#include "Materials/MaterialExpressionTextureCoordinate.h" +#include "Materials/MaterialExpressionComponentMask.h" +#include "Materials/MaterialExpressionCustom.h" +#include "Materials/MaterialExpressionFunctionInput.h" +#include "Materials/MaterialExpressionFunctionOutput.h" +#include "Materials/MaterialExpressionMaterialFunctionCall.h" +#include "MaterialGraph/MaterialGraph.h" +#include "MaterialGraph/MaterialGraphNode.h" +#include "MaterialGraph/MaterialGraphSchema.h" +#include "Factories/MaterialFactoryNew.h" +#include "Factories/MaterialFunctionFactoryNew.h" +#include "AssetToolsModule.h" +#include "IAssetTools.h" +#include "AssetRegistry/AssetRegistryModule.h" +#include "EdGraph/EdGraph.h" +#include "EdGraph/EdGraphNode.h" +#include "Serialization/JsonReader.h" +#include "Serialization/JsonWriter.h" +#include "Serialization/JsonSerializer.h" +#include "Misc/Guid.h" +#include "Misc/FileHelper.h" +#include "Misc/Paths.h" +#include "UObject/SavePackage.h" +#include "UObject/UObjectIterator.h" +#include "Kismet2/BlueprintEditorUtils.h" +#include "UMCPHandler_SetMaterialExpressionProperty.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +UCLASS() +class UMCPHandler_SetMaterialExpressionProperty : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Optional, Description="Material name or package path (specify this or materialFunction)")) + FString Material; + + UPROPERTY(meta=(Optional, Description="Material function name or package path (specify this or material)")) + FString MaterialFunction; + + UPROPERTY(meta=(Description="Node GUID of the expression to modify")) + FString Node; + + virtual FString GetDescription() const override + { + return TEXT("Set the value or properties on a material expression node. " + "The 'value' field in the JSON payload provides the new value, whose format depends on the expression type."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override + { + if (Material.IsEmpty() && MaterialFunction.IsEmpty()) + { + return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: 'material' or 'materialFunction'")); + } + + if (!Json->HasField(TEXT("value"))) + { + return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: value")); + } + + // Load material or material function + UMaterial* MaterialObj = nullptr; + UMaterialFunction* MatFunc = nullptr; + FString AssetDisplayName; + + if (!MaterialFunction.IsEmpty()) + { + MCPAssets MFAssets; + if (!MFAssets.Exact(MaterialFunction).Errors(Result).ENone().ETwo().Load()) return; + MatFunc = MFAssets.Object(); + AssetDisplayName = MatFunc->GetName(); + } + else + { + MCPAssets MatAssets; + if (!MatAssets.Exact(Material).Errors(Result).ENone().ETwo().Load()) return; + MaterialObj = MatAssets.Object(); + AssetDisplayName = MaterialObj->GetName(); + } + + if (MaterialObj) MCPUtils::EnsureMaterialGraph(MaterialObj); + UEdGraph* Graph = MaterialObj ? (UEdGraph*)MaterialObj->MaterialGraph : (MatFunc ? MatFunc->MaterialGraph : nullptr); + if (!Graph) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("'%s' has no material graph"), *AssetDisplayName)); + } + + // Find the node by GUID + UMaterialGraphNode* TargetMatNode = nullptr; + for (UEdGraphNode* GraphNode : Graph->Nodes) + { + if (!GraphNode) continue; + if (GraphNode->NodeGuid.ToString() == Node) + { + TargetMatNode = Cast(GraphNode); + break; + } + } + + if (!TargetMatNode) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found in material graph"), *Node)); + } + + UMaterialExpression* Expr = TargetMatNode->MaterialExpression; + if (!Expr) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' has no associated material expression"), *Node)); + } + + FString ExprType; + FString NewValueStr; + + UObject* Asset = MaterialObj ? (UObject*)MaterialObj : (UObject*)MatFunc; + Asset->PreEditChange(nullptr); + + // Handle based on expression type + if (UMaterialExpressionConstant* ConstExpr = Cast(Expr)) + { + ExprType = TEXT("Constant"); + double Value = Json->GetNumberField(TEXT("value")); + ConstExpr->R = (float)Value; + NewValueStr = FString::Printf(TEXT("%f"), Value); + } + else if (UMaterialExpressionConstant3Vector* C3Expr = Cast(Expr)) + { + ExprType = TEXT("Constant3Vector"); + const TSharedPtr* ValueObj = nullptr; + if (Json->TryGetObjectField(TEXT("value"), ValueObj) && ValueObj && (*ValueObj).IsValid()) + { + double R = 0, G = 0, B = 0; + (*ValueObj)->TryGetNumberField(TEXT("r"), R); + (*ValueObj)->TryGetNumberField(TEXT("g"), G); + (*ValueObj)->TryGetNumberField(TEXT("b"), B); + C3Expr->Constant = FLinearColor((float)R, (float)G, (float)B); + NewValueStr = FString::Printf(TEXT("(%f, %f, %f)"), R, G, B); + } + else + { + Asset->PostEditChange(); + return MCPUtils::MakeErrorJson(Result, TEXT("Constant3Vector requires value as object {r, g, b}")); + } + } + else if (UMaterialExpressionConstant4Vector* C4Expr = Cast(Expr)) + { + ExprType = TEXT("Constant4Vector"); + const TSharedPtr* ValueObj = nullptr; + if (Json->TryGetObjectField(TEXT("value"), ValueObj) && ValueObj && (*ValueObj).IsValid()) + { + double R = 0, G = 0, B = 0, A = 1; + (*ValueObj)->TryGetNumberField(TEXT("r"), R); + (*ValueObj)->TryGetNumberField(TEXT("g"), G); + (*ValueObj)->TryGetNumberField(TEXT("b"), B); + (*ValueObj)->TryGetNumberField(TEXT("a"), A); + C4Expr->Constant = FLinearColor((float)R, (float)G, (float)B, (float)A); + NewValueStr = FString::Printf(TEXT("(%f, %f, %f, %f)"), R, G, B, A); + } + else + { + Asset->PostEditChange(); + return MCPUtils::MakeErrorJson(Result, TEXT("Constant4Vector requires value as object {r, g, b, a}")); + } + } + else if (UMaterialExpressionScalarParameter* SPExpr = Cast(Expr)) + { + ExprType = TEXT("ScalarParameter"); + double Value = Json->GetNumberField(TEXT("value")); + SPExpr->DefaultValue = (float)Value; + NewValueStr = FString::Printf(TEXT("%f"), Value); + + FString ParamName; + if (Json->TryGetStringField(TEXT("parameterName"), ParamName) && !ParamName.IsEmpty()) + { + SPExpr->ParameterName = FName(*ParamName); + } + } + else if (UMaterialExpressionVectorParameter* VPExpr = Cast(Expr)) + { + ExprType = TEXT("VectorParameter"); + const TSharedPtr* ValueObj = nullptr; + if (Json->TryGetObjectField(TEXT("value"), ValueObj) && ValueObj && (*ValueObj).IsValid()) + { + double R = 0, G = 0, B = 0, A = 1; + (*ValueObj)->TryGetNumberField(TEXT("r"), R); + (*ValueObj)->TryGetNumberField(TEXT("g"), G); + (*ValueObj)->TryGetNumberField(TEXT("b"), B); + (*ValueObj)->TryGetNumberField(TEXT("a"), A); + VPExpr->DefaultValue = FLinearColor((float)R, (float)G, (float)B, (float)A); + NewValueStr = FString::Printf(TEXT("(%f, %f, %f, %f)"), R, G, B, A); + } + else + { + Asset->PostEditChange(); + return MCPUtils::MakeErrorJson(Result, TEXT("VectorParameter requires value as object {r, g, b, a}")); + } + + FString ParamName; + if (Json->TryGetStringField(TEXT("parameterName"), ParamName) && !ParamName.IsEmpty()) + { + VPExpr->ParameterName = FName(*ParamName); + } + } + else if (UMaterialExpressionTextureCoordinate* TCExpr = Cast(Expr)) + { + ExprType = TEXT("TextureCoordinate"); + const TSharedPtr* ValueObj = nullptr; + if (Json->TryGetObjectField(TEXT("value"), ValueObj) && ValueObj && (*ValueObj).IsValid()) + { + double CoordIndex = 0, UTiling = 1, VTiling = 1; + (*ValueObj)->TryGetNumberField(TEXT("coordinateIndex"), CoordIndex); + (*ValueObj)->TryGetNumberField(TEXT("uTiling"), UTiling); + (*ValueObj)->TryGetNumberField(TEXT("vTiling"), VTiling); + TCExpr->CoordinateIndex = (int32)CoordIndex; + TCExpr->UTiling = (float)UTiling; + TCExpr->VTiling = (float)VTiling; + NewValueStr = FString::Printf(TEXT("(index=%d, uTiling=%f, vTiling=%f)"), (int32)CoordIndex, UTiling, VTiling); + } + else + { + Asset->PostEditChange(); + return MCPUtils::MakeErrorJson(Result, TEXT("TextureCoordinate requires value as object {coordinateIndex, uTiling, vTiling}")); + } + } + else if (UMaterialExpressionCustom* CustomExpr = Cast(Expr)) + { + ExprType = TEXT("Custom"); + FString Code; + if (Json->TryGetStringField(TEXT("code"), Code)) + { + CustomExpr->Code = Code; + NewValueStr = FString::Printf(TEXT("Code: %d chars"), Code.Len()); + } + else if (Json->HasField(TEXT("value"))) + { + // Also accept code via value field as string + FString ValueStr = Json->GetStringField(TEXT("value")); + if (!ValueStr.IsEmpty()) + { + CustomExpr->Code = ValueStr; + NewValueStr = FString::Printf(TEXT("Code: %d chars"), ValueStr.Len()); + } + } + + FString OutputTypeStr; + if (Json->TryGetStringField(TEXT("outputType"), OutputTypeStr) && !OutputTypeStr.IsEmpty()) + { + int64 EnumVal = StaticEnum()->GetValueByNameString(OutputTypeStr); + if (EnumVal != INDEX_NONE) + { + CustomExpr->OutputType = (ECustomMaterialOutputType)EnumVal; + } + } + } + else if (UMaterialExpressionComponentMask* CMExpr = Cast(Expr)) + { + ExprType = TEXT("ComponentMask"); + const TSharedPtr* ValueObj = nullptr; + if (Json->TryGetObjectField(TEXT("value"), ValueObj) && ValueObj && (*ValueObj).IsValid()) + { + bool bR = false, bG = false, bB = false, bA = false; + (*ValueObj)->TryGetBoolField(TEXT("r"), bR); + (*ValueObj)->TryGetBoolField(TEXT("g"), bG); + (*ValueObj)->TryGetBoolField(TEXT("b"), bB); + (*ValueObj)->TryGetBoolField(TEXT("a"), bA); + CMExpr->R = bR ? 1 : 0; + CMExpr->G = bG ? 1 : 0; + CMExpr->B = bB ? 1 : 0; + CMExpr->A = bA ? 1 : 0; + NewValueStr = FString::Printf(TEXT("(R=%s, G=%s, B=%s, A=%s)"), + bR ? TEXT("true") : TEXT("false"), + bG ? TEXT("true") : TEXT("false"), + bB ? TEXT("true") : TEXT("false"), + bA ? TEXT("true") : TEXT("false")); + } + else + { + Asset->PostEditChange(); + return MCPUtils::MakeErrorJson(Result, TEXT("ComponentMask requires value as object {r, g, b, a} (booleans)")); + } + } + else + { + Asset->PostEditChange(); + return MCPUtils::MakeErrorJson(Result, FString::Printf( + TEXT("Expression type '%s' does not support direct value setting. Supported types: Constant, " + "Constant3Vector, Constant4Vector, ScalarParameter, VectorParameter, TextureCoordinate, " + "Custom, ComponentMask"), + *Expr->GetClass()->GetName())); + } + + Asset->PostEditChange(); + Asset->MarkPackageDirty(); + + // Save + bool bSaved = MaterialObj ? MCPUtils::SaveMaterialPackage(MaterialObj) : MCPUtils::SaveGenericPackage(MatFunc); + + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Set expression value on node '%s' (%s) in '%s': %s"), + *Node, *ExprType, *AssetDisplayName, *NewValueStr); + + Result->SetStringField(TEXT("material"), AssetDisplayName); + Result->SetStringField(TEXT("expressionType"), ExprType); + Result->SetStringField(TEXT("newValue"), NewValueStr); + Result->SetBoolField(TEXT("saved"), bSaved); + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_SetMaterialInstanceParameter.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_SetMaterialInstanceParameter.h new file mode 100644 index 00000000..e6504006 --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_SetMaterialInstanceParameter.h @@ -0,0 +1,258 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPAssetFinder.h" +#include "MCPUtils.h" +#include "Materials/Material.h" +#include "Materials/MaterialInterface.h" +#include "Materials/MaterialInstanceConstant.h" +#include "Materials/MaterialExpressionScalarParameter.h" +#include "Materials/MaterialExpressionVectorParameter.h" +#include "Materials/MaterialExpressionTextureSampleParameter2D.h" +#include "Materials/MaterialExpressionStaticSwitchParameter.h" +#include "Factories/MaterialInstanceConstantFactoryNew.h" +#include "AssetToolsModule.h" +#include "IAssetTools.h" +#include "Engine/Texture.h" +#include "UMCPHandler_SetMaterialInstanceParameter.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +UCLASS() +class UMCPHandler_SetMaterialInstanceParameter : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Material Instance name or path")) + FString MaterialInstance; + + UPROPERTY(meta=(Description="Parameter name to set")) + FString ParameterName; + + UPROPERTY(meta=(Description="Value to set (number for scalar, object with r/g/b/a for vector, string path for texture, bool for staticSwitch)")) + FMCPJsonObject Value; + + UPROPERTY(meta=(Optional, Description="Parameter type: scalar, vector, texture, staticSwitch. Auto-detected from parent if omitted.")) + FString Type; + + UPROPERTY(meta=(Optional, Description="If true, validate without applying changes")) + bool DryRun = false; + + virtual FString GetDescription() const override + { + return TEXT("Set a parameter override on a Material Instance. " + "Supports scalar, vector, texture, and static switch parameter types."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override + { + if (!Json->HasField(TEXT("value"))) + { + return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: value")); + } + + // Load the Material Instance + MCPAssets Assets; + if (!Assets.Exact(MaterialInstance).Errors(Result).ENone().ETwo().Load()) return; + UMaterialInstanceConstant* MI = Assets.Object(); + + // Determine the parameter type — explicit or auto-detect from parent + FString TypeStr = Type; + + // Auto-detect type from parent material's parameters if not provided + if (TypeStr.IsEmpty()) + { + UMaterialInterface* ParentMat = MI->Parent; + while (ParentMat) + { + UMaterial* BaseMat = ParentMat->GetMaterial(); + if (BaseMat) + { + // Check scalar parameters + for (UMaterialExpression* Expr : BaseMat->GetExpressions()) + { + if (auto* SP = Cast(Expr)) + { + if (SP->ParameterName.ToString() == ParameterName) + { + TypeStr = TEXT("scalar"); + break; + } + } + else if (auto* VP = Cast(Expr)) + { + if (VP->ParameterName.ToString() == ParameterName) + { + TypeStr = TEXT("vector"); + break; + } + } + else if (auto* TP = Cast(Expr)) + { + if (TP->ParameterName.ToString() == ParameterName) + { + TypeStr = TEXT("texture"); + break; + } + } + else if (auto* SSP = Cast(Expr)) + { + if (SSP->ParameterName.ToString() == ParameterName) + { + TypeStr = TEXT("staticSwitch"); + break; + } + } + } + break; // Only need to check the base material + } + // Walk up the parent chain if it's an MI parented to another MI + UMaterialInstanceConstant* ParentMI = Cast(ParentMat); + if (ParentMI) + { + ParentMat = ParentMI->Parent; + } + else + { + break; + } + } + } + + if (TypeStr.IsEmpty()) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf( + TEXT("Could not determine parameter type for '%s'. Specify the 'type' field explicitly (scalar, vector, texture, staticSwitch)."), + *ParameterName)); + } + + FString NewValueDescription; + FMaterialParameterInfo ParamInfo(*ParameterName); + + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: %s parameter '%s' (type=%s) on Material Instance '%s'"), + DryRun ? TEXT("[DRY RUN] Setting") : TEXT("Setting"), + *ParameterName, *TypeStr, *MaterialInstance); + + if (TypeStr.Equals(TEXT("scalar"), ESearchCase::IgnoreCase)) + { + // Scalar parameter — value is a number + double FloatValue = Json->GetNumberField(TEXT("value")); + + if (!DryRun) + { + MI->SetScalarParameterValueEditorOnly(ParamInfo, (float)FloatValue); + } + NewValueDescription = FString::Printf(TEXT("%f"), FloatValue); + } + else if (TypeStr.Equals(TEXT("vector"), ESearchCase::IgnoreCase)) + { + // Vector parameter — value is { r, g, b, a? } + const TSharedPtr* ValueObj = nullptr; + if (!Json->TryGetObjectField(TEXT("value"), ValueObj) || !ValueObj || !(*ValueObj).IsValid()) + { + return MCPUtils::MakeErrorJson(Result, TEXT("For vector parameters, 'value' must be an object with r, g, b (and optional a) fields.")); + } + + double R = (*ValueObj)->GetNumberField(TEXT("r")); + double G = (*ValueObj)->GetNumberField(TEXT("g")); + double B = (*ValueObj)->GetNumberField(TEXT("b")); + double A = (*ValueObj)->HasField(TEXT("a")) ? (*ValueObj)->GetNumberField(TEXT("a")) : 1.0; + + FLinearColor Color((float)R, (float)G, (float)B, (float)A); + + if (!DryRun) + { + MI->SetVectorParameterValueEditorOnly(ParamInfo, Color); + } + NewValueDescription = FString::Printf(TEXT("(R=%f, G=%f, B=%f, A=%f)"), R, G, B, A); + } + else if (TypeStr.Equals(TEXT("texture"), ESearchCase::IgnoreCase)) + { + // Texture parameter — value is a texture path string + FString TexturePath = Json->GetStringField(TEXT("value")); + if (TexturePath.IsEmpty()) + { + return MCPUtils::MakeErrorJson(Result, TEXT("For texture parameters, 'value' must be a texture asset path string.")); + } + + UTexture* TextureObj = LoadObject(nullptr, *TexturePath); + if (!TextureObj) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Could not load texture at path '%s'"), *TexturePath)); + } + + if (!DryRun) + { + MI->SetTextureParameterValueEditorOnly(ParamInfo, TextureObj); + } + NewValueDescription = TexturePath; + } + else if (TypeStr.Equals(TEXT("staticSwitch"), ESearchCase::IgnoreCase)) + { + // Static switch parameter — value is a bool + bool bSwitchValue = Json->GetBoolField(TEXT("value")); + + if (!DryRun) + { + // Modify static parameters + FStaticParameterSet StaticParams; + MI->GetStaticParameterValues(StaticParams); + + bool bFound = false; + for (FStaticSwitchParameter& Param : StaticParams.StaticSwitchParameters) + { + if (Param.ParameterInfo.Name == FName(*ParameterName)) + { + Param.Value = bSwitchValue; + Param.bOverride = true; + bFound = true; + break; + } + } + + if (!bFound) + { + // Add new static switch parameter entry + FStaticSwitchParameter NewParam; + NewParam.ParameterInfo.Name = FName(*ParameterName); + NewParam.Value = bSwitchValue; + NewParam.bOverride = true; + StaticParams.StaticSwitchParameters.Add(NewParam); + } + + MI->UpdateStaticPermutation(StaticParams); + } + NewValueDescription = bSwitchValue ? TEXT("true") : TEXT("false"); + } + else + { + return MCPUtils::MakeErrorJson(Result, FString::Printf( + TEXT("Unknown parameter type '%s'. Valid types: scalar, vector, texture, staticSwitch"), + *TypeStr)); + } + + if (!DryRun) + { + MI->PreEditChange(nullptr); + MI->PostEditChange(); + MI->MarkPackageDirty(); + MCPUtils::SaveGenericPackage(MI); + } + + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: %s parameter '%s' = %s on '%s'"), + DryRun ? TEXT("[DRY RUN] Would set") : TEXT("Set"), + *ParameterName, *NewValueDescription, *MaterialInstance); + + Result->SetStringField(TEXT("type"), TypeStr); + Result->SetStringField(TEXT("newValue"), NewValueDescription); + if (DryRun) + { + Result->SetBoolField(TEXT("dryRun"), true); + } + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_SetMaterialProperty.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_SetMaterialProperty.h new file mode 100644 index 00000000..09d01587 --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_SetMaterialProperty.h @@ -0,0 +1,257 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPAssetFinder.h" +#include "MCPUtils.h" +#include "Materials/Material.h" +#include "MaterialDomain.h" +#include "Materials/MaterialInstanceConstant.h" +#include "Materials/MaterialFunction.h" +#include "Materials/MaterialExpression.h" +#include "Materials/MaterialExpressionScalarParameter.h" +#include "Materials/MaterialExpressionVectorParameter.h" +#include "Materials/MaterialExpressionTextureObjectParameter.h" +#include "Materials/MaterialExpressionTextureSampleParameter2D.h" +#include "Materials/MaterialExpressionStaticSwitchParameter.h" +#include "Materials/MaterialExpressionConstant.h" +#include "Materials/MaterialExpressionConstant3Vector.h" +#include "Materials/MaterialExpressionConstant4Vector.h" +#include "Materials/MaterialExpressionTextureSample.h" +#include "Materials/MaterialExpressionTextureCoordinate.h" +#include "Materials/MaterialExpressionComponentMask.h" +#include "Materials/MaterialExpressionCustom.h" +#include "Materials/MaterialExpressionFunctionInput.h" +#include "Materials/MaterialExpressionFunctionOutput.h" +#include "Materials/MaterialExpressionMaterialFunctionCall.h" +#include "MaterialGraph/MaterialGraph.h" +#include "MaterialGraph/MaterialGraphNode.h" +#include "MaterialGraph/MaterialGraphSchema.h" +#include "Factories/MaterialFactoryNew.h" +#include "Factories/MaterialFunctionFactoryNew.h" +#include "AssetToolsModule.h" +#include "IAssetTools.h" +#include "AssetRegistry/AssetRegistryModule.h" +#include "EdGraph/EdGraph.h" +#include "EdGraph/EdGraphNode.h" +#include "Serialization/JsonReader.h" +#include "Serialization/JsonWriter.h" +#include "Serialization/JsonSerializer.h" +#include "Misc/Guid.h" +#include "Misc/FileHelper.h" +#include "Misc/Paths.h" +#include "UObject/SavePackage.h" +#include "UObject/UObjectIterator.h" +#include "Kismet2/BlueprintEditorUtils.h" +#include "UMCPHandler_SetMaterialProperty.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +UCLASS() +class UMCPHandler_SetMaterialProperty : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Material name or package path")) + FString Material; + + UPROPERTY(meta=(Description="Property name to set (domain, blendMode, twoSided, shadingModel, opacity, opacityMaskClipValue, bUsedWithSkeletalMesh, bUsedWithMorphTargets, bUsedWithNiagaraSprites, ditheredLODTransition, bAllowNegativeEmissiveColor)")) + FString Property; + + UPROPERTY(meta=(Optional, Description="If true, preview the change without applying it")) + bool DryRun = false; + + virtual FString GetDescription() const override + { + return TEXT("Set a top-level material property such as domain, blend mode, shading model, or usage flags. " + "The 'value' field in the JSON payload provides the new value."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override + { + if (!Json->HasField(TEXT("value"))) + { + return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: value")); + } + + // Load material + MCPAssets Assets; + if (!Assets.Exact(Material).Errors(Result).ENone().ETwo().Load()) return; + UMaterial* MaterialObj = Assets.Object(); + + FString OldValue; + FString NewValue; + + if (Property == TEXT("domain")) + { + FString ValueStr = Json->GetStringField(TEXT("value")); + OldValue = MCPUtils::EnumToString(MaterialObj->MaterialDomain, TEXT("MD_")); + + EMaterialDomain NewDomain; + if (!MCPUtils::StringToEnum(ValueStr, NewDomain, Result, TEXT("MD_"))) return; + NewValue = MCPUtils::EnumToString(NewDomain, TEXT("MD_")); + + if (!DryRun) + { + MaterialObj->PreEditChange(nullptr); + MaterialObj->MaterialDomain = NewDomain; + MaterialObj->PostEditChange(); + } + } + else if (Property == TEXT("blendMode")) + { + FString ValueStr = Json->GetStringField(TEXT("value")); + OldValue = MCPUtils::EnumToString(MaterialObj->BlendMode, TEXT("BLEND_")); + + EBlendMode NewBlend; + if (!MCPUtils::StringToEnum(ValueStr, NewBlend, Result, TEXT("BLEND_"))) return; + NewValue = MCPUtils::EnumToString(NewBlend, TEXT("BLEND_")); + + if (!DryRun) + { + MaterialObj->PreEditChange(nullptr); + MaterialObj->BlendMode = NewBlend; + MaterialObj->PostEditChange(); + } + } + else if (Property == TEXT("twoSided")) + { + bool bValue = Json->GetBoolField(TEXT("value")); + OldValue = MaterialObj->TwoSided ? TEXT("true") : TEXT("false"); + NewValue = bValue ? TEXT("true") : TEXT("false"); + + if (!DryRun) + { + MaterialObj->PreEditChange(nullptr); + MaterialObj->TwoSided = bValue ? 1 : 0; + MaterialObj->PostEditChange(); + } + } + else if (Property == TEXT("shadingModel")) + { + FString ValueStr = Json->GetStringField(TEXT("value")); + OldValue = MCPUtils::EnumToString(MaterialObj->GetShadingModels().GetFirstShadingModel(), TEXT("MSM_")); + + EMaterialShadingModel NewModel; + if (!MCPUtils::StringToEnum(ValueStr, NewModel, Result, TEXT("MSM_"))) return; + NewValue = MCPUtils::EnumToString(NewModel, TEXT("MSM_")); + + if (!DryRun) + { + MaterialObj->PreEditChange(nullptr); + MaterialObj->SetShadingModel(NewModel); + MaterialObj->PostEditChange(); + } + } + else if (Property == TEXT("opacity") || Property == TEXT("opacityMaskClipValue")) + { + double OpacityValue = Json->GetNumberField(TEXT("value")); + OldValue = FString::Printf(TEXT("%f"), MaterialObj->OpacityMaskClipValue); + NewValue = FString::Printf(TEXT("%f"), OpacityValue); + + if (!DryRun) + { + MaterialObj->PreEditChange(nullptr); + MaterialObj->OpacityMaskClipValue = (float)OpacityValue; + MaterialObj->PostEditChange(); + } + } + else if (Property == TEXT("bUsedWithSkeletalMesh")) + { + bool bValue = Json->GetBoolField(TEXT("value")); + OldValue = MaterialObj->bUsedWithSkeletalMesh ? TEXT("true") : TEXT("false"); + NewValue = bValue ? TEXT("true") : TEXT("false"); + + if (!DryRun) + { + MaterialObj->PreEditChange(nullptr); + MaterialObj->bUsedWithSkeletalMesh = bValue ? 1 : 0; + MaterialObj->PostEditChange(); + } + } + else if (Property == TEXT("bUsedWithMorphTargets")) + { + bool bValue = Json->GetBoolField(TEXT("value")); + OldValue = MaterialObj->bUsedWithMorphTargets ? TEXT("true") : TEXT("false"); + NewValue = bValue ? TEXT("true") : TEXT("false"); + + if (!DryRun) + { + MaterialObj->PreEditChange(nullptr); + MaterialObj->bUsedWithMorphTargets = bValue ? 1 : 0; + MaterialObj->PostEditChange(); + } + } + else if (Property == TEXT("bUsedWithNiagaraSprites")) + { + bool bValue = Json->GetBoolField(TEXT("value")); + OldValue = MaterialObj->bUsedWithNiagaraSprites ? TEXT("true") : TEXT("false"); + NewValue = bValue ? TEXT("true") : TEXT("false"); + + if (!DryRun) + { + MaterialObj->PreEditChange(nullptr); + MaterialObj->bUsedWithNiagaraSprites = bValue ? 1 : 0; + MaterialObj->PostEditChange(); + } + } + else if (Property == TEXT("ditheredLODTransition") || Property == TEXT("DitheredLODTransition")) + { + bool bValue = Json->GetBoolField(TEXT("value")); + OldValue = MaterialObj->DitheredLODTransition ? TEXT("true") : TEXT("false"); + NewValue = bValue ? TEXT("true") : TEXT("false"); + + if (!DryRun) + { + MaterialObj->PreEditChange(nullptr); + MaterialObj->DitheredLODTransition = bValue ? 1 : 0; + MaterialObj->PostEditChange(); + } + } + else if (Property == TEXT("bAllowNegativeEmissiveColor")) + { + bool bValue = Json->GetBoolField(TEXT("value")); + OldValue = MaterialObj->bAllowNegativeEmissiveColor ? TEXT("true") : TEXT("false"); + NewValue = bValue ? TEXT("true") : TEXT("false"); + + if (!DryRun) + { + MaterialObj->PreEditChange(nullptr); + MaterialObj->bAllowNegativeEmissiveColor = bValue ? 1 : 0; + MaterialObj->PostEditChange(); + } + } + else + { + return MCPUtils::MakeErrorJson(Result, FString::Printf( + TEXT("Unknown property '%s'. Valid properties: domain, blendMode, twoSided, shadingModel, opacity, " + "opacityMaskClipValue, bUsedWithSkeletalMesh, bUsedWithMorphTargets, bUsedWithNiagaraSprites, " + "ditheredLODTransition, bAllowNegativeEmissiveColor"), + *Property)); + } + + // Save if not dry run + bool bSaved = false; + if (!DryRun) + { + bSaved = MCPUtils::SaveMaterialPackage(MaterialObj); + } + + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: %sSet material property '%s' on '%s': '%s' -> '%s'"), + DryRun ? TEXT("[DRY RUN] ") : TEXT(""), + *Property, *Material, *OldValue, *NewValue); + + Result->SetStringField(TEXT("material"), MaterialObj->GetName()); + Result->SetStringField(TEXT("oldValue"), OldValue); + Result->SetStringField(TEXT("newValue"), NewValue); + Result->SetBoolField(TEXT("dryRun"), DryRun); + if (!DryRun) + { + Result->SetBoolField(TEXT("saved"), bSaved); + } + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_SetNodeComment.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_SetNodeComment.h new file mode 100644 index 00000000..d161ffa6 --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_SetNodeComment.h @@ -0,0 +1,108 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPAssetFinder.h" +#include "MCPServer.h" +#include "MCPUtils.h" +#include "Engine/Blueprint.h" +#include "Materials/Material.h" +#include "Materials/MaterialInstanceConstant.h" +#include "Materials/MaterialFunction.h" +#include "Engine/World.h" +#include "Engine/LevelScriptBlueprint.h" +#include "EdGraph/EdGraph.h" +#include "EdGraph/EdGraphNode.h" +#include "EdGraph/EdGraphPin.h" +#include "EdGraphSchema_K2.h" +#include "K2Node.h" +#include "K2Node_CallFunction.h" +#include "K2Node_Event.h" +#include "K2Node_CustomEvent.h" +#include "K2Node_FunctionEntry.h" +#include "K2Node_EditablePinBase.h" +#include "K2Node_VariableGet.h" +#include "K2Node_VariableSet.h" +#include "K2Node_BreakStruct.h" +#include "K2Node_MakeStruct.h" +#include "K2Node_DynamicCast.h" +#include "K2Node_CallParentFunction.h" +#include "K2Node_IfThenElse.h" +#include "K2Node_ExecutionSequence.h" +#include "K2Node_MacroInstance.h" +#include "K2Node_SpawnActorFromClass.h" +#include "K2Node_Select.h" +#include "K2Node_Knot.h" +#include "EdGraphNode_Comment.h" +#include "GameFramework/Actor.h" +#include "Kismet2/BlueprintEditorUtils.h" +#include "Kismet2/KismetEditorUtilities.h" +#include "Serialization/JsonReader.h" +#include "Serialization/JsonWriter.h" +#include "Serialization/JsonSerializer.h" +#include "UObject/SavePackage.h" +#include "UObject/UObjectIterator.h" +#include "Misc/PackageName.h" +#include "AssetRegistry/AssetRegistryModule.h" +#include "AssetRegistry/IAssetRegistry.h" +#include "AssetToolsModule.h" +#include "IAssetTools.h" +#include "BlueprintNodeSpawner.h" +#include "UMCPHandler_SetNodeComment.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +UCLASS() +class UMCPHandler_SetNodeComment : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Blueprint name or package path")) + FString Blueprint; + + UPROPERTY(meta=(Description="Node GUID")) + FString Node; + + UPROPERTY(meta=(Description="Comment text to set")) + FString Comment; + + virtual FString GetDescription() const override + { + return TEXT("Set a node's comment text. Makes the comment bubble visible if non-empty."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override + { + + MCPAssets Assets; + if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; + UBlueprint* BP = Assets.Object(); + + UEdGraphNode* FoundNode = MCPUtils::FindNodeByGuid(BP, Node); + if (!FoundNode) + { + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found"), *Node)); + } + + FString OldComment = FoundNode->NodeComment; + FoundNode->NodeComment = Comment; + + // Make the comment bubble visible if setting a non-empty comment + if (!Comment.IsEmpty()) + { + FoundNode->bCommentBubbleVisible = true; + FoundNode->bCommentBubblePinned = true; + } + + FBlueprintEditorUtils::MarkBlueprintAsModified(BP); + + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Set comment on node '%s' in '%s'"), + *Node, *Blueprint); + + Result->SetStringField(TEXT("oldComment"), OldComment); + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_SetNodePositions.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_SetNodePositions.h new file mode 100644 index 00000000..c5dd39cd --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_SetNodePositions.h @@ -0,0 +1,136 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPAssetFinder.h" +#include "MCPServer.h" +#include "MCPUtils.h" +#include "Engine/Blueprint.h" +#include "Materials/Material.h" +#include "Materials/MaterialInstanceConstant.h" +#include "Materials/MaterialFunction.h" +#include "Engine/World.h" +#include "Engine/LevelScriptBlueprint.h" +#include "EdGraph/EdGraph.h" +#include "EdGraph/EdGraphNode.h" +#include "EdGraph/EdGraphPin.h" +#include "EdGraphSchema_K2.h" +#include "K2Node.h" +#include "K2Node_CallFunction.h" +#include "K2Node_Event.h" +#include "K2Node_CustomEvent.h" +#include "K2Node_FunctionEntry.h" +#include "K2Node_EditablePinBase.h" +#include "K2Node_VariableGet.h" +#include "K2Node_VariableSet.h" +#include "K2Node_BreakStruct.h" +#include "K2Node_MakeStruct.h" +#include "K2Node_DynamicCast.h" +#include "K2Node_CallParentFunction.h" +#include "K2Node_IfThenElse.h" +#include "K2Node_ExecutionSequence.h" +#include "K2Node_MacroInstance.h" +#include "K2Node_SpawnActorFromClass.h" +#include "K2Node_Select.h" +#include "K2Node_Knot.h" +#include "EdGraphNode_Comment.h" +#include "GameFramework/Actor.h" +#include "Kismet2/BlueprintEditorUtils.h" +#include "Kismet2/KismetEditorUtilities.h" +#include "Serialization/JsonReader.h" +#include "Serialization/JsonWriter.h" +#include "Serialization/JsonSerializer.h" +#include "UObject/SavePackage.h" +#include "UObject/UObjectIterator.h" +#include "Misc/PackageName.h" +#include "AssetRegistry/AssetRegistryModule.h" +#include "AssetRegistry/IAssetRegistry.h" +#include "AssetToolsModule.h" +#include "IAssetTools.h" +#include "BlueprintNodeSpawner.h" +#include "UMCPHandler_SetNodePositions.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +USTRUCT() +struct FMoveNodeEntry +{ + GENERATED_BODY() + + UPROPERTY() + FString Node; + + UPROPERTY() + int32 X = 0; + + UPROPERTY() + int32 Y = 0; +}; + + +UCLASS() +class UMCPHandler_SetNodePositions : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Blueprint name or package path")) + FString Blueprint; + + UPROPERTY(meta=(Description="Array of {nodeId, x, y} objects")) + FMCPJsonArray Nodes; + + virtual FString GetDescription() const override + { + return TEXT("Reposition one or more nodes in a Blueprint graph."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override + { + + MCPAssets Assets; + if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; + UBlueprint* BP = Assets.Object(); + + TArray> Results; + int32 SuccessCount = 0; + + for (const TSharedPtr& NodeVal : Nodes.Array) + { + TSharedRef EntryResult = MakeShared(); + Results.Add(MakeShared(EntryResult)); + + FMoveNodeEntry Entry; + if (!MCPUtils::PopulateFromJson(FMoveNodeEntry::StaticStruct(), &Entry, NodeVal, &*EntryResult)) continue; + + UEdGraphNode* Node = MCPUtils::FindNodeByGuid(BP, Entry.Node); + if (!Node) + { + EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Node '%s' not found"), *Entry.Node)); + continue; + } + + int32 OldX = Node->NodePosX; + int32 OldY = Node->NodePosY; + Node->NodePosX = Entry.X; + Node->NodePosY = Entry.Y; + EntryResult->SetNumberField(TEXT("oldX"), OldX); + EntryResult->SetNumberField(TEXT("oldY"), OldY); + EntryResult->SetNumberField(TEXT("newX"), Node->NodePosX); + EntryResult->SetNumberField(TEXT("newY"), Node->NodePosY); + SuccessCount++; + } + + FBlueprintEditorUtils::MarkBlueprintAsModified(BP); + + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: MoveNode — %d/%d succeeded"), + SuccessCount, Nodes.Array.Num()); + + Result->SetNumberField(TEXT("movedCount"), SuccessCount); + Result->SetNumberField(TEXT("totalRequested"), Nodes.Array.Num()); + Result->SetArrayField(TEXT("results"), Results); + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_SetPinDefaultValues.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_SetPinDefaultValues.h new file mode 100644 index 00000000..ed7917c4 --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_SetPinDefaultValues.h @@ -0,0 +1,133 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPAssetFinder.h" +#include "MCPUtils.h" +#include "Engine/Blueprint.h" +#include "Engine/World.h" +#include "EdGraph/EdGraph.h" +#include "EdGraph/EdGraphNode.h" +#include "EdGraph/EdGraphPin.h" +#include "Kismet2/BlueprintEditorUtils.h" +#include "UMCPHandler_SetPinDefaultValues.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +USTRUCT() +struct FSetPinDefaultEntry +{ + GENERATED_BODY() + + UPROPERTY() + FString Blueprint; + + UPROPERTY() + FString Node; + + UPROPERTY() + FString PinName; + + UPROPERTY() + FString Value; +}; + + +UCLASS() +class UMCPHandler_SetPinDefaultValues : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Array of {blueprint, node, pinName, value} objects")) + FMCPJsonArray Pins; + + virtual FString GetDescription() const override + { + return TEXT("Set the default value of input pins on nodes."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override + { + + TArray> Results; + int32 SuccessCount = 0; + TSet ModifiedNodes; + TSet ModifiedBlueprints; + + for (const TSharedPtr& PinVal : Pins.Array) + { + TSharedRef EntryResult = MakeShared(); + Results.Add(MakeShared(EntryResult)); + + FSetPinDefaultEntry Entry; + if (!MCPUtils::PopulateFromJson(FSetPinDefaultEntry::StaticStruct(), &Entry, PinVal, &*EntryResult)) continue; + + MCPAssets Assets; + if (!Assets.Scan().Scan().Exact(Entry.Blueprint).Errors(&*EntryResult).ENone().ETwo().Load()) + continue; + UBlueprint* BP = Assets.Object(); + + UEdGraph* Graph = nullptr; + UEdGraphNode* Node = MCPUtils::FindNodeByGuid(BP, Entry.Node, &Graph); + if (!Node) + { + EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Node '%s' not found"), *Entry.Node)); + continue; + } + + UEdGraphPin* Pin = Node->FindPin(FName(*Entry.PinName)); + if (!Pin) + { + EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Pin '%s' not found on node '%s'"), *Entry.PinName, *Entry.Node)); + continue; + } + + if (Pin->Direction != EGPD_Input) + { + EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Pin '%s' is an output pin"), *Entry.PinName)); + continue; + } + + const UEdGraphSchema* Schema = Graph->GetSchema(); + if (Schema) + { + FString ValidationError = Schema->IsPinDefaultValid(Pin, Entry.Value, nullptr, FText::GetEmpty()); + if (!ValidationError.IsEmpty()) + { + EntryResult->SetStringField(TEXT("error"), FString::Printf( + TEXT("Invalid value for pin '%s': %s"), *Entry.PinName, *ValidationError)); + continue; + } + } + + FString OldValue = Pin->DefaultValue; + Pin->DefaultValue = Entry.Value; + + EntryResult->SetStringField(TEXT("oldValue"), OldValue); + SuccessCount++; + ModifiedNodes.Add(Node); + ModifiedBlueprints.Add(BP); + } + + for (UEdGraphNode* Node : ModifiedNodes) + { + Node->ReconstructNode(); + } + + for (UBlueprint* BP : ModifiedBlueprints) + { + FBlueprintEditorUtils::MarkBlueprintAsModified(BP); + } + + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: SetPinDefault — %d/%d succeeded"), + SuccessCount, Pins.Array.Num()); + + Result->SetNumberField(TEXT("successCount"), SuccessCount); + Result->SetNumberField(TEXT("totalCount"), Pins.Array.Num()); + Result->SetArrayField(TEXT("results"), Results); + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_ShowCommands.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_ShowCommands.h new file mode 100644 index 00000000..d87aa027 --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_ShowCommands.h @@ -0,0 +1,79 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPAssetFinder.h" +#include "MCPUtils.h" +#include "Engine/Blueprint.h" +#include "EdGraph/EdGraph.h" +#include "EdGraph/EdGraphNode.h" +#include "EdGraph/EdGraphPin.h" +#include "EdGraphSchema_K2.h" +#include "UObject/UObjectIterator.h" +#include "UMCPHandler_ShowCommands.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +UCLASS() +class UMCPHandler_ShowCommands : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Optional, Description="If true, return full details including parameter types and descriptions")) + bool Verbose = false; + + virtual FString GetDescription() const override + { + return TEXT("List all available commands with their descriptions."); + } + + // Collect all handler classes sorted by tool name. + TArray> CollectHandlers() const + { + TArray> Handlers; + for (TObjectIterator It; It; ++It) + { + UClass* Class = *It; + if (Class->HasAnyClassFlags(CLASS_Abstract)) continue; + const IMCPHandler* Handler = Cast(Class->GetDefaultObject()); + if (!Handler) continue; + FString ToolName = MCPUtils::GetToolName(Class); + Handlers.Add({ToolName, Class}); + } + Handlers.Sort(); + return Handlers; + } + + virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override + { + auto Handlers = CollectHandlers(); + Result.Appendf(TEXT("%d commands:\n"), Handlers.Num()); + + for (const auto& Pair : Handlers) + { + if (Verbose) + { + MCPUtils::FormatCommandHelp(Pair.Value, Result); + continue; + } + + // Non-verbose: just the signature line + UClass* Class = Pair.Value; + Result.Append(Pair.Key); + Result.Append(TEXT("(")); + bool bFirst = true; + for (TFieldIterator PropIt(Class, EFieldIterationFlags::None); PropIt; ++PropIt) + { + if (!bFirst) Result.Append(TEXT(",")); + bFirst = false; + if (PropIt->HasMetaData(TEXT("Optional"))) Result.Append(TEXT("?")); + Result.Append(MCPUtils::PropertyNameToJsonKey(PropIt->GetName())); + } + Result.Append(TEXT(")\n")); + } + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_SpawnNodesInGraph.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_SpawnNodesInGraph.h new file mode 100644 index 00000000..b961d93e --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_SpawnNodesInGraph.h @@ -0,0 +1,195 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPAssetFinder.h" +#include "MCPServer.h" +#include "MCPUtils.h" +#include "Engine/Blueprint.h" +#include "Materials/Material.h" +#include "Materials/MaterialInstanceConstant.h" +#include "Materials/MaterialFunction.h" +#include "Engine/World.h" +#include "Engine/LevelScriptBlueprint.h" +#include "EdGraph/EdGraph.h" +#include "EdGraph/EdGraphNode.h" +#include "EdGraph/EdGraphPin.h" +#include "EdGraphSchema_K2.h" +#include "K2Node.h" +#include "K2Node_CallFunction.h" +#include "K2Node_Event.h" +#include "K2Node_CustomEvent.h" +#include "K2Node_FunctionEntry.h" +#include "K2Node_EditablePinBase.h" +#include "K2Node_VariableGet.h" +#include "K2Node_VariableSet.h" +#include "K2Node_BreakStruct.h" +#include "K2Node_MakeStruct.h" +#include "K2Node_DynamicCast.h" +#include "K2Node_CallParentFunction.h" +#include "K2Node_IfThenElse.h" +#include "K2Node_ExecutionSequence.h" +#include "K2Node_MacroInstance.h" +#include "K2Node_SpawnActorFromClass.h" +#include "K2Node_Select.h" +#include "K2Node_Knot.h" +#include "EdGraphNode_Comment.h" +#include "GameFramework/Actor.h" +#include "Kismet2/BlueprintEditorUtils.h" +#include "Kismet2/KismetEditorUtilities.h" +#include "Serialization/JsonReader.h" +#include "Serialization/JsonWriter.h" +#include "Serialization/JsonSerializer.h" +#include "UObject/SavePackage.h" +#include "UObject/UObjectIterator.h" +#include "Misc/PackageName.h" +#include "AssetRegistry/AssetRegistryModule.h" +#include "AssetRegistry/IAssetRegistry.h" +#include "AssetToolsModule.h" +#include "IAssetTools.h" +#include "BlueprintNodeSpawner.h" +#include "UMCPHandler_SpawnNodesInGraph.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +USTRUCT() +struct FSpawnNodeEntry +{ + GENERATED_BODY() + + UPROPERTY() + FString ActionName; + + UPROPERTY() + int32 PosX = 0; + + UPROPERTY() + int32 PosY = 0; +}; + + +UCLASS() +class UMCPHandler_SpawnNodesInGraph : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Blueprint name or package path")) + FString Blueprint; + + UPROPERTY(meta=(Description="Graph name (e.g. 'EventGraph')")) + FString Graph; + + UPROPERTY(meta=(Description="Array of {actionName, posX, posY} objects. Use search_node_types to find action names.")) + FMCPJsonArray Nodes; + + virtual FString GetDescription() const override + { + return TEXT("Create nodes in a Blueprint graph using the editor's action database. " + "Can create ANY node type that appears in the editor's right-click menu, including custom K2 nodes. " + "Use search_node_types first to find the exact action name."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override + { + + // Load Blueprint + MCPAssets Assets; + if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; + UBlueprint* BP = Assets.Object(); + + // Find the target graph + FString DecodedGraphName = MCPUtils::UrlDecode(Graph); + TArray MatchingGraphs = MCPUtils::AllGraphsNamed(BP, DecodedGraphName); + if (MatchingGraphs.Num() == 0) + { + TArray> GraphNames; + for (UEdGraph* G : MCPUtils::AllGraphs(BP)) + { + GraphNames.Add(MakeShared(G->GetName())); + } + MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Graph '%s' not found"), *DecodedGraphName)); + Result->SetArrayField(TEXT("availableGraphs"), GraphNames); + return; + } + UEdGraph* TargetGraph = MatchingGraphs[0]; + + TArray> Results; + int32 SuccessCount = 0; + + for (const TSharedPtr& NodeVal : Nodes.Array) + { + TSharedRef EntryResult = MakeShared(); + Results.Add(MakeShared(EntryResult)); + + FSpawnNodeEntry Entry; + if (!MCPUtils::PopulateFromJson(FSpawnNodeEntry::StaticStruct(), &Entry, NodeVal, &*EntryResult)) continue; + + // Find the spawner by exact full name + TArray Matches = MCPUtils::SearchNodeSpawners(Entry.ActionName, 0, /*ExactMatch=*/true, TargetGraph); + if (Matches.Num() == 0) + { + EntryResult->SetStringField(TEXT("error"), FString::Printf( + TEXT("No action found matching '%s'. Use search_node_types to find available actions."), + *Entry.ActionName)); + continue; + } + if (Matches.Num() > 1) + { + EntryResult->SetStringField(TEXT("error"), FString::Printf( + TEXT("Ambiguous: %d spawners match '%s'. Cannot determine which one to use."), + Matches.Num(), *Entry.ActionName)); + continue; + } + UBlueprintNodeSpawner* Spawner = Matches[0]; + + // Invoke the spawner + FVector2D Location(Entry.PosX, Entry.PosY); + IBlueprintNodeBinder::FBindingSet Bindings; + UEdGraphNode* NewNode = Spawner->Invoke(TargetGraph, Bindings, Location); + + if (!NewNode) + { + EntryResult->SetStringField(TEXT("error"), TEXT("Spawner Invoke() returned null — node creation failed.")); + continue; + } + + // Ensure valid GUID + if (!NewNode->NodeGuid.IsValid()) + { + NewNode->CreateNewGuid(); + } + + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Spawned node '%s' (class %s) via action '%s' in graph '%s' of '%s'"), + *NewNode->NodeGuid.ToString(), + *NewNode->GetClass()->GetName(), + *Entry.ActionName, + *DecodedGraphName, + *Blueprint); + + // Serialize result + TSharedPtr NodeState = MCPUtils::SerializeNode(NewNode); + + EntryResult->SetStringField(TEXT("nodeId"), NewNode->NodeGuid.ToString()); + EntryResult->SetStringField(TEXT("nodeClass"), NewNode->GetClass()->GetName()); + EntryResult->SetStringField(TEXT("nodeTitle"), NewNode->GetNodeTitle(ENodeTitleType::ListView).ToString()); + if (NodeState.IsValid()) + { + EntryResult->SetObjectField(TEXT("node"), NodeState); + } + SuccessCount++; + } + + FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP); + + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: SpawnNode — %d/%d succeeded in graph '%s' of '%s'"), + SuccessCount, Nodes.Array.Num(), *DecodedGraphName, *Blueprint); + + Result->SetNumberField(TEXT("successCount"), SuccessCount); + Result->SetNumberField(TEXT("totalCount"), Nodes.Array.Num()); + Result->SetArrayField(TEXT("results"), Results); + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_TestSaveBlueprintPackage.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_TestSaveBlueprintPackage.h new file mode 100644 index 00000000..26ffe6ba --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_TestSaveBlueprintPackage.h @@ -0,0 +1,67 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPAssetFinder.h" +#include "MCPUtils.h" +#include "Engine/Blueprint.h" +#include "Engine/World.h" +#include "Engine/Level.h" +#include "Engine/LevelScriptBlueprint.h" +#include "EdGraph/EdGraph.h" +#include "EdGraph/EdGraphNode.h" +#include "K2Node_CallFunction.h" +#include "K2Node_Event.h" +#include "K2Node_CustomEvent.h" +#include "K2Node_VariableGet.h" +#include "K2Node_VariableSet.h" +#include "K2Node_BreakStruct.h" +#include "K2Node_MakeStruct.h" +#include "K2Node_FunctionEntry.h" +#include "K2Node_EditablePinBase.h" +#include "AssetRegistry/IAssetRegistry.h" +#include "UMCPHandler_TestSaveBlueprintPackage.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +// ============================================================ +// HandleTestSave — load a Blueprint and save it unmodified (diagnostic) +// ============================================================ + +UCLASS() +class UMCPHandler_TestSaveBlueprintPackage : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Blueprint name or package path")) + FString Blueprint; + + virtual FString GetDescription() const override + { + return TEXT("Load a Blueprint and save it unmodified as a diagnostic test."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override + { + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: test-save requested for '%s'"), *Blueprint); + + MCPAssets Assets; + if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; + UBlueprint* BP = Assets.Object(); + + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: test-save — loaded '%s', GeneratedClass=%s"), + *BP->GetName(), + BP->GeneratedClass ? *BP->GeneratedClass->GetName() : TEXT("null")); + + // Attempt save with NO modifications + bool bSaved = MCPUtils::SaveBlueprintPackage(BP); + + Result->SetStringField(TEXT("packagePath"), BP->GetPackage()->GetName()); + Result->SetBoolField(TEXT("hasGeneratedClass"), BP->GeneratedClass != nullptr); + Result->SetBoolField(TEXT("saved"), bSaved); + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers.cpp index 676ee129..af217d59 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers.cpp +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers.cpp @@ -1,19 +1,90 @@ -#include "Handlers/MCPHandlers_DiffBlueprints.h" -#include "Handlers/MCPHandlers_Interfaces.h" -#include "Handlers/MCPHandlers_Mutation.h" -#include "Handlers/MCPHandlers_PinMutation.h" -#include "Handlers/MCPHandlers_AssetMutation.h" -#include "Handlers/MCPHandlers_Validation.h" -#include "Handlers/MCPHandlers_UserTypes.h" -#include "Handlers/MCPHandlers_AnimMutation.h" -#include "Handlers/MCPHandlers_Read.h" -#include "Handlers/MCPHandlers_Discovery.h" -#include "Handlers/MCPHandlers_Graphs.h" -#include "Handlers/MCPHandlers_Variables.h" -#include "Handlers/MCPHandlers_Params.h" -#include "Handlers/MCPHandlers_Dispatchers.h" -#include "Handlers/MCPHandlers_Components.h" -#include "Handlers/MCPHandlers_MaterialRead.h" -#include "Handlers/MCPHandlers_MaterialMutation.h" -#include "Handlers/MCPHandlers_MaterialInstance.h" -#include "Handlers/MCPHandlers_StateMachine.h" +#include "Handlers/UMCPHandler_AddAnimStateToMachine.h" +#include "Handlers/UMCPHandler_AddAnimStateTransition.h" +#include "Handlers/UMCPHandler_AddBlueprintComponent.h" +#include "Handlers/UMCPHandler_AddBlueprintInterface.h" +#include "Handlers/UMCPHandler_AddBlueprintVariable.h" +#include "Handlers/UMCPHandler_AddEventDispatcher.h" +#include "Handlers/UMCPHandler_AddFunctionParameter.h" +#include "Handlers/UMCPHandler_AddMaterialExpression.h" +#include "Handlers/UMCPHandler_AddStructField.h" +#include "Handlers/UMCPHandler_BackupAsset.h" +#include "Handlers/UMCPHandler_ChangeBlueprintVariableType.h" +#include "Handlers/UMCPHandler_ChangeFunctionParameterType.h" +#include "Handlers/UMCPHandler_ChangeStructNodeType.h" +#include "Handlers/UMCPHandler_CheckPinConnectionCompatibility.h" +#include "Handlers/UMCPHandler_CompileBlueprint.h" +#include "Handlers/UMCPHandler_CompileMaterial.h" +#include "Handlers/UMCPHandler_ConnectBlueprintPins.h" +#include "Handlers/UMCPHandler_ConnectMaterialExpressionPins.h" +#include "Handlers/UMCPHandler_CreateAnimBlueprintAsset.h" +#include "Handlers/UMCPHandler_CreateBlendSpaceAsset.h" +#include "Handlers/UMCPHandler_CreateBlueprintAsset.h" +#include "Handlers/UMCPHandler_CreateBlueprintGraph.h" +#include "Handlers/UMCPHandler_CreateEnumAsset.h" +#include "Handlers/UMCPHandler_CreateMaterialAsset.h" +#include "Handlers/UMCPHandler_CreateMaterialFunctionAsset.h" +#include "Handlers/UMCPHandler_CreateMaterialInstanceAsset.h" +#include "Handlers/UMCPHandler_CreateStructAsset.h" +#include "Handlers/UMCPHandler_DeleteAsset.h" +#include "Handlers/UMCPHandler_DeleteBlueprintGraph.h" +#include "Handlers/UMCPHandler_DeleteMaterialExpression.h" +#include "Handlers/UMCPHandler_DeleteNodeFromGraph.h" +#include "Handlers/UMCPHandler_DescribeMaterialInEnglish.h" +#include "Handlers/UMCPHandler_DiffTwoBlueprints.h" +#include "Handlers/UMCPHandler_DisconnectBlueprintPins.h" +#include "Handlers/UMCPHandler_DisconnectMaterialExpressionPin.h" +#include "Handlers/UMCPHandler_DumpBlueprint.h" +#include "Handlers/UMCPHandler_DumpBlueprintGraph.h" +#include "Handlers/UMCPHandler_DumpMaterial.h" +#include "Handlers/UMCPHandler_DumpMaterialExpressionGraph.h" +#include "Handlers/UMCPHandler_DumpMaterialFunction.h" +#include "Handlers/UMCPHandler_DumpMaterialInstanceParameters.h" +#include "Handlers/UMCPHandler_DuplicateNodesInGraph.h" +#include "Handlers/UMCPHandler_FindAssetReferences.h" +#include "Handlers/UMCPHandler_FindMaterialReferences.h" +#include "Handlers/UMCPHandler_GetNodeComment.h" +#include "Handlers/UMCPHandler_GetPinDetails.h" +#include "Handlers/UMCPHandler_ListAnimSlotNames.h" +#include "Handlers/UMCPHandler_ListAnimSyncGroups.h" +#include "Handlers/UMCPHandler_ListBlueprintAssets.h" +#include "Handlers/UMCPHandler_ListBlueprintComponents.h" +#include "Handlers/UMCPHandler_ListBlueprintInterfaces.h" +#include "Handlers/UMCPHandler_ListClassFunctions.h" +#include "Handlers/UMCPHandler_ListClassProperties.h" +#include "Handlers/UMCPHandler_ListEventDispatchers.h" +#include "Handlers/UMCPHandler_ListMaterialAssets.h" +#include "Handlers/UMCPHandler_ListMaterialFunctionAssets.h" +#include "Handlers/UMCPHandler_RefreshAllNodesInGraph.h" +#include "Handlers/UMCPHandler_RemoveAnimStateFromMachine.h" +#include "Handlers/UMCPHandler_RemoveBlueprintComponent.h" +#include "Handlers/UMCPHandler_RemoveBlueprintInterface.h" +#include "Handlers/UMCPHandler_RemoveBlueprintVariable.h" +#include "Handlers/UMCPHandler_RemoveFunctionParameter.h" +#include "Handlers/UMCPHandler_RemoveStructField.h" +#include "Handlers/UMCPHandler_RenameAsset.h" +#include "Handlers/UMCPHandler_RenameBlueprintGraph.h" +#include "Handlers/UMCPHandler_ReparentBlueprint.h" +#include "Handlers/UMCPHandler_ReparentMaterialInstance.h" +#include "Handlers/UMCPHandler_ReplaceFunctionCallsInBlueprint.h" +#include "Handlers/UMCPHandler_RestoreAsset.h" +#include "Handlers/UMCPHandler_SearchSpawnableNodeTypes.h" +#include "Handlers/UMCPHandler_SearchTypeUsageInBlueprints.h" +#include "Handlers/UMCPHandler_SearchUnrealClasses.h" +#include "Handlers/UMCPHandler_SearchWithinBlueprints.h" +#include "Handlers/UMCPHandler_SearchWithinMaterials.h" +#include "Handlers/UMCPHandler_SetAnimStateAnimation.h" +#include "Handlers/UMCPHandler_SetAnimStateBlendSpace.h" +#include "Handlers/UMCPHandler_SetAnimTransitionRule.h" +#include "Handlers/UMCPHandler_SetBlendSpaceSamplePoints.h" +#include "Handlers/UMCPHandler_SetBlueprintVariableMetadata.h" +#include "Handlers/UMCPHandler_SetClassDefaultValue.h" +#include "Handlers/UMCPHandler_SetMaterialExpressionPosition.h" +#include "Handlers/UMCPHandler_SetMaterialExpressionProperty.h" +#include "Handlers/UMCPHandler_SetMaterialInstanceParameter.h" +#include "Handlers/UMCPHandler_SetMaterialProperty.h" +#include "Handlers/UMCPHandler_SetNodeComment.h" +#include "Handlers/UMCPHandler_SetNodePositions.h" +#include "Handlers/UMCPHandler_SetPinDefaultValues.h" +#include "Handlers/UMCPHandler_ShowCommands.h" +#include "Handlers/UMCPHandler_SpawnNodesInGraph.h" +#include "Handlers/UMCPHandler_TestSaveBlueprintPackage.h" diff --git a/tools/split-handlers.py b/tools/split-handlers.py new file mode 100644 index 00000000..75570299 --- /dev/null +++ b/tools/split-handlers.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 +"""Split a handler source file into one file per UMCPHandler class. + +Usage: python3 tools/split-handlers.py + +Reads the #include block from the top of the file, then splits at the +closing }; (column 0) of each UMCPHandler class. Each output file is +named after the handler class and placed in the same directory. The +shared #include block is prepended to every output file. +""" + +import sys +import re +import os + + +def main(): + if len(sys.argv) != 2: + print(f"Usage: {sys.argv[0]} ") + sys.exit(1) + + filepath = sys.argv[1] + with open(filepath) as f: + lines = f.readlines() + + # Extract the #include block from the top of the file. + # Collect #pragma, #include, blank lines, and // comment lines + # until we hit something else. + include_block = [] + body_start = 0 + for i, line in enumerate(lines): + stripped = line.strip() + if (stripped == '' or stripped.startswith('#pragma') or + stripped.startswith('#include') or stripped.startswith('//')): + include_block.append(line) + else: + body_start = i + break + + # Strip trailing blank/comment lines from include block + while include_block and include_block[-1].strip() in ('', ) or ( + include_block and include_block[-1].strip().startswith('//')): + body_start -= 1 + include_block.pop() + if not include_block: + break + + # Remove the .generated.h include from the shared block (each output + # file gets its own). + include_block = [l for l in include_block if '.generated.h' not in l] + + body_lines = lines[body_start:] + + # Find split points in the body: after each }; that closes a UMCPHandler. + handler_name = None + splits = [] # list of (end_index_exclusive_in_body, class_name) + + for i, line in enumerate(body_lines): + m = re.match(r'class (UMCPHandler_\w+)', line) + if m: + handler_name = m.group(1) + if line.startswith('};') and handler_name: + splits.append((i + 1, handler_name)) + handler_name = None + + if not splits: + print("No UMCPHandler classes found.") + sys.exit(1) + + dirname = os.path.dirname(filepath) + start = 0 + for end, name in splits: + outpath = os.path.join(dirname, name + '.h') + with open(outpath, 'w') as f: + f.writelines(include_block) + f.write(f'#include "{name}.generated.h"\n') + f.write('\n') + f.writelines(body_lines[start:end]) + print(f" {name}.h ({end - start} lines)") + start = end + + # Warn about trailing content + if start < len(body_lines): + remaining = ''.join(body_lines[start:]).strip() + if remaining: + print(f"Warning: {len(body_lines) - start} trailing lines not included in any output file:") + for line in body_lines[start:]: + print(f" | {line}", end='') + + +if __name__ == '__main__': + main()