diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_AnimMutation.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_AnimMutation.cpp index 0e8b7907..d45fce6e 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_AnimMutation.cpp +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_AnimMutation.cpp @@ -1,4 +1,5 @@ #include "BlueprintMCPServer.h" +#include "MCPUtils.h" #include "Engine/Blueprint.h" #include "EdGraph/EdGraph.h" #include "EdGraph/EdGraphNode.h" @@ -47,19 +48,19 @@ void FBlueprintMCPServer::HandleCreateAnimBlueprint(const FJsonObject* Json, FJs if (Name.IsEmpty() || PackagePath.IsEmpty() || SkeletonName.IsEmpty()) { - return MakeErrorJson(Result, TEXT("Missing required fields: name, packagePath, skeleton")); + return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: name, packagePath, skeleton")); } if (!PackagePath.StartsWith(TEXT("/Game"))) { - return MakeErrorJson(Result, TEXT("packagePath must start with '/Game'")); + return MCPUtils::MakeErrorJson(Result, TEXT("packagePath must start with '/Game'")); } // Check if asset already exists FString FullAssetPath = PackagePath / Name; if (FindBlueprintAsset(Name) || FindBlueprintAsset(FullAssetPath)) { - return MakeErrorJson(Result, FString::Printf( + return MCPUtils::MakeErrorJson(Result, FString::Printf( TEXT("Blueprint '%s' already exists. Use a different name or delete the existing asset first."), *Name)); } @@ -114,7 +115,7 @@ void FBlueprintMCPServer::HandleCreateAnimBlueprint(const FJsonObject* Json, FJs if (!Skeleton) { - return MakeErrorJson(Result, FString::Printf( + return MCPUtils::MakeErrorJson(Result, FString::Printf( TEXT("Skeleton '%s' not found. Provide the skeleton asset name or path. Use '__create_test_skeleton__' for testing."), *SkeletonName)); } @@ -141,7 +142,7 @@ void FBlueprintMCPServer::HandleCreateAnimBlueprint(const FJsonObject* Json, FJs UPackage* Package = CreatePackage(*FullPackagePath); if (!Package) { - return MakeErrorJson(Result, FString::Printf(TEXT("Failed to create package at '%s'"), *FullPackagePath)); + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Failed to create package at '%s'"), *FullPackagePath)); } // Create the Animation Blueprint @@ -157,7 +158,7 @@ void FBlueprintMCPServer::HandleCreateAnimBlueprint(const FJsonObject* Json, FJs if (!NewAnimBP) { - return MakeErrorJson(Result, TEXT("FKismetEditorUtilities::CreateBlueprint returned null for AnimBlueprint")); + return MCPUtils::MakeErrorJson(Result, TEXT("FKismetEditorUtilities::CreateBlueprint returned null for AnimBlueprint")); } // Set target skeleton @@ -167,7 +168,7 @@ void FBlueprintMCPServer::HandleCreateAnimBlueprint(const FJsonObject* Json, FJs FKismetEditorUtilities::CompileBlueprint(NewAnimBP); // Save - bool bSaved = SaveBlueprintPackage(NewAnimBP); + bool bSaved = MCPUtils::SaveBlueprintPackage(NewAnimBP); // Refresh asset cache FAssetRegistryModule& ARM = FModuleManager::LoadModuleChecked("AssetRegistry"); @@ -204,61 +205,6 @@ void FBlueprintMCPServer::HandleCreateAnimBlueprint(const FJsonObject* Json, FJs // Tier 2: State Machine Mutation // ============================================================ -// Helper: find a state machine graph by name within an AnimBlueprint -static UAnimationStateMachineGraph* FindStateMachineGraph(UBlueprint* BP, const FString& GraphName) -{ - TArray AllGraphs; - BP->GetAllGraphs(AllGraphs); - for (UEdGraph* Graph : AllGraphs) - { - if (UAnimationStateMachineGraph* SMGraph = Cast(Graph)) - { - if (SMGraph->GetName() == GraphName) - { - return SMGraph; - } - } - } - return nullptr; -} - -// Helper: find a state node by name within a state machine graph -static UAnimStateNode* FindStateByName(UAnimationStateMachineGraph* SMGraph, const FString& StateName) -{ - for (UEdGraphNode* Node : SMGraph->Nodes) - { - if (UAnimStateNode* StateNode = Cast(Node)) - { - if (StateNode->GetStateName() == StateName) - { - return StateNode; - } - } - } - return nullptr; -} - -// Helper: find a transition between two states -static UAnimStateTransitionNode* FindTransition(UAnimationStateMachineGraph* SMGraph, - const FString& FromStateName, const FString& ToStateName) -{ - for (UEdGraphNode* Node : SMGraph->Nodes) - { - if (UAnimStateTransitionNode* TransNode = Cast(Node)) - { - UAnimStateNode* FromState = Cast(TransNode->GetPreviousState()); - UAnimStateNode* ToState = Cast(TransNode->GetNextState()); - if (FromState && ToState && - FromState->GetStateName() == FromStateName && - ToState->GetStateName() == ToStateName) - { - return TransNode; - } - } - } - return nullptr; -} - void FBlueprintMCPServer::HandleAddAnimState(const FJsonObject* Json, FJsonObject* Result) { FString BlueprintName = Json->GetStringField(TEXT("blueprint")); @@ -267,32 +213,32 @@ void FBlueprintMCPServer::HandleAddAnimState(const FJsonObject* Json, FJsonObjec if (BlueprintName.IsEmpty() || GraphName.IsEmpty() || StateName.IsEmpty()) { - return MakeErrorJson(Result, TEXT("Missing required fields: blueprint, graph, stateName")); + return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: blueprint, graph, stateName")); } FString LoadError; UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError); if (!BP) { - return MakeErrorJson(Result, LoadError); + return MCPUtils::MakeErrorJson(Result, LoadError); } UAnimBlueprint* AnimBP = Cast(BP); if (!AnimBP) { - return MakeErrorJson(Result, FString::Printf(TEXT("'%s' is not an Animation Blueprint"), *BlueprintName)); + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("'%s' is not an Animation Blueprint"), *BlueprintName)); } - UAnimationStateMachineGraph* SMGraph = FindStateMachineGraph(BP, GraphName); + UAnimationStateMachineGraph* SMGraph = MCPUtils::FindStateMachineGraph(BP, GraphName); if (!SMGraph) { - return MakeErrorJson(Result, FString::Printf(TEXT("State machine graph '%s' not found"), *GraphName)); + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("State machine graph '%s' not found"), *GraphName)); } // Check for duplicate state name - if (FindStateByName(SMGraph, StateName)) + if (MCPUtils::FindStateByName(SMGraph, StateName)) { - return MakeErrorJson(Result, FString::Printf(TEXT("State '%s' already exists in graph '%s'"), *StateName, *GraphName)); + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("State '%s' already exists in graph '%s'"), *StateName, *GraphName)); } // Get position @@ -352,7 +298,7 @@ void FBlueprintMCPServer::HandleAddAnimState(const FJsonObject* Json, FJsonObjec // Compile and save FKismetEditorUtilities::CompileBlueprint(AnimBP); - bool bSaved = SaveBlueprintPackage(AnimBP); + bool bSaved = MCPUtils::SaveBlueprintPackage(AnimBP); Result->SetBoolField(TEXT("success"), true); Result->SetStringField(TEXT("stateName"), StateName); @@ -369,32 +315,32 @@ void FBlueprintMCPServer::HandleRemoveAnimState(const FJsonObject* Json, FJsonOb if (BlueprintName.IsEmpty() || GraphName.IsEmpty() || StateName.IsEmpty()) { - return MakeErrorJson(Result, TEXT("Missing required fields: blueprint, graph, stateName")); + return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: blueprint, graph, stateName")); } FString LoadError; UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError); if (!BP) { - return MakeErrorJson(Result, LoadError); + return MCPUtils::MakeErrorJson(Result, LoadError); } UAnimBlueprint* AnimBP = Cast(BP); if (!AnimBP) { - return MakeErrorJson(Result, FString::Printf(TEXT("'%s' is not an Animation Blueprint"), *BlueprintName)); + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("'%s' is not an Animation Blueprint"), *BlueprintName)); } - UAnimationStateMachineGraph* SMGraph = FindStateMachineGraph(BP, GraphName); + UAnimationStateMachineGraph* SMGraph = MCPUtils::FindStateMachineGraph(BP, GraphName); if (!SMGraph) { - return MakeErrorJson(Result, FString::Printf(TEXT("State machine graph '%s' not found"), *GraphName)); + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("State machine graph '%s' not found"), *GraphName)); } - UAnimStateNode* StateNode = FindStateByName(SMGraph, StateName); + UAnimStateNode* StateNode = MCPUtils::FindStateByName(SMGraph, StateName); if (!StateNode) { - return MakeErrorJson(Result, FString::Printf(TEXT("State '%s' not found in graph '%s'"), *StateName, *GraphName)); + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("State '%s' not found in graph '%s'"), *StateName, *GraphName)); } // Collect and remove transitions connected to this state @@ -423,7 +369,7 @@ void FBlueprintMCPServer::HandleRemoveAnimState(const FJsonObject* Json, FJsonOb // Compile and save FKismetEditorUtilities::CompileBlueprint(AnimBP); - bool bSaved = SaveBlueprintPackage(AnimBP); + bool bSaved = MCPUtils::SaveBlueprintPackage(AnimBP); Result->SetBoolField(TEXT("success"), true); Result->SetStringField(TEXT("removedState"), StateName); @@ -440,38 +386,38 @@ void FBlueprintMCPServer::HandleAddAnimTransition(const FJsonObject* Json, FJson if (BlueprintName.IsEmpty() || GraphName.IsEmpty() || FromStateName.IsEmpty() || ToStateName.IsEmpty()) { - return MakeErrorJson(Result, TEXT("Missing required fields: blueprint, graph, fromState, toState")); + return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: blueprint, graph, fromState, toState")); } FString LoadError; UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError); if (!BP) { - return MakeErrorJson(Result, LoadError); + return MCPUtils::MakeErrorJson(Result, LoadError); } UAnimBlueprint* AnimBP = Cast(BP); if (!AnimBP) { - return MakeErrorJson(Result, FString::Printf(TEXT("'%s' is not an Animation Blueprint"), *BlueprintName)); + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("'%s' is not an Animation Blueprint"), *BlueprintName)); } - UAnimationStateMachineGraph* SMGraph = FindStateMachineGraph(BP, GraphName); + UAnimationStateMachineGraph* SMGraph = MCPUtils::FindStateMachineGraph(BP, GraphName); if (!SMGraph) { - return MakeErrorJson(Result, FString::Printf(TEXT("State machine graph '%s' not found"), *GraphName)); + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("State machine graph '%s' not found"), *GraphName)); } - UAnimStateNode* FromState = FindStateByName(SMGraph, FromStateName); + UAnimStateNode* FromState = MCPUtils::FindStateByName(SMGraph, FromStateName); if (!FromState) { - return MakeErrorJson(Result, FString::Printf(TEXT("From state '%s' not found"), *FromStateName)); + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("From state '%s' not found"), *FromStateName)); } - UAnimStateNode* ToState = FindStateByName(SMGraph, ToStateName); + UAnimStateNode* ToState = MCPUtils::FindStateByName(SMGraph, ToStateName); if (!ToState) { - return MakeErrorJson(Result, FString::Printf(TEXT("To state '%s' not found"), *ToStateName)); + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("To state '%s' not found"), *ToStateName)); } // Create transition node @@ -506,7 +452,7 @@ void FBlueprintMCPServer::HandleAddAnimTransition(const FJsonObject* Json, FJson // Compile and save FKismetEditorUtilities::CompileBlueprint(AnimBP); - bool bSaved = SaveBlueprintPackage(AnimBP); + bool bSaved = MCPUtils::SaveBlueprintPackage(AnimBP); Result->SetBoolField(TEXT("success"), true); Result->SetStringField(TEXT("fromState"), FromStateName); @@ -527,32 +473,32 @@ void FBlueprintMCPServer::HandleSetTransitionRule(const FJsonObject* Json, FJson if (BlueprintName.IsEmpty() || GraphName.IsEmpty() || FromStateName.IsEmpty() || ToStateName.IsEmpty()) { - return MakeErrorJson(Result, TEXT("Missing required fields: blueprint, graph, fromState, toState")); + return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: blueprint, graph, fromState, toState")); } FString LoadError; UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError); if (!BP) { - return MakeErrorJson(Result, LoadError); + return MCPUtils::MakeErrorJson(Result, LoadError); } UAnimBlueprint* AnimBP = Cast(BP); if (!AnimBP) { - return MakeErrorJson(Result, FString::Printf(TEXT("'%s' is not an Animation Blueprint"), *BlueprintName)); + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("'%s' is not an Animation Blueprint"), *BlueprintName)); } - UAnimationStateMachineGraph* SMGraph = FindStateMachineGraph(BP, GraphName); + UAnimationStateMachineGraph* SMGraph = MCPUtils::FindStateMachineGraph(BP, GraphName); if (!SMGraph) { - return MakeErrorJson(Result, FString::Printf(TEXT("State machine graph '%s' not found"), *GraphName)); + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("State machine graph '%s' not found"), *GraphName)); } - UAnimStateTransitionNode* TransNode = FindTransition(SMGraph, FromStateName, ToStateName); + UAnimStateTransitionNode* TransNode = MCPUtils::FindTransition(SMGraph, FromStateName, ToStateName); if (!TransNode) { - return MakeErrorJson(Result, FString::Printf( + return MCPUtils::MakeErrorJson(Result, FString::Printf( TEXT("Transition from '%s' to '%s' not found in graph '%s'"), *FromStateName, *ToStateName, *GraphName)); } @@ -588,12 +534,12 @@ void FBlueprintMCPServer::HandleSetTransitionRule(const FJsonObject* Json, FJson if (ChangedCount == 0) { - return MakeErrorJson(Result, TEXT("No properties to update. Provide at least one of: crossfadeDuration, blendMode, priorityOrder, logicType, bBidirectional")); + return MCPUtils::MakeErrorJson(Result, TEXT("No properties to update. Provide at least one of: crossfadeDuration, blendMode, priorityOrder, logicType, bBidirectional")); } // Compile and save FKismetEditorUtilities::CompileBlueprint(AnimBP); - bool bSaved = SaveBlueprintPackage(AnimBP); + bool bSaved = MCPUtils::SaveBlueprintPackage(AnimBP); Result->SetBoolField(TEXT("success"), true); Result->SetStringField(TEXT("fromState"), FromStateName); @@ -619,20 +565,20 @@ void FBlueprintMCPServer::HandleAddAnimNode(const FJsonObject* Json, FJsonObject if (BlueprintName.IsEmpty() || NodeType.IsEmpty()) { - return MakeErrorJson(Result, TEXT("Missing required fields: blueprint, nodeType")); + return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: blueprint, nodeType")); } FString LoadError; UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError); if (!BP) { - return MakeErrorJson(Result, LoadError); + return MCPUtils::MakeErrorJson(Result, LoadError); } UAnimBlueprint* AnimBP = Cast(BP); if (!AnimBP) { - return MakeErrorJson(Result, FString::Printf(TEXT("'%s' is not an Animation Blueprint"), *BlueprintName)); + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("'%s' is not an Animation Blueprint"), *BlueprintName)); } // Find target graph (default to AnimGraph if not specified) @@ -655,7 +601,7 @@ void FBlueprintMCPServer::HandleAddAnimNode(const FJsonObject* Json, FJsonObject if (!TargetGraph) { - return MakeErrorJson(Result, FString::Printf(TEXT("Graph '%s' not found"), *GraphName)); + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Graph '%s' not found"), *GraphName)); } int32 PosX = Json->HasField(TEXT("posX")) ? (int32)Json->GetNumberField(TEXT("posX")) : 0; @@ -735,14 +681,14 @@ void FBlueprintMCPServer::HandleAddAnimNode(const FJsonObject* Json, FJsonObject } else { - return MakeErrorJson(Result, FString::Printf( + return MCPUtils::MakeErrorJson(Result, FString::Printf( TEXT("Unsupported nodeType '%s'. Supported: SequencePlayer, BlendSpacePlayer, StateMachine"), *NodeType)); } if (!NewNode) { - return MakeErrorJson(Result, TEXT("Failed to create anim node")); + return MCPUtils::MakeErrorJson(Result, TEXT("Failed to create anim node")); } NewNode->NodePosX = PosX; @@ -752,7 +698,7 @@ void FBlueprintMCPServer::HandleAddAnimNode(const FJsonObject* Json, FJsonObject // Compile and save FKismetEditorUtilities::CompileBlueprint(AnimBP); - bool bSaved = SaveBlueprintPackage(AnimBP); + bool bSaved = MCPUtils::SaveBlueprintPackage(AnimBP); Result->SetBoolField(TEXT("success"), true); Result->SetStringField(TEXT("nodeType"), NodeType); @@ -784,7 +730,7 @@ void FBlueprintMCPServer::HandleAddStateMachine(const FJsonObject* Json, FJsonOb if (BlueprintName.IsEmpty()) { - return MakeErrorJson(Result, TEXT("Missing required field: blueprint")); + return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: blueprint")); } // Default name @@ -815,38 +761,38 @@ void FBlueprintMCPServer::HandleSetStateAnimation(const FJsonObject* Json, FJson if (BlueprintName.IsEmpty() || GraphName.IsEmpty() || StateName.IsEmpty() || AnimAssetName.IsEmpty()) { - return MakeErrorJson(Result, TEXT("Missing required fields: blueprint, graph, stateName, animationAsset")); + return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: blueprint, graph, stateName, animationAsset")); } FString LoadError; UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError); if (!BP) { - return MakeErrorJson(Result, LoadError); + return MCPUtils::MakeErrorJson(Result, LoadError); } UAnimBlueprint* AnimBP = Cast(BP); if (!AnimBP) { - return MakeErrorJson(Result, FString::Printf(TEXT("'%s' is not an Animation Blueprint"), *BlueprintName)); + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("'%s' is not an Animation Blueprint"), *BlueprintName)); } - UAnimationStateMachineGraph* SMGraph = FindStateMachineGraph(BP, GraphName); + UAnimationStateMachineGraph* SMGraph = MCPUtils::FindStateMachineGraph(BP, GraphName); if (!SMGraph) { - return MakeErrorJson(Result, FString::Printf(TEXT("State machine graph '%s' not found"), *GraphName)); + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("State machine graph '%s' not found"), *GraphName)); } - UAnimStateNode* StateNode = FindStateByName(SMGraph, StateName); + UAnimStateNode* StateNode = MCPUtils::FindStateByName(SMGraph, StateName); if (!StateNode) { - return MakeErrorJson(Result, FString::Printf(TEXT("State '%s' not found in graph '%s'"), *StateName, *GraphName)); + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("State '%s' not found in graph '%s'"), *StateName, *GraphName)); } UEdGraph* InnerGraph = StateNode->GetBoundGraph(); if (!InnerGraph) { - return MakeErrorJson(Result, FString::Printf(TEXT("State '%s' has no bound graph"), *StateName)); + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("State '%s' has no bound graph"), *StateName)); } // Find the animation asset @@ -866,7 +812,7 @@ void FBlueprintMCPServer::HandleSetStateAnimation(const FJsonObject* Json, FJson if (!AnimSeq) { - return MakeErrorJson(Result, FString::Printf(TEXT("Animation asset '%s' not found"), *AnimAssetName)); + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Animation asset '%s' not found"), *AnimAssetName)); } // Find existing SequencePlayer or create one @@ -894,7 +840,7 @@ void FBlueprintMCPServer::HandleSetStateAnimation(const FJsonObject* Json, FJson // Compile and save FKismetEditorUtilities::CompileBlueprint(AnimBP); - bool bSaved = SaveBlueprintPackage(AnimBP); + bool bSaved = MCPUtils::SaveBlueprintPackage(AnimBP); Result->SetBoolField(TEXT("success"), true); Result->SetStringField(TEXT("stateName"), StateName); @@ -908,20 +854,20 @@ void FBlueprintMCPServer::HandleListAnimSlots(const FJsonObject* Json, FJsonObje FString BlueprintName = Json->GetStringField(TEXT("blueprint")); if (BlueprintName.IsEmpty()) { - return MakeErrorJson(Result, TEXT("Missing required field: blueprint")); + return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: blueprint")); } FString LoadError; UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError); if (!BP) { - return MakeErrorJson(Result, LoadError); + return MCPUtils::MakeErrorJson(Result, LoadError); } UAnimBlueprint* AnimBP = Cast(BP); if (!AnimBP) { - return MakeErrorJson(Result, FString::Printf(TEXT("'%s' is not an Animation Blueprint"), *BlueprintName)); + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("'%s' is not an Animation Blueprint"), *BlueprintName)); } // Walk all anim nodes to collect slot names @@ -967,20 +913,20 @@ void FBlueprintMCPServer::HandleListSyncGroups(const FJsonObject* Json, FJsonObj FString BlueprintName = Json->GetStringField(TEXT("blueprint")); if (BlueprintName.IsEmpty()) { - return MakeErrorJson(Result, TEXT("Missing required field: blueprint")); + return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: blueprint")); } FString LoadError; UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError); if (!BP) { - return MakeErrorJson(Result, LoadError); + return MCPUtils::MakeErrorJson(Result, LoadError); } UAnimBlueprint* AnimBP = Cast(BP); if (!AnimBP) { - return MakeErrorJson(Result, FString::Printf(TEXT("'%s' is not an Animation Blueprint"), *BlueprintName)); + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("'%s' is not an Animation Blueprint"), *BlueprintName)); } // Walk all anim nodes to collect sync group names @@ -1033,12 +979,12 @@ void FBlueprintMCPServer::HandleCreateBlendSpace(const FJsonObject* Json, FJsonO if (Name.IsEmpty() || PackagePath.IsEmpty() || SkeletonName.IsEmpty()) { - return MakeErrorJson(Result, TEXT("Missing required fields: name, packagePath, skeleton")); + return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: name, packagePath, skeleton")); } if (!PackagePath.StartsWith(TEXT("/Game"))) { - return MakeErrorJson(Result, TEXT("packagePath must start with '/Game'")); + return MCPUtils::MakeErrorJson(Result, TEXT("packagePath must start with '/Game'")); } // Check if asset already exists @@ -1051,7 +997,7 @@ void FBlueprintMCPServer::HandleCreateBlendSpace(const FJsonObject* Json, FJsonO { if (Asset.AssetName.ToString() == Name || Asset.GetObjectPathString() == FullAssetPath) { - return MakeErrorJson(Result, FString::Printf( + return MCPUtils::MakeErrorJson(Result, FString::Printf( TEXT("Blend Space '%s' already exists. Use a different name or delete the existing asset first."), *Name)); } @@ -1106,7 +1052,7 @@ void FBlueprintMCPServer::HandleCreateBlendSpace(const FJsonObject* Json, FJsonO if (!Skeleton) { - return MakeErrorJson(Result, FString::Printf( + return MCPUtils::MakeErrorJson(Result, FString::Printf( TEXT("Skeleton '%s' not found. Provide the skeleton asset name or path. Use '__create_test_skeleton__' for testing."), *SkeletonName)); } @@ -1119,14 +1065,14 @@ void FBlueprintMCPServer::HandleCreateBlendSpace(const FJsonObject* Json, FJsonO UPackage* Package = CreatePackage(*FullPackagePath); if (!Package) { - return MakeErrorJson(Result, FString::Printf(TEXT("Failed to create package at '%s'"), *FullPackagePath)); + 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 MakeErrorJson(Result, TEXT("Failed to create Blend Space object")); + return MCPUtils::MakeErrorJson(Result, TEXT("Failed to create Blend Space object")); } // Set skeleton @@ -1134,7 +1080,7 @@ void FBlueprintMCPServer::HandleCreateBlendSpace(const FJsonObject* Json, FJsonO // Mark dirty and save NewBS->MarkPackageDirty(); - bool bSaved = SaveGenericPackage(NewBS); + bool bSaved = MCPUtils::SaveGenericPackage(NewBS); UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Created Blend Space '%s' (saved: %s)"), *Name, bSaved ? TEXT("true") : TEXT("false")); @@ -1154,7 +1100,7 @@ void FBlueprintMCPServer::HandleSetBlendSpaceSamples(const FJsonObject* Json, FJ FString BlendSpaceName = Json->GetStringField(TEXT("blendSpace")); if (BlendSpaceName.IsEmpty()) { - return MakeErrorJson(Result, TEXT("Missing required field: blendSpace")); + return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: blendSpace")); } // Load the blend space @@ -1190,7 +1136,7 @@ void FBlueprintMCPServer::HandleSetBlendSpaceSamples(const FJsonObject* Json, FJ if (!BS) { - return MakeErrorJson(Result, FString::Printf(TEXT("Blend Space '%s' not found"), *BlendSpaceName)); + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Blend Space '%s' not found"), *BlendSpaceName)); } // Set axis parameters @@ -1288,7 +1234,7 @@ void FBlueprintMCPServer::HandleSetBlendSpaceSamples(const FJsonObject* Json, FJ // Save BS->MarkPackageDirty(); - bool bSaved = SaveGenericPackage(BS); + 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")); @@ -1314,38 +1260,38 @@ void FBlueprintMCPServer::HandleSetStateBlendSpace(const FJsonObject* Json, FJso if (BlueprintName.IsEmpty() || GraphName.IsEmpty() || StateName.IsEmpty() || BlendSpaceName.IsEmpty()) { - return MakeErrorJson(Result, TEXT("Missing required fields: blueprint, graph, stateName, blendSpace")); + return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: blueprint, graph, stateName, blendSpace")); } FString LoadError; UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError); if (!BP) { - return MakeErrorJson(Result, LoadError); + return MCPUtils::MakeErrorJson(Result, LoadError); } UAnimBlueprint* AnimBP = Cast(BP); if (!AnimBP) { - return MakeErrorJson(Result, FString::Printf(TEXT("'%s' is not an Animation Blueprint"), *BlueprintName)); + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("'%s' is not an Animation Blueprint"), *BlueprintName)); } - UAnimationStateMachineGraph* SMGraph = FindStateMachineGraph(BP, GraphName); + UAnimationStateMachineGraph* SMGraph = MCPUtils::FindStateMachineGraph(BP, GraphName); if (!SMGraph) { - return MakeErrorJson(Result, FString::Printf(TEXT("State machine graph '%s' not found"), *GraphName)); + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("State machine graph '%s' not found"), *GraphName)); } - UAnimStateNode* StateNode = FindStateByName(SMGraph, StateName); + UAnimStateNode* StateNode = MCPUtils::FindStateByName(SMGraph, StateName); if (!StateNode) { - return MakeErrorJson(Result, FString::Printf(TEXT("State '%s' not found in graph '%s'"), *StateName, *GraphName)); + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("State '%s' not found in graph '%s'"), *StateName, *GraphName)); } UEdGraph* InnerGraph = StateNode->GetBoundGraph(); if (!InnerGraph) { - return MakeErrorJson(Result, FString::Printf(TEXT("State '%s' has no bound graph"), *StateName)); + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("State '%s' has no bound graph"), *StateName)); } // Find the blend space asset @@ -1381,7 +1327,7 @@ void FBlueprintMCPServer::HandleSetStateBlendSpace(const FJsonObject* Json, FJso if (!BlendSpaceAsset) { - return MakeErrorJson(Result, FString::Printf(TEXT("Blend Space '%s' not found"), *BlendSpaceName)); + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Blend Space '%s' not found"), *BlendSpaceName)); } // Find existing BlendSpacePlayer or create one @@ -1529,7 +1475,7 @@ void FBlueprintMCPServer::HandleSetStateBlendSpace(const FJsonObject* Json, FJso // Compile and save FKismetEditorUtilities::CompileBlueprint(AnimBP); - bool bSaved = SaveBlueprintPackage(AnimBP); + bool bSaved = MCPUtils::SaveBlueprintPackage(AnimBP); Result->SetBoolField(TEXT("success"), true); Result->SetStringField(TEXT("stateName"), StateName); diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_Components.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_Components.cpp index 03bf89b8..ff1eeca2 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_Components.cpp +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_Components.cpp @@ -1,4 +1,5 @@ #include "BlueprintMCPServer.h" +#include "MCPUtils.h" #include "Engine/Blueprint.h" #include "Engine/SimpleConstructionScript.h" #include "Engine/SCS_Node.h" @@ -18,20 +19,20 @@ void FBlueprintMCPServer::HandleListComponents(const FJsonObject* Json, FJsonObj FString BlueprintName = Json->GetStringField(TEXT("blueprint")); if (BlueprintName.IsEmpty()) { - return MakeErrorJson(Result, TEXT("Missing required field: blueprint")); + return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: blueprint")); } FString LoadError; UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError); if (!BP) { - return MakeErrorJson(Result, LoadError); + return MCPUtils::MakeErrorJson(Result, LoadError); } USimpleConstructionScript* SCS = BP->SimpleConstructionScript; if (!SCS) { - return MakeErrorJson(Result, FString::Printf( + return MCPUtils::MakeErrorJson(Result, FString::Printf( TEXT("Blueprint '%s' does not have a SimpleConstructionScript (not an Actor Blueprint)"), *BlueprintName)); } @@ -106,7 +107,7 @@ void FBlueprintMCPServer::HandleAddComponent(const FJsonObject* Json, FJsonObjec if (BlueprintName.IsEmpty() || ComponentClassName.IsEmpty() || ComponentName.IsEmpty()) { - return MakeErrorJson(Result, TEXT("Missing required fields: blueprint, componentClass, name")); + return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: blueprint, componentClass, name")); } FString ParentComponentName; @@ -119,13 +120,13 @@ void FBlueprintMCPServer::HandleAddComponent(const FJsonObject* Json, FJsonObjec UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError); if (!BP) { - return MakeErrorJson(Result, LoadError); + return MCPUtils::MakeErrorJson(Result, LoadError); } USimpleConstructionScript* SCS = BP->SimpleConstructionScript; if (!SCS) { - return MakeErrorJson(Result, FString::Printf( + return MCPUtils::MakeErrorJson(Result, FString::Printf( TEXT("Blueprint '%s' does not have a SimpleConstructionScript (not an Actor Blueprint)"), *BlueprintName)); } @@ -136,7 +137,7 @@ void FBlueprintMCPServer::HandleAddComponent(const FJsonObject* Json, FJsonObjec { if (Existing && Existing->GetVariableName().ToString().Equals(ComponentName, ESearchCase::IgnoreCase)) { - return MakeErrorJson(Result, FString::Printf( + return MCPUtils::MakeErrorJson(Result, FString::Printf( TEXT("A component named '%s' already exists in Blueprint '%s'"), *ComponentName, *BlueprintName)); } @@ -183,7 +184,7 @@ void FBlueprintMCPServer::HandleAddComponent(const FJsonObject* Json, FJsonObjec if (!ComponentClass) { - return MakeErrorJson(Result, FString::Printf( + 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, " @@ -207,7 +208,7 @@ void FBlueprintMCPServer::HandleAddComponent(const FJsonObject* Json, FJsonObjec if (!ParentSCSNode) { - return MakeErrorJson(Result, FString::Printf( + return MCPUtils::MakeErrorJson(Result, FString::Printf( TEXT("Parent component '%s' not found in Blueprint '%s'"), *ParentComponentName, *BlueprintName)); } @@ -220,7 +221,7 @@ void FBlueprintMCPServer::HandleAddComponent(const FJsonObject* Json, FJsonObjec USCS_Node* NewNode = SCS->CreateNode(ComponentClass, FName(*ComponentName)); if (!NewNode) { - return MakeErrorJson(Result, FString::Printf( + return MCPUtils::MakeErrorJson(Result, FString::Printf( TEXT("Failed to create SCS node for component '%s' with class '%s'"), *ComponentName, *ComponentClass->GetName())); } @@ -236,7 +237,7 @@ void FBlueprintMCPServer::HandleAddComponent(const FJsonObject* Json, FJsonObjec } FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP); - bool bSaved = SaveBlueprintPackage(BP); + bool bSaved = MCPUtils::SaveBlueprintPackage(BP); UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Added component '%s' (%s) to '%s' (parent: %s, saved: %s)"), *ComponentName, *ComponentClass->GetName(), *BlueprintName, @@ -265,20 +266,20 @@ void FBlueprintMCPServer::HandleRemoveComponent(const FJsonObject* Json, FJsonOb if (BlueprintName.IsEmpty() || ComponentName.IsEmpty()) { - return MakeErrorJson(Result, TEXT("Missing required fields: blueprint, name")); + return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: blueprint, name")); } FString LoadError; UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError); if (!BP) { - return MakeErrorJson(Result, LoadError); + return MCPUtils::MakeErrorJson(Result, LoadError); } USimpleConstructionScript* SCS = BP->SimpleConstructionScript; if (!SCS) { - return MakeErrorJson(Result, FString::Printf( + return MCPUtils::MakeErrorJson(Result, FString::Printf( TEXT("Blueprint '%s' does not have a SimpleConstructionScript (not an Actor Blueprint)"), *BlueprintName)); } @@ -307,7 +308,7 @@ void FBlueprintMCPServer::HandleRemoveComponent(const FJsonObject* Json, FJsonOb } } - MakeErrorJson(Result, FString::Printf( + MCPUtils::MakeErrorJson(Result, FString::Printf( TEXT("Component '%s' not found in Blueprint '%s'"), *ComponentName, *BlueprintName)); Result->SetArrayField(TEXT("existingComponents"), CompList); @@ -318,7 +319,7 @@ void FBlueprintMCPServer::HandleRemoveComponent(const FJsonObject* Json, FJsonOb const TArray& RootNodes = SCS->GetRootNodes(); if (RootNodes.Contains(NodeToRemove) && NodeToRemove->GetChildNodes().Num() > 0) { - return MakeErrorJson(Result, FString::Printf( + 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."), *ComponentName, NodeToRemove->GetChildNodes().Num())); @@ -331,7 +332,7 @@ void FBlueprintMCPServer::HandleRemoveComponent(const FJsonObject* Json, FJsonOb SCS->RemoveNodeAndPromoteChildren(NodeToRemove); FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP); - bool bSaved = SaveBlueprintPackage(BP); + bool bSaved = MCPUtils::SaveBlueprintPackage(BP); UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Removed component '%s' from '%s' (saved: %s)"), *ComponentName, *BlueprintName, bSaved ? TEXT("true") : TEXT("false")); diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_DiffBlueprints.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_DiffBlueprints.cpp index 7bf3b835..6d4c897d 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_DiffBlueprints.cpp +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_DiffBlueprints.cpp @@ -1,5 +1,6 @@ #include "BlueprintMCPHandlers_DiffBlueprints.h" #include "BlueprintMCPServer.h" +#include "MCPUtils.h" #include "Engine/Blueprint.h" #include "EdGraph/EdGraph.h" #include "EdGraph/EdGraphNode.h" @@ -12,10 +13,10 @@ void UMCPHandler_DiffBlueprints::Handle(const FJsonObject* Json, FJsonObject* Re // Load both blueprints FString LoadErrorA, LoadErrorB; UBlueprint* BPA = Helper->LoadBlueprintByName(BlueprintA, LoadErrorA); - if (!BPA) { Helper->MakeErrorJson(Result, FString::Printf(TEXT("blueprintA: %s"), *LoadErrorA)); return; } + if (!BPA) { MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("blueprintA: %s"), *LoadErrorA)); return; } UBlueprint* BPB = Helper->LoadBlueprintByName(BlueprintB, LoadErrorB); - if (!BPB) { Helper->MakeErrorJson(Result, FString::Printf(TEXT("blueprintB: %s"), *LoadErrorB)); return; } + if (!BPB) { MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("blueprintB: %s"), *LoadErrorB)); return; } // Helper to gather graphs from a Blueprint auto GatherGraphs = [this](UBlueprint* BP) -> TArray diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_Discovery.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_Discovery.cpp index 0add4bf7..92f01d9f 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_Discovery.cpp +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_Discovery.cpp @@ -1,4 +1,5 @@ #include "BlueprintMCPServer.h" +#include "MCPUtils.h" #include "Engine/Blueprint.h" #include "EdGraph/EdGraph.h" #include "EdGraph/EdGraphNode.h" @@ -23,21 +24,21 @@ void FBlueprintMCPServer::HandleGetPinInfo(const FJsonObject* Json, FJsonObject* if (BlueprintName.IsEmpty() || NodeId.IsEmpty() || PinName.IsEmpty()) { - return MakeErrorJson(Result, TEXT("Missing required fields: blueprint, nodeId, pinName")); + return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: blueprint, nodeId, pinName")); } FString LoadError; UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError); if (!BP) { - return MakeErrorJson(Result, LoadError); + return MCPUtils::MakeErrorJson(Result, LoadError); } UEdGraph* Graph = nullptr; - UEdGraphNode* Node = FindNodeByGuid(BP, NodeId, &Graph); + UEdGraphNode* Node = MCPUtils::FindNodeByGuid(BP, NodeId, &Graph); if (!Node) { - return MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found"), *NodeId)); + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found"), *NodeId)); } UEdGraphPin* Pin = Node->FindPin(FName(*PinName)); @@ -56,7 +57,7 @@ void FBlueprintMCPServer::HandleGetPinInfo(const FJsonObject* Json, FJsonObject* AvailPins.Add(MakeShared(PinObj)); } } - MakeErrorJson(Result, FString::Printf(TEXT("Pin '%s' not found on node '%s'"), *PinName, *NodeId)); + MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Pin '%s' not found on node '%s'"), *PinName, *NodeId)); Result->SetArrayField(TEXT("availablePins"), AvailPins); return; } @@ -128,45 +129,45 @@ void FBlueprintMCPServer::HandleCheckPinCompatibility(const FJsonObject* Json, F if (BlueprintName.IsEmpty() || SourceNodeId.IsEmpty() || SourcePinName.IsEmpty() || TargetNodeId.IsEmpty() || TargetPinName.IsEmpty()) { - return MakeErrorJson(Result, TEXT("Missing required fields: blueprint, sourceNodeId, sourcePinName, targetNodeId, targetPinName")); + return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: blueprint, sourceNodeId, sourcePinName, targetNodeId, targetPinName")); } FString LoadError; UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError); if (!BP) { - return MakeErrorJson(Result, LoadError); + return MCPUtils::MakeErrorJson(Result, LoadError); } UEdGraph* SourceGraph = nullptr; - UEdGraphNode* SourceNode = FindNodeByGuid(BP, SourceNodeId, &SourceGraph); + UEdGraphNode* SourceNode = MCPUtils::FindNodeByGuid(BP, SourceNodeId, &SourceGraph); if (!SourceNode) { - return MakeErrorJson(Result, FString::Printf(TEXT("Source node '%s' not found"), *SourceNodeId)); + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Source node '%s' not found"), *SourceNodeId)); } - UEdGraphNode* TargetNode = FindNodeByGuid(BP, TargetNodeId); + UEdGraphNode* TargetNode = MCPUtils::FindNodeByGuid(BP, TargetNodeId); if (!TargetNode) { - return MakeErrorJson(Result, FString::Printf(TEXT("Target node '%s' not found"), *TargetNodeId)); + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Target node '%s' not found"), *TargetNodeId)); } UEdGraphPin* SourcePin = SourceNode->FindPin(FName(*SourcePinName)); if (!SourcePin) { - return MakeErrorJson(Result, FString::Printf(TEXT("Source pin '%s' not found on node '%s'"), *SourcePinName, *SourceNodeId)); + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Source pin '%s' not found on node '%s'"), *SourcePinName, *SourceNodeId)); } UEdGraphPin* TargetPin = TargetNode->FindPin(FName(*TargetPinName)); if (!TargetPin) { - return MakeErrorJson(Result, FString::Printf(TEXT("Target pin '%s' not found on node '%s'"), *TargetPinName, *TargetNodeId)); + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Target pin '%s' not found on node '%s'"), *TargetPinName, *TargetNodeId)); } const UEdGraphSchema* Schema = SourceGraph ? SourceGraph->GetSchema() : nullptr; if (!Schema) { - return MakeErrorJson(Result, TEXT("Graph schema not found")); + return MCPUtils::MakeErrorJson(Result, TEXT("Graph schema not found")); } // Check compatibility using the schema @@ -248,7 +249,7 @@ void FBlueprintMCPServer::HandleListClasses(const FJsonObject* Json, FJsonObject } if (!ParentClass) { - return MakeErrorJson(Result, FString::Printf(TEXT("Parent class '%s' not found"), *ParentClassName)); + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Parent class '%s' not found"), *ParentClassName)); } } @@ -333,7 +334,7 @@ void FBlueprintMCPServer::HandleListFunctions(const FJsonObject* Json, FJsonObje if (ClassName.IsEmpty()) { - return MakeErrorJson(Result, TEXT("Missing required field: className")); + return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: className")); } // Find the class @@ -348,7 +349,7 @@ void FBlueprintMCPServer::HandleListFunctions(const FJsonObject* Json, FJsonObje } if (!FoundClass) { - return MakeErrorJson(Result, FString::Printf(TEXT("Class '%s' not found"), *ClassName)); + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Class '%s' not found"), *ClassName)); } TArray> FuncList; @@ -437,7 +438,7 @@ void FBlueprintMCPServer::HandleListProperties(const FJsonObject* Json, FJsonObj if (ClassName.IsEmpty()) { - return MakeErrorJson(Result, TEXT("Missing required field: className")); + return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: className")); } // Find the class @@ -452,7 +453,7 @@ void FBlueprintMCPServer::HandleListProperties(const FJsonObject* Json, FJsonObj } if (!FoundClass) { - return MakeErrorJson(Result, FString::Printf(TEXT("Class '%s' not found"), *ClassName)); + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Class '%s' not found"), *ClassName)); } TArray> PropList; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_Dispatchers.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_Dispatchers.cpp index a1227f11..08949be3 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_Dispatchers.cpp +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_Dispatchers.cpp @@ -1,4 +1,5 @@ #include "BlueprintMCPServer.h" +#include "MCPUtils.h" #include "Engine/Blueprint.h" #include "EdGraph/EdGraph.h" #include "EdGraph/EdGraphPin.h" @@ -21,7 +22,7 @@ void FBlueprintMCPServer::HandleAddEventDispatcher(const FJsonObject* Json, FJso if (BlueprintName.IsEmpty() || DispatcherName.IsEmpty()) { - return MakeErrorJson(Result, TEXT("Missing required fields: blueprint, dispatcherName")); + return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: blueprint, dispatcherName")); } // Load Blueprint @@ -29,7 +30,7 @@ void FBlueprintMCPServer::HandleAddEventDispatcher(const FJsonObject* Json, FJso UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError); if (!BP) { - return MakeErrorJson(Result, LoadError); + return MCPUtils::MakeErrorJson(Result, LoadError); } FName DispatcherFName(*DispatcherName); @@ -39,7 +40,7 @@ void FBlueprintMCPServer::HandleAddEventDispatcher(const FJsonObject* Json, FJso { if (Var.VarName == DispatcherFName) { - return MakeErrorJson(Result, FString::Printf( + return MCPUtils::MakeErrorJson(Result, FString::Printf( TEXT("A variable or dispatcher named '%s' already exists in Blueprint '%s'"), *DispatcherName, *BlueprintName)); } @@ -52,7 +53,7 @@ void FBlueprintMCPServer::HandleAddEventDispatcher(const FJsonObject* Json, FJso { if (Existing && Existing->GetName().Equals(DispatcherName, ESearchCase::IgnoreCase)) { - return MakeErrorJson(Result, FString::Printf( + return MCPUtils::MakeErrorJson(Result, FString::Printf( TEXT("A graph named '%s' already exists in Blueprint '%s'"), *DispatcherName, *BlueprintName)); } @@ -67,7 +68,7 @@ void FBlueprintMCPServer::HandleAddEventDispatcher(const FJsonObject* Json, FJso bool bVarAdded = FBlueprintEditorUtils::AddMemberVariable(BP, DispatcherFName, DelegateType); if (!bVarAdded) { - return MakeErrorJson(Result, FString::Printf( + return MCPUtils::MakeErrorJson(Result, FString::Printf( TEXT("Failed to add delegate variable for '%s'"), *DispatcherName)); } @@ -78,7 +79,7 @@ void FBlueprintMCPServer::HandleAddEventDispatcher(const FJsonObject* Json, FJso UEdGraph::StaticClass(), UEdGraphSchema_K2::StaticClass()); if (!SigGraph) { - return MakeErrorJson(Result, TEXT("Failed to create delegate signature graph")); + return MCPUtils::MakeErrorJson(Result, TEXT("Failed to create delegate signature graph")); } K2Schema->CreateDefaultNodesForGraph(*SigGraph); @@ -114,8 +115,8 @@ void FBlueprintMCPServer::HandleAddEventDispatcher(const FJsonObject* Json, FJso { // Still save what we have FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP); - SaveBlueprintPackage(BP); - return MakeErrorJson(Result, TEXT("Event dispatcher created but entry node not found — parameters could not be added")); + MCPUtils::SaveBlueprintPackage(BP); + return MCPUtils::MakeErrorJson(Result, TEXT("Event dispatcher created but entry node not found — parameters could not be added")); } for (const TSharedPtr& ParamVal : ParamsArr) @@ -130,9 +131,9 @@ void FBlueprintMCPServer::HandleAddEventDispatcher(const FJsonObject* Json, FJso FEdGraphPinType PinType; FString TypeError; - if (!ResolveTypeFromString(ParamType, PinType, TypeError)) + if (!MCPUtils::ResolveTypeFromString(ParamType, PinType, TypeError)) { - return MakeErrorJson(Result, FString::Printf( + return MCPUtils::MakeErrorJson(Result, FString::Printf( TEXT("Parameter '%s': %s"), *ParamName, *TypeError)); } @@ -146,7 +147,7 @@ void FBlueprintMCPServer::HandleAddEventDispatcher(const FJsonObject* Json, FJso } FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP); - bool bSaved = SaveBlueprintPackage(BP); + bool bSaved = MCPUtils::SaveBlueprintPackage(BP); UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Added event dispatcher '%s' to '%s' with %d params (saved: %s)"), *DispatcherName, *BlueprintName, AddedParamsJson.Num(), bSaved ? TEXT("true") : TEXT("false")); @@ -167,14 +168,14 @@ void FBlueprintMCPServer::HandleListEventDispatchers(const FJsonObject* Json, FJ FString BlueprintName = Json->GetStringField(TEXT("blueprint")); if (BlueprintName.IsEmpty()) { - return MakeErrorJson(Result, TEXT("Missing required field: blueprint")); + return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: blueprint")); } FString LoadError; UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError); if (!BP) { - return MakeErrorJson(Result, LoadError); + return MCPUtils::MakeErrorJson(Result, LoadError); } TSet DelegateNameSet; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_Graphs.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_Graphs.cpp index dbeac1c6..300a384e 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_Graphs.cpp +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_Graphs.cpp @@ -1,4 +1,5 @@ #include "BlueprintMCPServer.h" +#include "MCPUtils.h" #include "Engine/Blueprint.h" #include "Engine/World.h" #include "EdGraph/EdGraph.h" @@ -26,7 +27,7 @@ void FBlueprintMCPServer::HandleReparentBlueprint(const FJsonObject* Json, FJson if (BlueprintName.IsEmpty() || NewParentName.IsEmpty()) { - return MakeErrorJson(Result, TEXT("Missing required fields: blueprint, newParentClass")); + return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: blueprint, newParentClass")); } // Load Blueprint @@ -34,7 +35,7 @@ void FBlueprintMCPServer::HandleReparentBlueprint(const FJsonObject* Json, FJson UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError); if (!BP) { - return MakeErrorJson(Result, LoadError); + return MCPUtils::MakeErrorJson(Result, LoadError); } FString OldParentName = BP->ParentClass ? BP->ParentClass->GetName() : TEXT("None"); @@ -66,7 +67,7 @@ void FBlueprintMCPServer::HandleReparentBlueprint(const FJsonObject* Json, FJson if (!NewParentClass) { - return MakeErrorJson(Result, FString::Printf( + return MCPUtils::MakeErrorJson(Result, FString::Printf( TEXT("Could not find class '%s'. Provide a C++ class name (e.g. 'WebUIHUD') or Blueprint name."), *NewParentName)); } @@ -94,7 +95,7 @@ void FBlueprintMCPServer::HandleReparentBlueprint(const FJsonObject* Json, FJson FKismetEditorUtilities::CompileBlueprint(BP); // Save - bool bSaved = SaveBlueprintPackage(BP); + bool bSaved = MCPUtils::SaveBlueprintPackage(BP); FString NewParentActualName = NewParentClass->GetName(); @@ -121,20 +122,20 @@ void FBlueprintMCPServer::HandleCreateBlueprint(const FJsonObject* Json, FJsonOb if (BlueprintName.IsEmpty() || PackagePath.IsEmpty() || ParentClassName.IsEmpty()) { - return MakeErrorJson(Result, TEXT("Missing required fields: blueprintName, packagePath, parentClass")); + return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: blueprintName, packagePath, parentClass")); } // Validate packagePath starts with /Game if (!PackagePath.StartsWith(TEXT("/Game"))) { - return MakeErrorJson(Result, TEXT("packagePath must start with '/Game'")); + return MCPUtils::MakeErrorJson(Result, TEXT("packagePath must start with '/Game'")); } // Check if asset already exists FString FullAssetPath = PackagePath / BlueprintName; if (FindBlueprintAsset(BlueprintName) || FindBlueprintAsset(FullAssetPath)) { - return MakeErrorJson(Result, FString::Printf( + return MCPUtils::MakeErrorJson(Result, FString::Printf( TEXT("Blueprint '%s' already exists. Use a different name or delete the existing asset first."), *BlueprintName)); } @@ -163,7 +164,7 @@ void FBlueprintMCPServer::HandleCreateBlueprint(const FJsonObject* Json, FJsonOb if (!ParentClass) { - return MakeErrorJson(Result, FString::Printf( + 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."), *ParentClassName)); } @@ -186,7 +187,7 @@ void FBlueprintMCPServer::HandleCreateBlueprint(const FJsonObject* Json, FJsonOb } else if (BlueprintTypeStr != TEXT("Normal")) { - return MakeErrorJson(Result, FString::Printf( + return MCPUtils::MakeErrorJson(Result, FString::Printf( TEXT("Invalid blueprintType '%s'. Valid values: Normal, Interface, FunctionLibrary, MacroLibrary"), *BlueprintTypeStr)); } @@ -207,7 +208,7 @@ void FBlueprintMCPServer::HandleCreateBlueprint(const FJsonObject* Json, FJsonOb UPackage* Package = CreatePackage(*FullPackagePath); if (!Package) { - return MakeErrorJson(Result, FString::Printf(TEXT("Failed to create package at '%s'"), *FullPackagePath)); + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Failed to create package at '%s'"), *FullPackagePath)); } // Create the Blueprint @@ -222,14 +223,14 @@ void FBlueprintMCPServer::HandleCreateBlueprint(const FJsonObject* Json, FJsonOb if (!NewBP) { - return MakeErrorJson(Result, TEXT("FKismetEditorUtilities::CreateBlueprint returned null")); + return MCPUtils::MakeErrorJson(Result, TEXT("FKismetEditorUtilities::CreateBlueprint returned null")); } // Compile FKismetEditorUtilities::CompileBlueprint(NewBP); // Save - bool bSaved = SaveBlueprintPackage(NewBP); + bool bSaved = MCPUtils::SaveBlueprintPackage(NewBP); // Refresh asset cache FAssetRegistryModule& ARM = FModuleManager::LoadModuleChecked("AssetRegistry"); @@ -276,12 +277,12 @@ void FBlueprintMCPServer::HandleCreateGraph(const FJsonObject* Json, FJsonObject if (BlueprintName.IsEmpty() || GraphName.IsEmpty() || GraphType.IsEmpty()) { - return MakeErrorJson(Result, TEXT("Missing required fields: blueprint, graphName, graphType")); + return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: blueprint, graphName, graphType")); } if (GraphType != TEXT("function") && GraphType != TEXT("macro") && GraphType != TEXT("customEvent")) { - return MakeErrorJson(Result, FString::Printf( + return MCPUtils::MakeErrorJson(Result, FString::Printf( TEXT("Invalid graphType '%s'. Valid values: function, macro, customEvent"), *GraphType)); } @@ -290,7 +291,7 @@ void FBlueprintMCPServer::HandleCreateGraph(const FJsonObject* Json, FJsonObject UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError); if (!BP) { - return MakeErrorJson(Result, LoadError); + return MCPUtils::MakeErrorJson(Result, LoadError); } // Check graph name uniqueness @@ -300,7 +301,7 @@ void FBlueprintMCPServer::HandleCreateGraph(const FJsonObject* Json, FJsonObject { if (Existing && Existing->GetName().Equals(GraphName, ESearchCase::IgnoreCase)) { - return MakeErrorJson(Result, FString::Printf( + return MCPUtils::MakeErrorJson(Result, FString::Printf( TEXT("A graph named '%s' already exists in Blueprint '%s'"), *GraphName, *BlueprintName)); } } @@ -317,7 +318,7 @@ void FBlueprintMCPServer::HandleCreateGraph(const FJsonObject* Json, FJsonObject { if (CE->CustomFunctionName == FName(*GraphName)) { - return MakeErrorJson(Result, FString::Printf( + return MCPUtils::MakeErrorJson(Result, FString::Printf( TEXT("A custom event named '%s' already exists in Blueprint '%s'"), *GraphName, *BlueprintName)); } } @@ -336,7 +337,7 @@ void FBlueprintMCPServer::HandleCreateGraph(const FJsonObject* Json, FJsonObject UEdGraph::StaticClass(), UEdGraphSchema_K2::StaticClass()); if (!NewGraph) { - return MakeErrorJson(Result, TEXT("Failed to create function graph")); + return MCPUtils::MakeErrorJson(Result, TEXT("Failed to create function graph")); } FBlueprintEditorUtils::AddFunctionGraph(BP, NewGraph, /*bIsUserCreated=*/true, /*SignatureFromObject=*/static_cast(nullptr)); } @@ -346,7 +347,7 @@ void FBlueprintMCPServer::HandleCreateGraph(const FJsonObject* Json, FJsonObject UEdGraph::StaticClass(), UEdGraphSchema_K2::StaticClass()); if (!NewGraph) { - return MakeErrorJson(Result, TEXT("Failed to create macro graph")); + return MCPUtils::MakeErrorJson(Result, TEXT("Failed to create macro graph")); } FBlueprintEditorUtils::AddMacroGraph(BP, NewGraph, /*bIsUserCreated=*/true, /*SignatureFromClass=*/nullptr); } @@ -360,7 +361,7 @@ void FBlueprintMCPServer::HandleCreateGraph(const FJsonObject* Json, FJsonObject } if (!EventGraph) { - return MakeErrorJson(Result, TEXT("Blueprint has no EventGraph to add a custom event to")); + return MCPUtils::MakeErrorJson(Result, TEXT("Blueprint has no EventGraph to add a custom event to")); } // Create a custom event node in the EventGraph @@ -375,7 +376,7 @@ void FBlueprintMCPServer::HandleCreateGraph(const FJsonObject* Json, FJsonObject } FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP); - bool bSaved = SaveBlueprintPackage(BP); + bool bSaved = MCPUtils::SaveBlueprintPackage(BP); UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Created %s graph '%s' in '%s' (saved: %s)"), *GraphType, *GraphName, *BlueprintName, bSaved ? TEXT("true") : TEXT("false")); @@ -402,14 +403,14 @@ void FBlueprintMCPServer::HandleDeleteGraph(const FJsonObject* Json, FJsonObject if (BlueprintName.IsEmpty() || GraphName.IsEmpty()) { - return MakeErrorJson(Result, TEXT("Missing required fields: blueprint, graphName")); + return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: blueprint, graphName")); } FString LoadError; UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError); if (!BP) { - return MakeErrorJson(Result, LoadError); + return MCPUtils::MakeErrorJson(Result, LoadError); } // Find the graph @@ -445,12 +446,12 @@ void FBlueprintMCPServer::HandleDeleteGraph(const FJsonObject* Json, FJsonObject { if (Graph && Graph->GetName().Equals(GraphName, ESearchCase::IgnoreCase)) { - return MakeErrorJson(Result, FString::Printf( + return MCPUtils::MakeErrorJson(Result, FString::Printf( TEXT("Cannot delete UbergraphPage '%s'. EventGraph and other Ubergraph pages cannot be deleted."), *GraphName)); } } - return MakeErrorJson(Result, FString::Printf(TEXT("Graph '%s' not found in Blueprint '%s'"), *GraphName, *BlueprintName)); + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Graph '%s' not found in Blueprint '%s'"), *GraphName, *BlueprintName)); } UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Deleting %s graph '%s' from Blueprint '%s'"), @@ -463,7 +464,7 @@ void FBlueprintMCPServer::HandleDeleteGraph(const FJsonObject* Json, FJsonObject FBlueprintEditorUtils::RemoveGraph(BP, TargetGraph, EGraphRemoveFlags::Default); FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP); - bool bSaved = SaveBlueprintPackage(BP); + bool bSaved = MCPUtils::SaveBlueprintPackage(BP); UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Deleted graph '%s' (%d nodes), save %s"), *GraphName, NodeCount, bSaved ? TEXT("true") : TEXT("false")); @@ -488,14 +489,14 @@ void FBlueprintMCPServer::HandleRenameGraph(const FJsonObject* Json, FJsonObject if (BlueprintName.IsEmpty() || GraphName.IsEmpty() || NewName.IsEmpty()) { - return MakeErrorJson(Result, TEXT("Missing required fields: blueprint, graphName, newName")); + return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: blueprint, graphName, newName")); } FString LoadError; UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError); if (!BP) { - return MakeErrorJson(Result, LoadError); + return MCPUtils::MakeErrorJson(Result, LoadError); } // Check if it's an UbergraphPage — disallow rename @@ -503,7 +504,7 @@ void FBlueprintMCPServer::HandleRenameGraph(const FJsonObject* Json, FJsonObject { if (Graph && Graph->GetName().Equals(GraphName, ESearchCase::IgnoreCase)) { - return MakeErrorJson(Result, FString::Printf( + return MCPUtils::MakeErrorJson(Result, FString::Printf( TEXT("Cannot rename UbergraphPage '%s'. EventGraph and other Ubergraph pages cannot be renamed."), *GraphName)); } @@ -537,7 +538,7 @@ void FBlueprintMCPServer::HandleRenameGraph(const FJsonObject* Json, FJsonObject if (!TargetGraph) { - return MakeErrorJson(Result, FString::Printf(TEXT("Graph '%s' not found in Blueprint '%s'"), *GraphName, *BlueprintName)); + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Graph '%s' not found in Blueprint '%s'"), *GraphName, *BlueprintName)); } // Check for name collision @@ -547,7 +548,7 @@ void FBlueprintMCPServer::HandleRenameGraph(const FJsonObject* Json, FJsonObject { if (Existing && Existing != TargetGraph && Existing->GetName().Equals(NewName, ESearchCase::IgnoreCase)) { - return MakeErrorJson(Result, FString::Printf( + return MCPUtils::MakeErrorJson(Result, FString::Printf( TEXT("A graph named '%s' already exists in Blueprint '%s'"), *NewName, *BlueprintName)); } } @@ -558,7 +559,7 @@ void FBlueprintMCPServer::HandleRenameGraph(const FJsonObject* Json, FJsonObject FBlueprintEditorUtils::RenameGraph(TargetGraph, NewName); FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP); - bool bSaved = SaveBlueprintPackage(BP); + bool bSaved = MCPUtils::SaveBlueprintPackage(BP); UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Renamed graph '%s' to '%s', save %s"), *GraphName, *NewName, bSaved ? TEXT("true") : TEXT("false")); diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_Interfaces.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_Interfaces.cpp index 1b365fde..9690cc5c 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_Interfaces.cpp +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_Interfaces.cpp @@ -1,5 +1,6 @@ #include "BlueprintMCPHandlers_Interfaces.h" #include "BlueprintMCPServer.h" +#include "MCPUtils.h" #include "Engine/Blueprint.h" #include "EdGraph/EdGraph.h" #include "Kismet2/BlueprintEditorUtils.h" @@ -17,7 +18,7 @@ void UMCPHandler_ListInterfaces::Handle(const FJsonObject* Json, FJsonObject* Re UBlueprint* BP = Helper->LoadBlueprintByName(Blueprint, LoadError); if (!BP) { - return Helper->MakeErrorJson(Result, LoadError); + return MCPUtils::MakeErrorJson(Result, LoadError); } TArray> InterfacesArr; @@ -62,7 +63,7 @@ void UMCPHandler_AddInterface::Handle(const FJsonObject* Json, FJsonObject* Resu UBlueprint* BP = Helper->LoadBlueprintByName(Blueprint, LoadError); if (!BP) { - return Helper->MakeErrorJson(Result, LoadError); + return MCPUtils::MakeErrorJson(Result, LoadError); } // Resolve the interface class @@ -109,7 +110,7 @@ void UMCPHandler_AddInterface::Handle(const FJsonObject* Json, FJsonObject* Resu if (!InterfaceClass) { - return Helper->MakeErrorJson(Result, FString::Printf( + 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)); } @@ -119,7 +120,7 @@ void UMCPHandler_AddInterface::Handle(const FJsonObject* Json, FJsonObject* Resu { if (IfaceDesc.Interface == InterfaceClass) { - return Helper->MakeErrorJson(Result, FString::Printf( + return MCPUtils::MakeErrorJson(Result, FString::Printf( TEXT("Interface '%s' is already implemented by Blueprint '%s'"), *InterfaceName, *Blueprint)); } @@ -133,7 +134,7 @@ void UMCPHandler_AddInterface::Handle(const FJsonObject* Json, FJsonObject* Resu bool bAdded = FBlueprintEditorUtils::ImplementNewInterface(BP, InterfacePath); if (!bAdded) { - return Helper->MakeErrorJson(Result, FString::Printf( + return MCPUtils::MakeErrorJson(Result, FString::Printf( TEXT("FBlueprintEditorUtils::ImplementNewInterface failed for interface '%s' on Blueprint '%s'"), *InterfaceName, *Blueprint)); } @@ -186,7 +187,7 @@ void UMCPHandler_RemoveInterface::Handle(const FJsonObject* Json, FJsonObject* R UBlueprint* BP = Helper->LoadBlueprintByName(Blueprint, LoadError); if (!BP) { - return Helper->MakeErrorJson(Result, LoadError); + return MCPUtils::MakeErrorJson(Result, LoadError); } // Find the interface in ImplementedInterfaces by name (case-insensitive) @@ -229,7 +230,7 @@ void UMCPHandler_RemoveInterface::Handle(const FJsonObject* Json, FJsonObject* R } } - Helper->MakeErrorJson(Result, FString::Printf( + MCPUtils::MakeErrorJson(Result, FString::Printf( TEXT("Interface '%s' is not implemented by Blueprint '%s'"), *InterfaceName, *Blueprint)); Result->SetArrayField(TEXT("implementedInterfaces"), IfaceList); diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_MaterialInstance.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_MaterialInstance.cpp index effe98e3..ec254bf5 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_MaterialInstance.cpp +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_MaterialInstance.cpp @@ -1,4 +1,5 @@ #include "BlueprintMCPServer.h" +#include "MCPUtils.h" #include "Materials/Material.h" #include "Materials/MaterialInterface.h" #include "Materials/MaterialInstanceConstant.h" @@ -28,20 +29,20 @@ void FBlueprintMCPServer::HandleCreateMaterialInstance(const FJsonObject* Json, if (Name.IsEmpty() || PackagePath.IsEmpty() || ParentMaterialName.IsEmpty()) { - return MakeErrorJson(Result, TEXT("Missing required fields: name, packagePath, parentMaterial")); + return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: name, packagePath, parentMaterial")); } // Validate packagePath starts with /Game if (!PackagePath.StartsWith(TEXT("/Game"))) { - return MakeErrorJson(Result, TEXT("packagePath must start with '/Game'")); + return MCPUtils::MakeErrorJson(Result, TEXT("packagePath must start with '/Game'")); } // Check if asset already exists FString FullAssetPath = PackagePath / Name; if (FindMaterialInstanceAsset(Name) || FindMaterialInstanceAsset(FullAssetPath)) { - return MakeErrorJson(Result, FString::Printf( + return MCPUtils::MakeErrorJson(Result, FString::Printf( TEXT("Material Instance '%s' already exists. Use a different name or delete the existing asset first."), *Name)); } @@ -74,7 +75,7 @@ void FBlueprintMCPServer::HandleCreateMaterialInstance(const FJsonObject* Json, if (!ParentMaterial) { - return MakeErrorJson(Result, FString::Printf( + return MCPUtils::MakeErrorJson(Result, FString::Printf( TEXT("Parent material '%s' not found. Provide a Material or Material Instance name/path."), *ParentMaterialName)); } @@ -89,13 +90,13 @@ void FBlueprintMCPServer::HandleCreateMaterialInstance(const FJsonObject* Json, UObject* NewAsset = AssetTools.CreateAsset(Name, PackagePath, UMaterialInstanceConstant::StaticClass(), Factory); if (!NewAsset) { - return MakeErrorJson(Result, FString::Printf(TEXT("Failed to create Material Instance asset '%s' in '%s'"), *Name, *PackagePath)); + 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 MakeErrorJson(Result, TEXT("Created asset is not a UMaterialInstanceConstant")); + return MCPUtils::MakeErrorJson(Result, TEXT("Created asset is not a UMaterialInstanceConstant")); } // Set parent @@ -104,7 +105,7 @@ void FBlueprintMCPServer::HandleCreateMaterialInstance(const FJsonObject* Json, MI->PostEditChange(); // Save - bool bSaved = SaveGenericPackage(MI); + bool bSaved = MCPUtils::SaveGenericPackage(MI); // Refresh asset cache FAssetRegistryModule& ARM = FModuleManager::LoadModuleChecked("AssetRegistry"); @@ -132,12 +133,12 @@ void FBlueprintMCPServer::HandleSetMaterialInstanceParameter(const FJsonObject* if (MIName.IsEmpty() || ParamName.IsEmpty()) { - return MakeErrorJson(Result, TEXT("Missing required fields: materialInstance, parameterName")); + return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: materialInstance, parameterName")); } if (!Json->HasField(TEXT("value"))) { - return MakeErrorJson(Result, TEXT("Missing required field: value")); + return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: value")); } bool bDryRun = false; @@ -151,7 +152,7 @@ void FBlueprintMCPServer::HandleSetMaterialInstanceParameter(const FJsonObject* UMaterialInstanceConstant* MI = LoadMaterialInstanceByName(MIName, LoadError); if (!MI) { - return MakeErrorJson(Result, LoadError); + return MCPUtils::MakeErrorJson(Result, LoadError); } // Determine the parameter type — explicit or auto-detect from parent @@ -223,7 +224,7 @@ void FBlueprintMCPServer::HandleSetMaterialInstanceParameter(const FJsonObject* if (TypeStr.IsEmpty()) { - return MakeErrorJson(Result, FString::Printf( + return MCPUtils::MakeErrorJson(Result, FString::Printf( TEXT("Could not determine parameter type for '%s'. Specify the 'type' field explicitly (scalar, vector, texture, staticSwitch)."), *ParamName)); } @@ -252,7 +253,7 @@ void FBlueprintMCPServer::HandleSetMaterialInstanceParameter(const FJsonObject* const TSharedPtr* ValueObj = nullptr; if (!Json->TryGetObjectField(TEXT("value"), ValueObj) || !ValueObj || !(*ValueObj).IsValid()) { - return MakeErrorJson(Result, TEXT("For vector parameters, 'value' must be an object with r, g, b (and optional a) fields.")); + 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")); @@ -274,13 +275,13 @@ void FBlueprintMCPServer::HandleSetMaterialInstanceParameter(const FJsonObject* FString TexturePath = Json->GetStringField(TEXT("value")); if (TexturePath.IsEmpty()) { - return MakeErrorJson(Result, TEXT("For texture parameters, 'value' must be a texture asset path string.")); + return MCPUtils::MakeErrorJson(Result, TEXT("For texture parameters, 'value' must be a texture asset path string.")); } UTexture* TextureObj = LoadObject(nullptr, *TexturePath); if (!TextureObj) { - return MakeErrorJson(Result, FString::Printf(TEXT("Could not load texture at path '%s'"), *TexturePath)); + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Could not load texture at path '%s'"), *TexturePath)); } if (!bDryRun) @@ -328,7 +329,7 @@ void FBlueprintMCPServer::HandleSetMaterialInstanceParameter(const FJsonObject* } else { - return MakeErrorJson(Result, FString::Printf( + return MCPUtils::MakeErrorJson(Result, FString::Printf( TEXT("Unknown parameter type '%s'. Valid types: scalar, vector, texture, staticSwitch"), *TypeStr)); } @@ -338,7 +339,7 @@ void FBlueprintMCPServer::HandleSetMaterialInstanceParameter(const FJsonObject* MI->PreEditChange(nullptr); MI->PostEditChange(); MI->MarkPackageDirty(); - SaveGenericPackage(MI); + MCPUtils::SaveGenericPackage(MI); } UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: %s parameter '%s' = %s on '%s'"), @@ -365,14 +366,14 @@ void FBlueprintMCPServer::HandleGetMaterialInstanceParameters(const FJsonObject* FString NameParam = Json->GetStringField(TEXT("name")); if (NameParam.IsEmpty()) { - return MakeErrorJson(Result, TEXT("Missing required query parameter: name")); + return MCPUtils::MakeErrorJson(Result, TEXT("Missing required query parameter: name")); } FString LoadError; UMaterialInstanceConstant* MI = LoadMaterialInstanceByName(NameParam, LoadError); if (!MI) { - return MakeErrorJson(Result, LoadError); + return MCPUtils::MakeErrorJson(Result, LoadError); } Result->SetStringField(TEXT("name"), MI->GetName()); @@ -606,7 +607,7 @@ void FBlueprintMCPServer::HandleReparentMaterialInstance(const FJsonObject* Json if (MIName.IsEmpty() || NewParentName.IsEmpty()) { - return MakeErrorJson(Result, TEXT("Missing required fields: materialInstance, newParent")); + return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: materialInstance, newParent")); } bool bDryRun = false; @@ -620,7 +621,7 @@ void FBlueprintMCPServer::HandleReparentMaterialInstance(const FJsonObject* Json UMaterialInstanceConstant* MI = LoadMaterialInstanceByName(MIName, LoadError); if (!MI) { - return MakeErrorJson(Result, LoadError); + return MCPUtils::MakeErrorJson(Result, LoadError); } // Capture old parent @@ -654,7 +655,7 @@ void FBlueprintMCPServer::HandleReparentMaterialInstance(const FJsonObject* Json if (!NewParent) { - return MakeErrorJson(Result, FString::Printf( + return MCPUtils::MakeErrorJson(Result, FString::Printf( TEXT("New parent material '%s' not found. Provide a Material or Material Instance name/path."), *NewParentName)); } @@ -666,7 +667,7 @@ void FBlueprintMCPServer::HandleReparentMaterialInstance(const FJsonObject* Json { if (Check == MI) { - return MakeErrorJson(Result, FString::Printf( + return MCPUtils::MakeErrorJson(Result, FString::Printf( TEXT("Cannot reparent '%s' to '%s' — this would create a circular parent chain."), *MIName, *NewParentName)); } @@ -692,7 +693,7 @@ void FBlueprintMCPServer::HandleReparentMaterialInstance(const FJsonObject* Json MI->Parent = NewParent; MI->PostEditChange(); - bool bSaved = SaveGenericPackage(MI); + bool bSaved = MCPUtils::SaveGenericPackage(MI); UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Reparented Material Instance '%s' (saved: %s)"), *MIName, bSaved ? TEXT("true") : TEXT("false")); diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_MaterialMutation.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_MaterialMutation.cpp index 0532f976..8d4f5f78 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_MaterialMutation.cpp +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_MaterialMutation.cpp @@ -1,4 +1,5 @@ #include "BlueprintMCPServer.h" +#include "MCPUtils.h" #include "Materials/Material.h" #include "MaterialDomain.h" #include "Materials/MaterialInstanceConstant.h" @@ -63,19 +64,19 @@ void FBlueprintMCPServer::HandleCreateMaterial(const FJsonObject* Json, FJsonObj if (Name.IsEmpty() || PackagePath.IsEmpty()) { - return MakeErrorJson(Result, TEXT("Missing required fields: name, packagePath")); + return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: name, packagePath")); } if (!PackagePath.StartsWith(TEXT("/Game"))) { - return MakeErrorJson(Result, TEXT("packagePath must start with '/Game'")); + return MCPUtils::MakeErrorJson(Result, TEXT("packagePath must start with '/Game'")); } // Check if asset already exists FString FullAssetPath = PackagePath / Name; if (FindMaterialAsset(Name) || FindMaterialAsset(FullAssetPath)) { - return MakeErrorJson(Result, FString::Printf( + return MCPUtils::MakeErrorJson(Result, FString::Printf( TEXT("Material '%s' already exists. Use a different name or delete the existing asset first."), *Name)); } @@ -89,13 +90,13 @@ void FBlueprintMCPServer::HandleCreateMaterial(const FJsonObject* Json, FJsonObj if (!NewAsset) { - return MakeErrorJson(Result, FString::Printf(TEXT("Failed to create Material '%s' in '%s'"), *Name, *PackagePath)); + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Failed to create Material '%s' in '%s'"), *Name, *PackagePath)); } UMaterial* Material = Cast(NewAsset); if (!Material) { - return MakeErrorJson(Result, TEXT("Created asset is not a UMaterial")); + return MCPUtils::MakeErrorJson(Result, TEXT("Created asset is not a UMaterial")); } // Apply optional properties @@ -150,7 +151,7 @@ void FBlueprintMCPServer::HandleCreateMaterial(const FJsonObject* Json, FJsonObj Material->PostEditChange(); // Save - bool bSaved = SaveMaterialPackage(Material); + bool bSaved = MCPUtils::SaveMaterialPackage(Material); // Refresh asset cache FAssetRegistryModule& ARM = FModuleManager::LoadModuleChecked("AssetRegistry"); @@ -208,12 +209,12 @@ void FBlueprintMCPServer::HandleSetMaterialProperty(const FJsonObject* Json, FJs if (MaterialName.IsEmpty() || Property.IsEmpty()) { - return MakeErrorJson(Result, TEXT("Missing required fields: material, property")); + return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: material, property")); } if (!Json->HasField(TEXT("value"))) { - return MakeErrorJson(Result, TEXT("Missing required field: value")); + return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: value")); } bool bDryRun = false; @@ -224,7 +225,7 @@ void FBlueprintMCPServer::HandleSetMaterialProperty(const FJsonObject* Json, FJs UMaterial* Material = LoadMaterialByName(MaterialName, LoadError); if (!Material) { - return MakeErrorJson(Result, LoadError); + return MCPUtils::MakeErrorJson(Result, LoadError); } FString OldValue; @@ -290,7 +291,7 @@ void FBlueprintMCPServer::HandleSetMaterialProperty(const FJsonObject* Json, FJs else if (ValueStr == TEXT("UI")) NewDomain = MD_UI; else { - return MakeErrorJson(Result, FString::Printf( + return MCPUtils::MakeErrorJson(Result, FString::Printf( TEXT("Invalid domain '%s'. Valid values: Surface, DeferredDecal, LightFunction, Volume, PostProcess, UI"), *ValueStr)); } @@ -317,7 +318,7 @@ void FBlueprintMCPServer::HandleSetMaterialProperty(const FJsonObject* Json, FJs else if (ValueStr == TEXT("Modulate")) NewBlend = BLEND_Modulate; else { - return MakeErrorJson(Result, FString::Printf( + return MCPUtils::MakeErrorJson(Result, FString::Printf( TEXT("Invalid blendMode '%s'. Valid values: Opaque, Masked, Translucent, Additive, Modulate"), *ValueStr)); } @@ -362,7 +363,7 @@ void FBlueprintMCPServer::HandleSetMaterialProperty(const FJsonObject* Json, FJs else if (ValueStr == TEXT("Eye")) NewModel = MSM_Eye; else { - return MakeErrorJson(Result, FString::Printf( + return MCPUtils::MakeErrorJson(Result, FString::Printf( TEXT("Invalid shadingModel '%s'. Valid values: Unlit, DefaultLit, Subsurface, PreintegratedSkin, ClearCoat, SubsurfaceProfile, TwoSidedFoliage, Hair, Cloth, Eye"), *ValueStr)); } @@ -456,7 +457,7 @@ void FBlueprintMCPServer::HandleSetMaterialProperty(const FJsonObject* Json, FJs } else { - return MakeErrorJson(Result, FString::Printf( + return MCPUtils::MakeErrorJson(Result, FString::Printf( TEXT("Unknown property '%s'. Valid properties: domain, blendMode, twoSided, shadingModel, opacity, " "opacityMaskClipValue, bUsedWithSkeletalMesh, bUsedWithMorphTargets, bUsedWithNiagaraSprites, " "ditheredLODTransition, bAllowNegativeEmissiveColor"), @@ -467,7 +468,7 @@ void FBlueprintMCPServer::HandleSetMaterialProperty(const FJsonObject* Json, FJs bool bSaved = false; if (!bDryRun) { - bSaved = SaveMaterialPackage(Material); + bSaved = MCPUtils::SaveMaterialPackage(Material); } UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: %sSet material property '%s' on '%s': '%s' -> '%s'"), @@ -497,11 +498,11 @@ void FBlueprintMCPServer::HandleAddMaterialExpression(const FJsonObject* Json, F if (MaterialName.IsEmpty() && !Json->HasField(TEXT("materialFunction"))) { - return MakeErrorJson(Result, TEXT("Missing required field: 'material' or 'materialFunction'")); + return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: 'material' or 'materialFunction'")); } if (ExpressionClassName.IsEmpty()) { - return MakeErrorJson(Result, TEXT("Missing required field: expressionClass")); + return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: expressionClass")); } int32 PosX = 0, PosY = 0; @@ -540,14 +541,14 @@ void FBlueprintMCPServer::HandleAddMaterialExpression(const FJsonObject* Json, F if (!ExprClass) { - return MakeErrorJson(Result, FString::Printf( + 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.)"), *ExpressionClassName)); } if (ExprClass->HasAnyClassFlags(CLASS_Abstract)) { - return MakeErrorJson(Result, FString::Printf( + return MCPUtils::MakeErrorJson(Result, FString::Printf( TEXT("Expression class '%s' is abstract and cannot be instantiated."), *ExpressionClassName)); } @@ -562,11 +563,11 @@ void FBlueprintMCPServer::HandleAddMaterialExpression(const FJsonObject* Json, F { if (!MaterialName.IsEmpty()) { - return MakeErrorJson(Result, TEXT("Specify either 'material' or 'materialFunction', not both")); + return MCPUtils::MakeErrorJson(Result, TEXT("Specify either 'material' or 'materialFunction', not both")); } FString LoadError; MatFunc = LoadMaterialFunctionByName(MaterialFunctionName, LoadError); - if (!MatFunc) { MakeErrorJson(Result, LoadError); return; } + if (!MatFunc) { MCPUtils::MakeErrorJson(Result, LoadError); return; } Owner = MatFunc; AssetDisplayName = MatFunc->GetName(); } @@ -574,7 +575,7 @@ void FBlueprintMCPServer::HandleAddMaterialExpression(const FJsonObject* Json, F { FString LoadError; Material = LoadMaterialByName(MaterialName, LoadError); - if (!Material) { MakeErrorJson(Result, LoadError); return; } + if (!Material) { MCPUtils::MakeErrorJson(Result, LoadError); return; } Owner = Material; AssetDisplayName = Material->GetName(); } @@ -594,7 +595,7 @@ void FBlueprintMCPServer::HandleAddMaterialExpression(const FJsonObject* Json, F } // Ensure the MaterialGraph exists (commandlet mode doesn't auto-create it) - if (Material) EnsureMaterialGraph(Material); + if (Material) MCPUtils::EnsureMaterialGraph(Material); // Create, register, and PostEditChange the expression — all inside an SEH wrapper because // some classes (e.g. UMaterialExpressionParameter) lack CLASS_Abstract but crash during @@ -604,7 +605,7 @@ void FBlueprintMCPServer::HandleAddMaterialExpression(const FJsonObject* Json, F int32 CreateResult = TryAddMaterialExpressionSEH(Owner, ExprClass, Material, MatFunc, PosX, PosY, &NewExpr); if (CreateResult != 0 || !NewExpr) { - return MakeErrorJson(Result, FString::Printf( + return MCPUtils::MakeErrorJson(Result, FString::Printf( TEXT("Expression class '%s' cannot be instantiated (may be abstract or have internal errors)."), *ExpressionClassName)); } @@ -612,7 +613,7 @@ void FBlueprintMCPServer::HandleAddMaterialExpression(const FJsonObject* Json, F NewExpr = NewObject(Owner, ExprClass); if (!NewExpr) { - return MakeErrorJson(Result, TEXT("Failed to create material expression object")); + return MCPUtils::MakeErrorJson(Result, TEXT("Failed to create material expression object")); } NewExpr->MaterialExpressionEditorX = PosX; NewExpr->MaterialExpressionEditorY = PosY; @@ -637,7 +638,7 @@ void FBlueprintMCPServer::HandleAddMaterialExpression(const FJsonObject* Json, F #endif // Save - bool bSaved = Material ? SaveMaterialPackage(Material) : SaveGenericPackage(MatFunc); + bool bSaved = Material ? MCPUtils::SaveMaterialPackage(Material) : MCPUtils::SaveGenericPackage(MatFunc); // Find the node GUID from the material graph (only for materials) FString NodeGuid; @@ -658,7 +659,7 @@ void FBlueprintMCPServer::HandleAddMaterialExpression(const FJsonObject* Json, F *ExpressionClassName, *AssetDisplayName, *NodeGuid, bSaved ? TEXT("true") : TEXT("false")); // Serialize the expression details - TSharedPtr ExprDetails = SerializeMaterialExpression(NewExpr); + TSharedPtr ExprDetails = MCPUtils::SerializeMaterialExpression(NewExpr); Result->SetBoolField(TEXT("success"), true); Result->SetStringField(TEXT("material"), AssetDisplayName); @@ -685,11 +686,11 @@ void FBlueprintMCPServer::HandleDeleteMaterialExpression(const FJsonObject* Json if (MaterialName.IsEmpty() && MaterialFunctionName.IsEmpty()) { - return MakeErrorJson(Result, TEXT("Missing required field: 'material' or 'materialFunction'")); + return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: 'material' or 'materialFunction'")); } if (NodeId.IsEmpty()) { - return MakeErrorJson(Result, TEXT("Missing required field: nodeId")); + return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: nodeId")); } bool bDryRun = false; @@ -704,23 +705,23 @@ void FBlueprintMCPServer::HandleDeleteMaterialExpression(const FJsonObject* Json { FString LoadError; MatFunc = LoadMaterialFunctionByName(MaterialFunctionName, LoadError); - if (!MatFunc) { MakeErrorJson(Result, LoadError); return; } + if (!MatFunc) { MCPUtils::MakeErrorJson(Result, LoadError); return; } AssetDisplayName = MatFunc->GetName(); } else { FString LoadError; Material = LoadMaterialByName(MaterialName, LoadError); - if (!Material) { MakeErrorJson(Result, LoadError); return; } + if (!Material) { MCPUtils::MakeErrorJson(Result, LoadError); return; } AssetDisplayName = Material->GetName(); } // For materials, we need the graph to find nodes by GUID - if (Material) EnsureMaterialGraph(Material); + if (Material) MCPUtils::EnsureMaterialGraph(Material); UEdGraph* Graph = Material ? (UEdGraph*)Material->MaterialGraph : (MatFunc ? MatFunc->MaterialGraph : nullptr); if (!Graph) { - return MakeErrorJson(Result, FString::Printf(TEXT("'%s' has no material graph"), *AssetDisplayName)); + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("'%s' has no material graph"), *AssetDisplayName)); } // Find the node by GUID @@ -737,12 +738,12 @@ void FBlueprintMCPServer::HandleDeleteMaterialExpression(const FJsonObject* Json if (!TargetMatNode) { - return MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found in material graph"), *NodeId)); + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found in material graph"), *NodeId)); } if (!TargetMatNode->MaterialExpression) { - return MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' has no associated material expression"), *NodeId)); + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' has no associated material expression"), *NodeId)); } // Capture info before deletion @@ -784,7 +785,7 @@ void FBlueprintMCPServer::HandleDeleteMaterialExpression(const FJsonObject* Json Asset->MarkPackageDirty(); // Save - bool bSaved = Material ? SaveMaterialPackage(Material) : SaveGenericPackage(MatFunc); + bool bSaved = Material ? MCPUtils::SaveMaterialPackage(Material) : MCPUtils::SaveGenericPackage(MatFunc); UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Deleted expression '%s' (nodeId: %s) from '%s' (saved: %s)"), *DeletedExprClass, *NodeId, *AssetDisplayName, bSaved ? TEXT("true") : TEXT("false")); @@ -812,11 +813,11 @@ void FBlueprintMCPServer::HandleConnectMaterialPins(const FJsonObject* Json, FJs if (MaterialName.IsEmpty() && MaterialFunctionName.IsEmpty()) { - return MakeErrorJson(Result, TEXT("Missing required field: 'material' or 'materialFunction'")); + return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: 'material' or 'materialFunction'")); } if (SourceNodeId.IsEmpty() || SourcePinName.IsEmpty() || TargetNodeId.IsEmpty() || TargetPinName.IsEmpty()) { - return MakeErrorJson(Result, TEXT("Missing required fields: sourceNodeId, sourcePinName, targetNodeId, targetPinName")); + return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: sourceNodeId, sourcePinName, targetNodeId, targetPinName")); } bool bDryRun = false; @@ -831,22 +832,22 @@ void FBlueprintMCPServer::HandleConnectMaterialPins(const FJsonObject* Json, FJs { FString LoadError; MatFunc = LoadMaterialFunctionByName(MaterialFunctionName, LoadError); - if (!MatFunc) { MakeErrorJson(Result, LoadError); return; } + if (!MatFunc) { MCPUtils::MakeErrorJson(Result, LoadError); return; } AssetDisplayName = MatFunc->GetName(); } else { FString LoadError; Material = LoadMaterialByName(MaterialName, LoadError); - if (!Material) { MakeErrorJson(Result, LoadError); return; } + if (!Material) { MCPUtils::MakeErrorJson(Result, LoadError); return; } AssetDisplayName = Material->GetName(); } - if (Material) EnsureMaterialGraph(Material); + if (Material) MCPUtils::EnsureMaterialGraph(Material); UEdGraph* Graph = Material ? (UEdGraph*)Material->MaterialGraph : (MatFunc ? MatFunc->MaterialGraph : nullptr); if (!Graph) { - return MakeErrorJson(Result, FString::Printf(TEXT("'%s' has no material graph"), *AssetDisplayName)); + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("'%s' has no material graph"), *AssetDisplayName)); } // Find source and target nodes by GUID @@ -866,11 +867,11 @@ void FBlueprintMCPServer::HandleConnectMaterialPins(const FJsonObject* Json, FJs if (!SourceNode) { - return MakeErrorJson(Result, FString::Printf(TEXT("Source node '%s' not found in material graph"), *SourceNodeId)); + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Source node '%s' not found in material graph"), *SourceNodeId)); } if (!TargetNode) { - return MakeErrorJson(Result, FString::Printf(TEXT("Target node '%s' not found in material graph"), *TargetNodeId)); + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Target node '%s' not found in material graph"), *TargetNodeId)); } // Find pins @@ -885,7 +886,7 @@ void FBlueprintMCPServer::HandleConnectMaterialPins(const FJsonObject* Json, FJs FString::Printf(TEXT("%s (%s)"), *P->PinName.ToString(), P->Direction == EGPD_Input ? TEXT("Input") : TEXT("Output")))); } - MakeErrorJson(Result, FString::Printf(TEXT("Source pin '%s' not found on node '%s'"), + MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Source pin '%s' not found on node '%s'"), *SourcePinName, *SourceNodeId)); Result->SetArrayField(TEXT("availablePins"), PinNames); return; @@ -901,7 +902,7 @@ void FBlueprintMCPServer::HandleConnectMaterialPins(const FJsonObject* Json, FJs FString::Printf(TEXT("%s (%s)"), *P->PinName.ToString(), P->Direction == EGPD_Input ? TEXT("Input") : TEXT("Output")))); } - MakeErrorJson(Result, FString::Printf(TEXT("Target pin '%s' not found on node '%s'"), + MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Target pin '%s' not found on node '%s'"), *TargetPinName, *TargetNodeId)); Result->SetArrayField(TEXT("availablePins"), PinNames); return; @@ -926,7 +927,7 @@ void FBlueprintMCPServer::HandleConnectMaterialPins(const FJsonObject* Json, FJs const UEdGraphSchema* Schema = Graph->GetSchema(); if (!Schema) { - return MakeErrorJson(Result, TEXT("Material graph schema not found")); + return MCPUtils::MakeErrorJson(Result, TEXT("Material graph schema not found")); } bool bConnected = Schema->TryCreateConnection(SourcePin, TargetPin); @@ -937,7 +938,7 @@ void FBlueprintMCPServer::HandleConnectMaterialPins(const FJsonObject* Json, FJs if (!bConnected) { - return MakeErrorJson(Result, FString::Printf( + return MCPUtils::MakeErrorJson(Result, FString::Printf( TEXT("Cannot connect %s.%s to %s.%s — types may be incompatible"), *SourceNodeId, *SourcePinName, *TargetNodeId, *TargetPinName)); } @@ -946,7 +947,7 @@ void FBlueprintMCPServer::HandleConnectMaterialPins(const FJsonObject* Json, FJs UObject* Asset = Material ? (UObject*)Material : (UObject*)MatFunc; Asset->PreEditChange(nullptr); Asset->PostEditChange(); - bool bSaved = Material ? SaveMaterialPackage(Material) : SaveGenericPackage(MatFunc); + bool bSaved = Material ? MCPUtils::SaveMaterialPackage(Material) : MCPUtils::SaveGenericPackage(MatFunc); Result->SetBoolField(TEXT("saved"), bSaved); } @@ -963,11 +964,11 @@ void FBlueprintMCPServer::HandleDisconnectMaterialPin(const FJsonObject* Json, F if (MaterialName.IsEmpty() && MaterialFunctionName.IsEmpty()) { - return MakeErrorJson(Result, TEXT("Missing required field: 'material' or 'materialFunction'")); + return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: 'material' or 'materialFunction'")); } if (NodeId.IsEmpty() || PinName.IsEmpty()) { - return MakeErrorJson(Result, TEXT("Missing required fields: nodeId, pinName")); + return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: nodeId, pinName")); } bool bDryRun = false; @@ -982,22 +983,22 @@ void FBlueprintMCPServer::HandleDisconnectMaterialPin(const FJsonObject* Json, F { FString LoadError; MatFunc = LoadMaterialFunctionByName(MaterialFunctionName, LoadError); - if (!MatFunc) { MakeErrorJson(Result, LoadError); return; } + if (!MatFunc) { MCPUtils::MakeErrorJson(Result, LoadError); return; } AssetDisplayName = MatFunc->GetName(); } else { FString LoadError; Material = LoadMaterialByName(MaterialName, LoadError); - if (!Material) { MakeErrorJson(Result, LoadError); return; } + if (!Material) { MCPUtils::MakeErrorJson(Result, LoadError); return; } AssetDisplayName = Material->GetName(); } - if (Material) EnsureMaterialGraph(Material); + if (Material) MCPUtils::EnsureMaterialGraph(Material); UEdGraph* Graph = Material ? (UEdGraph*)Material->MaterialGraph : (MatFunc ? MatFunc->MaterialGraph : nullptr); if (!Graph) { - return MakeErrorJson(Result, FString::Printf(TEXT("'%s' has no material graph"), *AssetDisplayName)); + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("'%s' has no material graph"), *AssetDisplayName)); } // Find node by GUID @@ -1014,7 +1015,7 @@ void FBlueprintMCPServer::HandleDisconnectMaterialPin(const FJsonObject* Json, F if (!TargetNode) { - return MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found in material graph"), *NodeId)); + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found in material graph"), *NodeId)); } // Find pin @@ -1028,7 +1029,7 @@ void FBlueprintMCPServer::HandleDisconnectMaterialPin(const FJsonObject* Json, F FString::Printf(TEXT("%s (%s)"), *P->PinName.ToString(), P->Direction == EGPD_Input ? TEXT("Input") : TEXT("Output")))); } - MakeErrorJson(Result, FString::Printf(TEXT("Pin '%s' not found on node '%s'"), + MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Pin '%s' not found on node '%s'"), *PinName, *NodeId)); Result->SetArrayField(TEXT("availablePins"), PinNames); return; @@ -1061,7 +1062,7 @@ void FBlueprintMCPServer::HandleDisconnectMaterialPin(const FJsonObject* Json, F Asset->PostEditChange(); // Save - bool bSaved = Material ? SaveMaterialPackage(Material) : SaveGenericPackage(MatFunc); + bool bSaved = Material ? MCPUtils::SaveMaterialPackage(Material) : MCPUtils::SaveGenericPackage(MatFunc); Result->SetBoolField(TEXT("success"), true); Result->SetStringField(TEXT("material"), AssetDisplayName); @@ -1083,16 +1084,16 @@ void FBlueprintMCPServer::HandleSetExpressionValue(const FJsonObject* Json, FJso if (MaterialName.IsEmpty() && MaterialFunctionName.IsEmpty()) { - return MakeErrorJson(Result, TEXT("Missing required field: 'material' or 'materialFunction'")); + return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: 'material' or 'materialFunction'")); } if (NodeId.IsEmpty()) { - return MakeErrorJson(Result, TEXT("Missing required field: nodeId")); + return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: nodeId")); } if (!Json->HasField(TEXT("value"))) { - return MakeErrorJson(Result, TEXT("Missing required field: value")); + return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: value")); } // Load material or material function @@ -1104,22 +1105,22 @@ void FBlueprintMCPServer::HandleSetExpressionValue(const FJsonObject* Json, FJso { FString LoadError; MatFunc = LoadMaterialFunctionByName(MaterialFunctionName, LoadError); - if (!MatFunc) { MakeErrorJson(Result, LoadError); return; } + if (!MatFunc) { MCPUtils::MakeErrorJson(Result, LoadError); return; } AssetDisplayName = MatFunc->GetName(); } else { FString LoadError; Material = LoadMaterialByName(MaterialName, LoadError); - if (!Material) { MakeErrorJson(Result, LoadError); return; } + if (!Material) { MCPUtils::MakeErrorJson(Result, LoadError); return; } AssetDisplayName = Material->GetName(); } - if (Material) EnsureMaterialGraph(Material); + if (Material) MCPUtils::EnsureMaterialGraph(Material); UEdGraph* Graph = Material ? (UEdGraph*)Material->MaterialGraph : (MatFunc ? MatFunc->MaterialGraph : nullptr); if (!Graph) { - return MakeErrorJson(Result, FString::Printf(TEXT("'%s' has no material graph"), *AssetDisplayName)); + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("'%s' has no material graph"), *AssetDisplayName)); } // Find the node by GUID @@ -1136,13 +1137,13 @@ void FBlueprintMCPServer::HandleSetExpressionValue(const FJsonObject* Json, FJso if (!TargetMatNode) { - return MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found in material graph"), *NodeId)); + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found in material graph"), *NodeId)); } UMaterialExpression* Expr = TargetMatNode->MaterialExpression; if (!Expr) { - return MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' has no associated material expression"), *NodeId)); + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' has no associated material expression"), *NodeId)); } FString ExprType; @@ -1175,7 +1176,7 @@ void FBlueprintMCPServer::HandleSetExpressionValue(const FJsonObject* Json, FJso else { Asset->PostEditChange(); - return MakeErrorJson(Result, TEXT("Constant3Vector requires value as object {r, g, b}")); + return MCPUtils::MakeErrorJson(Result, TEXT("Constant3Vector requires value as object {r, g, b}")); } } else if (UMaterialExpressionConstant4Vector* C4Expr = Cast(Expr)) @@ -1195,7 +1196,7 @@ void FBlueprintMCPServer::HandleSetExpressionValue(const FJsonObject* Json, FJso else { Asset->PostEditChange(); - return MakeErrorJson(Result, TEXT("Constant4Vector requires value as object {r, g, b, a}")); + return MCPUtils::MakeErrorJson(Result, TEXT("Constant4Vector requires value as object {r, g, b, a}")); } } else if (UMaterialExpressionScalarParameter* SPExpr = Cast(Expr)) @@ -1228,7 +1229,7 @@ void FBlueprintMCPServer::HandleSetExpressionValue(const FJsonObject* Json, FJso else { Asset->PostEditChange(); - return MakeErrorJson(Result, TEXT("VectorParameter requires value as object {r, g, b, a}")); + return MCPUtils::MakeErrorJson(Result, TEXT("VectorParameter requires value as object {r, g, b, a}")); } FString ParamName; @@ -1255,7 +1256,7 @@ void FBlueprintMCPServer::HandleSetExpressionValue(const FJsonObject* Json, FJso else { Asset->PostEditChange(); - return MakeErrorJson(Result, TEXT("TextureCoordinate requires value as object {coordinateIndex, uTiling, vTiling}")); + return MCPUtils::MakeErrorJson(Result, TEXT("TextureCoordinate requires value as object {coordinateIndex, uTiling, vTiling}")); } } else if (UMaterialExpressionCustom* CustomExpr = Cast(Expr)) @@ -1312,13 +1313,13 @@ void FBlueprintMCPServer::HandleSetExpressionValue(const FJsonObject* Json, FJso else { Asset->PostEditChange(); - return MakeErrorJson(Result, TEXT("ComponentMask requires value as object {r, g, b, a} (booleans)")); + return MCPUtils::MakeErrorJson(Result, TEXT("ComponentMask requires value as object {r, g, b, a} (booleans)")); } } else { Asset->PostEditChange(); - return MakeErrorJson(Result, FString::Printf( + 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"), @@ -1329,7 +1330,7 @@ void FBlueprintMCPServer::HandleSetExpressionValue(const FJsonObject* Json, FJso Asset->MarkPackageDirty(); // Save - bool bSaved = Material ? SaveMaterialPackage(Material) : SaveGenericPackage(MatFunc); + bool bSaved = Material ? MCPUtils::SaveMaterialPackage(Material) : MCPUtils::SaveGenericPackage(MatFunc); UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Set expression value on node '%s' (%s) in '%s': %s"), *NodeId, *ExprType, *AssetDisplayName, *NewValueStr); @@ -1354,16 +1355,16 @@ void FBlueprintMCPServer::HandleMoveMaterialExpression(const FJsonObject* Json, if (MaterialName.IsEmpty() && MaterialFunctionName.IsEmpty()) { - return MakeErrorJson(Result, TEXT("Missing required field: 'material' or 'materialFunction'")); + return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: 'material' or 'materialFunction'")); } if (NodeId.IsEmpty()) { - return MakeErrorJson(Result, TEXT("Missing required field: nodeId")); + return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: nodeId")); } if (!Json->HasField(TEXT("posX")) || !Json->HasField(TEXT("posY"))) { - return MakeErrorJson(Result, TEXT("Missing required fields: posX, posY")); + return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: posX, posY")); } int32 PosX = (int32)Json->GetNumberField(TEXT("posX")); @@ -1381,22 +1382,22 @@ void FBlueprintMCPServer::HandleMoveMaterialExpression(const FJsonObject* Json, { FString LoadError; MatFunc = LoadMaterialFunctionByName(MaterialFunctionName, LoadError); - if (!MatFunc) { MakeErrorJson(Result, LoadError); return; } + if (!MatFunc) { MCPUtils::MakeErrorJson(Result, LoadError); return; } AssetDisplayName = MatFunc->GetName(); } else { FString LoadError; Material = LoadMaterialByName(MaterialName, LoadError); - if (!Material) { MakeErrorJson(Result, LoadError); return; } + if (!Material) { MCPUtils::MakeErrorJson(Result, LoadError); return; } AssetDisplayName = Material->GetName(); } - if (Material) EnsureMaterialGraph(Material); + if (Material) MCPUtils::EnsureMaterialGraph(Material); UEdGraph* Graph = Material ? (UEdGraph*)Material->MaterialGraph : (MatFunc ? MatFunc->MaterialGraph : nullptr); if (!Graph) { - return MakeErrorJson(Result, FString::Printf(TEXT("'%s' has no material graph"), *AssetDisplayName)); + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("'%s' has no material graph"), *AssetDisplayName)); } // Find node by GUID @@ -1413,7 +1414,7 @@ void FBlueprintMCPServer::HandleMoveMaterialExpression(const FJsonObject* Json, if (!TargetMatNode) { - return MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found in material graph"), *NodeId)); + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found in material graph"), *NodeId)); } if (bDryRun) @@ -1446,7 +1447,7 @@ void FBlueprintMCPServer::HandleMoveMaterialExpression(const FJsonObject* Json, Asset->PostEditChange(); // Save - bool bSaved = Material ? SaveMaterialPackage(Material) : SaveGenericPackage(MatFunc); + bool bSaved = Material ? MCPUtils::SaveMaterialPackage(Material) : MCPUtils::SaveGenericPackage(MatFunc); UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Moved node '%s' to (%d, %d) in '%s' (saved: %s)"), *NodeId, PosX, PosY, *AssetDisplayName, bSaved ? TEXT("true") : TEXT("false")); @@ -1474,19 +1475,19 @@ void FBlueprintMCPServer::HandleCreateMaterialFunction(const FJsonObject* Json, if (Name.IsEmpty() || PackagePath.IsEmpty()) { - return MakeErrorJson(Result, TEXT("Missing required fields: name, packagePath")); + return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: name, packagePath")); } if (!PackagePath.StartsWith(TEXT("/Game"))) { - return MakeErrorJson(Result, TEXT("packagePath must start with '/Game'")); + return MCPUtils::MakeErrorJson(Result, TEXT("packagePath must start with '/Game'")); } // Check if asset already exists FString FullAssetPath = PackagePath / Name; if (FindMaterialFunctionAsset(Name) || FindMaterialFunctionAsset(FullAssetPath)) { - return MakeErrorJson(Result, FString::Printf( + return MCPUtils::MakeErrorJson(Result, FString::Printf( TEXT("Material Function '%s' already exists. Use a different name or delete the existing asset first."), *Name)); } @@ -1503,13 +1504,13 @@ void FBlueprintMCPServer::HandleCreateMaterialFunction(const FJsonObject* Json, if (!NewAsset) { - return MakeErrorJson(Result, FString::Printf(TEXT("Failed to create Material Function '%s' in '%s'"), *Name, *PackagePath)); + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Failed to create Material Function '%s' in '%s'"), *Name, *PackagePath)); } UMaterialFunction* MF = Cast(NewAsset); if (!MF) { - return MakeErrorJson(Result, TEXT("Created asset is not a UMaterialFunction")); + return MCPUtils::MakeErrorJson(Result, TEXT("Created asset is not a UMaterialFunction")); } // Set optional description @@ -1519,7 +1520,7 @@ void FBlueprintMCPServer::HandleCreateMaterialFunction(const FJsonObject* Json, } // Save - bool bSaved = SaveGenericPackage(MF); + bool bSaved = MCPUtils::SaveGenericPackage(MF); // Refresh asset cache FAssetRegistryModule& ARM = FModuleManager::LoadModuleChecked("AssetRegistry"); @@ -1552,7 +1553,7 @@ void FBlueprintMCPServer::HandleSnapshotMaterialGraph(const FJsonObject* Json, F FString MaterialName = Json->GetStringField(TEXT("material")); if (MaterialName.IsEmpty()) { - return MakeErrorJson(Result, TEXT("Missing required field: material")); + return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: material")); } // Load material @@ -1560,13 +1561,13 @@ void FBlueprintMCPServer::HandleSnapshotMaterialGraph(const FJsonObject* Json, F UMaterial* Material = LoadMaterialByName(MaterialName, LoadError); if (!Material) { - return MakeErrorJson(Result, LoadError); + return MCPUtils::MakeErrorJson(Result, LoadError); } - EnsureMaterialGraph(Material); + MCPUtils::EnsureMaterialGraph(Material); if (!Material->MaterialGraph) { - return MakeErrorJson(Result, FString::Printf(TEXT("Material '%s' has no material graph"), *MaterialName)); + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Material '%s' has no material graph"), *MaterialName)); } UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Creating snapshot for material '%s'"), *MaterialName); @@ -1639,7 +1640,7 @@ void FBlueprintMCPServer::HandleDiffMaterialGraph(const FJsonObject* Json, FJson if (MaterialName.IsEmpty() || SnapshotId.IsEmpty()) { - return MakeErrorJson(Result, TEXT("Missing required fields: material, snapshotId")); + return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: material, snapshotId")); } // Load snapshot from material snapshots (memory or disk) @@ -1649,7 +1650,7 @@ void FBlueprintMCPServer::HandleDiffMaterialGraph(const FJsonObject* Json, FJson { if (!LoadSnapshotFromDisk(SnapshotId, LoadedSnapshot)) { - return MakeErrorJson(Result, FString::Printf(TEXT("Snapshot '%s' not found in memory or on disk"), *SnapshotId)); + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Snapshot '%s' not found in memory or on disk"), *SnapshotId)); } SnapshotPtr = &LoadedSnapshot; } @@ -1659,13 +1660,13 @@ void FBlueprintMCPServer::HandleDiffMaterialGraph(const FJsonObject* Json, FJson UMaterial* Material = LoadMaterialByName(MaterialName, LoadError); if (!Material) { - return MakeErrorJson(Result, LoadError); + return MCPUtils::MakeErrorJson(Result, LoadError); } - EnsureMaterialGraph(Material); + MCPUtils::EnsureMaterialGraph(Material); if (!Material->MaterialGraph) { - return MakeErrorJson(Result, FString::Printf(TEXT("Material '%s' has no material graph"), *MaterialName)); + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Material '%s' has no material graph"), *MaterialName)); } UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Diffing material '%s' against snapshot '%s'"), *MaterialName, *SnapshotId); @@ -1686,7 +1687,7 @@ void FBlueprintMCPServer::HandleDiffMaterialGraph(const FJsonObject* Json, FJson const FGraphSnapshotData* SnapDataPtr = SnapshotPtr->Graphs.Find(TEXT("MaterialGraph")); if (!SnapDataPtr) { - return MakeErrorJson(Result, TEXT("Snapshot does not contain a MaterialGraph")); + return MCPUtils::MakeErrorJson(Result, TEXT("Snapshot does not contain a MaterialGraph")); } const FGraphSnapshotData& SnapData = *SnapDataPtr; @@ -1802,7 +1803,7 @@ void FBlueprintMCPServer::HandleRestoreMaterialGraph(const FJsonObject* Json, FJ if (MaterialName.IsEmpty() || SnapshotId.IsEmpty()) { - return MakeErrorJson(Result, TEXT("Missing required fields: material, snapshotId")); + return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: material, snapshotId")); } bool bDryRun = false; @@ -1815,7 +1816,7 @@ void FBlueprintMCPServer::HandleRestoreMaterialGraph(const FJsonObject* Json, FJ { if (!LoadSnapshotFromDisk(SnapshotId, LoadedSnapshot)) { - return MakeErrorJson(Result, FString::Printf(TEXT("Snapshot '%s' not found in memory or on disk"), *SnapshotId)); + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Snapshot '%s' not found in memory or on disk"), *SnapshotId)); } SnapshotPtr = &LoadedSnapshot; } @@ -1825,13 +1826,13 @@ void FBlueprintMCPServer::HandleRestoreMaterialGraph(const FJsonObject* Json, FJ UMaterial* Material = LoadMaterialByName(MaterialName, LoadError); if (!Material) { - return MakeErrorJson(Result, LoadError); + return MCPUtils::MakeErrorJson(Result, LoadError); } - EnsureMaterialGraph(Material); + MCPUtils::EnsureMaterialGraph(Material); if (!Material->MaterialGraph) { - return MakeErrorJson(Result, FString::Printf(TEXT("Material '%s' has no material graph"), *MaterialName)); + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Material '%s' has no material graph"), *MaterialName)); } UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Restoring material connections from snapshot '%s' for material '%s' (dryRun=%s)"), @@ -1865,7 +1866,7 @@ void FBlueprintMCPServer::HandleRestoreMaterialGraph(const FJsonObject* Json, FJ const FGraphSnapshotData* SnapDataPtr = SnapshotPtr->Graphs.Find(TEXT("MaterialGraph")); if (!SnapDataPtr) { - return MakeErrorJson(Result, TEXT("Snapshot does not contain a MaterialGraph")); + return MCPUtils::MakeErrorJson(Result, TEXT("Snapshot does not contain a MaterialGraph")); } int32 Reconnected = 0; @@ -1971,7 +1972,7 @@ void FBlueprintMCPServer::HandleRestoreMaterialGraph(const FJsonObject* Json, FJ { Material->PreEditChange(nullptr); Material->PostEditChange(); - bSaved = SaveMaterialPackage(Material); + bSaved = MCPUtils::SaveMaterialPackage(Material); } UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Material restore complete — %d reconnected, %d failed, saved=%s"), diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_MaterialRead.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_MaterialRead.cpp index 928ea8fd..ed450576 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_MaterialRead.cpp +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_MaterialRead.cpp @@ -1,4 +1,5 @@ #include "BlueprintMCPServer.h" +#include "MCPUtils.h" #include "Materials/Material.h" #include "Materials/MaterialInstanceConstant.h" #include "Materials/MaterialFunction.h" @@ -108,10 +109,10 @@ void FBlueprintMCPServer::HandleGetMaterial(const FJsonObject* Json, FJsonObject FString Name = Json->GetStringField(TEXT("name")); if (Name.IsEmpty()) { - return MakeErrorJson(Result, TEXT("Missing 'name' parameter")); + return MCPUtils::MakeErrorJson(Result, TEXT("Missing 'name' parameter")); } - FString DecodedName = UrlDecode(Name); + FString DecodedName = MCPUtils::UrlDecode(Name); // Try loading as UMaterial first FString LoadError; @@ -342,7 +343,7 @@ void FBlueprintMCPServer::HandleGetMaterial(const FJsonObject* Json, FJsonObject return; } - MakeErrorJson(Result, FString::Printf(TEXT("Material or MaterialInstance '%s' not found. Use list_materials to see available assets."), *DecodedName)); + MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Material or MaterialInstance '%s' not found. Use list_materials to see available assets."), *DecodedName)); } // ============================================================ @@ -354,16 +355,16 @@ void FBlueprintMCPServer::HandleGetMaterialGraph(const FJsonObject* Json, FJsonO FString Name = Json->GetStringField(TEXT("name")); if (Name.IsEmpty()) { - return MakeErrorJson(Result, TEXT("Missing 'name' parameter")); + return MCPUtils::MakeErrorJson(Result, TEXT("Missing 'name' parameter")); } - FString DecodedName = UrlDecode(Name); + FString DecodedName = MCPUtils::UrlDecode(Name); FString LoadError; UMaterial* Material = LoadMaterialByName(DecodedName, LoadError); if (!Material) { - return MakeErrorJson(Result, LoadError); + return MCPUtils::MakeErrorJson(Result, LoadError); } UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: GetMaterialGraph — material '%s'"), *Material->GetName()); @@ -381,16 +382,16 @@ void FBlueprintMCPServer::HandleGetMaterialGraph(const FJsonObject* Json, FJsonO if (!Material->MaterialGraph) { - return MakeErrorJson(Result, TEXT("Could not build MaterialGraph for this material")); + return MCPUtils::MakeErrorJson(Result, TEXT("Could not build MaterialGraph for this material")); } - TSharedPtr GraphJson = SerializeGraph(Material->MaterialGraph); + TSharedPtr GraphJson = MCPUtils::SerializeGraph(Material->MaterialGraph); if (!GraphJson.IsValid()) { - return MakeErrorJson(Result, TEXT("Failed to serialize material graph")); + return MCPUtils::MakeErrorJson(Result, TEXT("Failed to serialize material graph")); } - CopyJsonFields(GraphJson.Get(), Result); + MCPUtils::CopyJsonFields(GraphJson.Get(), Result); // Add material name context Result->SetStringField(TEXT("material"), Material->GetName()); @@ -406,14 +407,14 @@ void FBlueprintMCPServer::HandleDescribeMaterial(const FJsonObject* Json, FJsonO FString MaterialName = Json->GetStringField(TEXT("material")); if (MaterialName.IsEmpty()) { - return MakeErrorJson(Result, TEXT("Missing required field: material")); + return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: material")); } FString LoadError; UMaterial* Material = LoadMaterialByName(MaterialName, LoadError); if (!Material) { - return MakeErrorJson(Result, LoadError); + return MCPUtils::MakeErrorJson(Result, LoadError); } UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: DescribeMaterial — '%s'"), *Material->GetName()); @@ -429,7 +430,7 @@ void FBlueprintMCPServer::HandleDescribeMaterial(const FJsonObject* Json, FJsonO if (!Material->MaterialGraph) { - return MakeErrorJson(Result, TEXT("Could not build MaterialGraph for this material")); + return MCPUtils::MakeErrorJson(Result, TEXT("Could not build MaterialGraph for this material")); } // Recursive helper: trace backwards from a pin and build a description string @@ -549,7 +550,7 @@ void FBlueprintMCPServer::HandleDescribeMaterial(const FJsonObject* Json, FJsonO if (!RootNode) { - return MakeErrorJson(Result, TEXT("Could not find root node in material graph")); + return MCPUtils::MakeErrorJson(Result, TEXT("Could not find root node in material graph")); } for (UEdGraphPin* Pin : RootNode->Pins) @@ -609,10 +610,10 @@ void FBlueprintMCPServer::HandleSearchMaterials(const FJsonObject* Json, FJsonOb FString Query = Json->GetStringField(TEXT("query")); if (Query.IsEmpty()) { - return MakeErrorJson(Result, TEXT("Missing 'query' parameter")); + return MCPUtils::MakeErrorJson(Result, TEXT("Missing 'query' parameter")); } - FString DecodedQuery = UrlDecode(Query); + FString DecodedQuery = MCPUtils::UrlDecode(Query); int32 MaxResults = 50; if (Json->HasField(TEXT("maxResults"))) @@ -699,7 +700,7 @@ void FBlueprintMCPServer::HandleFindMaterialReferences(const FJsonObject* Json, FString MaterialName = Json->GetStringField(TEXT("material")); if (MaterialName.IsEmpty()) { - return MakeErrorJson(Result, TEXT("Missing required field: material")); + return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: material")); } // Try to find the material's package path @@ -721,7 +722,7 @@ void FBlueprintMCPServer::HandleFindMaterialReferences(const FJsonObject* Json, if (PackagePath.IsEmpty()) { - return MakeErrorJson(Result, FString::Printf(TEXT("Material '%s' not found. Use list_materials to see available assets."), *MaterialName)); + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Material '%s' not found. Use list_materials to see available assets."), *MaterialName)); } UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: FindMaterialReferences — '%s' (package: %s)"), *MaterialName, *PackagePath); @@ -791,16 +792,16 @@ void FBlueprintMCPServer::HandleGetMaterialFunction(const FJsonObject* Json, FJs FString Name = Json->GetStringField(TEXT("name")); if (Name.IsEmpty()) { - return MakeErrorJson(Result, TEXT("Missing 'name' parameter")); + return MCPUtils::MakeErrorJson(Result, TEXT("Missing 'name' parameter")); } - FString DecodedName = UrlDecode(Name); + FString DecodedName = MCPUtils::UrlDecode(Name); FString LoadError; UMaterialFunction* MF = LoadMaterialFunctionByName(DecodedName, LoadError); if (!MF) { - return MakeErrorJson(Result, LoadError); + return MCPUtils::MakeErrorJson(Result, LoadError); } UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: GetMaterialFunction — '%s'"), *MF->GetName()); @@ -843,7 +844,7 @@ void FBlueprintMCPServer::HandleGetMaterialFunction(const FJsonObject* Json, FJs } // Serialize every expression - TSharedPtr ExprJson = SerializeMaterialExpression(Expr); + TSharedPtr ExprJson = MCPUtils::SerializeMaterialExpression(Expr); if (ExprJson.IsValid()) { ExpressionList.Add(MakeShared(ExprJson.ToSharedRef())); @@ -859,7 +860,7 @@ void FBlueprintMCPServer::HandleGetMaterialFunction(const FJsonObject* Json, FJs UEdGraph* FuncGraph = MF->MaterialGraph; if (FuncGraph) { - TSharedPtr GraphJson = SerializeGraph(FuncGraph); + TSharedPtr GraphJson = MCPUtils::SerializeGraph(FuncGraph); if (GraphJson.IsValid()) { Result->SetObjectField(TEXT("graph"), GraphJson); @@ -876,7 +877,7 @@ void FBlueprintMCPServer::HandleValidateMaterial(const FJsonObject* Json, FJsonO FString MaterialName = Json->GetStringField(TEXT("material")); if (MaterialName.IsEmpty()) { - return MakeErrorJson(Result, TEXT("Missing required field: material")); + return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: material")); } // Load material @@ -884,7 +885,7 @@ void FBlueprintMCPServer::HandleValidateMaterial(const FJsonObject* Json, FJsonO UMaterial* Material = LoadMaterialByName(MaterialName, LoadError); if (!Material) { - return MakeErrorJson(Result, LoadError); + return MCPUtils::MakeErrorJson(Result, LoadError); } UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Validating material '%s'"), *Material->GetName()); diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_Mutation.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_Mutation.cpp index c931e64e..bcc146fe 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_Mutation.cpp +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_Mutation.cpp @@ -1,5 +1,6 @@ #include "BlueprintMCPHandlers_Mutation.h" #include "BlueprintMCPServer.h" +#include "MCPUtils.h" #include "Engine/Blueprint.h" #include "Materials/Material.h" #include "Materials/MaterialInstanceConstant.h" @@ -42,7 +43,6 @@ #include "AssetRegistry/IAssetRegistry.h" #include "AssetToolsModule.h" #include "IAssetTools.h" -#include "BlueprintActionDatabase.h" #include "BlueprintNodeSpawner.h" @@ -59,7 +59,7 @@ void UMCPHandler_ReplaceFunctionCalls::Handle(const FJsonObject* Json, FJsonObje UBlueprint* BP = Helper->LoadBlueprintByName(Blueprint, LoadError); if (!BP) { - return Helper->MakeErrorJson(Result, LoadError); + return MCPUtils::MakeErrorJson(Result, LoadError); } // Find the new class — try several search strategies @@ -93,7 +93,7 @@ void UMCPHandler_ReplaceFunctionCalls::Handle(const FJsonObject* Json, FJsonObje if (!NewClassPtr) { - return Helper->MakeErrorJson(Result, FString::Printf(TEXT("Could not find class '%s'"), *NewClass)); + 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)"), @@ -298,7 +298,7 @@ void UMCPHandler_DeleteAsset::Handle(const FJsonObject* Json, FJsonObject* Resul if (!IFileManager::Get().FileExists(*PackageFilename)) { - return Helper->MakeErrorJson(Result, FString::Printf(TEXT("Asset file not found on disk: %s"), *PackageFilename)); + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Asset file not found on disk: %s"), *PackageFilename)); } // Check references @@ -330,7 +330,7 @@ void UMCPHandler_DeleteAsset::Handle(const FJsonObject* Json, FJsonObject* Resul } } - Helper->MakeErrorJson(Result, TEXT("Asset is still referenced. Remove all references first.")); + 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()); @@ -415,7 +415,7 @@ void UMCPHandler_DeleteAsset::Handle(const FJsonObject* Json, FJsonObject* Resul Result->SetBoolField(TEXT("forced"), Force); if (!bDeleted) { - Helper->MakeErrorJson(Result, TEXT("Failed to delete file from disk")); + MCPUtils::MakeErrorJson(Result, TEXT("Failed to delete file from disk")); } if (RefWarnings.Num() > 0) { @@ -437,7 +437,7 @@ void UMCPHandler_ConnectPins::Handle(const FJsonObject* Json, FJsonObject* Resul UBlueprint* BP = Helper->LoadBlueprintByName(Blueprint, LoadError); if (!BP) { - return Helper->MakeErrorJson(Result, LoadError); + return MCPUtils::MakeErrorJson(Result, LoadError); } TArray> Results; @@ -449,7 +449,7 @@ void UMCPHandler_ConnectPins::Handle(const FJsonObject* Json, FJsonObject* Resul Results.Add(MakeShared(EntryResult)); FConnectPinsEntry Entry; - FString PopulateError = Helper->PopulateFromJson(FConnectPinsEntry::StaticStruct(), &Entry, ConnVal); + FString PopulateError = MCPUtils::PopulateFromJson(FConnectPinsEntry::StaticStruct(), &Entry, ConnVal); if (!PopulateError.IsEmpty()) { EntryResult->SetStringField(TEXT("error"), PopulateError); @@ -462,14 +462,14 @@ void UMCPHandler_ConnectPins::Handle(const FJsonObject* Json, FJsonObject* Resul EntryResult->SetStringField(TEXT("targetPinName"), Entry.TargetPinName); UEdGraph* SourceGraph = nullptr; - UEdGraphNode* SourceNode = Helper->FindNodeByGuid(BP, Entry.SourceNodeId, &SourceGraph); + UEdGraphNode* SourceNode = MCPUtils::FindNodeByGuid(BP, Entry.SourceNodeId, &SourceGraph); if (!SourceNode) { EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Source node '%s' not found"), *Entry.SourceNodeId)); continue; } - UEdGraphNode* TargetNode = Helper->FindNodeByGuid(BP, Entry.TargetNodeId); + UEdGraphNode* TargetNode = MCPUtils::FindNodeByGuid(BP, Entry.TargetNodeId); if (!TargetNode) { EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Target node '%s' not found"), *Entry.TargetNodeId)); @@ -540,7 +540,7 @@ void UMCPHandler_DisconnectPin::Handle(const FJsonObject* Json, FJsonObject* Res UBlueprint* BP = Helper->LoadBlueprintByName(Blueprint, LoadError); if (!BP) { - return Helper->MakeErrorJson(Result, LoadError); + return MCPUtils::MakeErrorJson(Result, LoadError); } TArray> Results; @@ -553,7 +553,7 @@ void UMCPHandler_DisconnectPin::Handle(const FJsonObject* Json, FJsonObject* Res Results.Add(MakeShared(EntryResult)); FDisconnectPinEntry Entry; - FString PopulateError = Helper->PopulateFromJson(FDisconnectPinEntry::StaticStruct(), &Entry, DiscVal); + FString PopulateError = MCPUtils::PopulateFromJson(FDisconnectPinEntry::StaticStruct(), &Entry, DiscVal); if (!PopulateError.IsEmpty()) { EntryResult->SetStringField(TEXT("error"), PopulateError); @@ -563,7 +563,7 @@ void UMCPHandler_DisconnectPin::Handle(const FJsonObject* Json, FJsonObject* Res EntryResult->SetStringField(TEXT("nodeId"), Entry.NodeId); EntryResult->SetStringField(TEXT("pinName"), Entry.PinName); - UEdGraphNode* Node = Helper->FindNodeByGuid(BP, Entry.NodeId); + UEdGraphNode* Node = MCPUtils::FindNodeByGuid(BP, Entry.NodeId); if (!Node) { EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Node '%s' not found"), *Entry.NodeId)); @@ -581,7 +581,7 @@ void UMCPHandler_DisconnectPin::Handle(const FJsonObject* Json, FJsonObject* Res if (!Entry.TargetNodeId.IsEmpty() && !Entry.TargetPinName.IsEmpty()) { - UEdGraphNode* TargetNode = Helper->FindNodeByGuid(BP, Entry.TargetNodeId); + UEdGraphNode* TargetNode = MCPUtils::FindNodeByGuid(BP, Entry.TargetNodeId); if (!TargetNode) { EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Target node '%s' not found"), *Entry.TargetNodeId)); @@ -648,7 +648,7 @@ void UMCPHandler_RefreshAllNodes::Handle(const FJsonObject* Json, FJsonObject* R UBlueprint* BP = Helper->LoadBlueprintByName(Blueprint, LoadError); if (!BP) { - return Helper->MakeErrorJson(Result, LoadError); + return MCPUtils::MakeErrorJson(Result, LoadError); } // Count graphs and nodes before refresh @@ -761,7 +761,7 @@ void UMCPHandler_SetPinDefault::Handle(const FJsonObject* Json, FJsonObject* Res Results.Add(MakeShared(EntryResult)); FSetPinDefaultEntry Entry; - FString PopulateError = Helper->PopulateFromJson(FSetPinDefaultEntry::StaticStruct(), &Entry, PinVal); + FString PopulateError = MCPUtils::PopulateFromJson(FSetPinDefaultEntry::StaticStruct(), &Entry, PinVal); if (!PopulateError.IsEmpty()) { EntryResult->SetStringField(TEXT("error"), PopulateError); @@ -781,7 +781,7 @@ void UMCPHandler_SetPinDefault::Handle(const FJsonObject* Json, FJsonObject* Res } UEdGraph* Graph = nullptr; - UEdGraphNode* Node = Helper->FindNodeByGuid(BP, Entry.NodeId, &Graph); + UEdGraphNode* Node = MCPUtils::FindNodeByGuid(BP, Entry.NodeId, &Graph); if (!Node) { EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Node '%s' not found"), *Entry.NodeId)); @@ -856,15 +856,15 @@ void UMCPHandler_ChangeStructNodeType::Handle(const FJsonObject* Json, FJsonObje UBlueprint* BP = Helper->LoadBlueprintByName(Blueprint, LoadError); if (!BP) { - return Helper->MakeErrorJson(Result, LoadError); + return MCPUtils::MakeErrorJson(Result, LoadError); } // Find node UEdGraph* Graph = nullptr; - UEdGraphNode* Node = Helper->FindNodeByGuid(BP, NodeId, &Graph); + UEdGraphNode* Node = MCPUtils::FindNodeByGuid(BP, NodeId, &Graph); if (!Node) { - return Helper->MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found"), *NodeId)); + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found"), *NodeId)); } // Determine what kind of struct node this is @@ -873,7 +873,7 @@ void UMCPHandler_ChangeStructNodeType::Handle(const FJsonObject* Json, FJsonObje if (!BreakNode && !MakeNode) { - return Helper->MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' is not a BreakStruct or MakeStruct node (class: %s)"), + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' is not a BreakStruct or MakeStruct node (class: %s)"), *NodeId, *Node->GetClass()->GetName())); } @@ -892,7 +892,7 @@ void UMCPHandler_ChangeStructNodeType::Handle(const FJsonObject* Json, FJsonObje } if (!NewStruct) { - return Helper->MakeErrorJson(Result, FString::Printf(TEXT("Struct type '%s' not found"), *NewType)); + 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'"), @@ -962,7 +962,7 @@ void UMCPHandler_ChangeStructNodeType::Handle(const FJsonObject* Json, FJsonObje const UEdGraphSchema* Schema = Graph->GetSchema(); if (!Schema) { - return Helper->MakeErrorJson(Result, TEXT("Graph schema not found")); + return MCPUtils::MakeErrorJson(Result, TEXT("Graph schema not found")); } // Reconstruct to rebuild pins for the new struct type (use schema version for MinimalAPI compat) @@ -1039,7 +1039,7 @@ void UMCPHandler_ChangeStructNodeType::Handle(const FJsonObject* Json, FJsonObje FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP); // Return updated node state - TSharedPtr UpdatedNodeState = Helper->SerializeNode(Node); + TSharedPtr UpdatedNodeState = MCPUtils::SerializeNode(Node); Result->SetBoolField(TEXT("success"), true); Result->SetStringField(TEXT("blueprint"), Blueprint); @@ -1069,18 +1069,18 @@ void UMCPHandler_DeleteNode::Handle(const FJsonObject* Json, FJsonObject* Result UBlueprint* BP = Helper->LoadBlueprintByName(Blueprint, LoadError); if (!BP) { - return Helper->MakeErrorJson(Result, LoadError); + return MCPUtils::MakeErrorJson(Result, LoadError); } UEdGraph* Graph = nullptr; - UEdGraphNode* Node = Helper->FindNodeByGuid(BP, NodeId, &Graph); + UEdGraphNode* Node = MCPUtils::FindNodeByGuid(BP, NodeId, &Graph); if (!Node) { - return Helper->MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found"), *NodeId)); + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found"), *NodeId)); } if (!Graph) { - return Helper->MakeErrorJson(Result, FString::Printf(TEXT("Graph not found for node '%s'"), *NodeId)); + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Graph not found for node '%s'"), *NodeId)); } FString NodeClass = Node->GetClass()->GetName(); @@ -1092,7 +1092,7 @@ void UMCPHandler_DeleteNode::Handle(const FJsonObject* Json, FJsonObject* Result // without recreating the entire function/event. if (Cast(Node)) { - return Helper->MakeErrorJson(Result, FString::Printf( + 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."), @@ -1100,14 +1100,14 @@ void UMCPHandler_DeleteNode::Handle(const FJsonObject* Json, FJsonObject* Result } if (Cast(Node)) { - return Helper->MakeErrorJson(Result, FString::Printf( + 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(Node)) { - return Helper->MakeErrorJson(Result, FString::Printf( + 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)); @@ -1154,13 +1154,13 @@ void UMCPHandler_RenameAsset::Handle(const FJsonObject* Json, FJsonObject* Resul FAssetData* FoundAsset = Helper->FindAnyAsset(AssetPath); if (!FoundAsset) { - return Helper->MakeErrorJson(Result, FString::Printf(TEXT("Asset '%s' not found. Checked Blueprints, Materials, Material Instances, and Material Functions."), *AssetPath)); + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Asset '%s' not found. Checked Blueprints, Materials, Material Instances, and Material Functions."), *AssetPath)); } UObject* AssetObj = FoundAsset->GetAsset(); if (!AssetObj) { - return Helper->MakeErrorJson(Result, FString::Printf(TEXT("Failed to load asset '%s'"), *AssetPath)); + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Failed to load asset '%s'"), *AssetPath)); } // Parse new path into package path and asset name @@ -1211,7 +1211,7 @@ void UMCPHandler_RenameAsset::Handle(const FJsonObject* Json, FJsonObject* Resul Result->SetStringField(TEXT("newAssetName"), NewAssetName); if (!bSuccess) { - Helper->MakeErrorJson(Result, TEXT("Asset rename failed. The target path may be invalid or a conflicting asset may exist.")); + MCPUtils::MakeErrorJson(Result, TEXT("Asset rename failed. The target path may be invalid or a conflicting asset may exist.")); } } @@ -1228,24 +1228,24 @@ void UMCPHandler_SetBlueprintDefault::Handle(const FJsonObject* Json, FJsonObjec UBlueprint* BP = Helper->LoadBlueprintByName(Blueprint, LoadError); if (!BP) { - return Helper->MakeErrorJson(Result, LoadError); + return MCPUtils::MakeErrorJson(Result, LoadError); } if (!BP->GeneratedClass) { - return Helper->MakeErrorJson(Result, TEXT("Blueprint has no GeneratedClass")); + return MCPUtils::MakeErrorJson(Result, TEXT("Blueprint has no GeneratedClass")); } UObject* CDO = BP->GeneratedClass->GetDefaultObject(); if (!CDO) { - return Helper->MakeErrorJson(Result, TEXT("Could not get Class Default Object")); + return MCPUtils::MakeErrorJson(Result, TEXT("Could not get Class Default Object")); } FProperty* Prop = BP->GeneratedClass->FindPropertyByName(*Property); if (!Prop) { - return Helper->MakeErrorJson(Result, FString::Printf(TEXT("Property '%s' not found on '%s'"), *Property, *Blueprint)); + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Property '%s' not found on '%s'"), *Property, *Blueprint)); } FString OldValue; @@ -1286,7 +1286,7 @@ void UMCPHandler_SetBlueprintDefault::Handle(const FJsonObject* Json, FJsonObjec if (!ResolvedClass) { - return Helper->MakeErrorJson(Result, FString::Printf(TEXT("Could not resolve '%s' to a class"), *Value)); + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Could not resolve '%s' to a class"), *Value)); } // Validate meta class compatibility @@ -1295,7 +1295,7 @@ void UMCPHandler_SetBlueprintDefault::Handle(const FJsonObject* Json, FJsonObjec UClass* MetaClass = ClassProp->MetaClass; if (MetaClass && !ResolvedClass->IsChildOf(MetaClass)) { - return Helper->MakeErrorJson(Result, FString::Printf( + return MCPUtils::MakeErrorJson(Result, FString::Printf( TEXT("'%s' is not a subclass of '%s' (required by property '%s')"), *ResolvedClass->GetName(), *MetaClass->GetName(), *Property)); } @@ -1325,7 +1325,7 @@ void UMCPHandler_SetBlueprintDefault::Handle(const FJsonObject* Json, FJsonObjec if (!ResolvedObj) { - return Helper->MakeErrorJson(Result, FString::Printf(TEXT("Could not resolve '%s' to an object"), *Value)); + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Could not resolve '%s' to an object"), *Value)); } ObjProp->SetPropertyValue_InContainer(CDO, ResolvedObj); @@ -1343,7 +1343,7 @@ void UMCPHandler_SetBlueprintDefault::Handle(const FJsonObject* Json, FJsonObjec } else { - return Helper->MakeErrorJson(Result, FString::Printf( + 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())); } @@ -1351,7 +1351,7 @@ void UMCPHandler_SetBlueprintDefault::Handle(const FJsonObject* Json, FJsonObjec if (!bSuccess) { - return Helper->MakeErrorJson(Result, TEXT("Failed to set property value")); + return MCPUtils::MakeErrorJson(Result, TEXT("Failed to set property value")); } // Mark modified and save @@ -1359,7 +1359,7 @@ void UMCPHandler_SetBlueprintDefault::Handle(const FJsonObject* Json, FJsonObjec BP->Modify(); FKismetEditorUtilities::CompileBlueprint(BP); - bool bSaved = Helper->SaveBlueprintPackage(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")); @@ -1385,7 +1385,7 @@ void UMCPHandler_MoveNode::Handle(const FJsonObject* Json, FJsonObject* Result) UBlueprint* BP = Helper->LoadBlueprintByName(Blueprint, LoadError); if (!BP) { - return Helper->MakeErrorJson(Result, LoadError); + return MCPUtils::MakeErrorJson(Result, LoadError); } TArray> Results; @@ -1397,7 +1397,7 @@ void UMCPHandler_MoveNode::Handle(const FJsonObject* Json, FJsonObject* Result) Results.Add(MakeShared(EntryResult)); FMoveNodeEntry Entry; - FString PopulateError = Helper->PopulateFromJson(FMoveNodeEntry::StaticStruct(), &Entry, NodeVal); + FString PopulateError = MCPUtils::PopulateFromJson(FMoveNodeEntry::StaticStruct(), &Entry, NodeVal); if (!PopulateError.IsEmpty()) { EntryResult->SetStringField(TEXT("error"), PopulateError); @@ -1406,7 +1406,7 @@ void UMCPHandler_MoveNode::Handle(const FJsonObject* Json, FJsonObject* Result) EntryResult->SetStringField(TEXT("nodeId"), Entry.NodeId); - UEdGraphNode* Node = Helper->FindNodeByGuid(BP, Entry.NodeId); + UEdGraphNode* Node = MCPUtils::FindNodeByGuid(BP, Entry.NodeId); if (!Node) { EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Node '%s' not found"), *Entry.NodeId)); @@ -1449,11 +1449,11 @@ void UMCPHandler_DuplicateNodes::Handle(const FJsonObject* Json, FJsonObject* Re UBlueprint* BP = Helper->LoadBlueprintByName(Blueprint, LoadError); if (!BP) { - return Helper->MakeErrorJson(Result, LoadError); + return MCPUtils::MakeErrorJson(Result, LoadError); } // Find the target graph - FString DecodedGraphName = MCPHelper::UrlDecode(Graph); + FString DecodedGraphName = MCPUtils::UrlDecode(Graph); UEdGraph* TargetGraph = nullptr; TArray AllGraphs; BP->GetAllGraphs(AllGraphs); @@ -1469,12 +1469,12 @@ void UMCPHandler_DuplicateNodes::Handle(const FJsonObject* Json, FJsonObject* Re if (!TargetGraph) { - return Helper->MakeErrorJson(Result, FString::Printf(TEXT("Graph '%s' not found"), *DecodedGraphName)); + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Graph '%s' not found"), *DecodedGraphName)); } if (NodeIds.Array.Num() == 0) { - return Helper->MakeErrorJson(Result, TEXT("nodeIds array is empty")); + return MCPUtils::MakeErrorJson(Result, TEXT("nodeIds array is empty")); } // Find all source nodes @@ -1484,7 +1484,7 @@ void UMCPHandler_DuplicateNodes::Handle(const FJsonObject* Json, FJsonObject* Re for (const TSharedPtr& IdVal : NodeIds.Array) { FString NodeId = IdVal->AsString(); - UEdGraphNode* Node = Helper->FindNodeByGuid(BP, NodeId); + UEdGraphNode* Node = MCPUtils::FindNodeByGuid(BP, NodeId); if (Node) { if (Node->GetGraph() == TargetGraph) @@ -1504,7 +1504,7 @@ void UMCPHandler_DuplicateNodes::Handle(const FJsonObject* Json, FJsonObject* Re if (SourceNodes.Num() == 0) { - return Helper->MakeErrorJson(Result, TEXT("No valid nodes found to duplicate")); + 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'"), @@ -1588,13 +1588,13 @@ void UMCPHandler_GetNodeComment::Handle(const FJsonObject* Json, FJsonObject* Re UBlueprint* BP = Helper->LoadBlueprintByName(Blueprint, LoadError); if (!BP) { - return Helper->MakeErrorJson(Result, LoadError); + return MCPUtils::MakeErrorJson(Result, LoadError); } - UEdGraphNode* Node = Helper->FindNodeByGuid(BP, NodeId); + UEdGraphNode* Node = MCPUtils::FindNodeByGuid(BP, NodeId); if (!Node) { - return Helper->MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found"), *NodeId)); + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found"), *NodeId)); } Result->SetBoolField(TEXT("success"), true); @@ -1618,13 +1618,13 @@ void UMCPHandler_SetNodeComment::Handle(const FJsonObject* Json, FJsonObject* Re UBlueprint* BP = Helper->LoadBlueprintByName(Blueprint, LoadError); if (!BP) { - return Helper->MakeErrorJson(Result, LoadError); + return MCPUtils::MakeErrorJson(Result, LoadError); } - UEdGraphNode* Node = Helper->FindNodeByGuid(BP, NodeId); + UEdGraphNode* Node = MCPUtils::FindNodeByGuid(BP, NodeId); if (!Node) { - return Helper->MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found"), *NodeId)); + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found"), *NodeId)); } FString OldComment = Node->NodeComment; @@ -1651,81 +1651,6 @@ void UMCPHandler_SetNodeComment::Handle(const FJsonObject* Json, FJsonObject* Re // ============================================================ // Shared helper: iterate the blueprint action database. -// Full name = "Category|MenuName", e.g. "Luprex|Lua|Read Lua Values". -// Substring search matches against full name and keywords. -// Exact search matches full name exactly (case-insensitive). -// Returns full names only. FindSpawner returns the spawner for an exact match. -// ============================================================ - -struct FNodeActionSearch -{ - static FString MakeFullName(UBlueprintNodeSpawner* Spawner) - { - const FBlueprintActionUiSpec& UiSpec = Spawner->PrimeDefaultUiSpec(); - FString Category = UiSpec.Category.ToString(); - FString MenuName = UiSpec.MenuName.ToString(); - if (Category.IsEmpty()) - { - return MenuName; - } - return Category + TEXT("|") + MenuName; - } - - static TArray AllSpawners() - { - TArray Result; - for (const auto& Pair : FBlueprintActionDatabase::Get().GetAllActions()) - { - for (UBlueprintNodeSpawner* Spawner : Pair.Value) - { - if (!Spawner) continue; - if (Spawner->PrimeDefaultUiSpec().MenuName.IsEmpty()) continue; - Result.Add(Spawner); - } - } - return Result; - } - - static void Find(const FString& Query, int32 MaxResults, TArray& OutFullNames) - { - FString QueryLower = Query.ToLower(); - - for (UBlueprintNodeSpawner* Spawner : AllSpawners()) - { - FString FullName = MakeFullName(Spawner); - FString Keywords = Spawner->PrimeDefaultUiSpec().Keywords.ToString(); - - if (!FullName.ToLower().Contains(QueryLower) - && !Keywords.ToLower().Contains(QueryLower)) - { - continue; - } - - OutFullNames.Add(FullName); - - if (MaxResults > 0 && OutFullNames.Num() >= MaxResults) - { - return; - } - } - } - - static TArray FindSpawner(const FString& FullName) - { - FString FullNameLower = FullName.ToLower(); - TArray Result; - - for (UBlueprintNodeSpawner* Spawner : AllSpawners()) - { - if (MakeFullName(Spawner).ToLower() == FullNameLower) - { - Result.Add(Spawner); - } - } - return Result; - } -}; - // ============================================================ // SearchNodeTypes — search the blueprint action database // for spawners matching a query string (same pool as the right-click menu) @@ -1735,13 +1660,12 @@ void UMCPHandler_SearchNodeTypes::Handle(const FJsonObject* Json, FJsonObject* R { int32 ClampedMax = FMath::Clamp(MaxResults, 1, 500); - TArray FullNames; - FNodeActionSearch::Find(Query, ClampedMax, FullNames); + TArray Spawners = MCPUtils::SearchNodeSpawners(Query, ClampedMax); TArray> ResultArray; - for (const FString& Name : FullNames) + for (UBlueprintNodeSpawner* Spawner : Spawners) { - ResultArray.Add(MakeShared(Name)); + ResultArray.Add(MakeShared(MCPUtils::NodeSpawnerFullName(Spawner))); } Result->SetBoolField(TEXT("success"), true); @@ -1763,11 +1687,11 @@ void UMCPHandler_SpawnNode::Handle(const FJsonObject* Json, FJsonObject* Result) UBlueprint* BP = Helper->LoadBlueprintByName(Blueprint, LoadError); if (!BP) { - return Helper->MakeErrorJson(Result, LoadError); + return MCPUtils::MakeErrorJson(Result, LoadError); } // Find the target graph - FString DecodedGraphName = MCPHelper::UrlDecode(Graph); + FString DecodedGraphName = MCPUtils::UrlDecode(Graph); UEdGraph* TargetGraph = nullptr; TArray AllGraphs; BP->GetAllGraphs(AllGraphs); @@ -1788,7 +1712,7 @@ void UMCPHandler_SpawnNode::Handle(const FJsonObject* Json, FJsonObject* Result) { if (G) GraphNames.Add(MakeShared(G->GetName())); } - Helper->MakeErrorJson(Result, FString::Printf(TEXT("Graph '%s' not found"), *DecodedGraphName)); + MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Graph '%s' not found"), *DecodedGraphName)); Result->SetArrayField(TEXT("availableGraphs"), GraphNames); return; } @@ -1802,7 +1726,7 @@ void UMCPHandler_SpawnNode::Handle(const FJsonObject* Json, FJsonObject* Result) Results.Add(MakeShared(EntryResult)); FSpawnNodeEntry Entry; - FString PopulateError = Helper->PopulateFromJson(FSpawnNodeEntry::StaticStruct(), &Entry, NodeVal); + FString PopulateError = MCPUtils::PopulateFromJson(FSpawnNodeEntry::StaticStruct(), &Entry, NodeVal); if (!PopulateError.IsEmpty()) { EntryResult->SetStringField(TEXT("error"), PopulateError); @@ -1812,7 +1736,7 @@ void UMCPHandler_SpawnNode::Handle(const FJsonObject* Json, FJsonObject* Result) EntryResult->SetStringField(TEXT("actionName"), Entry.ActionName); // Find the spawner by exact full name - TArray Matches = FNodeActionSearch::FindSpawner(Entry.ActionName); + TArray Matches = MCPUtils::SearchNodeSpawners(Entry.ActionName, 0, /*ExactMatch=*/true); if (Matches.Num() == 0) { EntryResult->SetStringField(TEXT("error"), FString::Printf( @@ -1854,7 +1778,7 @@ void UMCPHandler_SpawnNode::Handle(const FJsonObject* Json, FJsonObject* Result) *Blueprint); // Serialize result - TSharedPtr NodeState = Helper->SerializeNode(NewNode); + TSharedPtr NodeState = MCPUtils::SerializeNode(NewNode); EntryResult->SetBoolField(TEXT("success"), true); EntryResult->SetStringField(TEXT("nodeId"), NewNode->NodeGuid.ToString()); diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_Params.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_Params.cpp index 16a27d7a..9076f561 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_Params.cpp +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_Params.cpp @@ -1,4 +1,5 @@ #include "BlueprintMCPServer.h" +#include "MCPUtils.h" #include "Engine/Blueprint.h" #include "EdGraph/EdGraph.h" #include "EdGraph/EdGraphPin.h" @@ -22,7 +23,7 @@ void FBlueprintMCPServer::HandleChangeFunctionParamType(const FJsonObject* Json, if (BlueprintName.IsEmpty() || FunctionName.IsEmpty() || ParamName.IsEmpty() || NewTypeName.IsEmpty()) { - return MakeErrorJson(Result, TEXT("Missing required fields: blueprint, functionName, paramName, newType")); + return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: blueprint, functionName, paramName, newType")); } // Load Blueprint @@ -30,15 +31,15 @@ void FBlueprintMCPServer::HandleChangeFunctionParamType(const FJsonObject* Json, UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError); if (!BP) { - return MakeErrorJson(Result, LoadError); + return MCPUtils::MakeErrorJson(Result, LoadError); } // Resolve the new type using the shared resolver (supports primitives, structs, enums, and object references) FEdGraphPinType NewPinType; FString TypeError; - if (!ResolveTypeFromString(NewTypeName, NewPinType, TypeError)) + if (!MCPUtils::ResolveTypeFromString(NewTypeName, NewPinType, TypeError)) { - return MakeErrorJson(Result, TypeError); + return MCPUtils::MakeErrorJson(Result, TypeError); } // Find the entry node: K2Node_FunctionEntry in a function graph, @@ -111,7 +112,7 @@ void FBlueprintMCPServer::HandleChangeFunctionParamType(const FJsonObject* Json, } } - MakeErrorJson(Result, FString::Printf( + MCPUtils::MakeErrorJson(Result, FString::Printf( TEXT("Function or custom event '%s' not found in Blueprint '%s'"), *FunctionName, *BlueprintName)); Result->SetArrayField(TEXT("availableFunctionsAndEvents"), Available); @@ -142,7 +143,7 @@ void FBlueprintMCPServer::HandleChangeFunctionParamType(const FJsonObject* Json, } } - MakeErrorJson(Result, FString::Printf( + MCPUtils::MakeErrorJson(Result, FString::Printf( TEXT("Parameter '%s' not found in %s '%s'"), *ParamName, *FoundNodeType, *FunctionName)); Result->SetArrayField(TEXT("availableParams"), ParamNames); @@ -206,13 +207,13 @@ void FBlueprintMCPServer::HandleChangeFunctionParamType(const FJsonObject* Json, } // Save - bool bSaved = SaveBlueprintPackage(BP); + 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 = SerializeNode(EntryNode); + TSharedPtr UpdatedNodeState = MCPUtils::SerializeNode(EntryNode); Result->SetBoolField(TEXT("success"), true); Result->SetStringField(TEXT("blueprint"), BlueprintName); @@ -240,7 +241,7 @@ void FBlueprintMCPServer::HandleRemoveFunctionParameter(const FJsonObject* Json, if (BlueprintName.IsEmpty() || FunctionName.IsEmpty() || ParamName.IsEmpty()) { - return MakeErrorJson(Result, TEXT("Missing required fields: blueprint, functionName, paramName")); + return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: blueprint, functionName, paramName")); } // Load Blueprint @@ -248,7 +249,7 @@ void FBlueprintMCPServer::HandleRemoveFunctionParameter(const FJsonObject* Json, UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError); if (!BP) { - return MakeErrorJson(Result, LoadError); + return MCPUtils::MakeErrorJson(Result, LoadError); } // Find the entry node @@ -320,7 +321,7 @@ void FBlueprintMCPServer::HandleRemoveFunctionParameter(const FJsonObject* Json, } } - MakeErrorJson(Result, FString::Printf( + MCPUtils::MakeErrorJson(Result, FString::Printf( TEXT("Function or custom event '%s' not found in Blueprint '%s'"), *FunctionName, *BlueprintName)); Result->SetArrayField(TEXT("availableFunctionsAndEvents"), Available); @@ -351,7 +352,7 @@ void FBlueprintMCPServer::HandleRemoveFunctionParameter(const FJsonObject* Json, } } - MakeErrorJson(Result, FString::Printf( + MCPUtils::MakeErrorJson(Result, FString::Printf( TEXT("Parameter '%s' not found in %s '%s'"), *ParamName, *FoundNodeType, *FunctionName)); Result->SetArrayField(TEXT("availableParams"), ParamNames); @@ -374,7 +375,7 @@ void FBlueprintMCPServer::HandleRemoveFunctionParameter(const FJsonObject* Json, } // Save - bool bSaved = SaveBlueprintPackage(BP); + bool bSaved = MCPUtils::SaveBlueprintPackage(BP); UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Parameter removed, save %s"), bSaved ? TEXT("succeeded") : TEXT("failed")); @@ -401,7 +402,7 @@ void FBlueprintMCPServer::HandleAddFunctionParameter(const FJsonObject* Json, FJ if (BlueprintName.IsEmpty() || FunctionName.IsEmpty() || ParamName.IsEmpty() || ParamType.IsEmpty()) { - return MakeErrorJson(Result, TEXT("Missing required fields: blueprint, functionName, paramName, paramType")); + return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: blueprint, functionName, paramName, paramType")); } // Load Blueprint @@ -409,15 +410,15 @@ void FBlueprintMCPServer::HandleAddFunctionParameter(const FJsonObject* Json, FJ UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError); if (!BP) { - return MakeErrorJson(Result, LoadError); + return MCPUtils::MakeErrorJson(Result, LoadError); } // Resolve param type FEdGraphPinType PinType; FString TypeError; - if (!ResolveTypeFromString(ParamType, PinType, TypeError)) + if (!MCPUtils::ResolveTypeFromString(ParamType, PinType, TypeError)) { - return MakeErrorJson(Result, TypeError); + return MCPUtils::MakeErrorJson(Result, TypeError); } // Find the entry node using 3 strategies @@ -532,7 +533,7 @@ void FBlueprintMCPServer::HandleAddFunctionParameter(const FJsonObject* Json, FJ FString::Printf(TEXT("%s (event dispatcher)"), *DN.ToString()))); } - MakeErrorJson(Result, FString::Printf( + MCPUtils::MakeErrorJson(Result, FString::Printf( TEXT("Function, custom event, or event dispatcher '%s' not found in Blueprint '%s'"), *FunctionName, *BlueprintName)); Result->SetArrayField(TEXT("availableFunctions"), AvailFuncs); @@ -544,7 +545,7 @@ void FBlueprintMCPServer::HandleAddFunctionParameter(const FJsonObject* Json, FJ { if (Existing.IsValid() && Existing->PinName.ToString().Equals(ParamName, ESearchCase::IgnoreCase)) { - return MakeErrorJson(Result, FString::Printf( + return MCPUtils::MakeErrorJson(Result, FString::Printf( TEXT("Parameter '%s' already exists on '%s'"), *ParamName, *FunctionName)); } } @@ -556,7 +557,7 @@ void FBlueprintMCPServer::HandleAddFunctionParameter(const FJsonObject* Json, FJ EntryNode->CreateUserDefinedPin(FName(*ParamName), PinType, EGPD_Output); FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP); - bool bSaved = SaveBlueprintPackage(BP); + bool bSaved = MCPUtils::SaveBlueprintPackage(BP); UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Added parameter '%s' to '%s' in '%s' (saved: %s)"), *ParamName, *FunctionName, *BlueprintName, bSaved ? TEXT("true") : TEXT("false")); diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_Read.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_Read.cpp index abacd4d9..97b18fb0 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_Read.cpp +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_Read.cpp @@ -1,4 +1,5 @@ #include "BlueprintMCPServer.h" +#include "MCPUtils.h" #include "Engine/Blueprint.h" #include "Engine/World.h" #include "Engine/Level.h" @@ -116,18 +117,18 @@ void FBlueprintMCPServer::HandleGetBlueprint(const FJsonObject* Json, FJsonObjec FString Name = Json->GetStringField(TEXT("name")); if (Name.IsEmpty()) { - return MakeErrorJson(Result, TEXT("Missing 'name' parameter")); + return MCPUtils::MakeErrorJson(Result, TEXT("Missing 'name' parameter")); } FString LoadError; UBlueprint* BP = LoadBlueprintByName(Name, LoadError); if (!BP) { - return MakeErrorJson(Result, LoadError); + return MCPUtils::MakeErrorJson(Result, LoadError); } - TSharedRef Tmp = SerializeBlueprint(BP); - CopyJsonFields(&*Tmp, Result); + TSharedRef Tmp = MCPUtils::SerializeBlueprint(BP); + MCPUtils::CopyJsonFields(&*Tmp, Result); } void FBlueprintMCPServer::HandleGetGraph(const FJsonObject* Json, FJsonObject* Result) @@ -136,17 +137,17 @@ void FBlueprintMCPServer::HandleGetGraph(const FJsonObject* Json, FJsonObject* R FString GraphName = Json->GetStringField(TEXT("graph")); if (Name.IsEmpty() || GraphName.IsEmpty()) { - return MakeErrorJson(Result, TEXT("Missing 'name' or 'graph' parameter")); + return MCPUtils::MakeErrorJson(Result, TEXT("Missing 'name' or 'graph' parameter")); } // URL-decode graph name to handle spaces encoded as '+' or '%20' - FString DecodedGraphName = UrlDecode(GraphName); + FString DecodedGraphName = MCPUtils::UrlDecode(GraphName); FString LoadError; UBlueprint* BP = LoadBlueprintByName(Name, LoadError); if (!BP) { - return MakeErrorJson(Result, LoadError); + return MCPUtils::MakeErrorJson(Result, LoadError); } TArray AllGraphs; @@ -156,10 +157,10 @@ void FBlueprintMCPServer::HandleGetGraph(const FJsonObject* Json, FJsonObject* R { if (Graph && Graph->GetName().Equals(DecodedGraphName, ESearchCase::IgnoreCase)) { - TSharedPtr GraphJson = SerializeGraph(Graph); + TSharedPtr GraphJson = MCPUtils::SerializeGraph(Graph); if (GraphJson.IsValid()) { - CopyJsonFields(GraphJson.Get(), Result); + MCPUtils::CopyJsonFields(GraphJson.Get(), Result); return; } } @@ -174,7 +175,7 @@ void FBlueprintMCPServer::HandleGetGraph(const FJsonObject* Json, FJsonObject* R GraphNames.Add(MakeShared(Graph->GetName())); } } - MakeErrorJson(Result, FString::Printf(TEXT("Graph '%s' not found"), *DecodedGraphName)); + MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Graph '%s' not found"), *DecodedGraphName)); Result->SetArrayField(TEXT("availableGraphs"), GraphNames); } @@ -183,7 +184,7 @@ void FBlueprintMCPServer::HandleSearch(const FJsonObject* Json, FJsonObject* Res FString Query = Json->GetStringField(TEXT("query")); if (Query.IsEmpty()) { - return MakeErrorJson(Result, TEXT("Missing 'query' parameter")); + return MCPUtils::MakeErrorJson(Result, TEXT("Missing 'query' parameter")); } FString PathFilter = Json->GetStringField(TEXT("path")); @@ -310,7 +311,7 @@ void FBlueprintMCPServer::HandleTestSave(const FJsonObject* Json, FJsonObject* R FString Name = Json->GetStringField(TEXT("name")); if (Name.IsEmpty()) { - return MakeErrorJson(Result, TEXT("Missing 'name' query parameter")); + return MCPUtils::MakeErrorJson(Result, TEXT("Missing 'name' query parameter")); } UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: test-save requested for '%s'"), *Name); @@ -319,7 +320,7 @@ void FBlueprintMCPServer::HandleTestSave(const FJsonObject* Json, FJsonObject* R UBlueprint* BP = LoadBlueprintByName(Name, LoadError); if (!BP) { - return MakeErrorJson(Result, LoadError); + return MCPUtils::MakeErrorJson(Result, LoadError); } UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: test-save — loaded '%s', GeneratedClass=%s"), @@ -327,7 +328,7 @@ void FBlueprintMCPServer::HandleTestSave(const FJsonObject* Json, FJsonObject* R BP->GeneratedClass ? *BP->GeneratedClass->GetName() : TEXT("null")); // Attempt save with NO modifications - bool bSaved = SaveBlueprintPackage(BP); + bool bSaved = MCPUtils::SaveBlueprintPackage(BP); Result->SetStringField(TEXT("blueprint"), Name); Result->SetStringField(TEXT("packagePath"), BP->GetPackage()->GetName()); @@ -344,7 +345,7 @@ void FBlueprintMCPServer::HandleFindReferences(const FJsonObject* Json, FJsonObj FString AssetPath = Json->GetStringField(TEXT("assetPath")); if (AssetPath.IsEmpty()) { - return MakeErrorJson(Result, TEXT("Missing 'assetPath' query parameter")); + return MCPUtils::MakeErrorJson(Result, TEXT("Missing 'assetPath' query parameter")); } IAssetRegistry& Registry = *IAssetRegistry::Get(); @@ -391,12 +392,12 @@ void FBlueprintMCPServer::HandleSearchByType(const FJsonObject* Json, FJsonObjec FString TypeNameRaw = Json->GetStringField(TEXT("typeName")); if (TypeNameRaw.IsEmpty()) { - return MakeErrorJson(Result, TEXT("Missing 'typeName' query parameter")); + return MCPUtils::MakeErrorJson(Result, TEXT("Missing 'typeName' query parameter")); } - FString TypeName = UrlDecode(TypeNameRaw); + FString TypeName = MCPUtils::UrlDecode(TypeNameRaw); FString FilterRaw = Json->GetStringField(TEXT("filter")); - FString FilterStr = FilterRaw.IsEmpty() ? FString() : UrlDecode(FilterRaw); + FString FilterStr = FilterRaw.IsEmpty() ? FString() : MCPUtils::UrlDecode(FilterRaw); int32 MaxResults = 200; if (Json->HasField(TEXT("maxResults"))) diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_Snapshot.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_Snapshot.cpp index 8b8b082b..7e0ae0b9 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_Snapshot.cpp +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_Snapshot.cpp @@ -1,4 +1,5 @@ #include "BlueprintMCPServer.h" +#include "MCPUtils.h" #include "Engine/Blueprint.h" #include "EdGraph/EdGraph.h" #include "EdGraph/EdGraphNode.h" @@ -161,7 +162,7 @@ bool FBlueprintMCPServer::SaveSnapshotToDisk(const FString& SnapshotId, const FG } Root->SetObjectField(TEXT("graphs"), GraphsObj); - FString JsonString = JsonToString(Root); + FString JsonString = MCPUtils::JsonToString(Root); bool bSuccess = FFileHelper::SaveStringToFile(JsonString, *FilePath, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM); if (bSuccess) { @@ -259,7 +260,7 @@ void FBlueprintMCPServer::HandleSnapshotGraph(const FJsonObject* Json, FJsonObje FString BlueprintName = Json->GetStringField(TEXT("blueprint")); if (BlueprintName.IsEmpty()) { - return MakeErrorJson(Result, TEXT("Missing required field: blueprint")); + return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: blueprint")); } FString GraphFilter; @@ -270,7 +271,7 @@ void FBlueprintMCPServer::HandleSnapshotGraph(const FJsonObject* Json, FJsonObje UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError); if (!BP) { - return MakeErrorJson(Result, LoadError); + return MCPUtils::MakeErrorJson(Result, LoadError); } UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Creating snapshot for blueprint '%s'"), *BlueprintName); @@ -299,7 +300,7 @@ void FBlueprintMCPServer::HandleSnapshotGraph(const FJsonObject* Json, FJsonObje if (GraphsToCapture.Num() == 0 && !GraphFilter.IsEmpty()) { - return MakeErrorJson(Result, FString::Printf(TEXT("Graph '%s' not found in blueprint '%s'"), *GraphFilter, *BlueprintName)); + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Graph '%s' not found in blueprint '%s'"), *GraphFilter, *BlueprintName)); } int32 TotalConnections = 0; @@ -347,7 +348,7 @@ void FBlueprintMCPServer::HandleDiffGraph(const FJsonObject* Json, FJsonObject* FString SnapshotId = Json->GetStringField(TEXT("snapshotId")); if (BlueprintName.IsEmpty() || SnapshotId.IsEmpty()) { - return MakeErrorJson(Result, TEXT("Missing required fields: blueprint, snapshotId")); + return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: blueprint, snapshotId")); } FString GraphFilter; @@ -360,7 +361,7 @@ void FBlueprintMCPServer::HandleDiffGraph(const FJsonObject* Json, FJsonObject* { if (!LoadSnapshotFromDisk(SnapshotId, LoadedSnapshot)) { - return MakeErrorJson(Result, FString::Printf(TEXT("Snapshot '%s' not found in memory or on disk"), *SnapshotId)); + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Snapshot '%s' not found in memory or on disk"), *SnapshotId)); } SnapshotPtr = &LoadedSnapshot; } @@ -370,7 +371,7 @@ void FBlueprintMCPServer::HandleDiffGraph(const FJsonObject* Json, FJsonObject* UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError); if (!BP) { - return MakeErrorJson(Result, LoadError); + return MCPUtils::MakeErrorJson(Result, LoadError); } UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Diffing blueprint '%s' against snapshot '%s'"), *BlueprintName, *SnapshotId); @@ -560,7 +561,7 @@ void FBlueprintMCPServer::HandleRestoreGraph(const FJsonObject* Json, FJsonObjec FString SnapshotId = Json->GetStringField(TEXT("snapshotId")); if (BlueprintName.IsEmpty() || SnapshotId.IsEmpty()) { - return MakeErrorJson(Result, TEXT("Missing required fields: blueprint, snapshotId")); + return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: blueprint, snapshotId")); } FString GraphFilter; @@ -579,7 +580,7 @@ void FBlueprintMCPServer::HandleRestoreGraph(const FJsonObject* Json, FJsonObjec { if (!LoadSnapshotFromDisk(SnapshotId, LoadedSnapshot)) { - return MakeErrorJson(Result, FString::Printf(TEXT("Snapshot '%s' not found in memory or on disk"), *SnapshotId)); + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Snapshot '%s' not found in memory or on disk"), *SnapshotId)); } SnapshotPtr = &LoadedSnapshot; } @@ -589,7 +590,7 @@ void FBlueprintMCPServer::HandleRestoreGraph(const FJsonObject* Json, FJsonObjec UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError); if (!BP) { - return MakeErrorJson(Result, LoadError); + return MCPUtils::MakeErrorJson(Result, LoadError); } UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Restoring connections from snapshot '%s' for blueprint '%s' (dryRun=%s)"), @@ -659,8 +660,8 @@ void FBlueprintMCPServer::HandleRestoreGraph(const FJsonObject* Json, FJsonObjec // Find source and target nodes UEdGraph* SourceGraph = nullptr; - UEdGraphNode* SourceNode = FindNodeByGuid(BP, Conn.SourceNodeGuid, &SourceGraph); - UEdGraphNode* TargetNode = FindNodeByGuid(BP, Conn.TargetNodeGuid); + UEdGraphNode* SourceNode = MCPUtils::FindNodeByGuid(BP, Conn.SourceNodeGuid, &SourceGraph); + UEdGraphNode* TargetNode = MCPUtils::FindNodeByGuid(BP, Conn.TargetNodeGuid); if (!SourceNode) { @@ -746,7 +747,7 @@ void FBlueprintMCPServer::HandleRestoreGraph(const FJsonObject* Json, FJsonObjec bool bSaved = false; if (!bDryRun && Reconnected > 0) { - bSaved = SaveBlueprintPackage(BP); + bSaved = MCPUtils::SaveBlueprintPackage(BP); } UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Restore complete — %d reconnected, %d failed, saved=%s"), @@ -777,7 +778,7 @@ void FBlueprintMCPServer::HandleFindDisconnectedPins(const FJsonObject* Json, FJ if (BlueprintName.IsEmpty() && PathFilter.IsEmpty() && SnapshotId.IsEmpty()) { - return MakeErrorJson(Result, TEXT("Provide at least one of: blueprint, filter, or snapshotId")); + return MCPUtils::MakeErrorJson(Result, TEXT("Provide at least one of: blueprint, filter, or snapshotId")); } // Optionally load snapshot for definite-break detection @@ -1074,7 +1075,7 @@ void FBlueprintMCPServer::HandleAnalyzeRebuildImpact(const FJsonObject* Json, FJ FString ModuleName = Json->GetStringField(TEXT("moduleName")); if (ModuleName.IsEmpty()) { - return MakeErrorJson(Result, TEXT("Missing required field: moduleName")); + return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: moduleName")); } // Optional struct name filter diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_UserTypes.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_UserTypes.cpp index cdac8ac1..5371f015 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_UserTypes.cpp +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_UserTypes.cpp @@ -1,4 +1,5 @@ #include "BlueprintMCPServer.h" +#include "MCPUtils.h" #include "Engine/UserDefinedStruct.h" #include "Engine/UserDefinedEnum.h" #include "Kismet2/BlueprintEditorUtils.h" @@ -25,7 +26,7 @@ void FBlueprintMCPServer::HandleCreateStruct(const FJsonObject* Json, FJsonObjec FString AssetPath = Json->GetStringField(TEXT("assetPath")); if (AssetPath.IsEmpty()) { - return MakeErrorJson(Result, TEXT("Missing required field: assetPath (e.g. '/Game/DataTypes/S_MyStruct')")); + return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: assetPath (e.g. '/Game/DataTypes/S_MyStruct')")); } // Split path into package path and asset name @@ -38,12 +39,12 @@ void FBlueprintMCPServer::HandleCreateStruct(const FJsonObject* Json, FJsonObjec } else { - return MakeErrorJson(Result, TEXT("assetPath must be a full path (e.g. '/Game/DataTypes/S_MyStruct')")); + return MCPUtils::MakeErrorJson(Result, TEXT("assetPath must be a full path (e.g. '/Game/DataTypes/S_MyStruct')")); } if (AssetName.IsEmpty()) { - return MakeErrorJson(Result, TEXT("Invalid asset name in assetPath")); + return MCPUtils::MakeErrorJson(Result, TEXT("Invalid asset name in assetPath")); } // Check if asset already exists @@ -51,7 +52,7 @@ void FBlueprintMCPServer::HandleCreateStruct(const FJsonObject* Json, FJsonObjec FAssetData ExistingAsset = ARM.Get().GetAssetByObjectPath(FSoftObjectPath(AssetPath + TEXT(".") + AssetName)); if (ExistingAsset.IsValid()) { - return MakeErrorJson(Result, FString::Printf(TEXT("Asset already exists at '%s'"), *AssetPath)); + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Asset already exists at '%s'"), *AssetPath)); } // Create the struct using the AssetTools factory @@ -63,13 +64,13 @@ void FBlueprintMCPServer::HandleCreateStruct(const FJsonObject* Json, FJsonObjec if (!NewAsset) { - return MakeErrorJson(Result, TEXT("Failed to create UserDefinedStruct asset")); + return MCPUtils::MakeErrorJson(Result, TEXT("Failed to create UserDefinedStruct asset")); } UUserDefinedStruct* NewStruct = Cast(NewAsset); if (!NewStruct) { - return MakeErrorJson(Result, TEXT("Created asset is not a UserDefinedStruct")); + return MCPUtils::MakeErrorJson(Result, TEXT("Created asset is not a UserDefinedStruct")); } // Add properties if specified @@ -88,7 +89,7 @@ void FBlueprintMCPServer::HandleCreateStruct(const FJsonObject* Json, FJsonObjec FEdGraphPinType PinType; FString TypeError; - if (!ResolveTypeFromString(PropType, PinType, TypeError)) + if (!MCPUtils::ResolveTypeFromString(PropType, PinType, TypeError)) { UE_LOG(LogTemp, Warning, TEXT("BlueprintMCP: Could not resolve type '%s' for property '%s': %s"), *PropType, *PropName, *TypeError); continue; @@ -149,7 +150,7 @@ void FBlueprintMCPServer::HandleCreateEnum(const FJsonObject* Json, FJsonObject* FString AssetPath = Json->GetStringField(TEXT("assetPath")); if (AssetPath.IsEmpty()) { - return MakeErrorJson(Result, TEXT("Missing required field: assetPath (e.g. '/Game/DataTypes/E_MyEnum')")); + return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: assetPath (e.g. '/Game/DataTypes/E_MyEnum')")); } // Split path @@ -162,19 +163,19 @@ void FBlueprintMCPServer::HandleCreateEnum(const FJsonObject* Json, FJsonObject* } else { - return MakeErrorJson(Result, TEXT("assetPath must be a full path (e.g. '/Game/DataTypes/E_MyEnum')")); + return MCPUtils::MakeErrorJson(Result, TEXT("assetPath must be a full path (e.g. '/Game/DataTypes/E_MyEnum')")); } if (AssetName.IsEmpty()) { - return MakeErrorJson(Result, TEXT("Invalid asset name in assetPath")); + return MCPUtils::MakeErrorJson(Result, TEXT("Invalid asset name in assetPath")); } // Get values const TArray>* ValuesArray = nullptr; if (!Json->TryGetArrayField(TEXT("values"), ValuesArray) || !ValuesArray || ValuesArray->Num() == 0) { - return MakeErrorJson(Result, TEXT("Missing or empty required field: values (array of strings)")); + return MCPUtils::MakeErrorJson(Result, TEXT("Missing or empty required field: values (array of strings)")); } TArray EnumValues; @@ -185,7 +186,7 @@ void FBlueprintMCPServer::HandleCreateEnum(const FJsonObject* Json, FJsonObject* } if (EnumValues.Num() == 0) { - return MakeErrorJson(Result, TEXT("No valid enum values provided")); + return MCPUtils::MakeErrorJson(Result, TEXT("No valid enum values provided")); } // Create the enum using AssetTools @@ -197,13 +198,13 @@ void FBlueprintMCPServer::HandleCreateEnum(const FJsonObject* Json, FJsonObject* if (!NewAsset) { - return MakeErrorJson(Result, TEXT("Failed to create UserDefinedEnum asset")); + return MCPUtils::MakeErrorJson(Result, TEXT("Failed to create UserDefinedEnum asset")); } UUserDefinedEnum* NewEnum = Cast(NewAsset); if (!NewEnum) { - return MakeErrorJson(Result, TEXT("Created asset is not a UserDefinedEnum")); + return MCPUtils::MakeErrorJson(Result, TEXT("Created asset is not a UserDefinedEnum")); } // Add enum values — UUserDefinedEnum starts with a MAX value. @@ -246,7 +247,7 @@ void FBlueprintMCPServer::HandleAddStructProperty(const FJsonObject* Json, FJson if (AssetPath.IsEmpty() || PropName.IsEmpty() || PropType.IsEmpty()) { - return MakeErrorJson(Result, TEXT("Missing required fields: assetPath, name, type")); + return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: assetPath, name, type")); } // Find the struct @@ -259,15 +260,15 @@ void FBlueprintMCPServer::HandleAddStructProperty(const FJsonObject* Json, FJson } if (!Struct) { - return MakeErrorJson(Result, FString::Printf(TEXT("UserDefinedStruct not found at '%s'"), *AssetPath)); + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("UserDefinedStruct not found at '%s'"), *AssetPath)); } // Resolve type FEdGraphPinType PinType; FString TypeError; - if (!ResolveTypeFromString(PropType, PinType, TypeError)) + if (!MCPUtils::ResolveTypeFromString(PropType, PinType, TypeError)) { - return MakeErrorJson(Result, FString::Printf(TEXT("Cannot resolve type '%s': %s"), *PropType, *TypeError)); + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Cannot resolve type '%s': %s"), *PropType, *TypeError)); } // Snapshot existing GUIDs so we can find the newly added one @@ -280,7 +281,7 @@ void FBlueprintMCPServer::HandleAddStructProperty(const FJsonObject* Json, FJson bool bAdded = FStructureEditorUtils::AddVariable(Struct, PinType); if (!bAdded) { - return MakeErrorJson(Result, TEXT("Failed to add property to struct")); + return MCPUtils::MakeErrorJson(Result, TEXT("Failed to add property to struct")); } // Find the new variable by diffing GUID sets and rename it @@ -321,7 +322,7 @@ void FBlueprintMCPServer::HandleRemoveStructProperty(const FJsonObject* Json, FJ if (AssetPath.IsEmpty() || PropName.IsEmpty()) { - return MakeErrorJson(Result, TEXT("Missing required fields: assetPath, name")); + return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: assetPath, name")); } // Find the struct @@ -333,7 +334,7 @@ void FBlueprintMCPServer::HandleRemoveStructProperty(const FJsonObject* Json, FJ } if (!Struct) { - return MakeErrorJson(Result, FString::Printf(TEXT("UserDefinedStruct not found at '%s'"), *AssetPath)); + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("UserDefinedStruct not found at '%s'"), *AssetPath)); } // Find the property GUID by name @@ -357,7 +358,7 @@ void FBlueprintMCPServer::HandleRemoveStructProperty(const FJsonObject* Json, FJ { AvailProps.Add(MakeShared(Var.FriendlyName)); } - MakeErrorJson(Result, FString::Printf(TEXT("Property '%s' not found in struct '%s'"), *PropName, *AssetPath)); + MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Property '%s' not found in struct '%s'"), *PropName, *AssetPath)); Result->SetArrayField(TEXT("availableProperties"), AvailProps); return; } @@ -365,7 +366,7 @@ void FBlueprintMCPServer::HandleRemoveStructProperty(const FJsonObject* Json, FJ bool bRemoved = FStructureEditorUtils::RemoveVariable(Struct, TargetGuid); if (!bRemoved) { - return MakeErrorJson(Result, FString::Printf(TEXT("Failed to remove property '%s'"), *PropName)); + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Failed to remove property '%s'"), *PropName)); } // Save diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_Validation.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_Validation.cpp index 53b1eac1..2dc9be82 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_Validation.cpp +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_Validation.cpp @@ -1,4 +1,5 @@ #include "BlueprintMCPServer.h" +#include "MCPUtils.h" #include "Engine/Blueprint.h" #include "EdGraph/EdGraph.h" #include "EdGraph/EdGraphNode.h" @@ -180,7 +181,7 @@ void FBlueprintMCPServer::HandleValidateBlueprint(const FJsonObject* Json, FJson FString BlueprintName = Json->GetStringField(TEXT("blueprint")); if (BlueprintName.IsEmpty()) { - return MakeErrorJson(Result, TEXT("Missing required field: blueprint")); + return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: blueprint")); } // Load Blueprint @@ -188,13 +189,13 @@ void FBlueprintMCPServer::HandleValidateBlueprint(const FJsonObject* Json, FJson UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError); if (!BP) { - return MakeErrorJson(Result, LoadError); + return MCPUtils::MakeErrorJson(Result, LoadError); } UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Validating blueprint '%s'"), *BlueprintName); TSharedRef ValidationResult = ValidateSingleBlueprint(BP, BlueprintName); - CopyJsonFields(&*ValidationResult, Result); + MCPUtils::CopyJsonFields(&*ValidationResult, Result); } // ============================================================ diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_Variables.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_Variables.cpp index edf1d925..5ab19cd8 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_Variables.cpp +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_Variables.cpp @@ -1,4 +1,5 @@ #include "BlueprintMCPServer.h" +#include "MCPUtils.h" #include "Engine/Blueprint.h" #include "EdGraph/EdGraph.h" #include "EdGraph/EdGraphPin.h" @@ -28,7 +29,7 @@ void FBlueprintMCPServer::HandleChangeVariableType(const FJsonObject* Json, FJso if (BlueprintName.IsEmpty() || VariableName.IsEmpty() || NewTypeName.IsEmpty()) { - return MakeErrorJson(Result, TEXT("Missing required fields: blueprint, variable, newType")); + return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: blueprint, variable, newType")); } // Load Blueprint @@ -36,7 +37,7 @@ void FBlueprintMCPServer::HandleChangeVariableType(const FJsonObject* Json, FJso UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError); if (!BP) { - return MakeErrorJson(Result, LoadError); + return MCPUtils::MakeErrorJson(Result, LoadError); } // Verify variable exists @@ -51,7 +52,7 @@ void FBlueprintMCPServer::HandleChangeVariableType(const FJsonObject* Json, FJso } if (!bVarFound) { - return MakeErrorJson(Result, FString::Printf(TEXT("Variable '%s' not found in Blueprint '%s'"), *VariableName, *BlueprintName)); + return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Variable '%s' not found in Blueprint '%s'"), *VariableName, *BlueprintName)); } // Build the new pin type using shared resolver @@ -67,9 +68,9 @@ void FBlueprintMCPServer::HandleChangeVariableType(const FJsonObject* Json, FJso } FString TypeError; - if (!ResolveTypeFromString(ResolveInput, NewPinType, TypeError)) + if (!MCPUtils::ResolveTypeFromString(ResolveInput, NewPinType, TypeError)) { - return MakeErrorJson(Result, TypeError); + return MCPUtils::MakeErrorJson(Result, TypeError); } // Derive typeCategory from the resolved pin type for the response @@ -185,7 +186,7 @@ void FBlueprintMCPServer::HandleChangeVariableType(const FJsonObject* Json, FJso } // Save - bool bSaved = SaveBlueprintPackage(BP); + bool bSaved = MCPUtils::SaveBlueprintPackage(BP); UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Variable type changed, save %s"), bSaved ? TEXT("succeeded") : TEXT("failed")); @@ -226,7 +227,7 @@ void FBlueprintMCPServer::HandleAddVariable(const FJsonObject* Json, FJsonObject if (BlueprintName.IsEmpty() || VariableName.IsEmpty() || VariableType.IsEmpty()) { - return MakeErrorJson(Result, TEXT("Missing required fields: blueprint, variableName, variableType")); + return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: blueprint, variableName, variableType")); } FString Category; @@ -252,7 +253,7 @@ void FBlueprintMCPServer::HandleAddVariable(const FJsonObject* Json, FJsonObject UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError); if (!BP) { - return MakeErrorJson(Result, LoadError); + return MCPUtils::MakeErrorJson(Result, LoadError); } // Check for duplicate variable name @@ -261,7 +262,7 @@ void FBlueprintMCPServer::HandleAddVariable(const FJsonObject* Json, FJsonObject { if (Var.VarName == VarFName) { - return MakeErrorJson(Result, FString::Printf( + return MCPUtils::MakeErrorJson(Result, FString::Printf( TEXT("Variable '%s' already exists in Blueprint '%s'"), *VariableName, *BlueprintName)); } } @@ -269,9 +270,9 @@ void FBlueprintMCPServer::HandleAddVariable(const FJsonObject* Json, FJsonObject // Resolve the type using the shared helper FEdGraphPinType PinType; FString TypeError; - if (!ResolveTypeFromString(VariableType, PinType, TypeError)) + if (!MCPUtils::ResolveTypeFromString(VariableType, PinType, TypeError)) { - return MakeErrorJson(Result, TypeError); + return MCPUtils::MakeErrorJson(Result, TypeError); } // Set container type for arrays @@ -287,7 +288,7 @@ void FBlueprintMCPServer::HandleAddVariable(const FJsonObject* Json, FJsonObject bool bSuccess = FBlueprintEditorUtils::AddMemberVariable(BP, VarFName, PinType, DefaultValue); if (!bSuccess) { - return MakeErrorJson(Result, FString::Printf( + return MCPUtils::MakeErrorJson(Result, FString::Printf( TEXT("FBlueprintEditorUtils::AddMemberVariable failed for '%s'"), *VariableName)); } @@ -298,7 +299,7 @@ void FBlueprintMCPServer::HandleAddVariable(const FJsonObject* Json, FJsonObject } FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP); - bool bSaved = SaveBlueprintPackage(BP); + bool bSaved = MCPUtils::SaveBlueprintPackage(BP); UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Added variable '%s' to '%s' (saved: %s)"), *VariableName, *BlueprintName, bSaved ? TEXT("true") : TEXT("false")); @@ -326,7 +327,7 @@ void FBlueprintMCPServer::HandleRemoveVariable(const FJsonObject* Json, FJsonObj if (BlueprintName.IsEmpty() || VariableName.IsEmpty()) { - return MakeErrorJson(Result, TEXT("Missing required fields: blueprint, variableName")); + return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: blueprint, variableName")); } // Load Blueprint @@ -334,7 +335,7 @@ void FBlueprintMCPServer::HandleRemoveVariable(const FJsonObject* Json, FJsonObj UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError); if (!BP) { - return MakeErrorJson(Result, LoadError); + return MCPUtils::MakeErrorJson(Result, LoadError); } // Find variable by name (case-insensitive) @@ -359,7 +360,7 @@ void FBlueprintMCPServer::HandleRemoveVariable(const FJsonObject* Json, FJsonObj AvailVars.Add(MakeShared(Var.VarName.ToString())); } - MakeErrorJson(Result, FString::Printf( + MCPUtils::MakeErrorJson(Result, FString::Printf( TEXT("Variable '%s' not found in Blueprint '%s'"), *VariableName, *BlueprintName)); Result->SetArrayField(TEXT("availableVariables"), AvailVars); return; @@ -372,7 +373,7 @@ void FBlueprintMCPServer::HandleRemoveVariable(const FJsonObject* Json, FJsonObj FBlueprintEditorUtils::RemoveMemberVariable(BP, VarFName); FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP); - bool bSaved = SaveBlueprintPackage(BP); + bool bSaved = MCPUtils::SaveBlueprintPackage(BP); UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Removed variable '%s' from '%s' (saved: %s)"), *VariableName, *BlueprintName, bSaved ? TEXT("true") : TEXT("false")); @@ -394,7 +395,7 @@ void FBlueprintMCPServer::HandleSetVariableMetadata(const FJsonObject* Json, FJs if (BlueprintName.IsEmpty() || VariableName.IsEmpty()) { - return MakeErrorJson(Result, TEXT("Missing required fields: blueprint, variable")); + return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: blueprint, variable")); } // Load Blueprint @@ -402,7 +403,7 @@ void FBlueprintMCPServer::HandleSetVariableMetadata(const FJsonObject* Json, FJs UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError); if (!BP) { - return MakeErrorJson(Result, LoadError); + return MCPUtils::MakeErrorJson(Result, LoadError); } // Find the variable @@ -424,7 +425,7 @@ void FBlueprintMCPServer::HandleSetVariableMetadata(const FJsonObject* Json, FJs { AvailableVars.Add(MakeShared(Var.VarName.ToString())); } - MakeErrorJson(Result, FString::Printf( + MCPUtils::MakeErrorJson(Result, FString::Printf( TEXT("Variable '%s' not found in Blueprint '%s'"), *VariableName, *BlueprintName)); Result->SetArrayField(TEXT("availableVariables"), AvailableVars); return; @@ -488,7 +489,7 @@ void FBlueprintMCPServer::HandleSetVariableMetadata(const FJsonObject* Json, FJs } else { - return MakeErrorJson(Result, FString::Printf( + return MCPUtils::MakeErrorJson(Result, FString::Printf( TEXT("Invalid replication value '%s'. Valid: none, replicated, repNotify"), *ReplicationStr)); } @@ -557,7 +558,7 @@ void FBlueprintMCPServer::HandleSetVariableMetadata(const FJsonObject* Json, FJs } else { - return MakeErrorJson(Result, FString::Printf( + return MCPUtils::MakeErrorJson(Result, FString::Printf( TEXT("Invalid editability value '%s'. Valid: editAnywhere, editDefaultsOnly, editInstanceOnly, none"), *Editability)); } @@ -570,14 +571,14 @@ void FBlueprintMCPServer::HandleSetVariableMetadata(const FJsonObject* Json, FJs if (Changes.Num() == 0) { - return MakeErrorJson(Result, TEXT("No metadata fields specified. Provide at least one of: category, tooltip, replication, exposeOnSpawn, isPrivate, editability")); + 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"), *BlueprintName, *VariableName, Changes.Num()); FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP); - bool bSaved = SaveBlueprintPackage(BP); + bool bSaved = MCPUtils::SaveBlueprintPackage(BP); Result->SetBoolField(TEXT("success"), true); Result->SetStringField(TEXT("blueprint"), BlueprintName); diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPServer.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPServer.cpp index 61fbfad8..b95dd58e 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPServer.cpp +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPServer.cpp @@ -1,5 +1,6 @@ #include "BlueprintMCPServer.h" #include "MCPHandler.h" +#include "MCPUtils.h" #include "UObject/StrongObjectPtr.h" #include "Materials/MaterialExpression.h" #include "AssetRegistry/AssetRegistryModule.h" @@ -115,14 +116,6 @@ FBlueprintMCPServer* FBlueprintMCPServer::Get() // Helpers // ============================================================ -FString FBlueprintMCPServer::JsonToString(TSharedRef JsonObj) -{ - FString Output; - TSharedRef> Writer = TJsonWriterFactory<>::Create(&Output); - FJsonSerializer::Serialize(JsonObj, Writer); - return Output; -} - FAssetData* FBlueprintMCPServer::FindAnyAsset(const FString& NameOrPath) { FAssetData* Asset = FindBlueprintAsset(NameOrPath); @@ -210,105 +203,71 @@ UBlueprint* FBlueprintMCPServer::LoadBlueprintByName(const FString& NameOrPath, return nullptr; } -TSharedPtr FBlueprintMCPServer::ParseBodyJson(const FString& Body) +UMaterial* FBlueprintMCPServer::LoadMaterialByName(const FString& NameOrPath, FString& OutError) { - TSharedPtr JsonObj; - TSharedRef> Reader = TJsonReaderFactory<>::Create(Body); - FJsonSerializer::Deserialize(Reader, JsonObj); - return JsonObj; -} - -FString FBlueprintMCPServer::MakeErrorJson(const FString& Message) -{ - TSharedRef E = MakeShared(); - E->SetStringField(TEXT("error"), Message); - return JsonToString(E); -} - -void FBlueprintMCPServer::MakeErrorJson(FJsonObject* Result, const FString& Message) -{ - Result->Values.Empty(); - Result->SetStringField(TEXT("error"), Message); -} - -void FBlueprintMCPServer::CopyJsonFields(const FJsonObject* Source, FJsonObject* Dest) -{ - for (const auto& KV : Source->Values) + FAssetData* Asset = FindMaterialAsset(NameOrPath); + if (Asset) { - Dest->SetField(KV.Key, KV.Value); + UMaterial* Mat = Cast(Asset->GetAsset()); + if (Mat) return Mat; } + OutError = FString::Printf(TEXT("Material '%s' not found. Use list_materials to see available assets."), *NameOrPath); + return nullptr; } -FString FBlueprintMCPServer::UrlDecode(const FString& EncodedString) +FAssetData* FBlueprintMCPServer::FindMaterialInstanceAsset(const FString& NameOrPath) { - FString Result; - Result.Reserve(EncodedString.Len()); - - for (int32 i = 0; i < EncodedString.Len(); ++i) + for (FAssetData& Asset : AllMaterialInstanceAssets) { - TCHAR C = EncodedString[i]; - if (C == TEXT('+')) - { - Result += TEXT(' '); - } - else if (C == TEXT('%') && i + 2 < EncodedString.Len()) - { - FString HexStr = EncodedString.Mid(i + 1, 2); - int32 HexVal = 0; - bool bValid = true; - for (TCHAR H : HexStr) - { - HexVal <<= 4; - if (H >= TEXT('0') && H <= TEXT('9')) - HexVal += H - TEXT('0'); - else if (H >= TEXT('a') && H <= TEXT('f')) - HexVal += 10 + H - TEXT('a'); - else if (H >= TEXT('A') && H <= TEXT('F')) - HexVal += 10 + H - TEXT('A'); - else - { - bValid = false; - break; - } - } - if (bValid) - { - Result += (TCHAR)HexVal; - i += 2; - } - else - { - Result += C; - } - } - else - { - Result += C; - } + if (Asset.AssetName.ToString() == NameOrPath || Asset.PackageName.ToString() == NameOrPath) + return &Asset; } - return Result; + for (FAssetData& Asset : AllMaterialInstanceAssets) + { + if (Asset.AssetName.ToString().Equals(NameOrPath, ESearchCase::IgnoreCase) || + Asset.PackageName.ToString().Equals(NameOrPath, ESearchCase::IgnoreCase)) + return &Asset; + } + return nullptr; } -UEdGraphNode* FBlueprintMCPServer::FindNodeByGuid( - UBlueprint* BP, const FString& GuidString, UEdGraph** OutGraph) +UMaterialInstanceConstant* FBlueprintMCPServer::LoadMaterialInstanceByName(const FString& NameOrPath, FString& OutError) { - FGuid TargetGuid; - FGuid::Parse(GuidString, TargetGuid); - - TArray AllGraphs; - BP->GetAllGraphs(AllGraphs); - - for (UEdGraph* Graph : AllGraphs) + FAssetData* Asset = FindMaterialInstanceAsset(NameOrPath); + if (Asset) { - for (UEdGraphNode* Node : Graph->Nodes) - { - if (Node && Node->NodeGuid == TargetGuid) - { - if (OutGraph) *OutGraph = Graph; - return Node; - } - } + UMaterialInstanceConstant* MI = Cast(Asset->GetAsset()); + if (MI) return MI; } + OutError = FString::Printf(TEXT("Material Instance '%s' not found. Use list_materials to see available assets."), *NameOrPath); + return nullptr; +} + +FAssetData* FBlueprintMCPServer::FindMaterialFunctionAsset(const FString& NameOrPath) +{ + for (FAssetData& Asset : AllMaterialFunctionAssets) + { + if (Asset.AssetName.ToString() == NameOrPath || Asset.PackageName.ToString() == NameOrPath) + return &Asset; + } + for (FAssetData& Asset : AllMaterialFunctionAssets) + { + if (Asset.AssetName.ToString().Equals(NameOrPath, ESearchCase::IgnoreCase) || + Asset.PackageName.ToString().Equals(NameOrPath, ESearchCase::IgnoreCase)) + return &Asset; + } + return nullptr; +} + +UMaterialFunction* FBlueprintMCPServer::LoadMaterialFunctionByName(const FString& NameOrPath, FString& OutError) +{ + FAssetData* Asset = FindMaterialFunctionAsset(NameOrPath); + if (Asset) + { + UMaterialFunction* MF = Cast(Asset->GetAsset()); + if (MF) return MF; + } + OutError = FString::Printf(TEXT("Material Function '%s' not found. Use list_material_functions to see available assets."), *NameOrPath); return nullptr; } @@ -524,7 +483,7 @@ bool FBlueprintMCPServer::Start(int32 InPort, bool bEditorMode) J->SetNumberField(TEXT("materialInstanceCount"), AllMaterialInstanceAssets.Num()); J->SetNumberField(TEXT("materialFunctionCount"), AllMaterialFunctionAssets.Num()); TUniquePtr R = FHttpServerResponse::Create( - JsonToString(J), TEXT("application/json")); + MCPUtils::JsonToString(J), TEXT("application/json")); OnComplete(MoveTemp(R)); return true; })); @@ -545,7 +504,7 @@ bool FBlueprintMCPServer::Start(int32 InPort, bool bEditorMode) RequestEngineExit(TEXT("BlueprintMCP /api/shutdown")); } TUniquePtr R = FHttpServerResponse::Create( - JsonToString(J), TEXT("application/json")); + MCPUtils::JsonToString(J), TEXT("application/json")); OnComplete(MoveTemp(R)); return true; })); @@ -567,7 +526,7 @@ bool FBlueprintMCPServer::Start(int32 InPort, bool bEditorMode) TSharedRef ListResult = MakeShared(); HandleList(&*Params, &*ListResult); TUniquePtr R = FHttpServerResponse::Create( - JsonToString(ListResult), TEXT("application/json")); + MCPUtils::JsonToString(ListResult), TEXT("application/json")); OnComplete(MoveTemp(R)); return true; })); @@ -665,7 +624,7 @@ bool FBlueprintMCPServer::Start(int32 InPort, bool bEditorMode) TSharedRef ListResult = MakeShared(); HandleListMaterials(&*Params, &*ListResult); TUniquePtr R = FHttpServerResponse::Create( - JsonToString(ListResult), TEXT("application/json")); + MCPUtils::JsonToString(ListResult), TEXT("application/json")); OnComplete(MoveTemp(R)); return true; })); @@ -721,7 +680,7 @@ bool FBlueprintMCPServer::Start(int32 InPort, bool bEditorMode) TSharedRef ListResult = MakeShared(); HandleListMaterialFunctions(&*Params, &*ListResult); TUniquePtr R = FHttpServerResponse::Create( - JsonToString(ListResult), TEXT("application/json")); + MCPUtils::JsonToString(ListResult), TEXT("application/json")); OnComplete(MoveTemp(R)); return true; })); @@ -841,7 +800,7 @@ bool FBlueprintMCPServer::ProcessOneRequest() TSharedPtr Params; if (!Req->Body.IsEmpty()) { - Params = ParseBodyJson(Req->Body); + Params = MCPUtils::ParseBodyJson(Req->Body); } if (!Params.IsValid()) { @@ -864,14 +823,14 @@ bool FBlueprintMCPServer::ProcessOneRequest() TStrongObjectPtr HandlerObj(NewObject(GetTransientPackage(), *HandlerClass)); IMCPHandler* Handler = Cast(HandlerObj.Get()); - FString PopulateError = PopulateFromJson(HandlerObj->GetClass(), HandlerObj.Get(), Params.Get()); + FString PopulateError = MCPUtils::PopulateFromJson(HandlerObj->GetClass(), HandlerObj.Get(), Params.Get()); if (PopulateError.IsEmpty()) { Handler->Handle(Params.Get(), &*Result); } else { - MakeErrorJson(&*Result, PopulateError); + MCPUtils::MakeErrorJson(&*Result, PopulateError); } if (bIsMutation && GEditor) @@ -897,11 +856,11 @@ bool FBlueprintMCPServer::ProcessOneRequest() } else { - MakeErrorJson(&*Result, FString::Printf(TEXT("Unknown endpoint: %s"), *Req->Endpoint)); + MCPUtils::MakeErrorJson(&*Result, FString::Printf(TEXT("Unknown endpoint: %s"), *Req->Endpoint)); } // Send the response back via the HTTP callback (non-blocking) - FString Response = JsonToString(Result); + FString Response = MCPUtils::JsonToString(Result); TUniquePtr HttpResp = FHttpServerResponse::Create( Response, TEXT("application/json")); Req->OnComplete(MoveTemp(HttpResp)); @@ -1139,728 +1098,9 @@ void FBlueprintMCPServer::HandleRescan(const FJsonObject* Json, FJsonObject* Res Result->SetObjectField(TEXT("delta"), Delta); } -// ============================================================ -// SaveBlueprintPackage // ============================================================ -bool FBlueprintMCPServer::SaveBlueprintPackage(UBlueprint* BP) -{ - UPackage* Package = BP->GetPackage(); - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: SaveBlueprintPackage — begin for '%s'"), *BP->GetName()); - - // 1. Build absolute package filename — use .umap for map packages, .uasset otherwise - FString PackageExtension = Package->ContainsMap() - ? FPackageName::GetMapPackageExtension() - : FPackageName::GetAssetPackageExtension(); - FString PackageFilename = FPackageName::LongPackageNameToFilename( - Package->GetName(), PackageExtension); - PackageFilename = FPaths::ConvertRelativePathToFull(PackageFilename); - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Save target: %s"), *PackageFilename); - - // 2. Phase 1: Try explicit compilation (same flags as UCompileAllBlueprintsCommandlet) - bool bCompiled = false; - { - EBlueprintCompileOptions CompileOpts = - EBlueprintCompileOptions::SkipSave | - EBlueprintCompileOptions::BatchCompile | - EBlueprintCompileOptions::SkipGarbageCollection | - EBlueprintCompileOptions::SkipFiBSearchMetaUpdate; - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Phase 1: Attempting explicit compilation...")); - -#if PLATFORM_WINDOWS - int32 CompileResult = TryCompileBlueprintSEH(BP, CompileOpts); - if (CompileResult == 0) - { - bCompiled = (BP->Status == BS_UpToDate); - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Compilation %s (status=%d)"), - bCompiled ? TEXT("succeeded") : TEXT("completed with warnings"), (int32)BP->Status); - } - else - { - UE_LOG(LogTemp, Warning, TEXT("BlueprintMCP: Compilation crashed (SEH), proceeding uncompiled")); - } -#else - FKismetEditorUtilities::CompileBlueprint(BP, CompileOpts, nullptr); - bCompiled = (BP->Status == BS_UpToDate); -#endif - } - - // 3. Phase 2: Set guards for save - uint8 OldRegen = BP->bIsRegeneratingOnLoad; - BP->bIsRegeneratingOnLoad = true; - - EBlueprintStatus OldStatus = (EBlueprintStatus)(uint8)BP->Status; - if (!bCompiled) - { - // Tell PreSave the BP is up-to-date so it doesn't try to compile - BP->Status = BS_UpToDate; - } - - // 4. Clear read-only attribute if present (source control or LFS may set this) - if (FPlatformFileManager::Get().GetPlatformFile().IsReadOnly(*PackageFilename)) - { - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Clearing read-only attribute on %s"), *PackageFilename); - FPlatformFileManager::Get().GetPlatformFile().SetReadOnly(*PackageFilename, false); - } - - // 5. Phase 3: Save with SAVE_NoError + SEH protection - FSavePackageArgs SaveArgs; - SaveArgs.TopLevelFlags = RF_Public | RF_Standalone; - SaveArgs.SaveFlags = SAVE_NoError; - - // For level blueprints (map packages), the base object should be the UWorld, not the BP - bool bIsMapPackage = Package->ContainsMap(); - UObject* BaseObject = BP; - if (bIsMapPackage) - { - // Find the UWorld in this package — it's the actual asset for .umap files - UWorld* World = FindObject(Package, *Package->GetName().Mid(Package->GetName().Find(TEXT("/"), ESearchCase::IgnoreCase, ESearchDir::FromEnd) + 1)); - if (!World) - { - // Fallback: iterate the package to find any UWorld - ForEachObjectWithPackage(Package, [&World](UObject* Obj) { - if (UWorld* W = Cast(Obj)) - { - World = W; - return false; // stop - } - return true; // continue - }); - } - if (World) - { - BaseObject = World; - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Map package detected — saving UWorld '%s'"), *World->GetName()); - } - else - { - UE_LOG(LogTemp, Warning, TEXT("BlueprintMCP: Map package detected but no UWorld found — saving with BP as base")); - } - } - - ESavePackageResult SaveResult = ESavePackageResult::Error; - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Phase 3: Calling UPackage::Save (compiled=%s, isMap=%s)..."), - bCompiled ? TEXT("yes") : TEXT("no"), bIsMapPackage ? TEXT("yes") : TEXT("no")); - -#if PLATFORM_WINDOWS - int32 SEHCode = TrySavePackageSEH(Package, BaseObject, *PackageFilename, &SaveArgs, &SaveResult); - if (SEHCode != 0) - { - UE_LOG(LogTemp, Error, TEXT("BlueprintMCP: UPackage::Save CRASHED (SEH exception caught)")); - } -#else - FSavePackageResultStruct Result = UPackage::Save(Package, BaseObject, *PackageFilename, SaveArgs); - SaveResult = Result.Result; -#endif - - // 6. Restore guards - BP->bIsRegeneratingOnLoad = OldRegen; - if (!bCompiled) - { - BP->Status = (TEnumAsByte)OldStatus; - } - - bool bSuccess = (SaveResult == ESavePackageResult::Success); - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: SaveBlueprintPackage — %s for '%s' (compiled=%s, result=%d)"), - bSuccess ? TEXT("SUCCEEDED") : TEXT("FAILED"), - *BP->GetName(), bCompiled ? TEXT("yes") : TEXT("no"), (int32)SaveResult); - - return bSuccess; -} - -// ============================================================ -// Blueprint serialization (graphs / nodes / pins) -// ============================================================ - -TSharedRef FBlueprintMCPServer::SerializeBlueprint(UBlueprint* BP) -{ - TSharedRef J = MakeShared(); - J->SetStringField(TEXT("name"), BP->GetName()); - J->SetStringField(TEXT("path"), BP->GetPackage()->GetName()); - J->SetStringField(TEXT("parentClass"), BP->ParentClass ? BP->ParentClass->GetName() : TEXT("None")); - J->SetStringField(TEXT("blueprintType"), - StaticEnum()->GetNameStringByValue((int64)BP->BlueprintType)); - - // Animation Blueprint detection - if (UAnimBlueprint* AnimBP = Cast(BP)) - { - J->SetBoolField(TEXT("isAnimBlueprint"), true); - if (AnimBP->TargetSkeleton) - { - J->SetStringField(TEXT("targetSkeleton"), AnimBP->TargetSkeleton->GetName()); - J->SetStringField(TEXT("targetSkeletonPath"), AnimBP->TargetSkeleton->GetPathName()); - } - } - - // Variables - TArray> Vars; - for (const FBPVariableDescription& V : BP->NewVariables) - { - TSharedRef VJ = MakeShared(); - VJ->SetStringField(TEXT("name"), V.VarName.ToString()); - VJ->SetStringField(TEXT("type"), V.VarType.PinCategory.ToString()); - if (V.VarType.PinSubCategoryObject.IsValid()) - VJ->SetStringField(TEXT("subtype"), V.VarType.PinSubCategoryObject->GetName()); - VJ->SetBoolField(TEXT("isArray"), V.VarType.IsArray()); - VJ->SetBoolField(TEXT("isSet"), V.VarType.IsSet()); - VJ->SetBoolField(TEXT("isMap"), V.VarType.IsMap()); - VJ->SetStringField(TEXT("category"), V.Category.ToString()); - VJ->SetStringField(TEXT("defaultValue"), V.DefaultValue); - Vars.Add(MakeShared(VJ)); - } - J->SetArrayField(TEXT("variables"), Vars); - - // Interfaces - TArray> Ifaces; - for (const FBPInterfaceDescription& I : BP->ImplementedInterfaces) - { - if (I.Interface) - Ifaces.Add(MakeShared(I.Interface->GetName())); - } - J->SetArrayField(TEXT("interfaces"), Ifaces); - - // Graphs - TArray> GraphArr; - TArray AllGraphs; - BP->GetAllGraphs(AllGraphs); - for (UEdGraph* Graph : AllGraphs) - { - if (!Graph) continue; - TSharedPtr GJ = SerializeGraph(Graph); - if (GJ.IsValid()) - GraphArr.Add(MakeShared(GJ.ToSharedRef())); - } - J->SetArrayField(TEXT("graphs"), GraphArr); - return J; -} - -TSharedPtr FBlueprintMCPServer::SerializeGraph(UEdGraph* Graph) -{ - TSharedRef GJ = MakeShared(); - GJ->SetStringField(TEXT("name"), Graph->GetName()); - GJ->SetStringField(TEXT("schema"), Graph->Schema ? Graph->Schema->GetClass()->GetName() : TEXT("Unknown")); - - // Detect animation graph subtypes - if (Cast(Graph)) - { - GJ->SetStringField(TEXT("graphType"), TEXT("StateMachine")); - // Find entry state by following entry node's output pin - for (UEdGraphNode* Node : Graph->Nodes) - { - if (UAnimStateEntryNode* EntryNode = Cast(Node)) - { - for (UEdGraphPin* Pin : EntryNode->Pins) - { - if (Pin->Direction == EGPD_Output && Pin->LinkedTo.Num() > 0) - { - UEdGraphNode* LinkedNode = Pin->LinkedTo[0]->GetOwningNode(); - if (UAnimStateNode* StateNode = Cast(LinkedNode)) - { - GJ->SetStringField(TEXT("entryState"), StateNode->GetStateName()); - } - } - } - break; - } - } - } - else if (Cast(Graph)) - { - GJ->SetStringField(TEXT("graphType"), TEXT("AnimGraph")); - } - else if (Cast(Graph)) - { - GJ->SetStringField(TEXT("graphType"), TEXT("TransitionRule")); - } - - TArray> Nodes; - for (UEdGraphNode* Node : Graph->Nodes) - { - if (!Node) continue; - TSharedPtr NJ = SerializeNode(Node); - if (NJ.IsValid()) - Nodes.Add(MakeShared(NJ.ToSharedRef())); - } - GJ->SetArrayField(TEXT("nodes"), Nodes); - return GJ; -} - -TSharedPtr FBlueprintMCPServer::SerializeNode(UEdGraphNode* Node) -{ - TSharedRef NJ = MakeShared(); - NJ->SetStringField(TEXT("id"), Node->NodeGuid.ToString()); - NJ->SetStringField(TEXT("class"), Node->GetClass()->GetName()); - NJ->SetStringField(TEXT("title"), Node->GetNodeTitle(ENodeTitleType::FullTitle).ToString()); - if (!Node->NodeComment.IsEmpty()) - NJ->SetStringField(TEXT("comment"), Node->NodeComment); - NJ->SetNumberField(TEXT("posX"), Node->NodePosX); - NJ->SetNumberField(TEXT("posY"), Node->NodePosY); - - // Material graph node — extract UMaterialExpression data - if (UMaterialGraphNode* MatNode = Cast(Node)) - { - NJ->SetStringField(TEXT("nodeType"), TEXT("MaterialExpression")); - if (MatNode->MaterialExpression) - { - TSharedPtr ExprJson = SerializeMaterialExpression(MatNode->MaterialExpression); - if (ExprJson.IsValid()) - { - NJ->SetObjectField(TEXT("expression"), ExprJson); - } - } - } - // Animation Blueprint node types - else if (auto* SMNode = Cast(Node)) - { - NJ->SetStringField(TEXT("nodeType"), TEXT("AnimStateMachine")); - if (SMNode->EditorStateMachineGraph) - { - NJ->SetStringField(TEXT("stateMachineName"), SMNode->EditorStateMachineGraph->GetName()); - int32 StateCount = 0, TransitionCount = 0; - for (UEdGraphNode* SubNode : SMNode->EditorStateMachineGraph->Nodes) - { - if (Cast(SubNode)) StateCount++; - else if (Cast(SubNode)) TransitionCount++; - } - NJ->SetNumberField(TEXT("stateCount"), StateCount); - NJ->SetNumberField(TEXT("transitionCount"), TransitionCount); - } - } - else if (auto* SeqPlayer = Cast(Node)) - { - NJ->SetStringField(TEXT("nodeType"), TEXT("AnimSequencePlayer")); - if (UAnimationAsset* Asset = SeqPlayer->GetAnimationAsset()) - { - NJ->SetStringField(TEXT("animationAsset"), Asset->GetName()); - NJ->SetStringField(TEXT("animationAssetPath"), Asset->GetPathName()); - } - } - else if (auto* BSPlayer = Cast(Node)) - { - NJ->SetStringField(TEXT("nodeType"), TEXT("AnimBlendSpacePlayer")); - if (UAnimationAsset* Asset = BSPlayer->GetAnimationAsset()) - { - NJ->SetStringField(TEXT("blendSpaceAsset"), Asset->GetName()); - NJ->SetStringField(TEXT("blendSpaceAssetPath"), Asset->GetPathName()); - } - } - else if (auto* AssetPlayer = Cast(Node)) - { - NJ->SetStringField(TEXT("nodeType"), TEXT("AnimAssetPlayer")); - if (UAnimationAsset* Asset = AssetPlayer->GetAnimationAsset()) - { - NJ->SetStringField(TEXT("animationAsset"), Asset->GetName()); - NJ->SetStringField(TEXT("animationAssetPath"), Asset->GetPathName()); - } - } - else if (Cast(Node)) - { - NJ->SetStringField(TEXT("nodeType"), TEXT("AnimNode")); - } - else if (auto* StateNode = Cast(Node)) - { - NJ->SetStringField(TEXT("nodeType"), TEXT("AnimState")); - NJ->SetStringField(TEXT("stateName"), StateNode->GetStateName()); - NJ->SetBoolField(TEXT("bAlwaysResetOnEntry"), StateNode->bAlwaysResetOnEntry); - } - else if (auto* TransNode = Cast(Node)) - { - NJ->SetStringField(TEXT("nodeType"), TEXT("AnimTransition")); - if (UAnimStateNode* FromState = Cast(TransNode->GetPreviousState())) - { - NJ->SetStringField(TEXT("fromState"), FromState->GetStateName()); - } - if (UAnimStateNode* ToState = Cast(TransNode->GetNextState())) - { - NJ->SetStringField(TEXT("toState"), ToState->GetStateName()); - } - NJ->SetNumberField(TEXT("crossfadeDuration"), TransNode->CrossfadeDuration); - NJ->SetNumberField(TEXT("blendMode"), (int32)TransNode->BlendMode); - NJ->SetNumberField(TEXT("priorityOrder"), TransNode->PriorityOrder); - NJ->SetNumberField(TEXT("logicType"), (int32)TransNode->LogicType.GetValue()); - NJ->SetBoolField(TEXT("bBidirectional"), TransNode->Bidirectional); - } - else if (Cast(Node)) - { - NJ->SetStringField(TEXT("nodeType"), TEXT("AnimConduit")); - } - else if (Cast(Node)) - { - NJ->SetStringField(TEXT("nodeType"), TEXT("AnimStateEntry")); - } - // K2Node specifics — check CallParentFunction before CallFunction (inheritance) - else if (auto* CPF = Cast(Node)) - { - NJ->SetStringField(TEXT("functionName"), CPF->FunctionReference.GetMemberName().ToString()); - if (CPF->FunctionReference.GetMemberParentClass()) - NJ->SetStringField(TEXT("targetClass"), CPF->FunctionReference.GetMemberParentClass()->GetName()); - NJ->SetStringField(TEXT("nodeType"), TEXT("CallParentFunction")); - } - else if (auto* CF = Cast(Node)) - { - NJ->SetStringField(TEXT("functionName"), CF->FunctionReference.GetMemberName().ToString()); - if (CF->FunctionReference.GetMemberParentClass()) - NJ->SetStringField(TEXT("targetClass"), CF->FunctionReference.GetMemberParentClass()->GetName()); - } - else if (auto* FE = Cast(Node)) - { - NJ->SetStringField(TEXT("nodeType"), TEXT("FunctionEntry")); - - // Serialize UserDefinedPins (parameter names and types) - TArray> ParamArr; - for (const TSharedPtr& PinInfo : FE->UserDefinedPins) - { - if (!PinInfo.IsValid()) continue; - TSharedRef ParamJ = MakeShared(); - ParamJ->SetStringField(TEXT("name"), PinInfo->PinName.ToString()); - FString ParamType = PinInfo->PinType.PinCategory.ToString(); - ParamJ->SetStringField(TEXT("type"), ParamType); - if (PinInfo->PinType.PinSubCategoryObject.IsValid()) - ParamJ->SetStringField(TEXT("subtype"), PinInfo->PinType.PinSubCategoryObject->GetName()); - else if (ParamType == TEXT("None") || ParamType.IsEmpty()) - ParamJ->SetBoolField(TEXT("typeUnknown"), true); - ParamArr.Add(MakeShared(ParamJ)); - } - NJ->SetArrayField(TEXT("parameters"), ParamArr); - } - else if (auto* Ev = Cast(Node)) - { - NJ->SetStringField(TEXT("eventName"), Ev->EventReference.GetMemberName().ToString()); - NJ->SetStringField(TEXT("nodeType"), Ev->bOverrideFunction ? TEXT("OverrideEvent") : TEXT("Event")); - } - else if (auto* CE = Cast(Node)) - { - NJ->SetStringField(TEXT("eventName"), CE->CustomFunctionName.ToString()); - NJ->SetStringField(TEXT("nodeType"), TEXT("CustomEvent")); - - // Serialize UserDefinedPins (parameter names and types) - TArray> ParamArr; - for (const TSharedPtr& PinInfo : CE->UserDefinedPins) - { - if (!PinInfo.IsValid()) continue; - TSharedRef ParamJ = MakeShared(); - ParamJ->SetStringField(TEXT("name"), PinInfo->PinName.ToString()); - FString ParamType = PinInfo->PinType.PinCategory.ToString(); - ParamJ->SetStringField(TEXT("type"), ParamType); - if (PinInfo->PinType.PinSubCategoryObject.IsValid()) - ParamJ->SetStringField(TEXT("subtype"), PinInfo->PinType.PinSubCategoryObject->GetName()); - else if (ParamType == TEXT("None") || ParamType.IsEmpty()) - ParamJ->SetBoolField(TEXT("typeUnknown"), true); - ParamArr.Add(MakeShared(ParamJ)); - } - NJ->SetArrayField(TEXT("parameters"), ParamArr); - } - else if (auto* VG = Cast(Node)) - { - NJ->SetStringField(TEXT("variableName"), VG->GetVarName().ToString()); - NJ->SetStringField(TEXT("nodeType"), TEXT("VariableGet")); - } - else if (auto* VS = Cast(Node)) - { - NJ->SetStringField(TEXT("variableName"), VS->GetVarName().ToString()); - NJ->SetStringField(TEXT("nodeType"), TEXT("VariableSet")); - } - else if (auto* MI = Cast(Node)) - { - if (MI->GetMacroGraph()) - NJ->SetStringField(TEXT("macroName"), MI->GetMacroGraph()->GetName()); - NJ->SetStringField(TEXT("nodeType"), TEXT("MacroInstance")); - } - else if (auto* DC = Cast(Node)) - { - if (DC->TargetType) - NJ->SetStringField(TEXT("castTarget"), DC->TargetType->GetName()); - NJ->SetStringField(TEXT("nodeType"), TEXT("DynamicCast")); - } - else if (Cast(Node)) - { - NJ->SetStringField(TEXT("nodeType"), TEXT("Branch")); - } - - // Pins - TArray> Pins; - for (UEdGraphPin* Pin : Node->Pins) - { - if (!Pin || Pin->bHidden) continue; - TSharedPtr PJ = SerializePin(Pin); - if (PJ.IsValid()) - Pins.Add(MakeShared(PJ.ToSharedRef())); - } - NJ->SetArrayField(TEXT("pins"), Pins); - return NJ; -} - -TSharedPtr FBlueprintMCPServer::SerializePin(UEdGraphPin* Pin) -{ - TSharedRef PJ = MakeShared(); - PJ->SetStringField(TEXT("name"), Pin->PinName.ToString()); - PJ->SetStringField(TEXT("direction"), Pin->Direction == EGPD_Input ? TEXT("Input") : TEXT("Output")); - PJ->SetStringField(TEXT("type"), Pin->PinType.PinCategory.ToString()); - if (Pin->PinType.PinSubCategoryObject.IsValid()) - PJ->SetStringField(TEXT("subtype"), Pin->PinType.PinSubCategoryObject->GetName()); - if (!Pin->DefaultValue.IsEmpty()) - PJ->SetStringField(TEXT("defaultValue"), Pin->DefaultValue); - - 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()); - Conns.Add(MakeShared(CJ)); - } - PJ->SetArrayField(TEXT("connections"), Conns); - } - return PJ; -} - -// ============================================================ -// FindClassByName — locate a UClass by name (C++ or Blueprint) -// ============================================================ - -UClass* FBlueprintMCPServer::FindClassByName(const FString& ClassName) -{ - // Exact match first (handles both C++ classes and Blueprint _C classes) - for (TObjectIterator It; It; ++It) - { - FString Name = It->GetName(); - if (Name == ClassName || Name == ClassName + TEXT("_C")) - { - return *It; - } - } - - // Case-insensitive fallback - for (TObjectIterator It; It; ++It) - { - FString Name = It->GetName(); - if (Name.Equals(ClassName, ESearchCase::IgnoreCase) || - Name.Equals(ClassName + TEXT("_C"), ESearchCase::IgnoreCase)) - { - return *It; - } - } - - return nullptr; -} - -// ============================================================ -// ResolveTypeFromString — shared type resolution helper -// ============================================================ - -bool FBlueprintMCPServer::ResolveTypeFromString( - const FString& TypeName, FEdGraphPinType& OutPinType, FString& OutError) -{ - FString TypeLower = TypeName.ToLower(); - - if (TypeLower == TEXT("bool") || TypeLower == TEXT("boolean")) - { - OutPinType.PinCategory = UEdGraphSchema_K2::PC_Boolean; - } - else if (TypeLower == TEXT("int") || TypeLower == TEXT("int32") || TypeLower == TEXT("integer")) - { - OutPinType.PinCategory = UEdGraphSchema_K2::PC_Int; - } - else if (TypeLower == TEXT("int64")) - { - OutPinType.PinCategory = UEdGraphSchema_K2::PC_Int64; - } - else if (TypeLower == TEXT("float") || TypeLower == TEXT("double") || TypeLower == TEXT("real")) - { - OutPinType.PinCategory = UEdGraphSchema_K2::PC_Real; - OutPinType.PinSubCategory = TEXT("double"); - } - else if (TypeLower == TEXT("string")) - { - OutPinType.PinCategory = UEdGraphSchema_K2::PC_String; - } - else if (TypeLower == TEXT("name")) - { - OutPinType.PinCategory = UEdGraphSchema_K2::PC_Name; - } - else if (TypeLower == TEXT("text")) - { - OutPinType.PinCategory = UEdGraphSchema_K2::PC_Text; - } - else if (TypeLower == TEXT("byte")) - { - OutPinType.PinCategory = UEdGraphSchema_K2::PC_Byte; - } - else if (TypeLower == TEXT("vector") || TypeLower == TEXT("fvector")) - { - OutPinType.PinCategory = UEdGraphSchema_K2::PC_Struct; - OutPinType.PinSubCategoryObject = TBaseStructure::Get(); - } - else if (TypeLower == TEXT("rotator") || TypeLower == TEXT("frotator")) - { - OutPinType.PinCategory = UEdGraphSchema_K2::PC_Struct; - OutPinType.PinSubCategoryObject = TBaseStructure::Get(); - } - else if (TypeLower == TEXT("transform") || TypeLower == TEXT("ftransform")) - { - OutPinType.PinCategory = UEdGraphSchema_K2::PC_Struct; - OutPinType.PinSubCategoryObject = TBaseStructure::Get(); - } - else if (TypeLower == TEXT("linearcolor") || TypeLower == TEXT("flinearcolor")) - { - OutPinType.PinCategory = UEdGraphSchema_K2::PC_Struct; - OutPinType.PinSubCategoryObject = TBaseStructure::Get(); - } - else if (TypeLower == TEXT("vector2d") || TypeLower == TEXT("fvector2d")) - { - OutPinType.PinCategory = UEdGraphSchema_K2::PC_Struct; - OutPinType.PinSubCategoryObject = TBaseStructure::Get(); - } - else if (TypeLower == TEXT("object")) - { - OutPinType.PinCategory = UEdGraphSchema_K2::PC_Object; - OutPinType.PinSubCategoryObject = UObject::StaticClass(); - } - else if (TypeName.StartsWith(TEXT("object:"), ESearchCase::IgnoreCase)) - { - FString ClassName = TypeName.Mid(7); // after "object:" - UClass* FoundClass = FindClassByName(ClassName); - if (!FoundClass) - { - OutError = FString::Printf(TEXT("Class '%s' not found for object reference type"), *ClassName); - return false; - } - OutPinType.PinCategory = UEdGraphSchema_K2::PC_Object; - OutPinType.PinSubCategoryObject = FoundClass; - } - else if (TypeName.StartsWith(TEXT("softobject:"), ESearchCase::IgnoreCase)) - { - FString ClassName = TypeName.Mid(11); // after "softobject:" - UClass* FoundClass = FindClassByName(ClassName); - if (!FoundClass) - { - OutError = FString::Printf(TEXT("Class '%s' not found for soft object reference type"), *ClassName); - return false; - } - OutPinType.PinCategory = UEdGraphSchema_K2::PC_SoftObject; - OutPinType.PinSubCategoryObject = FoundClass; - } - else if (TypeName.StartsWith(TEXT("class:"), ESearchCase::IgnoreCase)) - { - FString ClassName = TypeName.Mid(6); // after "class:" - UClass* FoundClass = FindClassByName(ClassName); - if (!FoundClass) - { - OutError = FString::Printf(TEXT("Class '%s' not found for class reference type (TSubclassOf)"), *ClassName); - return false; - } - OutPinType.PinCategory = UEdGraphSchema_K2::PC_Class; - OutPinType.PinSubCategoryObject = FoundClass; - } - else if (TypeName.StartsWith(TEXT("softclass:"), ESearchCase::IgnoreCase)) - { - FString ClassName = TypeName.Mid(10); // after "softclass:" - UClass* FoundClass = FindClassByName(ClassName); - if (!FoundClass) - { - OutError = FString::Printf(TEXT("Class '%s' not found for soft class reference type"), *ClassName); - return false; - } - OutPinType.PinCategory = UEdGraphSchema_K2::PC_SoftClass; - OutPinType.PinSubCategoryObject = FoundClass; - } - else if (TypeName.StartsWith(TEXT("interface:"), ESearchCase::IgnoreCase)) - { - FString ClassName = TypeName.Mid(10); // after "interface:" - UClass* FoundClass = FindClassByName(ClassName); - if (!FoundClass) - { - OutError = FString::Printf(TEXT("Class '%s' not found for interface reference type"), *ClassName); - return false; - } - OutPinType.PinCategory = UEdGraphSchema_K2::PC_Interface; - OutPinType.PinSubCategoryObject = FoundClass; - } - else - { - // Try as a struct (F-prefix or raw name) - FString InternalName = TypeName; - bool bTriedAsStruct = false; - - if (TypeName.StartsWith(TEXT("F")) || TypeName.StartsWith(TEXT("S_")) || (!TypeName.StartsWith(TEXT("E")))) - { - if (TypeName.StartsWith(TEXT("F"))) - { - InternalName = TypeName.Mid(1); - } - - UScriptStruct* FoundStruct = FindFirstObject(*InternalName); - if (!FoundStruct) - { - for (TObjectIterator It; It; ++It) - { - if (It->GetName() == InternalName || It->GetName() == TypeName) - { - FoundStruct = *It; - break; - } - } - } - - if (FoundStruct) - { - OutPinType.PinCategory = UEdGraphSchema_K2::PC_Struct; - OutPinType.PinSubCategoryObject = FoundStruct; - bTriedAsStruct = true; - } - } - - if (!bTriedAsStruct) - { - // Try as an enum (E-prefix or raw name) - FString EnumInternalName = TypeName; - if (TypeName.StartsWith(TEXT("E"))) - { - EnumInternalName = TypeName.Mid(1); - } - - UEnum* FoundEnum = FindFirstObject(*EnumInternalName); - if (!FoundEnum) - { - for (TObjectIterator It; It; ++It) - { - if (It->GetName() == EnumInternalName || It->GetName() == TypeName) - { - FoundEnum = *It; - break; - } - } - } - - if (FoundEnum) - { - if (FoundEnum->GetCppForm() == UEnum::ECppForm::EnumClass) - { - OutPinType.PinCategory = UEdGraphSchema_K2::PC_Enum; - } - else - { - OutPinType.PinCategory = UEdGraphSchema_K2::PC_Byte; - } - OutPinType.PinSubCategoryObject = FoundEnum; - } - else - { - OutError = FString::Printf( - TEXT("Unknown type '%s'. Use: bool, int, float, string, name, text, byte, vector, rotator, transform, object, a struct/enum name (e.g. FVector, EMyEnum), or colon syntax for references (object:Actor, softobject:Actor, class:Actor, softclass:Actor, interface:MyInterface)"), - *TypeName); - return false; - } - } - } - - return true; -} +// (SerializeBlueprint, SerializeGraph, SerializeNode, SerializePin moved to MCPUtils.cpp) // ============================================================ // Material helpers @@ -1882,250 +1122,3 @@ FAssetData* FBlueprintMCPServer::FindMaterialAsset(const FString& NameOrPath) return nullptr; } -void FBlueprintMCPServer::EnsureMaterialGraph(UMaterial* Material) -{ - if (!Material) return; - if (!Material->MaterialGraph) - { - // In commandlet/headless mode the MaterialGraph is not auto-created. - // Replicate what the Material Editor does on open (MaterialEditor.cpp:619). - Material->MaterialGraph = CastChecked( - FBlueprintEditorUtils::CreateNewGraph( - Material, NAME_None, - UMaterialGraph::StaticClass(), - UMaterialGraphSchema::StaticClass())); - Material->MaterialGraph->Material = Material; - Material->MaterialGraph->RebuildGraph(); - } -} - -UMaterial* FBlueprintMCPServer::LoadMaterialByName(const FString& NameOrPath, FString& OutError) -{ - FAssetData* Asset = FindMaterialAsset(NameOrPath); - if (Asset) - { - UMaterial* Mat = Cast(Asset->GetAsset()); - if (Mat) return Mat; - } - OutError = FString::Printf(TEXT("Material '%s' not found. Use list_materials to see available assets."), *NameOrPath); - return nullptr; -} - -FAssetData* FBlueprintMCPServer::FindMaterialInstanceAsset(const FString& NameOrPath) -{ - for (FAssetData& Asset : AllMaterialInstanceAssets) - { - if (Asset.AssetName.ToString() == NameOrPath || Asset.PackageName.ToString() == NameOrPath) - return &Asset; - } - for (FAssetData& Asset : AllMaterialInstanceAssets) - { - if (Asset.AssetName.ToString().Equals(NameOrPath, ESearchCase::IgnoreCase) || - Asset.PackageName.ToString().Equals(NameOrPath, ESearchCase::IgnoreCase)) - return &Asset; - } - return nullptr; -} - -UMaterialInstanceConstant* FBlueprintMCPServer::LoadMaterialInstanceByName(const FString& NameOrPath, FString& OutError) -{ - FAssetData* Asset = FindMaterialInstanceAsset(NameOrPath); - if (Asset) - { - UMaterialInstanceConstant* MI = Cast(Asset->GetAsset()); - if (MI) return MI; - } - OutError = FString::Printf(TEXT("Material Instance '%s' not found. Use list_materials to see available assets."), *NameOrPath); - return nullptr; -} - -FAssetData* FBlueprintMCPServer::FindMaterialFunctionAsset(const FString& NameOrPath) -{ - for (FAssetData& Asset : AllMaterialFunctionAssets) - { - if (Asset.AssetName.ToString() == NameOrPath || Asset.PackageName.ToString() == NameOrPath) - return &Asset; - } - for (FAssetData& Asset : AllMaterialFunctionAssets) - { - if (Asset.AssetName.ToString().Equals(NameOrPath, ESearchCase::IgnoreCase) || - Asset.PackageName.ToString().Equals(NameOrPath, ESearchCase::IgnoreCase)) - return &Asset; - } - return nullptr; -} - -UMaterialFunction* FBlueprintMCPServer::LoadMaterialFunctionByName(const FString& NameOrPath, FString& OutError) -{ - FAssetData* Asset = FindMaterialFunctionAsset(NameOrPath); - if (Asset) - { - UMaterialFunction* MF = Cast(Asset->GetAsset()); - if (MF) return MF; - } - OutError = FString::Printf(TEXT("Material Function '%s' not found. Use list_material_functions to see available assets."), *NameOrPath); - return nullptr; -} - -bool FBlueprintMCPServer::SaveMaterialPackage(UMaterial* Material) -{ - if (!Material) return false; - return SaveGenericPackage(Material); -} - -bool FBlueprintMCPServer::SaveGenericPackage(UObject* Asset) -{ - if (!Asset) return false; - UPackage* Package = Asset->GetPackage(); - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: SaveGenericPackage — begin for '%s'"), *Asset->GetName()); - - FString PackageFilename = FPackageName::LongPackageNameToFilename( - Package->GetName(), FPackageName::GetAssetPackageExtension()); - PackageFilename = FPaths::ConvertRelativePathToFull(PackageFilename); - - if (FPlatformFileManager::Get().GetPlatformFile().IsReadOnly(*PackageFilename)) - { - FPlatformFileManager::Get().GetPlatformFile().SetReadOnly(*PackageFilename, false); - } - - FSavePackageArgs SaveArgs; - SaveArgs.TopLevelFlags = RF_Public | RF_Standalone; - SaveArgs.SaveFlags = SAVE_NoError; - - ESavePackageResult SaveResult = ESavePackageResult::Error; -#if PLATFORM_WINDOWS - int32 SEHCode = TrySavePackageSEH(Package, Asset, *PackageFilename, &SaveArgs, &SaveResult); - if (SEHCode != 0) - { - UE_LOG(LogTemp, Error, TEXT("BlueprintMCP: SaveGenericPackage CRASHED (SEH exception)")); - } -#else - FSavePackageResultStruct Result = UPackage::Save(Package, Asset, *PackageFilename, SaveArgs); - SaveResult = Result.Result; -#endif - - bool bSuccess = (SaveResult == ESavePackageResult::Success); - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: SaveGenericPackage — %s for '%s'"), - bSuccess ? TEXT("SUCCEEDED") : TEXT("FAILED"), *Asset->GetName()); - return bSuccess; -} - -TSharedPtr FBlueprintMCPServer::SerializeMaterialExpression(UMaterialExpression* Expression) -{ - if (!Expression) return nullptr; - - TSharedRef EJ = MakeShared(); - EJ->SetStringField(TEXT("class"), Expression->GetClass()->GetName()); - EJ->SetStringField(TEXT("name"), Expression->GetName()); - EJ->SetStringField(TEXT("description"), Expression->GetDescription()); - EJ->SetNumberField(TEXT("posX"), Expression->MaterialExpressionEditorX); - EJ->SetNumberField(TEXT("posY"), Expression->MaterialExpressionEditorY); - - if (auto* SP = Cast(Expression)) - { - EJ->SetStringField(TEXT("expressionType"), TEXT("ScalarParameter")); - EJ->SetStringField(TEXT("parameterName"), SP->ParameterName.ToString()); - EJ->SetNumberField(TEXT("defaultValue"), SP->DefaultValue); - EJ->SetStringField(TEXT("group"), SP->Group.ToString()); - } - else if (auto* VP = Cast(Expression)) - { - EJ->SetStringField(TEXT("expressionType"), TEXT("VectorParameter")); - EJ->SetStringField(TEXT("parameterName"), VP->ParameterName.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); - EJ->SetObjectField(TEXT("defaultValue"), DefVal); - EJ->SetStringField(TEXT("group"), VP->Group.ToString()); - } - else if (auto* TP = Cast(Expression)) - { - EJ->SetStringField(TEXT("expressionType"), TEXT("TextureSampleParameter2D")); - EJ->SetStringField(TEXT("parameterName"), TP->ParameterName.ToString()); - if (TP->Texture) - EJ->SetStringField(TEXT("texture"), TP->Texture->GetPathName()); - EJ->SetStringField(TEXT("group"), TP->Group.ToString()); - } - else if (auto* SSP = Cast(Expression)) - { - EJ->SetStringField(TEXT("expressionType"), TEXT("StaticSwitchParameter")); - EJ->SetStringField(TEXT("parameterName"), SSP->ParameterName.ToString()); - EJ->SetBoolField(TEXT("defaultValue"), SSP->DefaultValue); - EJ->SetStringField(TEXT("group"), SSP->Group.ToString()); - } - else if (auto* SC = Cast(Expression)) - { - EJ->SetStringField(TEXT("expressionType"), TEXT("Constant")); - EJ->SetNumberField(TEXT("value"), SC->R); - } - else if (auto* C3 = Cast(Expression)) - { - EJ->SetStringField(TEXT("expressionType"), TEXT("Constant3Vector")); - TSharedRef Val = MakeShared(); - Val->SetNumberField(TEXT("r"), C3->Constant.R); - Val->SetNumberField(TEXT("g"), C3->Constant.G); - Val->SetNumberField(TEXT("b"), C3->Constant.B); - EJ->SetObjectField(TEXT("value"), Val); - } - else if (auto* C4 = Cast(Expression)) - { - EJ->SetStringField(TEXT("expressionType"), TEXT("Constant4Vector")); - TSharedRef Val = MakeShared(); - Val->SetNumberField(TEXT("r"), C4->Constant.R); - Val->SetNumberField(TEXT("g"), C4->Constant.G); - Val->SetNumberField(TEXT("b"), C4->Constant.B); - Val->SetNumberField(TEXT("a"), C4->Constant.A); - EJ->SetObjectField(TEXT("value"), Val); - } - else if (auto* TS = Cast(Expression)) - { - EJ->SetStringField(TEXT("expressionType"), TEXT("TextureSample")); - if (TS->Texture) - EJ->SetStringField(TEXT("texture"), TS->Texture->GetPathName()); - } - else if (auto* TC = Cast(Expression)) - { - EJ->SetStringField(TEXT("expressionType"), TEXT("TextureCoordinate")); - EJ->SetNumberField(TEXT("coordinateIndex"), TC->CoordinateIndex); - EJ->SetNumberField(TEXT("uTiling"), TC->UTiling); - EJ->SetNumberField(TEXT("vTiling"), TC->VTiling); - } - else if (auto* CM = Cast(Expression)) - { - EJ->SetStringField(TEXT("expressionType"), TEXT("ComponentMask")); - EJ->SetBoolField(TEXT("r"), CM->R != 0); - EJ->SetBoolField(TEXT("g"), CM->G != 0); - EJ->SetBoolField(TEXT("b"), CM->B != 0); - EJ->SetBoolField(TEXT("a"), CM->A != 0); - } - else if (auto* Custom = Cast(Expression)) - { - EJ->SetStringField(TEXT("expressionType"), TEXT("Custom")); - EJ->SetStringField(TEXT("code"), Custom->Code); - EJ->SetStringField(TEXT("outputType"), StaticEnum()->GetNameStringByValue((int64)Custom->OutputType)); - } - else if (auto* FI = Cast(Expression)) - { - EJ->SetStringField(TEXT("expressionType"), TEXT("FunctionInput")); - EJ->SetStringField(TEXT("inputName"), FI->InputName.ToString()); - } - else if (auto* FO = Cast(Expression)) - { - EJ->SetStringField(TEXT("expressionType"), TEXT("FunctionOutput")); - EJ->SetStringField(TEXT("outputName"), FO->OutputName.ToString()); - } - else if (auto* MFC = Cast(Expression)) - { - EJ->SetStringField(TEXT("expressionType"), TEXT("MaterialFunctionCall")); - if (MFC->MaterialFunction) - EJ->SetStringField(TEXT("functionName"), MFC->MaterialFunction->GetName()); - } - else - { - EJ->SetStringField(TEXT("expressionType"), Expression->GetClass()->GetName()); - } - - return EJ; -} diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlerPopulate.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlerPopulate.cpp index 62703f74..a038e772 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlerPopulate.cpp +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlerPopulate.cpp @@ -1,5 +1,5 @@ #include "MCPHandler.h" -#include "BlueprintMCPServer.h" +#include "MCPUtils.h" #include "Dom/JsonObject.h" #include "UObject/UnrealType.h" #include "UObject/EnumProperty.h" @@ -164,7 +164,7 @@ FString PropertyNameToJsonKey(const FString& PropName) } // namespace MCPPopulate -FString FBlueprintMCPServer::PopulateFromJson( +FString MCPUtils::PopulateFromJson( UStruct* StructType, void* Container, const TSharedPtr& JsonValue) @@ -176,7 +176,7 @@ FString FBlueprintMCPServer::PopulateFromJson( return PopulateFromJson(StructType, Container, JsonValue->AsObject().Get()); } -FString FBlueprintMCPServer::PopulateFromJson( +FString MCPUtils::PopulateFromJson( UStruct* StructType, void* Container, const FJsonObject* Json) diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPUtils.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPUtils.cpp new file mode 100644 index 00000000..789698c4 --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPUtils.cpp @@ -0,0 +1,1222 @@ +#include "MCPUtils.h" +#include "BlueprintActionDatabase.h" +#include "BlueprintNodeSpawner.h" +#include "Dom/JsonValue.h" +#include "Serialization/JsonReader.h" +#include "Serialization/JsonWriter.h" +#include "Serialization/JsonSerializer.h" +#include "Engine/Blueprint.h" +#include "EdGraph/EdGraph.h" +#include "EdGraph/EdGraphNode.h" +#include "EdGraph/EdGraphPin.h" +#include "EdGraphSchema_K2.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_MacroInstance.h" +#include "K2Node_DynamicCast.h" +#include "K2Node_CallParentFunction.h" +#include "K2Node_IfThenElse.h" +#include "Kismet2/BlueprintEditorUtils.h" +#include "Kismet2/KismetEditorUtilities.h" +#include "UObject/SavePackage.h" +#include "UObject/UObjectIterator.h" +#include "Misc/Paths.h" +#include "Misc/PackageName.h" + +// Animation Blueprint support +#include "Animation/AnimBlueprint.h" +#include "Animation/Skeleton.h" +#include "AnimGraphNode_StateMachine.h" +#include "AnimGraphNode_AssetPlayerBase.h" +#include "AnimGraphNode_SequencePlayer.h" +#include "AnimGraphNode_BlendSpacePlayer.h" +#include "AnimGraphNode_Base.h" +#include "AnimStateNode.h" +#include "AnimStateTransitionNode.h" +#include "AnimStateConduitNode.h" +#include "AnimStateEntryNode.h" +#include "AnimationStateMachineGraph.h" +#include "AnimationGraph.h" +#include "AnimationTransitionGraph.h" + +// Material support +#include "Materials/Material.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 "Materials/MaterialFunction.h" +#include "MaterialGraph/MaterialGraph.h" +#include "MaterialGraph/MaterialGraphNode.h" +#include "MaterialGraph/MaterialGraphSchema.h" + +// SEH support (Windows only) — defined in BlueprintMCPServer.cpp +#if PLATFORM_WINDOWS +extern int32 TryCompileBlueprintSEH(UBlueprint* BP, EBlueprintCompileOptions Opts); +extern int32 TrySavePackageSEH( + UPackage* Package, UObject* Asset, const TCHAR* Filename, + FSavePackageArgs* SaveArgs, ESavePackageResult* OutResult); +#endif + +// ============================================================ +// JSON helpers +// ============================================================ + +FString MCPUtils::JsonToString(TSharedRef JsonObj) +{ + FString Output; + TSharedRef> Writer = TJsonWriterFactory<>::Create(&Output); + FJsonSerializer::Serialize(JsonObj, Writer); + return Output; +} + +TSharedPtr MCPUtils::ParseBodyJson(const FString& Body) +{ + TSharedPtr JsonObj; + TSharedRef> Reader = TJsonReaderFactory<>::Create(Body); + FJsonSerializer::Deserialize(Reader, JsonObj); + return JsonObj; +} + +FString MCPUtils::MakeErrorJson(const FString& Message) +{ + TSharedRef E = MakeShared(); + E->SetStringField(TEXT("error"), Message); + return JsonToString(E); +} + +void MCPUtils::MakeErrorJson(FJsonObject* Result, const FString& Message) +{ + Result->Values.Empty(); + Result->SetStringField(TEXT("error"), Message); +} + +void MCPUtils::CopyJsonFields(const FJsonObject* Source, FJsonObject* Dest) +{ + for (const auto& KV : Source->Values) + { + Dest->SetField(KV.Key, KV.Value); + } +} + +FString MCPUtils::UrlDecode(const FString& EncodedString) +{ + FString Result; + Result.Reserve(EncodedString.Len()); + + for (int32 i = 0; i < EncodedString.Len(); ++i) + { + TCHAR C = EncodedString[i]; + if (C == TEXT('+')) + { + Result += TEXT(' '); + } + else if (C == TEXT('%') && i + 2 < EncodedString.Len()) + { + FString HexStr = EncodedString.Mid(i + 1, 2); + int32 HexVal = 0; + bool bValid = true; + for (TCHAR H : HexStr) + { + HexVal <<= 4; + if (H >= TEXT('0') && H <= TEXT('9')) + HexVal += H - TEXT('0'); + else if (H >= TEXT('a') && H <= TEXT('f')) + HexVal += 10 + H - TEXT('a'); + else if (H >= TEXT('A') && H <= TEXT('F')) + HexVal += 10 + H - TEXT('A'); + else + { + bValid = false; + break; + } + } + if (bValid) + { + Result += (TCHAR)HexVal; + i += 2; + } + else + { + Result += C; + } + } + else + { + Result += C; + } + } + return Result; +} + +// ============================================================ +// Blueprint helpers +// ============================================================ + +UEdGraphNode* MCPUtils::FindNodeByGuid( + UBlueprint* BP, const FString& GuidString, UEdGraph** OutGraph) +{ + FGuid TargetGuid; + FGuid::Parse(GuidString, TargetGuid); + + TArray AllGraphs; + BP->GetAllGraphs(AllGraphs); + + for (UEdGraph* Graph : AllGraphs) + { + for (UEdGraphNode* Node : Graph->Nodes) + { + if (Node && Node->NodeGuid == TargetGuid) + { + if (OutGraph) *OutGraph = Graph; + return Node; + } + } + } + return nullptr; +} + +bool MCPUtils::SaveBlueprintPackage(UBlueprint* BP) +{ + UPackage* Package = BP->GetPackage(); + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: SaveBlueprintPackage — begin for '%s'"), *BP->GetName()); + + // 1. Build absolute package filename — use .umap for map packages, .uasset otherwise + FString PackageExtension = Package->ContainsMap() + ? FPackageName::GetMapPackageExtension() + : FPackageName::GetAssetPackageExtension(); + FString PackageFilename = FPackageName::LongPackageNameToFilename( + Package->GetName(), PackageExtension); + PackageFilename = FPaths::ConvertRelativePathToFull(PackageFilename); + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Save target: %s"), *PackageFilename); + + // 2. Phase 1: Try explicit compilation (same flags as UCompileAllBlueprintsCommandlet) + bool bCompiled = false; + { + EBlueprintCompileOptions CompileOpts = + EBlueprintCompileOptions::SkipSave | + EBlueprintCompileOptions::BatchCompile | + EBlueprintCompileOptions::SkipGarbageCollection | + EBlueprintCompileOptions::SkipFiBSearchMetaUpdate; + + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Phase 1: Attempting explicit compilation...")); + +#if PLATFORM_WINDOWS + int32 CompileResult = TryCompileBlueprintSEH(BP, CompileOpts); + if (CompileResult == 0) + { + bCompiled = (BP->Status == BS_UpToDate); + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Compilation %s (status=%d)"), + bCompiled ? TEXT("succeeded") : TEXT("completed with warnings"), (int32)BP->Status); + } + else + { + UE_LOG(LogTemp, Warning, TEXT("BlueprintMCP: Compilation crashed (SEH), proceeding uncompiled")); + } +#else + FKismetEditorUtilities::CompileBlueprint(BP, CompileOpts, nullptr); + bCompiled = (BP->Status == BS_UpToDate); +#endif + } + + // 3. Phase 2: Set guards for save + uint8 OldRegen = BP->bIsRegeneratingOnLoad; + BP->bIsRegeneratingOnLoad = true; + + EBlueprintStatus OldStatus = (EBlueprintStatus)(uint8)BP->Status; + if (!bCompiled) + { + // Tell PreSave the BP is up-to-date so it doesn't try to compile + BP->Status = BS_UpToDate; + } + + // 4. Clear read-only attribute if present (source control or LFS may set this) + if (FPlatformFileManager::Get().GetPlatformFile().IsReadOnly(*PackageFilename)) + { + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Clearing read-only attribute on %s"), *PackageFilename); + FPlatformFileManager::Get().GetPlatformFile().SetReadOnly(*PackageFilename, false); + } + + // 5. Phase 3: Save with SAVE_NoError + SEH protection + FSavePackageArgs SaveArgs; + SaveArgs.TopLevelFlags = RF_Public | RF_Standalone; + SaveArgs.SaveFlags = SAVE_NoError; + + // For level blueprints (map packages), the base object should be the UWorld, not the BP + bool bIsMapPackage = Package->ContainsMap(); + UObject* BaseObject = BP; + if (bIsMapPackage) + { + // Find the UWorld in this package — it's the actual asset for .umap files + UWorld* World = FindObject(Package, *Package->GetName().Mid(Package->GetName().Find(TEXT("/"), ESearchCase::IgnoreCase, ESearchDir::FromEnd) + 1)); + if (!World) + { + // Fallback: iterate the package to find any UWorld + ForEachObjectWithPackage(Package, [&World](UObject* Obj) { + if (UWorld* W = Cast(Obj)) + { + World = W; + return false; // stop + } + return true; // continue + }); + } + if (World) + { + BaseObject = World; + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Map package detected — saving UWorld '%s'"), *World->GetName()); + } + else + { + UE_LOG(LogTemp, Warning, TEXT("BlueprintMCP: Map package detected but no UWorld found — saving with BP as base")); + } + } + + ESavePackageResult SaveResult = ESavePackageResult::Error; + + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Phase 3: Calling UPackage::Save (compiled=%s, isMap=%s)..."), + bCompiled ? TEXT("yes") : TEXT("no"), bIsMapPackage ? TEXT("yes") : TEXT("no")); + +#if PLATFORM_WINDOWS + int32 SEHCode = TrySavePackageSEH(Package, BaseObject, *PackageFilename, &SaveArgs, &SaveResult); + if (SEHCode != 0) + { + UE_LOG(LogTemp, Error, TEXT("BlueprintMCP: UPackage::Save CRASHED (SEH exception caught)")); + } +#else + FSavePackageResultStruct Result = UPackage::Save(Package, BaseObject, *PackageFilename, SaveArgs); + SaveResult = Result.Result; +#endif + + // 6. Restore guards + BP->bIsRegeneratingOnLoad = OldRegen; + if (!bCompiled) + { + BP->Status = (TEnumAsByte)OldStatus; + } + + bool bSuccess = (SaveResult == ESavePackageResult::Success); + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: SaveBlueprintPackage — %s for '%s' (compiled=%s, result=%d)"), + bSuccess ? TEXT("SUCCEEDED") : TEXT("FAILED"), + *BP->GetName(), bCompiled ? TEXT("yes") : TEXT("no"), (int32)SaveResult); + + return bSuccess; +} + +// ============================================================ +// Serialization +// ============================================================ + +TSharedRef MCPUtils::SerializeBlueprint(UBlueprint* BP) +{ + TSharedRef J = MakeShared(); + J->SetStringField(TEXT("name"), BP->GetName()); + J->SetStringField(TEXT("path"), BP->GetPackage()->GetName()); + J->SetStringField(TEXT("parentClass"), BP->ParentClass ? BP->ParentClass->GetName() : TEXT("None")); + J->SetStringField(TEXT("blueprintType"), + StaticEnum()->GetNameStringByValue((int64)BP->BlueprintType)); + + // Animation Blueprint detection + if (UAnimBlueprint* AnimBP = Cast(BP)) + { + J->SetBoolField(TEXT("isAnimBlueprint"), true); + if (AnimBP->TargetSkeleton) + { + J->SetStringField(TEXT("targetSkeleton"), AnimBP->TargetSkeleton->GetName()); + J->SetStringField(TEXT("targetSkeletonPath"), AnimBP->TargetSkeleton->GetPathName()); + } + } + + // Variables + TArray> Vars; + for (const FBPVariableDescription& V : BP->NewVariables) + { + TSharedRef VJ = MakeShared(); + VJ->SetStringField(TEXT("name"), V.VarName.ToString()); + VJ->SetStringField(TEXT("type"), V.VarType.PinCategory.ToString()); + if (V.VarType.PinSubCategoryObject.IsValid()) + VJ->SetStringField(TEXT("subtype"), V.VarType.PinSubCategoryObject->GetName()); + VJ->SetBoolField(TEXT("isArray"), V.VarType.IsArray()); + VJ->SetBoolField(TEXT("isSet"), V.VarType.IsSet()); + VJ->SetBoolField(TEXT("isMap"), V.VarType.IsMap()); + VJ->SetStringField(TEXT("category"), V.Category.ToString()); + VJ->SetStringField(TEXT("defaultValue"), V.DefaultValue); + Vars.Add(MakeShared(VJ)); + } + J->SetArrayField(TEXT("variables"), Vars); + + // Interfaces + TArray> Ifaces; + for (const FBPInterfaceDescription& I : BP->ImplementedInterfaces) + { + if (I.Interface) + Ifaces.Add(MakeShared(I.Interface->GetName())); + } + J->SetArrayField(TEXT("interfaces"), Ifaces); + + // Graphs + TArray> GraphArr; + TArray AllGraphs; + BP->GetAllGraphs(AllGraphs); + for (UEdGraph* Graph : AllGraphs) + { + if (!Graph) continue; + TSharedPtr GJ = SerializeGraph(Graph); + if (GJ.IsValid()) + GraphArr.Add(MakeShared(GJ.ToSharedRef())); + } + J->SetArrayField(TEXT("graphs"), GraphArr); + return J; +} + +TSharedPtr MCPUtils::SerializeGraph(UEdGraph* Graph) +{ + TSharedRef GJ = MakeShared(); + GJ->SetStringField(TEXT("name"), Graph->GetName()); + GJ->SetStringField(TEXT("schema"), Graph->Schema ? Graph->Schema->GetClass()->GetName() : TEXT("Unknown")); + + // Detect animation graph subtypes + if (Cast(Graph)) + { + GJ->SetStringField(TEXT("graphType"), TEXT("StateMachine")); + // Find entry state by following entry node's output pin + for (UEdGraphNode* Node : Graph->Nodes) + { + if (UAnimStateEntryNode* EntryNode = Cast(Node)) + { + for (UEdGraphPin* Pin : EntryNode->Pins) + { + if (Pin->Direction == EGPD_Output && Pin->LinkedTo.Num() > 0) + { + UEdGraphNode* LinkedNode = Pin->LinkedTo[0]->GetOwningNode(); + if (UAnimStateNode* StateNode = Cast(LinkedNode)) + { + GJ->SetStringField(TEXT("entryState"), StateNode->GetStateName()); + } + } + } + break; + } + } + } + else if (Cast(Graph)) + { + GJ->SetStringField(TEXT("graphType"), TEXT("AnimGraph")); + } + else if (Cast(Graph)) + { + GJ->SetStringField(TEXT("graphType"), TEXT("TransitionRule")); + } + + TArray> Nodes; + for (UEdGraphNode* Node : Graph->Nodes) + { + if (!Node) continue; + TSharedPtr NJ = SerializeNode(Node); + if (NJ.IsValid()) + Nodes.Add(MakeShared(NJ.ToSharedRef())); + } + GJ->SetArrayField(TEXT("nodes"), Nodes); + return GJ; +} + +TSharedPtr MCPUtils::SerializeNode(UEdGraphNode* Node) +{ + TSharedRef NJ = MakeShared(); + NJ->SetStringField(TEXT("id"), Node->NodeGuid.ToString()); + NJ->SetStringField(TEXT("class"), Node->GetClass()->GetName()); + NJ->SetStringField(TEXT("title"), Node->GetNodeTitle(ENodeTitleType::FullTitle).ToString()); + if (!Node->NodeComment.IsEmpty()) + NJ->SetStringField(TEXT("comment"), Node->NodeComment); + NJ->SetNumberField(TEXT("posX"), Node->NodePosX); + NJ->SetNumberField(TEXT("posY"), Node->NodePosY); + + // Material graph node — extract UMaterialExpression data + if (UMaterialGraphNode* MatNode = Cast(Node)) + { + NJ->SetStringField(TEXT("nodeType"), TEXT("MaterialExpression")); + if (MatNode->MaterialExpression) + { + TSharedPtr ExprJson = SerializeMaterialExpression(MatNode->MaterialExpression); + if (ExprJson.IsValid()) + { + NJ->SetObjectField(TEXT("expression"), ExprJson); + } + } + } + // Animation Blueprint node types + else if (auto* SMNode = Cast(Node)) + { + NJ->SetStringField(TEXT("nodeType"), TEXT("AnimStateMachine")); + if (SMNode->EditorStateMachineGraph) + { + NJ->SetStringField(TEXT("stateMachineName"), SMNode->EditorStateMachineGraph->GetName()); + int32 StateCount = 0, TransitionCount = 0; + for (UEdGraphNode* SubNode : SMNode->EditorStateMachineGraph->Nodes) + { + if (Cast(SubNode)) StateCount++; + else if (Cast(SubNode)) TransitionCount++; + } + NJ->SetNumberField(TEXT("stateCount"), StateCount); + NJ->SetNumberField(TEXT("transitionCount"), TransitionCount); + } + } + else if (auto* SeqPlayer = Cast(Node)) + { + NJ->SetStringField(TEXT("nodeType"), TEXT("AnimSequencePlayer")); + if (UAnimationAsset* Asset = SeqPlayer->GetAnimationAsset()) + { + NJ->SetStringField(TEXT("animationAsset"), Asset->GetName()); + NJ->SetStringField(TEXT("animationAssetPath"), Asset->GetPathName()); + } + } + else if (auto* BSPlayer = Cast(Node)) + { + NJ->SetStringField(TEXT("nodeType"), TEXT("AnimBlendSpacePlayer")); + if (UAnimationAsset* Asset = BSPlayer->GetAnimationAsset()) + { + NJ->SetStringField(TEXT("blendSpaceAsset"), Asset->GetName()); + NJ->SetStringField(TEXT("blendSpaceAssetPath"), Asset->GetPathName()); + } + } + else if (auto* AssetPlayer = Cast(Node)) + { + NJ->SetStringField(TEXT("nodeType"), TEXT("AnimAssetPlayer")); + if (UAnimationAsset* Asset = AssetPlayer->GetAnimationAsset()) + { + NJ->SetStringField(TEXT("animationAsset"), Asset->GetName()); + NJ->SetStringField(TEXT("animationAssetPath"), Asset->GetPathName()); + } + } + else if (Cast(Node)) + { + NJ->SetStringField(TEXT("nodeType"), TEXT("AnimNode")); + } + else if (auto* StateNode = Cast(Node)) + { + NJ->SetStringField(TEXT("nodeType"), TEXT("AnimState")); + NJ->SetStringField(TEXT("stateName"), StateNode->GetStateName()); + NJ->SetBoolField(TEXT("bAlwaysResetOnEntry"), StateNode->bAlwaysResetOnEntry); + } + else if (auto* TransNode = Cast(Node)) + { + NJ->SetStringField(TEXT("nodeType"), TEXT("AnimTransition")); + if (UAnimStateNode* FromState = Cast(TransNode->GetPreviousState())) + { + NJ->SetStringField(TEXT("fromState"), FromState->GetStateName()); + } + if (UAnimStateNode* ToState = Cast(TransNode->GetNextState())) + { + NJ->SetStringField(TEXT("toState"), ToState->GetStateName()); + } + NJ->SetNumberField(TEXT("crossfadeDuration"), TransNode->CrossfadeDuration); + NJ->SetNumberField(TEXT("blendMode"), (int32)TransNode->BlendMode); + NJ->SetNumberField(TEXT("priorityOrder"), TransNode->PriorityOrder); + NJ->SetNumberField(TEXT("logicType"), (int32)TransNode->LogicType.GetValue()); + NJ->SetBoolField(TEXT("bBidirectional"), TransNode->Bidirectional); + } + else if (Cast(Node)) + { + NJ->SetStringField(TEXT("nodeType"), TEXT("AnimConduit")); + } + else if (Cast(Node)) + { + NJ->SetStringField(TEXT("nodeType"), TEXT("AnimStateEntry")); + } + // K2Node specifics — check CallParentFunction before CallFunction (inheritance) + else if (auto* CPF = Cast(Node)) + { + NJ->SetStringField(TEXT("functionName"), CPF->FunctionReference.GetMemberName().ToString()); + if (CPF->FunctionReference.GetMemberParentClass()) + NJ->SetStringField(TEXT("targetClass"), CPF->FunctionReference.GetMemberParentClass()->GetName()); + NJ->SetStringField(TEXT("nodeType"), TEXT("CallParentFunction")); + } + else if (auto* CF = Cast(Node)) + { + NJ->SetStringField(TEXT("functionName"), CF->FunctionReference.GetMemberName().ToString()); + if (CF->FunctionReference.GetMemberParentClass()) + NJ->SetStringField(TEXT("targetClass"), CF->FunctionReference.GetMemberParentClass()->GetName()); + } + else if (auto* FE = Cast(Node)) + { + NJ->SetStringField(TEXT("nodeType"), TEXT("FunctionEntry")); + + // Serialize UserDefinedPins (parameter names and types) + TArray> ParamArr; + for (const TSharedPtr& PinInfo : FE->UserDefinedPins) + { + if (!PinInfo.IsValid()) continue; + TSharedRef ParamJ = MakeShared(); + ParamJ->SetStringField(TEXT("name"), PinInfo->PinName.ToString()); + FString ParamType = PinInfo->PinType.PinCategory.ToString(); + ParamJ->SetStringField(TEXT("type"), ParamType); + if (PinInfo->PinType.PinSubCategoryObject.IsValid()) + ParamJ->SetStringField(TEXT("subtype"), PinInfo->PinType.PinSubCategoryObject->GetName()); + else if (ParamType == TEXT("None") || ParamType.IsEmpty()) + ParamJ->SetBoolField(TEXT("typeUnknown"), true); + ParamArr.Add(MakeShared(ParamJ)); + } + NJ->SetArrayField(TEXT("parameters"), ParamArr); + } + else if (auto* Ev = Cast(Node)) + { + NJ->SetStringField(TEXT("eventName"), Ev->EventReference.GetMemberName().ToString()); + NJ->SetStringField(TEXT("nodeType"), Ev->bOverrideFunction ? TEXT("OverrideEvent") : TEXT("Event")); + } + else if (auto* CE = Cast(Node)) + { + NJ->SetStringField(TEXT("eventName"), CE->CustomFunctionName.ToString()); + NJ->SetStringField(TEXT("nodeType"), TEXT("CustomEvent")); + + // Serialize UserDefinedPins (parameter names and types) + TArray> ParamArr; + for (const TSharedPtr& PinInfo : CE->UserDefinedPins) + { + if (!PinInfo.IsValid()) continue; + TSharedRef ParamJ = MakeShared(); + ParamJ->SetStringField(TEXT("name"), PinInfo->PinName.ToString()); + FString ParamType = PinInfo->PinType.PinCategory.ToString(); + ParamJ->SetStringField(TEXT("type"), ParamType); + if (PinInfo->PinType.PinSubCategoryObject.IsValid()) + ParamJ->SetStringField(TEXT("subtype"), PinInfo->PinType.PinSubCategoryObject->GetName()); + else if (ParamType == TEXT("None") || ParamType.IsEmpty()) + ParamJ->SetBoolField(TEXT("typeUnknown"), true); + ParamArr.Add(MakeShared(ParamJ)); + } + NJ->SetArrayField(TEXT("parameters"), ParamArr); + } + else if (auto* VG = Cast(Node)) + { + NJ->SetStringField(TEXT("variableName"), VG->GetVarName().ToString()); + NJ->SetStringField(TEXT("nodeType"), TEXT("VariableGet")); + } + else if (auto* VS = Cast(Node)) + { + NJ->SetStringField(TEXT("variableName"), VS->GetVarName().ToString()); + NJ->SetStringField(TEXT("nodeType"), TEXT("VariableSet")); + } + else if (auto* MI = Cast(Node)) + { + if (MI->GetMacroGraph()) + NJ->SetStringField(TEXT("macroName"), MI->GetMacroGraph()->GetName()); + NJ->SetStringField(TEXT("nodeType"), TEXT("MacroInstance")); + } + else if (auto* DC = Cast(Node)) + { + if (DC->TargetType) + NJ->SetStringField(TEXT("castTarget"), DC->TargetType->GetName()); + NJ->SetStringField(TEXT("nodeType"), TEXT("DynamicCast")); + } + else if (Cast(Node)) + { + NJ->SetStringField(TEXT("nodeType"), TEXT("Branch")); + } + + // Pins + TArray> Pins; + for (UEdGraphPin* Pin : Node->Pins) + { + if (!Pin || Pin->bHidden) continue; + TSharedPtr PJ = SerializePin(Pin); + if (PJ.IsValid()) + Pins.Add(MakeShared(PJ.ToSharedRef())); + } + NJ->SetArrayField(TEXT("pins"), Pins); + return NJ; +} + +TSharedPtr MCPUtils::SerializePin(UEdGraphPin* Pin) +{ + TSharedRef PJ = MakeShared(); + PJ->SetStringField(TEXT("name"), Pin->PinName.ToString()); + PJ->SetStringField(TEXT("direction"), Pin->Direction == EGPD_Input ? TEXT("Input") : TEXT("Output")); + PJ->SetStringField(TEXT("type"), Pin->PinType.PinCategory.ToString()); + if (Pin->PinType.PinSubCategoryObject.IsValid()) + PJ->SetStringField(TEXT("subtype"), Pin->PinType.PinSubCategoryObject->GetName()); + if (!Pin->DefaultValue.IsEmpty()) + PJ->SetStringField(TEXT("defaultValue"), Pin->DefaultValue); + + 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()); + Conns.Add(MakeShared(CJ)); + } + PJ->SetArrayField(TEXT("connections"), Conns); + } + return PJ; +} + +// ============================================================ +// FindClassByName / ResolveTypeFromString +// ============================================================ + +UClass* MCPUtils::FindClassByName(const FString& ClassName) +{ + // Exact match first (handles both C++ classes and Blueprint _C classes) + for (TObjectIterator It; It; ++It) + { + FString Name = It->GetName(); + if (Name == ClassName || Name == ClassName + TEXT("_C")) + { + return *It; + } + } + + // Case-insensitive fallback + for (TObjectIterator It; It; ++It) + { + FString Name = It->GetName(); + if (Name.Equals(ClassName, ESearchCase::IgnoreCase) || + Name.Equals(ClassName + TEXT("_C"), ESearchCase::IgnoreCase)) + { + return *It; + } + } + + return nullptr; +} + +bool MCPUtils::ResolveTypeFromString( + const FString& TypeName, FEdGraphPinType& OutPinType, FString& OutError) +{ + FString TypeLower = TypeName.ToLower(); + + if (TypeLower == TEXT("bool") || TypeLower == TEXT("boolean")) + { + OutPinType.PinCategory = UEdGraphSchema_K2::PC_Boolean; + } + else if (TypeLower == TEXT("int") || TypeLower == TEXT("int32") || TypeLower == TEXT("integer")) + { + OutPinType.PinCategory = UEdGraphSchema_K2::PC_Int; + } + else if (TypeLower == TEXT("int64")) + { + OutPinType.PinCategory = UEdGraphSchema_K2::PC_Int64; + } + else if (TypeLower == TEXT("float") || TypeLower == TEXT("double") || TypeLower == TEXT("real")) + { + OutPinType.PinCategory = UEdGraphSchema_K2::PC_Real; + OutPinType.PinSubCategory = TEXT("double"); + } + else if (TypeLower == TEXT("string")) + { + OutPinType.PinCategory = UEdGraphSchema_K2::PC_String; + } + else if (TypeLower == TEXT("name")) + { + OutPinType.PinCategory = UEdGraphSchema_K2::PC_Name; + } + else if (TypeLower == TEXT("text")) + { + OutPinType.PinCategory = UEdGraphSchema_K2::PC_Text; + } + else if (TypeLower == TEXT("byte")) + { + OutPinType.PinCategory = UEdGraphSchema_K2::PC_Byte; + } + else if (TypeLower == TEXT("vector") || TypeLower == TEXT("fvector")) + { + OutPinType.PinCategory = UEdGraphSchema_K2::PC_Struct; + OutPinType.PinSubCategoryObject = TBaseStructure::Get(); + } + else if (TypeLower == TEXT("rotator") || TypeLower == TEXT("frotator")) + { + OutPinType.PinCategory = UEdGraphSchema_K2::PC_Struct; + OutPinType.PinSubCategoryObject = TBaseStructure::Get(); + } + else if (TypeLower == TEXT("transform") || TypeLower == TEXT("ftransform")) + { + OutPinType.PinCategory = UEdGraphSchema_K2::PC_Struct; + OutPinType.PinSubCategoryObject = TBaseStructure::Get(); + } + else if (TypeLower == TEXT("linearcolor") || TypeLower == TEXT("flinearcolor")) + { + OutPinType.PinCategory = UEdGraphSchema_K2::PC_Struct; + OutPinType.PinSubCategoryObject = TBaseStructure::Get(); + } + else if (TypeLower == TEXT("vector2d") || TypeLower == TEXT("fvector2d")) + { + OutPinType.PinCategory = UEdGraphSchema_K2::PC_Struct; + OutPinType.PinSubCategoryObject = TBaseStructure::Get(); + } + else if (TypeLower == TEXT("object")) + { + OutPinType.PinCategory = UEdGraphSchema_K2::PC_Object; + OutPinType.PinSubCategoryObject = UObject::StaticClass(); + } + else if (TypeName.StartsWith(TEXT("object:"), ESearchCase::IgnoreCase)) + { + FString ClassName = TypeName.Mid(7); // after "object:" + UClass* FoundClass = FindClassByName(ClassName); + if (!FoundClass) + { + OutError = FString::Printf(TEXT("Class '%s' not found for object reference type"), *ClassName); + return false; + } + OutPinType.PinCategory = UEdGraphSchema_K2::PC_Object; + OutPinType.PinSubCategoryObject = FoundClass; + } + else if (TypeName.StartsWith(TEXT("softobject:"), ESearchCase::IgnoreCase)) + { + FString ClassName = TypeName.Mid(11); // after "softobject:" + UClass* FoundClass = FindClassByName(ClassName); + if (!FoundClass) + { + OutError = FString::Printf(TEXT("Class '%s' not found for soft object reference type"), *ClassName); + return false; + } + OutPinType.PinCategory = UEdGraphSchema_K2::PC_SoftObject; + OutPinType.PinSubCategoryObject = FoundClass; + } + else if (TypeName.StartsWith(TEXT("class:"), ESearchCase::IgnoreCase)) + { + FString ClassName = TypeName.Mid(6); // after "class:" + UClass* FoundClass = FindClassByName(ClassName); + if (!FoundClass) + { + OutError = FString::Printf(TEXT("Class '%s' not found for class reference type (TSubclassOf)"), *ClassName); + return false; + } + OutPinType.PinCategory = UEdGraphSchema_K2::PC_Class; + OutPinType.PinSubCategoryObject = FoundClass; + } + else if (TypeName.StartsWith(TEXT("softclass:"), ESearchCase::IgnoreCase)) + { + FString ClassName = TypeName.Mid(10); // after "softclass:" + UClass* FoundClass = FindClassByName(ClassName); + if (!FoundClass) + { + OutError = FString::Printf(TEXT("Class '%s' not found for soft class reference type"), *ClassName); + return false; + } + OutPinType.PinCategory = UEdGraphSchema_K2::PC_SoftClass; + OutPinType.PinSubCategoryObject = FoundClass; + } + else if (TypeName.StartsWith(TEXT("interface:"), ESearchCase::IgnoreCase)) + { + FString ClassName = TypeName.Mid(10); // after "interface:" + UClass* FoundClass = FindClassByName(ClassName); + if (!FoundClass) + { + OutError = FString::Printf(TEXT("Class '%s' not found for interface reference type"), *ClassName); + return false; + } + OutPinType.PinCategory = UEdGraphSchema_K2::PC_Interface; + OutPinType.PinSubCategoryObject = FoundClass; + } + else + { + // Try as a struct (F-prefix or raw name) + FString InternalName = TypeName; + bool bTriedAsStruct = false; + + if (TypeName.StartsWith(TEXT("F")) || TypeName.StartsWith(TEXT("S_")) || (!TypeName.StartsWith(TEXT("E")))) + { + if (TypeName.StartsWith(TEXT("F"))) + { + InternalName = TypeName.Mid(1); + } + + UScriptStruct* FoundStruct = FindFirstObject(*InternalName); + if (!FoundStruct) + { + for (TObjectIterator It; It; ++It) + { + if (It->GetName() == InternalName || It->GetName() == TypeName) + { + FoundStruct = *It; + break; + } + } + } + + if (FoundStruct) + { + OutPinType.PinCategory = UEdGraphSchema_K2::PC_Struct; + OutPinType.PinSubCategoryObject = FoundStruct; + bTriedAsStruct = true; + } + } + + if (!bTriedAsStruct) + { + // Try as an enum (E-prefix or raw name) + FString EnumInternalName = TypeName; + if (TypeName.StartsWith(TEXT("E"))) + { + EnumInternalName = TypeName.Mid(1); + } + + UEnum* FoundEnum = FindFirstObject(*EnumInternalName); + if (!FoundEnum) + { + for (TObjectIterator It; It; ++It) + { + if (It->GetName() == EnumInternalName || It->GetName() == TypeName) + { + FoundEnum = *It; + break; + } + } + } + + if (FoundEnum) + { + if (FoundEnum->GetCppForm() == UEnum::ECppForm::EnumClass) + { + OutPinType.PinCategory = UEdGraphSchema_K2::PC_Enum; + } + else + { + OutPinType.PinCategory = UEdGraphSchema_K2::PC_Byte; + } + OutPinType.PinSubCategoryObject = FoundEnum; + } + else + { + OutError = FString::Printf( + TEXT("Unknown type '%s'. Use: bool, int, float, string, name, text, byte, vector, rotator, transform, object, a struct/enum name (e.g. FVector, EMyEnum), or colon syntax for references (object:Actor, softobject:Actor, class:Actor, softclass:Actor, interface:MyInterface)"), + *TypeName); + return false; + } + } + } + + return true; +} + +// ============================================================ +// Material helpers +// ============================================================ + +void MCPUtils::EnsureMaterialGraph(UMaterial* Material) +{ + if (!Material) return; + if (!Material->MaterialGraph) + { + // In commandlet/headless mode the MaterialGraph is not auto-created. + // Replicate what the Material Editor does on open (MaterialEditor.cpp:619). + Material->MaterialGraph = CastChecked( + FBlueprintEditorUtils::CreateNewGraph( + Material, NAME_None, + UMaterialGraph::StaticClass(), + UMaterialGraphSchema::StaticClass())); + Material->MaterialGraph->Material = Material; + Material->MaterialGraph->RebuildGraph(); + } +} + +bool MCPUtils::SaveMaterialPackage(UMaterial* Material) +{ + if (!Material) return false; + return SaveGenericPackage(Material); +} + +bool MCPUtils::SaveGenericPackage(UObject* Asset) +{ + if (!Asset) return false; + UPackage* Package = Asset->GetPackage(); + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: SaveGenericPackage — begin for '%s'"), *Asset->GetName()); + + FString PackageFilename = FPackageName::LongPackageNameToFilename( + Package->GetName(), FPackageName::GetAssetPackageExtension()); + PackageFilename = FPaths::ConvertRelativePathToFull(PackageFilename); + + if (FPlatformFileManager::Get().GetPlatformFile().IsReadOnly(*PackageFilename)) + { + FPlatformFileManager::Get().GetPlatformFile().SetReadOnly(*PackageFilename, false); + } + + FSavePackageArgs SaveArgs; + SaveArgs.TopLevelFlags = RF_Public | RF_Standalone; + SaveArgs.SaveFlags = SAVE_NoError; + + ESavePackageResult SaveResult = ESavePackageResult::Error; +#if PLATFORM_WINDOWS + int32 SEHCode = TrySavePackageSEH(Package, Asset, *PackageFilename, &SaveArgs, &SaveResult); + if (SEHCode != 0) + { + UE_LOG(LogTemp, Error, TEXT("BlueprintMCP: SaveGenericPackage CRASHED (SEH exception)")); + } +#else + FSavePackageResultStruct Result = UPackage::Save(Package, Asset, *PackageFilename, SaveArgs); + SaveResult = Result.Result; +#endif + + bool bSuccess = (SaveResult == ESavePackageResult::Success); + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: SaveGenericPackage — %s for '%s'"), + bSuccess ? TEXT("SUCCEEDED") : TEXT("FAILED"), *Asset->GetName()); + return bSuccess; +} + +TSharedPtr MCPUtils::SerializeMaterialExpression(UMaterialExpression* Expression) +{ + if (!Expression) return nullptr; + + TSharedRef EJ = MakeShared(); + EJ->SetStringField(TEXT("class"), Expression->GetClass()->GetName()); + EJ->SetStringField(TEXT("name"), Expression->GetName()); + EJ->SetStringField(TEXT("description"), Expression->GetDescription()); + EJ->SetNumberField(TEXT("posX"), Expression->MaterialExpressionEditorX); + EJ->SetNumberField(TEXT("posY"), Expression->MaterialExpressionEditorY); + + if (auto* SP = Cast(Expression)) + { + EJ->SetStringField(TEXT("expressionType"), TEXT("ScalarParameter")); + EJ->SetStringField(TEXT("parameterName"), SP->ParameterName.ToString()); + EJ->SetNumberField(TEXT("defaultValue"), SP->DefaultValue); + EJ->SetStringField(TEXT("group"), SP->Group.ToString()); + } + else if (auto* VP = Cast(Expression)) + { + EJ->SetStringField(TEXT("expressionType"), TEXT("VectorParameter")); + EJ->SetStringField(TEXT("parameterName"), VP->ParameterName.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); + EJ->SetObjectField(TEXT("defaultValue"), DefVal); + EJ->SetStringField(TEXT("group"), VP->Group.ToString()); + } + else if (auto* TP = Cast(Expression)) + { + EJ->SetStringField(TEXT("expressionType"), TEXT("TextureSampleParameter2D")); + EJ->SetStringField(TEXT("parameterName"), TP->ParameterName.ToString()); + if (TP->Texture) + EJ->SetStringField(TEXT("texture"), TP->Texture->GetPathName()); + EJ->SetStringField(TEXT("group"), TP->Group.ToString()); + } + else if (auto* SSP = Cast(Expression)) + { + EJ->SetStringField(TEXT("expressionType"), TEXT("StaticSwitchParameter")); + EJ->SetStringField(TEXT("parameterName"), SSP->ParameterName.ToString()); + EJ->SetBoolField(TEXT("defaultValue"), SSP->DefaultValue); + EJ->SetStringField(TEXT("group"), SSP->Group.ToString()); + } + else if (auto* SC = Cast(Expression)) + { + EJ->SetStringField(TEXT("expressionType"), TEXT("Constant")); + EJ->SetNumberField(TEXT("value"), SC->R); + } + else if (auto* C3 = Cast(Expression)) + { + EJ->SetStringField(TEXT("expressionType"), TEXT("Constant3Vector")); + TSharedRef Val = MakeShared(); + Val->SetNumberField(TEXT("r"), C3->Constant.R); + Val->SetNumberField(TEXT("g"), C3->Constant.G); + Val->SetNumberField(TEXT("b"), C3->Constant.B); + EJ->SetObjectField(TEXT("value"), Val); + } + else if (auto* C4 = Cast(Expression)) + { + EJ->SetStringField(TEXT("expressionType"), TEXT("Constant4Vector")); + TSharedRef Val = MakeShared(); + Val->SetNumberField(TEXT("r"), C4->Constant.R); + Val->SetNumberField(TEXT("g"), C4->Constant.G); + Val->SetNumberField(TEXT("b"), C4->Constant.B); + Val->SetNumberField(TEXT("a"), C4->Constant.A); + EJ->SetObjectField(TEXT("value"), Val); + } + else if (auto* TS = Cast(Expression)) + { + EJ->SetStringField(TEXT("expressionType"), TEXT("TextureSample")); + if (TS->Texture) + EJ->SetStringField(TEXT("texture"), TS->Texture->GetPathName()); + } + else if (auto* TC = Cast(Expression)) + { + EJ->SetStringField(TEXT("expressionType"), TEXT("TextureCoordinate")); + EJ->SetNumberField(TEXT("coordinateIndex"), TC->CoordinateIndex); + EJ->SetNumberField(TEXT("uTiling"), TC->UTiling); + EJ->SetNumberField(TEXT("vTiling"), TC->VTiling); + } + else if (auto* CM = Cast(Expression)) + { + EJ->SetStringField(TEXT("expressionType"), TEXT("ComponentMask")); + EJ->SetBoolField(TEXT("r"), CM->R != 0); + EJ->SetBoolField(TEXT("g"), CM->G != 0); + EJ->SetBoolField(TEXT("b"), CM->B != 0); + EJ->SetBoolField(TEXT("a"), CM->A != 0); + } + else if (auto* Custom = Cast(Expression)) + { + EJ->SetStringField(TEXT("expressionType"), TEXT("Custom")); + EJ->SetStringField(TEXT("code"), Custom->Code); + EJ->SetStringField(TEXT("outputType"), StaticEnum()->GetNameStringByValue((int64)Custom->OutputType)); + } + else if (auto* FI = Cast(Expression)) + { + EJ->SetStringField(TEXT("expressionType"), TEXT("FunctionInput")); + EJ->SetStringField(TEXT("inputName"), FI->InputName.ToString()); + } + else if (auto* FO = Cast(Expression)) + { + EJ->SetStringField(TEXT("expressionType"), TEXT("FunctionOutput")); + EJ->SetStringField(TEXT("outputName"), FO->OutputName.ToString()); + } + else if (auto* MFC = Cast(Expression)) + { + EJ->SetStringField(TEXT("expressionType"), TEXT("MaterialFunctionCall")); + if (MFC->MaterialFunction) + EJ->SetStringField(TEXT("functionName"), MFC->MaterialFunction->GetName()); + } + else + { + EJ->SetStringField(TEXT("expressionType"), Expression->GetClass()->GetName()); + } + + return EJ; +} + +// ============================================================ +// Anim blueprint helpers +// ============================================================ + +#include "AnimStateNode.h" +#include "AnimStateTransitionNode.h" +#include "AnimationStateMachineGraph.h" + +UAnimationStateMachineGraph* MCPUtils::FindStateMachineGraph(UBlueprint* BP, const FString& GraphName) +{ + TArray AllGraphs; + BP->GetAllGraphs(AllGraphs); + for (UEdGraph* Graph : AllGraphs) + { + if (UAnimationStateMachineGraph* SMGraph = Cast(Graph)) + { + if (SMGraph->GetName() == GraphName) + { + return SMGraph; + } + } + } + return nullptr; +} + +UAnimStateNode* MCPUtils::FindStateByName(UAnimationStateMachineGraph* SMGraph, const FString& StateName) +{ + for (UEdGraphNode* Node : SMGraph->Nodes) + { + if (UAnimStateNode* StateNode = Cast(Node)) + { + if (StateNode->GetStateName() == StateName) + { + return StateNode; + } + } + } + return nullptr; +} + +UAnimStateTransitionNode* MCPUtils::FindTransition(UAnimationStateMachineGraph* SMGraph, + const FString& FromStateName, const FString& ToStateName) +{ + for (UEdGraphNode* Node : SMGraph->Nodes) + { + if (UAnimStateTransitionNode* TransNode = Cast(Node)) + { + UAnimStateNode* FromState = Cast(TransNode->GetPreviousState()); + UAnimStateNode* ToState = Cast(TransNode->GetNextState()); + if (FromState && ToState && + (FromState->GetStateName() == FromStateName) && + (ToState->GetStateName() == ToStateName)) + { + return TransNode; + } + } + } + return nullptr; +} + +// ============================================================ +// Node spawners +// ============================================================ + +FString MCPUtils::NodeSpawnerFullName(UBlueprintNodeSpawner* Spawner) +{ + const FBlueprintActionUiSpec& UiSpec = Spawner->PrimeDefaultUiSpec(); + FString Category = UiSpec.Category.ToString(); + FString MenuName = UiSpec.MenuName.ToString(); + if (Category.IsEmpty()) + { + return MenuName; + } + return Category + TEXT("|") + MenuName; +} + +TArray MCPUtils::AllNodeSpawners() +{ + TArray Result; + for (const auto& Pair : FBlueprintActionDatabase::Get().GetAllActions()) + { + for (UBlueprintNodeSpawner* Spawner : Pair.Value) + { + if (!Spawner) continue; + if (Spawner->PrimeDefaultUiSpec().MenuName.IsEmpty()) continue; + Result.Add(Spawner); + } + } + return Result; +} + +TArray MCPUtils::SearchNodeSpawners(const FString& Query, int32 MaxResults, bool ExactMatch) +{ + FString QueryLower = Query.ToLower(); + TArray Result; + + for (UBlueprintNodeSpawner* Spawner : AllNodeSpawners()) + { + FString FullName = NodeSpawnerFullName(Spawner); + + if (ExactMatch) + { + if (FullName.ToLower() != QueryLower) + { + continue; + } + } + else + { + FString Keywords = Spawner->PrimeDefaultUiSpec().Keywords.ToString(); + if (!FullName.ToLower().Contains(QueryLower) + && !Keywords.ToLower().Contains(QueryLower)) + { + continue; + } + } + + Result.Add(Spawner); + + if ((MaxResults > 0) && (Result.Num() >= MaxResults)) + { + break; + } + } + return Result; +} diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/BlueprintMCPServer.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/BlueprintMCPServer.h index 5bab8836..aa6a7c9b 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/BlueprintMCPServer.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/BlueprintMCPServer.h @@ -254,47 +254,19 @@ private: void HandleSetStateBlendSpace(const FJsonObject* Json, FJsonObject* Result); public: - // ----- Serialization ----- - TSharedRef SerializeBlueprint(UBlueprint* BP); - TSharedPtr SerializeGraph(UEdGraph* Graph); - TSharedPtr SerializeNode(UEdGraphNode* Node); - TSharedPtr SerializePin(UEdGraphPin* Pin); - TSharedPtr SerializeMaterialExpression(UMaterialExpression* Expression); - FString JsonToString(TSharedRef JsonObj); - - // ----- Helpers ----- + // ----- Helpers (stateful — use cached asset lists) ----- FAssetData* FindAnyAsset(const FString& NameOrPath); FAssetData* FindBlueprintAsset(const FString& NameOrPath); FAssetData* FindMapAsset(const FString& NameOrPath); UBlueprint* LoadBlueprintByName(const FString& NameOrPath, FString& OutError); - UEdGraphNode* FindNodeByGuid(UBlueprint* BP, const FString& GuidString, UEdGraph** OutGraph = nullptr); - TSharedPtr ParseBodyJson(const FString& Body); - FString MakeErrorJson(const FString& Message); - void MakeErrorJson(FJsonObject* Result, const FString& Message); - bool SaveBlueprintPackage(UBlueprint* BP); - static void CopyJsonFields(const FJsonObject* Source, FJsonObject* Dest); - static FString UrlDecode(const FString& EncodedString); - // Populate UPROPERTY fields of a UStruct (or UClass) instance from JSON. - // Returns empty string on success, or an error message on failure. - FString PopulateFromJson(UStruct* StructType, void* Container, const TSharedPtr& JsonValue); - FString PopulateFromJson(UStruct* StructType, void* Container, const FJsonObject* Json); - - // ----- Material helpers ----- - /** Ensure that Material->MaterialGraph exists (creates it on demand for commandlet mode). */ - void EnsureMaterialGraph(UMaterial* Material); + // ----- Material helpers (stateful — use cached asset lists) ----- FAssetData* FindMaterialAsset(const FString& NameOrPath); UMaterial* LoadMaterialByName(const FString& NameOrPath, FString& OutError); FAssetData* FindMaterialInstanceAsset(const FString& NameOrPath); UMaterialInstanceConstant* LoadMaterialInstanceByName(const FString& NameOrPath, FString& OutError); FAssetData* FindMaterialFunctionAsset(const FString& NameOrPath); UMaterialFunction* LoadMaterialFunctionByName(const FString& NameOrPath, FString& OutError); - bool SaveMaterialPackage(UMaterial* Material); - bool SaveGenericPackage(UObject* Asset); - - // ----- Type resolution ----- - bool ResolveTypeFromString(const FString& TypeName, FEdGraphPinType& OutPinType, FString& OutError); - static UClass* FindClassByName(const FString& ClassName); // ----- Snapshot storage ----- TMap Snapshots; @@ -309,5 +281,5 @@ public: bool LoadSnapshotFromDisk(const FString& SnapshotId, FGraphSnapshot& OutSnapshot); }; -// Transitional alias — eventually MCPHelper will be its own class. +// Transitional alias — old-style handlers use this to access the server instance. using MCPHelper = FBlueprintMCPServer; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPUtils.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPUtils.h new file mode 100644 index 00000000..bc4ba3b3 --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPUtils.h @@ -0,0 +1,64 @@ +#pragma once + +#include "CoreMinimal.h" +#include "Dom/JsonObject.h" +#include "EdGraph/EdGraphPin.h" + +class UBlueprint; +class UEdGraph; +class UEdGraphNode; +class UEdGraphPin; +class UMaterial; +class UMaterialExpression; +class UBlueprintNodeSpawner; +class UAnimationStateMachineGraph; +class UAnimStateNode; +class UAnimStateTransitionNode; + +// Stateless utility functions used by MCP handlers and the MCP server. +// This is effectively a namespace — all methods are static. +class MCPUtils +{ +public: + // ----- JSON helpers ----- + static FString JsonToString(TSharedRef JsonObj); + static TSharedPtr ParseBodyJson(const FString& Body); + static FString MakeErrorJson(const FString& Message); + static void MakeErrorJson(FJsonObject* Result, const FString& Message); + static void CopyJsonFields(const FJsonObject* Source, FJsonObject* Dest); + static FString UrlDecode(const FString& EncodedString); + + // ----- Blueprint helpers ----- + static UEdGraphNode* FindNodeByGuid(UBlueprint* BP, const FString& GuidString, UEdGraph** OutGraph = nullptr); + static bool SaveBlueprintPackage(UBlueprint* BP); + + // ----- Serialization ----- + static TSharedRef SerializeBlueprint(UBlueprint* BP); + static TSharedPtr SerializeGraph(UEdGraph* Graph); + static TSharedPtr SerializeNode(UEdGraphNode* Node); + static TSharedPtr SerializePin(UEdGraphPin* Pin); + static TSharedPtr SerializeMaterialExpression(UMaterialExpression* Expression); + + // ----- Type resolution ----- + static UClass* FindClassByName(const FString& ClassName); + static bool ResolveTypeFromString(const FString& TypeName, FEdGraphPinType& OutPinType, FString& OutError); + + // ----- Material helpers ----- + static void EnsureMaterialGraph(UMaterial* Material); + static bool SaveMaterialPackage(UMaterial* Material); + static bool SaveGenericPackage(UObject* Asset); + + // ----- Anim blueprint helpers ----- + static UAnimationStateMachineGraph* FindStateMachineGraph(UBlueprint* BP, const FString& GraphName); + static UAnimStateNode* FindStateByName(UAnimationStateMachineGraph* SMGraph, const FString& StateName); + static UAnimStateTransitionNode* FindTransition(UAnimationStateMachineGraph* SMGraph, const FString& FromStateName, const FString& ToStateName); + + // ----- Node spawners ----- + static FString NodeSpawnerFullName(UBlueprintNodeSpawner* Spawner); + static TArray AllNodeSpawners(); + static TArray SearchNodeSpawners(const FString& Query, int32 MaxResults = 0, bool ExactMatch = false); + + // ----- Property population ----- + static FString PopulateFromJson(UStruct* StructType, void* Container, const TSharedPtr& JsonValue); + static FString PopulateFromJson(UStruct* StructType, void* Container, const FJsonObject* Json); +};