Create MCPUtils

This commit is contained in:
2026-03-06 15:06:12 -05:00
parent 7e3a4d2fea
commit 9f405ce5ba
22 changed files with 1872 additions and 1736 deletions

View File

@@ -1,4 +1,5 @@
#include "BlueprintMCPServer.h" #include "BlueprintMCPServer.h"
#include "MCPUtils.h"
#include "Engine/Blueprint.h" #include "Engine/Blueprint.h"
#include "EdGraph/EdGraph.h" #include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphNode.h" #include "EdGraph/EdGraphNode.h"
@@ -47,19 +48,19 @@ void FBlueprintMCPServer::HandleCreateAnimBlueprint(const FJsonObject* Json, FJs
if (Name.IsEmpty() || PackagePath.IsEmpty() || SkeletonName.IsEmpty()) 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"))) 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 // Check if asset already exists
FString FullAssetPath = PackagePath / Name; FString FullAssetPath = PackagePath / Name;
if (FindBlueprintAsset(Name) || FindBlueprintAsset(FullAssetPath)) 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."), TEXT("Blueprint '%s' already exists. Use a different name or delete the existing asset first."),
*Name)); *Name));
} }
@@ -114,7 +115,7 @@ void FBlueprintMCPServer::HandleCreateAnimBlueprint(const FJsonObject* Json, FJs
if (!Skeleton) 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."), TEXT("Skeleton '%s' not found. Provide the skeleton asset name or path. Use '__create_test_skeleton__' for testing."),
*SkeletonName)); *SkeletonName));
} }
@@ -141,7 +142,7 @@ void FBlueprintMCPServer::HandleCreateAnimBlueprint(const FJsonObject* Json, FJs
UPackage* Package = CreatePackage(*FullPackagePath); UPackage* Package = CreatePackage(*FullPackagePath);
if (!Package) 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 // Create the Animation Blueprint
@@ -157,7 +158,7 @@ void FBlueprintMCPServer::HandleCreateAnimBlueprint(const FJsonObject* Json, FJs
if (!NewAnimBP) 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 // Set target skeleton
@@ -167,7 +168,7 @@ void FBlueprintMCPServer::HandleCreateAnimBlueprint(const FJsonObject* Json, FJs
FKismetEditorUtilities::CompileBlueprint(NewAnimBP); FKismetEditorUtilities::CompileBlueprint(NewAnimBP);
// Save // Save
bool bSaved = SaveBlueprintPackage(NewAnimBP); bool bSaved = MCPUtils::SaveBlueprintPackage(NewAnimBP);
// Refresh asset cache // Refresh asset cache
FAssetRegistryModule& ARM = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry"); FAssetRegistryModule& ARM = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
@@ -204,61 +205,6 @@ void FBlueprintMCPServer::HandleCreateAnimBlueprint(const FJsonObject* Json, FJs
// Tier 2: State Machine Mutation // 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<UEdGraph*> AllGraphs;
BP->GetAllGraphs(AllGraphs);
for (UEdGraph* Graph : AllGraphs)
{
if (UAnimationStateMachineGraph* SMGraph = Cast<UAnimationStateMachineGraph>(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<UAnimStateNode>(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<UAnimStateTransitionNode>(Node))
{
UAnimStateNode* FromState = Cast<UAnimStateNode>(TransNode->GetPreviousState());
UAnimStateNode* ToState = Cast<UAnimStateNode>(TransNode->GetNextState());
if (FromState && ToState &&
FromState->GetStateName() == FromStateName &&
ToState->GetStateName() == ToStateName)
{
return TransNode;
}
}
}
return nullptr;
}
void FBlueprintMCPServer::HandleAddAnimState(const FJsonObject* Json, FJsonObject* Result) void FBlueprintMCPServer::HandleAddAnimState(const FJsonObject* Json, FJsonObject* Result)
{ {
FString BlueprintName = Json->GetStringField(TEXT("blueprint")); FString BlueprintName = Json->GetStringField(TEXT("blueprint"));
@@ -267,32 +213,32 @@ void FBlueprintMCPServer::HandleAddAnimState(const FJsonObject* Json, FJsonObjec
if (BlueprintName.IsEmpty() || GraphName.IsEmpty() || StateName.IsEmpty()) 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; FString LoadError;
UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError); UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError);
if (!BP) if (!BP)
{ {
return MakeErrorJson(Result, LoadError); return MCPUtils::MakeErrorJson(Result, LoadError);
} }
UAnimBlueprint* AnimBP = Cast<UAnimBlueprint>(BP); UAnimBlueprint* AnimBP = Cast<UAnimBlueprint>(BP);
if (!AnimBP) 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) 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 // 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 // Get position
@@ -352,7 +298,7 @@ void FBlueprintMCPServer::HandleAddAnimState(const FJsonObject* Json, FJsonObjec
// Compile and save // Compile and save
FKismetEditorUtilities::CompileBlueprint(AnimBP); FKismetEditorUtilities::CompileBlueprint(AnimBP);
bool bSaved = SaveBlueprintPackage(AnimBP); bool bSaved = MCPUtils::SaveBlueprintPackage(AnimBP);
Result->SetBoolField(TEXT("success"), true); Result->SetBoolField(TEXT("success"), true);
Result->SetStringField(TEXT("stateName"), StateName); Result->SetStringField(TEXT("stateName"), StateName);
@@ -369,32 +315,32 @@ void FBlueprintMCPServer::HandleRemoveAnimState(const FJsonObject* Json, FJsonOb
if (BlueprintName.IsEmpty() || GraphName.IsEmpty() || StateName.IsEmpty()) 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; FString LoadError;
UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError); UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError);
if (!BP) if (!BP)
{ {
return MakeErrorJson(Result, LoadError); return MCPUtils::MakeErrorJson(Result, LoadError);
} }
UAnimBlueprint* AnimBP = Cast<UAnimBlueprint>(BP); UAnimBlueprint* AnimBP = Cast<UAnimBlueprint>(BP);
if (!AnimBP) 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) 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) 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 // Collect and remove transitions connected to this state
@@ -423,7 +369,7 @@ void FBlueprintMCPServer::HandleRemoveAnimState(const FJsonObject* Json, FJsonOb
// Compile and save // Compile and save
FKismetEditorUtilities::CompileBlueprint(AnimBP); FKismetEditorUtilities::CompileBlueprint(AnimBP);
bool bSaved = SaveBlueprintPackage(AnimBP); bool bSaved = MCPUtils::SaveBlueprintPackage(AnimBP);
Result->SetBoolField(TEXT("success"), true); Result->SetBoolField(TEXT("success"), true);
Result->SetStringField(TEXT("removedState"), StateName); 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()) 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; FString LoadError;
UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError); UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError);
if (!BP) if (!BP)
{ {
return MakeErrorJson(Result, LoadError); return MCPUtils::MakeErrorJson(Result, LoadError);
} }
UAnimBlueprint* AnimBP = Cast<UAnimBlueprint>(BP); UAnimBlueprint* AnimBP = Cast<UAnimBlueprint>(BP);
if (!AnimBP) 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) 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) 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) 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 // Create transition node
@@ -506,7 +452,7 @@ void FBlueprintMCPServer::HandleAddAnimTransition(const FJsonObject* Json, FJson
// Compile and save // Compile and save
FKismetEditorUtilities::CompileBlueprint(AnimBP); FKismetEditorUtilities::CompileBlueprint(AnimBP);
bool bSaved = SaveBlueprintPackage(AnimBP); bool bSaved = MCPUtils::SaveBlueprintPackage(AnimBP);
Result->SetBoolField(TEXT("success"), true); Result->SetBoolField(TEXT("success"), true);
Result->SetStringField(TEXT("fromState"), FromStateName); 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()) 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; FString LoadError;
UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError); UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError);
if (!BP) if (!BP)
{ {
return MakeErrorJson(Result, LoadError); return MCPUtils::MakeErrorJson(Result, LoadError);
} }
UAnimBlueprint* AnimBP = Cast<UAnimBlueprint>(BP); UAnimBlueprint* AnimBP = Cast<UAnimBlueprint>(BP);
if (!AnimBP) 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) 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) if (!TransNode)
{ {
return MakeErrorJson(Result, FString::Printf( return MCPUtils::MakeErrorJson(Result, FString::Printf(
TEXT("Transition from '%s' to '%s' not found in graph '%s'"), TEXT("Transition from '%s' to '%s' not found in graph '%s'"),
*FromStateName, *ToStateName, *GraphName)); *FromStateName, *ToStateName, *GraphName));
} }
@@ -588,12 +534,12 @@ void FBlueprintMCPServer::HandleSetTransitionRule(const FJsonObject* Json, FJson
if (ChangedCount == 0) 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 // Compile and save
FKismetEditorUtilities::CompileBlueprint(AnimBP); FKismetEditorUtilities::CompileBlueprint(AnimBP);
bool bSaved = SaveBlueprintPackage(AnimBP); bool bSaved = MCPUtils::SaveBlueprintPackage(AnimBP);
Result->SetBoolField(TEXT("success"), true); Result->SetBoolField(TEXT("success"), true);
Result->SetStringField(TEXT("fromState"), FromStateName); Result->SetStringField(TEXT("fromState"), FromStateName);
@@ -619,20 +565,20 @@ void FBlueprintMCPServer::HandleAddAnimNode(const FJsonObject* Json, FJsonObject
if (BlueprintName.IsEmpty() || NodeType.IsEmpty()) 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; FString LoadError;
UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError); UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError);
if (!BP) if (!BP)
{ {
return MakeErrorJson(Result, LoadError); return MCPUtils::MakeErrorJson(Result, LoadError);
} }
UAnimBlueprint* AnimBP = Cast<UAnimBlueprint>(BP); UAnimBlueprint* AnimBP = Cast<UAnimBlueprint>(BP);
if (!AnimBP) 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) // Find target graph (default to AnimGraph if not specified)
@@ -655,7 +601,7 @@ void FBlueprintMCPServer::HandleAddAnimNode(const FJsonObject* Json, FJsonObject
if (!TargetGraph) 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; int32 PosX = Json->HasField(TEXT("posX")) ? (int32)Json->GetNumberField(TEXT("posX")) : 0;
@@ -735,14 +681,14 @@ void FBlueprintMCPServer::HandleAddAnimNode(const FJsonObject* Json, FJsonObject
} }
else else
{ {
return MakeErrorJson(Result, FString::Printf( return MCPUtils::MakeErrorJson(Result, FString::Printf(
TEXT("Unsupported nodeType '%s'. Supported: SequencePlayer, BlendSpacePlayer, StateMachine"), TEXT("Unsupported nodeType '%s'. Supported: SequencePlayer, BlendSpacePlayer, StateMachine"),
*NodeType)); *NodeType));
} }
if (!NewNode) if (!NewNode)
{ {
return MakeErrorJson(Result, TEXT("Failed to create anim node")); return MCPUtils::MakeErrorJson(Result, TEXT("Failed to create anim node"));
} }
NewNode->NodePosX = PosX; NewNode->NodePosX = PosX;
@@ -752,7 +698,7 @@ void FBlueprintMCPServer::HandleAddAnimNode(const FJsonObject* Json, FJsonObject
// Compile and save // Compile and save
FKismetEditorUtilities::CompileBlueprint(AnimBP); FKismetEditorUtilities::CompileBlueprint(AnimBP);
bool bSaved = SaveBlueprintPackage(AnimBP); bool bSaved = MCPUtils::SaveBlueprintPackage(AnimBP);
Result->SetBoolField(TEXT("success"), true); Result->SetBoolField(TEXT("success"), true);
Result->SetStringField(TEXT("nodeType"), NodeType); Result->SetStringField(TEXT("nodeType"), NodeType);
@@ -784,7 +730,7 @@ void FBlueprintMCPServer::HandleAddStateMachine(const FJsonObject* Json, FJsonOb
if (BlueprintName.IsEmpty()) if (BlueprintName.IsEmpty())
{ {
return MakeErrorJson(Result, TEXT("Missing required field: blueprint")); return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: blueprint"));
} }
// Default name // Default name
@@ -815,38 +761,38 @@ void FBlueprintMCPServer::HandleSetStateAnimation(const FJsonObject* Json, FJson
if (BlueprintName.IsEmpty() || GraphName.IsEmpty() || StateName.IsEmpty() || AnimAssetName.IsEmpty()) 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; FString LoadError;
UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError); UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError);
if (!BP) if (!BP)
{ {
return MakeErrorJson(Result, LoadError); return MCPUtils::MakeErrorJson(Result, LoadError);
} }
UAnimBlueprint* AnimBP = Cast<UAnimBlueprint>(BP); UAnimBlueprint* AnimBP = Cast<UAnimBlueprint>(BP);
if (!AnimBP) 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) 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) 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(); UEdGraph* InnerGraph = StateNode->GetBoundGraph();
if (!InnerGraph) 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 // Find the animation asset
@@ -866,7 +812,7 @@ void FBlueprintMCPServer::HandleSetStateAnimation(const FJsonObject* Json, FJson
if (!AnimSeq) 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 // Find existing SequencePlayer or create one
@@ -894,7 +840,7 @@ void FBlueprintMCPServer::HandleSetStateAnimation(const FJsonObject* Json, FJson
// Compile and save // Compile and save
FKismetEditorUtilities::CompileBlueprint(AnimBP); FKismetEditorUtilities::CompileBlueprint(AnimBP);
bool bSaved = SaveBlueprintPackage(AnimBP); bool bSaved = MCPUtils::SaveBlueprintPackage(AnimBP);
Result->SetBoolField(TEXT("success"), true); Result->SetBoolField(TEXT("success"), true);
Result->SetStringField(TEXT("stateName"), StateName); Result->SetStringField(TEXT("stateName"), StateName);
@@ -908,20 +854,20 @@ void FBlueprintMCPServer::HandleListAnimSlots(const FJsonObject* Json, FJsonObje
FString BlueprintName = Json->GetStringField(TEXT("blueprint")); FString BlueprintName = Json->GetStringField(TEXT("blueprint"));
if (BlueprintName.IsEmpty()) if (BlueprintName.IsEmpty())
{ {
return MakeErrorJson(Result, TEXT("Missing required field: blueprint")); return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: blueprint"));
} }
FString LoadError; FString LoadError;
UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError); UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError);
if (!BP) if (!BP)
{ {
return MakeErrorJson(Result, LoadError); return MCPUtils::MakeErrorJson(Result, LoadError);
} }
UAnimBlueprint* AnimBP = Cast<UAnimBlueprint>(BP); UAnimBlueprint* AnimBP = Cast<UAnimBlueprint>(BP);
if (!AnimBP) 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 // 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")); FString BlueprintName = Json->GetStringField(TEXT("blueprint"));
if (BlueprintName.IsEmpty()) if (BlueprintName.IsEmpty())
{ {
return MakeErrorJson(Result, TEXT("Missing required field: blueprint")); return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: blueprint"));
} }
FString LoadError; FString LoadError;
UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError); UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError);
if (!BP) if (!BP)
{ {
return MakeErrorJson(Result, LoadError); return MCPUtils::MakeErrorJson(Result, LoadError);
} }
UAnimBlueprint* AnimBP = Cast<UAnimBlueprint>(BP); UAnimBlueprint* AnimBP = Cast<UAnimBlueprint>(BP);
if (!AnimBP) 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 // 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()) 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"))) 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 // Check if asset already exists
@@ -1051,7 +997,7 @@ void FBlueprintMCPServer::HandleCreateBlendSpace(const FJsonObject* Json, FJsonO
{ {
if (Asset.AssetName.ToString() == Name || Asset.GetObjectPathString() == FullAssetPath) 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."), TEXT("Blend Space '%s' already exists. Use a different name or delete the existing asset first."),
*Name)); *Name));
} }
@@ -1106,7 +1052,7 @@ void FBlueprintMCPServer::HandleCreateBlendSpace(const FJsonObject* Json, FJsonO
if (!Skeleton) 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."), TEXT("Skeleton '%s' not found. Provide the skeleton asset name or path. Use '__create_test_skeleton__' for testing."),
*SkeletonName)); *SkeletonName));
} }
@@ -1119,14 +1065,14 @@ void FBlueprintMCPServer::HandleCreateBlendSpace(const FJsonObject* Json, FJsonO
UPackage* Package = CreatePackage(*FullPackagePath); UPackage* Package = CreatePackage(*FullPackagePath);
if (!Package) 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 // Create the Blend Space
UBlendSpace* NewBS = NewObject<UBlendSpace>(Package, FName(*Name), RF_Public | RF_Standalone); UBlendSpace* NewBS = NewObject<UBlendSpace>(Package, FName(*Name), RF_Public | RF_Standalone);
if (!NewBS) 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 // Set skeleton
@@ -1134,7 +1080,7 @@ void FBlueprintMCPServer::HandleCreateBlendSpace(const FJsonObject* Json, FJsonO
// Mark dirty and save // Mark dirty and save
NewBS->MarkPackageDirty(); NewBS->MarkPackageDirty();
bool bSaved = SaveGenericPackage(NewBS); bool bSaved = MCPUtils::SaveGenericPackage(NewBS);
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Created Blend Space '%s' (saved: %s)"), UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Created Blend Space '%s' (saved: %s)"),
*Name, bSaved ? TEXT("true") : TEXT("false")); *Name, bSaved ? TEXT("true") : TEXT("false"));
@@ -1154,7 +1100,7 @@ void FBlueprintMCPServer::HandleSetBlendSpaceSamples(const FJsonObject* Json, FJ
FString BlendSpaceName = Json->GetStringField(TEXT("blendSpace")); FString BlendSpaceName = Json->GetStringField(TEXT("blendSpace"));
if (BlendSpaceName.IsEmpty()) if (BlendSpaceName.IsEmpty())
{ {
return MakeErrorJson(Result, TEXT("Missing required field: blendSpace")); return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: blendSpace"));
} }
// Load the blend space // Load the blend space
@@ -1190,7 +1136,7 @@ void FBlueprintMCPServer::HandleSetBlendSpaceSamples(const FJsonObject* Json, FJ
if (!BS) 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 // Set axis parameters
@@ -1288,7 +1234,7 @@ void FBlueprintMCPServer::HandleSetBlendSpaceSamples(const FJsonObject* Json, FJ
// Save // Save
BS->MarkPackageDirty(); 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)"), UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Set %d samples on Blend Space '%s' (saved: %s)"),
SamplesSet, *BS->GetName(), bSaved ? TEXT("true") : TEXT("false")); 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()) 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; FString LoadError;
UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError); UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError);
if (!BP) if (!BP)
{ {
return MakeErrorJson(Result, LoadError); return MCPUtils::MakeErrorJson(Result, LoadError);
} }
UAnimBlueprint* AnimBP = Cast<UAnimBlueprint>(BP); UAnimBlueprint* AnimBP = Cast<UAnimBlueprint>(BP);
if (!AnimBP) 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) 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) 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(); UEdGraph* InnerGraph = StateNode->GetBoundGraph();
if (!InnerGraph) 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 // Find the blend space asset
@@ -1381,7 +1327,7 @@ void FBlueprintMCPServer::HandleSetStateBlendSpace(const FJsonObject* Json, FJso
if (!BlendSpaceAsset) 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 // Find existing BlendSpacePlayer or create one
@@ -1529,7 +1475,7 @@ void FBlueprintMCPServer::HandleSetStateBlendSpace(const FJsonObject* Json, FJso
// Compile and save // Compile and save
FKismetEditorUtilities::CompileBlueprint(AnimBP); FKismetEditorUtilities::CompileBlueprint(AnimBP);
bool bSaved = SaveBlueprintPackage(AnimBP); bool bSaved = MCPUtils::SaveBlueprintPackage(AnimBP);
Result->SetBoolField(TEXT("success"), true); Result->SetBoolField(TEXT("success"), true);
Result->SetStringField(TEXT("stateName"), StateName); Result->SetStringField(TEXT("stateName"), StateName);

View File

@@ -1,4 +1,5 @@
#include "BlueprintMCPServer.h" #include "BlueprintMCPServer.h"
#include "MCPUtils.h"
#include "Engine/Blueprint.h" #include "Engine/Blueprint.h"
#include "Engine/SimpleConstructionScript.h" #include "Engine/SimpleConstructionScript.h"
#include "Engine/SCS_Node.h" #include "Engine/SCS_Node.h"
@@ -18,20 +19,20 @@ void FBlueprintMCPServer::HandleListComponents(const FJsonObject* Json, FJsonObj
FString BlueprintName = Json->GetStringField(TEXT("blueprint")); FString BlueprintName = Json->GetStringField(TEXT("blueprint"));
if (BlueprintName.IsEmpty()) if (BlueprintName.IsEmpty())
{ {
return MakeErrorJson(Result, TEXT("Missing required field: blueprint")); return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: blueprint"));
} }
FString LoadError; FString LoadError;
UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError); UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError);
if (!BP) if (!BP)
{ {
return MakeErrorJson(Result, LoadError); return MCPUtils::MakeErrorJson(Result, LoadError);
} }
USimpleConstructionScript* SCS = BP->SimpleConstructionScript; USimpleConstructionScript* SCS = BP->SimpleConstructionScript;
if (!SCS) 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)"), TEXT("Blueprint '%s' does not have a SimpleConstructionScript (not an Actor Blueprint)"),
*BlueprintName)); *BlueprintName));
} }
@@ -106,7 +107,7 @@ void FBlueprintMCPServer::HandleAddComponent(const FJsonObject* Json, FJsonObjec
if (BlueprintName.IsEmpty() || ComponentClassName.IsEmpty() || ComponentName.IsEmpty()) 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; FString ParentComponentName;
@@ -119,13 +120,13 @@ void FBlueprintMCPServer::HandleAddComponent(const FJsonObject* Json, FJsonObjec
UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError); UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError);
if (!BP) if (!BP)
{ {
return MakeErrorJson(Result, LoadError); return MCPUtils::MakeErrorJson(Result, LoadError);
} }
USimpleConstructionScript* SCS = BP->SimpleConstructionScript; USimpleConstructionScript* SCS = BP->SimpleConstructionScript;
if (!SCS) 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)"), TEXT("Blueprint '%s' does not have a SimpleConstructionScript (not an Actor Blueprint)"),
*BlueprintName)); *BlueprintName));
} }
@@ -136,7 +137,7 @@ void FBlueprintMCPServer::HandleAddComponent(const FJsonObject* Json, FJsonObjec
{ {
if (Existing && Existing->GetVariableName().ToString().Equals(ComponentName, ESearchCase::IgnoreCase)) 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'"), TEXT("A component named '%s' already exists in Blueprint '%s'"),
*ComponentName, *BlueprintName)); *ComponentName, *BlueprintName));
} }
@@ -183,7 +184,7 @@ void FBlueprintMCPServer::HandleAddComponent(const FJsonObject* Json, FJsonObjec
if (!ComponentClass) 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. " TEXT("Component class '%s' not found or is not a subclass of UActorComponent. "
"Common classes: StaticMeshComponent, SkeletalMeshComponent, AudioComponent, " "Common classes: StaticMeshComponent, SkeletalMeshComponent, AudioComponent, "
"SceneComponent, BoxCollisionComponent, SphereCollisionComponent, CapsuleComponent, " "SceneComponent, BoxCollisionComponent, SphereCollisionComponent, CapsuleComponent, "
@@ -207,7 +208,7 @@ void FBlueprintMCPServer::HandleAddComponent(const FJsonObject* Json, FJsonObjec
if (!ParentSCSNode) if (!ParentSCSNode)
{ {
return MakeErrorJson(Result, FString::Printf( return MCPUtils::MakeErrorJson(Result, FString::Printf(
TEXT("Parent component '%s' not found in Blueprint '%s'"), TEXT("Parent component '%s' not found in Blueprint '%s'"),
*ParentComponentName, *BlueprintName)); *ParentComponentName, *BlueprintName));
} }
@@ -220,7 +221,7 @@ void FBlueprintMCPServer::HandleAddComponent(const FJsonObject* Json, FJsonObjec
USCS_Node* NewNode = SCS->CreateNode(ComponentClass, FName(*ComponentName)); USCS_Node* NewNode = SCS->CreateNode(ComponentClass, FName(*ComponentName));
if (!NewNode) 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'"), TEXT("Failed to create SCS node for component '%s' with class '%s'"),
*ComponentName, *ComponentClass->GetName())); *ComponentName, *ComponentClass->GetName()));
} }
@@ -236,7 +237,7 @@ void FBlueprintMCPServer::HandleAddComponent(const FJsonObject* Json, FJsonObjec
} }
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP); 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)"), UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Added component '%s' (%s) to '%s' (parent: %s, saved: %s)"),
*ComponentName, *ComponentClass->GetName(), *BlueprintName, *ComponentName, *ComponentClass->GetName(), *BlueprintName,
@@ -265,20 +266,20 @@ void FBlueprintMCPServer::HandleRemoveComponent(const FJsonObject* Json, FJsonOb
if (BlueprintName.IsEmpty() || ComponentName.IsEmpty()) 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; FString LoadError;
UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError); UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError);
if (!BP) if (!BP)
{ {
return MakeErrorJson(Result, LoadError); return MCPUtils::MakeErrorJson(Result, LoadError);
} }
USimpleConstructionScript* SCS = BP->SimpleConstructionScript; USimpleConstructionScript* SCS = BP->SimpleConstructionScript;
if (!SCS) 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)"), TEXT("Blueprint '%s' does not have a SimpleConstructionScript (not an Actor Blueprint)"),
*BlueprintName)); *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'"), TEXT("Component '%s' not found in Blueprint '%s'"),
*ComponentName, *BlueprintName)); *ComponentName, *BlueprintName));
Result->SetArrayField(TEXT("existingComponents"), CompList); Result->SetArrayField(TEXT("existingComponents"), CompList);
@@ -318,7 +319,7 @@ void FBlueprintMCPServer::HandleRemoveComponent(const FJsonObject* Json, FJsonOb
const TArray<USCS_Node*>& RootNodes = SCS->GetRootNodes(); const TArray<USCS_Node*>& RootNodes = SCS->GetRootNodes();
if (RootNodes.Contains(NodeToRemove) && NodeToRemove->GetChildNodes().Num() > 0) 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). " TEXT("Cannot remove component '%s' because it is a root component with %d child(ren). "
"Remove or re-parent the children first."), "Remove or re-parent the children first."),
*ComponentName, NodeToRemove->GetChildNodes().Num())); *ComponentName, NodeToRemove->GetChildNodes().Num()));
@@ -331,7 +332,7 @@ void FBlueprintMCPServer::HandleRemoveComponent(const FJsonObject* Json, FJsonOb
SCS->RemoveNodeAndPromoteChildren(NodeToRemove); SCS->RemoveNodeAndPromoteChildren(NodeToRemove);
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP); 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)"), UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Removed component '%s' from '%s' (saved: %s)"),
*ComponentName, *BlueprintName, bSaved ? TEXT("true") : TEXT("false")); *ComponentName, *BlueprintName, bSaved ? TEXT("true") : TEXT("false"));

View File

@@ -1,5 +1,6 @@
#include "BlueprintMCPHandlers_DiffBlueprints.h" #include "BlueprintMCPHandlers_DiffBlueprints.h"
#include "BlueprintMCPServer.h" #include "BlueprintMCPServer.h"
#include "MCPUtils.h"
#include "Engine/Blueprint.h" #include "Engine/Blueprint.h"
#include "EdGraph/EdGraph.h" #include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphNode.h" #include "EdGraph/EdGraphNode.h"
@@ -12,10 +13,10 @@ void UMCPHandler_DiffBlueprints::Handle(const FJsonObject* Json, FJsonObject* Re
// Load both blueprints // Load both blueprints
FString LoadErrorA, LoadErrorB; FString LoadErrorA, LoadErrorB;
UBlueprint* BPA = Helper->LoadBlueprintByName(BlueprintA, LoadErrorA); 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); 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 // Helper to gather graphs from a Blueprint
auto GatherGraphs = [this](UBlueprint* BP) -> TArray<UEdGraph*> auto GatherGraphs = [this](UBlueprint* BP) -> TArray<UEdGraph*>

View File

@@ -1,4 +1,5 @@
#include "BlueprintMCPServer.h" #include "BlueprintMCPServer.h"
#include "MCPUtils.h"
#include "Engine/Blueprint.h" #include "Engine/Blueprint.h"
#include "EdGraph/EdGraph.h" #include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphNode.h" #include "EdGraph/EdGraphNode.h"
@@ -23,21 +24,21 @@ void FBlueprintMCPServer::HandleGetPinInfo(const FJsonObject* Json, FJsonObject*
if (BlueprintName.IsEmpty() || NodeId.IsEmpty() || PinName.IsEmpty()) 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; FString LoadError;
UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError); UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError);
if (!BP) if (!BP)
{ {
return MakeErrorJson(Result, LoadError); return MCPUtils::MakeErrorJson(Result, LoadError);
} }
UEdGraph* Graph = nullptr; UEdGraph* Graph = nullptr;
UEdGraphNode* Node = FindNodeByGuid(BP, NodeId, &Graph); UEdGraphNode* Node = MCPUtils::FindNodeByGuid(BP, NodeId, &Graph);
if (!Node) 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)); UEdGraphPin* Pin = Node->FindPin(FName(*PinName));
@@ -56,7 +57,7 @@ void FBlueprintMCPServer::HandleGetPinInfo(const FJsonObject* Json, FJsonObject*
AvailPins.Add(MakeShared<FJsonValueObject>(PinObj)); AvailPins.Add(MakeShared<FJsonValueObject>(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); Result->SetArrayField(TEXT("availablePins"), AvailPins);
return; return;
} }
@@ -128,45 +129,45 @@ void FBlueprintMCPServer::HandleCheckPinCompatibility(const FJsonObject* Json, F
if (BlueprintName.IsEmpty() || SourceNodeId.IsEmpty() || SourcePinName.IsEmpty() || if (BlueprintName.IsEmpty() || SourceNodeId.IsEmpty() || SourcePinName.IsEmpty() ||
TargetNodeId.IsEmpty() || TargetPinName.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; FString LoadError;
UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError); UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError);
if (!BP) if (!BP)
{ {
return MakeErrorJson(Result, LoadError); return MCPUtils::MakeErrorJson(Result, LoadError);
} }
UEdGraph* SourceGraph = nullptr; UEdGraph* SourceGraph = nullptr;
UEdGraphNode* SourceNode = FindNodeByGuid(BP, SourceNodeId, &SourceGraph); UEdGraphNode* SourceNode = MCPUtils::FindNodeByGuid(BP, SourceNodeId, &SourceGraph);
if (!SourceNode) 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) 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)); UEdGraphPin* SourcePin = SourceNode->FindPin(FName(*SourcePinName));
if (!SourcePin) 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)); UEdGraphPin* TargetPin = TargetNode->FindPin(FName(*TargetPinName));
if (!TargetPin) 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; const UEdGraphSchema* Schema = SourceGraph ? SourceGraph->GetSchema() : nullptr;
if (!Schema) if (!Schema)
{ {
return MakeErrorJson(Result, TEXT("Graph schema not found")); return MCPUtils::MakeErrorJson(Result, TEXT("Graph schema not found"));
} }
// Check compatibility using the schema // Check compatibility using the schema
@@ -248,7 +249,7 @@ void FBlueprintMCPServer::HandleListClasses(const FJsonObject* Json, FJsonObject
} }
if (!ParentClass) 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()) if (ClassName.IsEmpty())
{ {
return MakeErrorJson(Result, TEXT("Missing required field: className")); return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: className"));
} }
// Find the class // Find the class
@@ -348,7 +349,7 @@ void FBlueprintMCPServer::HandleListFunctions(const FJsonObject* Json, FJsonObje
} }
if (!FoundClass) 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<TSharedPtr<FJsonValue>> FuncList; TArray<TSharedPtr<FJsonValue>> FuncList;
@@ -437,7 +438,7 @@ void FBlueprintMCPServer::HandleListProperties(const FJsonObject* Json, FJsonObj
if (ClassName.IsEmpty()) if (ClassName.IsEmpty())
{ {
return MakeErrorJson(Result, TEXT("Missing required field: className")); return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: className"));
} }
// Find the class // Find the class
@@ -452,7 +453,7 @@ void FBlueprintMCPServer::HandleListProperties(const FJsonObject* Json, FJsonObj
} }
if (!FoundClass) 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<TSharedPtr<FJsonValue>> PropList; TArray<TSharedPtr<FJsonValue>> PropList;

View File

@@ -1,4 +1,5 @@
#include "BlueprintMCPServer.h" #include "BlueprintMCPServer.h"
#include "MCPUtils.h"
#include "Engine/Blueprint.h" #include "Engine/Blueprint.h"
#include "EdGraph/EdGraph.h" #include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphPin.h" #include "EdGraph/EdGraphPin.h"
@@ -21,7 +22,7 @@ void FBlueprintMCPServer::HandleAddEventDispatcher(const FJsonObject* Json, FJso
if (BlueprintName.IsEmpty() || DispatcherName.IsEmpty()) 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 // Load Blueprint
@@ -29,7 +30,7 @@ void FBlueprintMCPServer::HandleAddEventDispatcher(const FJsonObject* Json, FJso
UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError); UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError);
if (!BP) if (!BP)
{ {
return MakeErrorJson(Result, LoadError); return MCPUtils::MakeErrorJson(Result, LoadError);
} }
FName DispatcherFName(*DispatcherName); FName DispatcherFName(*DispatcherName);
@@ -39,7 +40,7 @@ void FBlueprintMCPServer::HandleAddEventDispatcher(const FJsonObject* Json, FJso
{ {
if (Var.VarName == DispatcherFName) 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'"), TEXT("A variable or dispatcher named '%s' already exists in Blueprint '%s'"),
*DispatcherName, *BlueprintName)); *DispatcherName, *BlueprintName));
} }
@@ -52,7 +53,7 @@ void FBlueprintMCPServer::HandleAddEventDispatcher(const FJsonObject* Json, FJso
{ {
if (Existing && Existing->GetName().Equals(DispatcherName, ESearchCase::IgnoreCase)) 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'"), TEXT("A graph named '%s' already exists in Blueprint '%s'"),
*DispatcherName, *BlueprintName)); *DispatcherName, *BlueprintName));
} }
@@ -67,7 +68,7 @@ void FBlueprintMCPServer::HandleAddEventDispatcher(const FJsonObject* Json, FJso
bool bVarAdded = FBlueprintEditorUtils::AddMemberVariable(BP, DispatcherFName, DelegateType); bool bVarAdded = FBlueprintEditorUtils::AddMemberVariable(BP, DispatcherFName, DelegateType);
if (!bVarAdded) if (!bVarAdded)
{ {
return MakeErrorJson(Result, FString::Printf( return MCPUtils::MakeErrorJson(Result, FString::Printf(
TEXT("Failed to add delegate variable for '%s'"), *DispatcherName)); 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()); UEdGraph::StaticClass(), UEdGraphSchema_K2::StaticClass());
if (!SigGraph) 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); K2Schema->CreateDefaultNodesForGraph(*SigGraph);
@@ -114,8 +115,8 @@ void FBlueprintMCPServer::HandleAddEventDispatcher(const FJsonObject* Json, FJso
{ {
// Still save what we have // Still save what we have
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP); FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
SaveBlueprintPackage(BP); MCPUtils::SaveBlueprintPackage(BP);
return MakeErrorJson(Result, TEXT("Event dispatcher created but entry node not found — parameters could not be added")); return MCPUtils::MakeErrorJson(Result, TEXT("Event dispatcher created but entry node not found — parameters could not be added"));
} }
for (const TSharedPtr<FJsonValue>& ParamVal : ParamsArr) for (const TSharedPtr<FJsonValue>& ParamVal : ParamsArr)
@@ -130,9 +131,9 @@ void FBlueprintMCPServer::HandleAddEventDispatcher(const FJsonObject* Json, FJso
FEdGraphPinType PinType; FEdGraphPinType PinType;
FString TypeError; 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)); TEXT("Parameter '%s': %s"), *ParamName, *TypeError));
} }
@@ -146,7 +147,7 @@ void FBlueprintMCPServer::HandleAddEventDispatcher(const FJsonObject* Json, FJso
} }
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP); 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)"), 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")); *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")); FString BlueprintName = Json->GetStringField(TEXT("blueprint"));
if (BlueprintName.IsEmpty()) if (BlueprintName.IsEmpty())
{ {
return MakeErrorJson(Result, TEXT("Missing required field: blueprint")); return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: blueprint"));
} }
FString LoadError; FString LoadError;
UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError); UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError);
if (!BP) if (!BP)
{ {
return MakeErrorJson(Result, LoadError); return MCPUtils::MakeErrorJson(Result, LoadError);
} }
TSet<FName> DelegateNameSet; TSet<FName> DelegateNameSet;

View File

@@ -1,4 +1,5 @@
#include "BlueprintMCPServer.h" #include "BlueprintMCPServer.h"
#include "MCPUtils.h"
#include "Engine/Blueprint.h" #include "Engine/Blueprint.h"
#include "Engine/World.h" #include "Engine/World.h"
#include "EdGraph/EdGraph.h" #include "EdGraph/EdGraph.h"
@@ -26,7 +27,7 @@ void FBlueprintMCPServer::HandleReparentBlueprint(const FJsonObject* Json, FJson
if (BlueprintName.IsEmpty() || NewParentName.IsEmpty()) 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 // Load Blueprint
@@ -34,7 +35,7 @@ void FBlueprintMCPServer::HandleReparentBlueprint(const FJsonObject* Json, FJson
UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError); UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError);
if (!BP) if (!BP)
{ {
return MakeErrorJson(Result, LoadError); return MCPUtils::MakeErrorJson(Result, LoadError);
} }
FString OldParentName = BP->ParentClass ? BP->ParentClass->GetName() : TEXT("None"); FString OldParentName = BP->ParentClass ? BP->ParentClass->GetName() : TEXT("None");
@@ -66,7 +67,7 @@ void FBlueprintMCPServer::HandleReparentBlueprint(const FJsonObject* Json, FJson
if (!NewParentClass) 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."), TEXT("Could not find class '%s'. Provide a C++ class name (e.g. 'WebUIHUD') or Blueprint name."),
*NewParentName)); *NewParentName));
} }
@@ -94,7 +95,7 @@ void FBlueprintMCPServer::HandleReparentBlueprint(const FJsonObject* Json, FJson
FKismetEditorUtilities::CompileBlueprint(BP); FKismetEditorUtilities::CompileBlueprint(BP);
// Save // Save
bool bSaved = SaveBlueprintPackage(BP); bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
FString NewParentActualName = NewParentClass->GetName(); FString NewParentActualName = NewParentClass->GetName();
@@ -121,20 +122,20 @@ void FBlueprintMCPServer::HandleCreateBlueprint(const FJsonObject* Json, FJsonOb
if (BlueprintName.IsEmpty() || PackagePath.IsEmpty() || ParentClassName.IsEmpty()) 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 // Validate packagePath starts with /Game
if (!PackagePath.StartsWith(TEXT("/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 // Check if asset already exists
FString FullAssetPath = PackagePath / BlueprintName; FString FullAssetPath = PackagePath / BlueprintName;
if (FindBlueprintAsset(BlueprintName) || FindBlueprintAsset(FullAssetPath)) 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."), TEXT("Blueprint '%s' already exists. Use a different name or delete the existing asset first."),
*BlueprintName)); *BlueprintName));
} }
@@ -163,7 +164,7 @@ void FBlueprintMCPServer::HandleCreateBlueprint(const FJsonObject* Json, FJsonOb
if (!ParentClass) 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."), TEXT("Could not find parent class '%s'. Provide a C++ class name (e.g. 'Actor', 'Pawn') or Blueprint name."),
*ParentClassName)); *ParentClassName));
} }
@@ -186,7 +187,7 @@ void FBlueprintMCPServer::HandleCreateBlueprint(const FJsonObject* Json, FJsonOb
} }
else if (BlueprintTypeStr != TEXT("Normal")) 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"), TEXT("Invalid blueprintType '%s'. Valid values: Normal, Interface, FunctionLibrary, MacroLibrary"),
*BlueprintTypeStr)); *BlueprintTypeStr));
} }
@@ -207,7 +208,7 @@ void FBlueprintMCPServer::HandleCreateBlueprint(const FJsonObject* Json, FJsonOb
UPackage* Package = CreatePackage(*FullPackagePath); UPackage* Package = CreatePackage(*FullPackagePath);
if (!Package) 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 // Create the Blueprint
@@ -222,14 +223,14 @@ void FBlueprintMCPServer::HandleCreateBlueprint(const FJsonObject* Json, FJsonOb
if (!NewBP) if (!NewBP)
{ {
return MakeErrorJson(Result, TEXT("FKismetEditorUtilities::CreateBlueprint returned null")); return MCPUtils::MakeErrorJson(Result, TEXT("FKismetEditorUtilities::CreateBlueprint returned null"));
} }
// Compile // Compile
FKismetEditorUtilities::CompileBlueprint(NewBP); FKismetEditorUtilities::CompileBlueprint(NewBP);
// Save // Save
bool bSaved = SaveBlueprintPackage(NewBP); bool bSaved = MCPUtils::SaveBlueprintPackage(NewBP);
// Refresh asset cache // Refresh asset cache
FAssetRegistryModule& ARM = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry"); FAssetRegistryModule& ARM = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
@@ -276,12 +277,12 @@ void FBlueprintMCPServer::HandleCreateGraph(const FJsonObject* Json, FJsonObject
if (BlueprintName.IsEmpty() || GraphName.IsEmpty() || GraphType.IsEmpty()) 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")) 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)); 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); UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError);
if (!BP) if (!BP)
{ {
return MakeErrorJson(Result, LoadError); return MCPUtils::MakeErrorJson(Result, LoadError);
} }
// Check graph name uniqueness // Check graph name uniqueness
@@ -300,7 +301,7 @@ void FBlueprintMCPServer::HandleCreateGraph(const FJsonObject* Json, FJsonObject
{ {
if (Existing && Existing->GetName().Equals(GraphName, ESearchCase::IgnoreCase)) 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)); 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)) 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)); 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()); UEdGraph::StaticClass(), UEdGraphSchema_K2::StaticClass());
if (!NewGraph) 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<UClass*>(nullptr)); FBlueprintEditorUtils::AddFunctionGraph(BP, NewGraph, /*bIsUserCreated=*/true, /*SignatureFromObject=*/static_cast<UClass*>(nullptr));
} }
@@ -346,7 +347,7 @@ void FBlueprintMCPServer::HandleCreateGraph(const FJsonObject* Json, FJsonObject
UEdGraph::StaticClass(), UEdGraphSchema_K2::StaticClass()); UEdGraph::StaticClass(), UEdGraphSchema_K2::StaticClass());
if (!NewGraph) 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); FBlueprintEditorUtils::AddMacroGraph(BP, NewGraph, /*bIsUserCreated=*/true, /*SignatureFromClass=*/nullptr);
} }
@@ -360,7 +361,7 @@ void FBlueprintMCPServer::HandleCreateGraph(const FJsonObject* Json, FJsonObject
} }
if (!EventGraph) 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 // Create a custom event node in the EventGraph
@@ -375,7 +376,7 @@ void FBlueprintMCPServer::HandleCreateGraph(const FJsonObject* Json, FJsonObject
} }
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP); 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)"), UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Created %s graph '%s' in '%s' (saved: %s)"),
*GraphType, *GraphName, *BlueprintName, bSaved ? TEXT("true") : TEXT("false")); *GraphType, *GraphName, *BlueprintName, bSaved ? TEXT("true") : TEXT("false"));
@@ -402,14 +403,14 @@ void FBlueprintMCPServer::HandleDeleteGraph(const FJsonObject* Json, FJsonObject
if (BlueprintName.IsEmpty() || GraphName.IsEmpty()) 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; FString LoadError;
UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError); UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError);
if (!BP) if (!BP)
{ {
return MakeErrorJson(Result, LoadError); return MCPUtils::MakeErrorJson(Result, LoadError);
} }
// Find the graph // Find the graph
@@ -445,12 +446,12 @@ void FBlueprintMCPServer::HandleDeleteGraph(const FJsonObject* Json, FJsonObject
{ {
if (Graph && Graph->GetName().Equals(GraphName, ESearchCase::IgnoreCase)) 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."), TEXT("Cannot delete UbergraphPage '%s'. EventGraph and other Ubergraph pages cannot be deleted."),
*GraphName)); *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'"), 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::RemoveGraph(BP, TargetGraph, EGraphRemoveFlags::Default);
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP); 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"), UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Deleted graph '%s' (%d nodes), save %s"),
*GraphName, NodeCount, bSaved ? TEXT("true") : TEXT("false")); *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()) 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; FString LoadError;
UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError); UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError);
if (!BP) if (!BP)
{ {
return MakeErrorJson(Result, LoadError); return MCPUtils::MakeErrorJson(Result, LoadError);
} }
// Check if it's an UbergraphPage — disallow rename // 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)) 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."), TEXT("Cannot rename UbergraphPage '%s'. EventGraph and other Ubergraph pages cannot be renamed."),
*GraphName)); *GraphName));
} }
@@ -537,7 +538,7 @@ void FBlueprintMCPServer::HandleRenameGraph(const FJsonObject* Json, FJsonObject
if (!TargetGraph) 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 // 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)) 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)); 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::RenameGraph(TargetGraph, NewName);
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP); 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"), UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Renamed graph '%s' to '%s', save %s"),
*GraphName, *NewName, bSaved ? TEXT("true") : TEXT("false")); *GraphName, *NewName, bSaved ? TEXT("true") : TEXT("false"));

View File

@@ -1,5 +1,6 @@
#include "BlueprintMCPHandlers_Interfaces.h" #include "BlueprintMCPHandlers_Interfaces.h"
#include "BlueprintMCPServer.h" #include "BlueprintMCPServer.h"
#include "MCPUtils.h"
#include "Engine/Blueprint.h" #include "Engine/Blueprint.h"
#include "EdGraph/EdGraph.h" #include "EdGraph/EdGraph.h"
#include "Kismet2/BlueprintEditorUtils.h" #include "Kismet2/BlueprintEditorUtils.h"
@@ -17,7 +18,7 @@ void UMCPHandler_ListInterfaces::Handle(const FJsonObject* Json, FJsonObject* Re
UBlueprint* BP = Helper->LoadBlueprintByName(Blueprint, LoadError); UBlueprint* BP = Helper->LoadBlueprintByName(Blueprint, LoadError);
if (!BP) if (!BP)
{ {
return Helper->MakeErrorJson(Result, LoadError); return MCPUtils::MakeErrorJson(Result, LoadError);
} }
TArray<TSharedPtr<FJsonValue>> InterfacesArr; TArray<TSharedPtr<FJsonValue>> InterfacesArr;
@@ -62,7 +63,7 @@ void UMCPHandler_AddInterface::Handle(const FJsonObject* Json, FJsonObject* Resu
UBlueprint* BP = Helper->LoadBlueprintByName(Blueprint, LoadError); UBlueprint* BP = Helper->LoadBlueprintByName(Blueprint, LoadError);
if (!BP) if (!BP)
{ {
return Helper->MakeErrorJson(Result, LoadError); return MCPUtils::MakeErrorJson(Result, LoadError);
} }
// Resolve the interface class // Resolve the interface class
@@ -109,7 +110,7 @@ void UMCPHandler_AddInterface::Handle(const FJsonObject* Json, FJsonObject* Resu
if (!InterfaceClass) 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."), TEXT("Interface '%s' not found. Provide a Blueprint Interface asset name (e.g. 'BPI_MyInterface') or a native UInterface class name."),
*InterfaceName)); *InterfaceName));
} }
@@ -119,7 +120,7 @@ void UMCPHandler_AddInterface::Handle(const FJsonObject* Json, FJsonObject* Resu
{ {
if (IfaceDesc.Interface == InterfaceClass) 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'"), TEXT("Interface '%s' is already implemented by Blueprint '%s'"),
*InterfaceName, *Blueprint)); *InterfaceName, *Blueprint));
} }
@@ -133,7 +134,7 @@ void UMCPHandler_AddInterface::Handle(const FJsonObject* Json, FJsonObject* Resu
bool bAdded = FBlueprintEditorUtils::ImplementNewInterface(BP, InterfacePath); bool bAdded = FBlueprintEditorUtils::ImplementNewInterface(BP, InterfacePath);
if (!bAdded) if (!bAdded)
{ {
return Helper->MakeErrorJson(Result, FString::Printf( return MCPUtils::MakeErrorJson(Result, FString::Printf(
TEXT("FBlueprintEditorUtils::ImplementNewInterface failed for interface '%s' on Blueprint '%s'"), TEXT("FBlueprintEditorUtils::ImplementNewInterface failed for interface '%s' on Blueprint '%s'"),
*InterfaceName, *Blueprint)); *InterfaceName, *Blueprint));
} }
@@ -186,7 +187,7 @@ void UMCPHandler_RemoveInterface::Handle(const FJsonObject* Json, FJsonObject* R
UBlueprint* BP = Helper->LoadBlueprintByName(Blueprint, LoadError); UBlueprint* BP = Helper->LoadBlueprintByName(Blueprint, LoadError);
if (!BP) if (!BP)
{ {
return Helper->MakeErrorJson(Result, LoadError); return MCPUtils::MakeErrorJson(Result, LoadError);
} }
// Find the interface in ImplementedInterfaces by name (case-insensitive) // 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'"), TEXT("Interface '%s' is not implemented by Blueprint '%s'"),
*InterfaceName, *Blueprint)); *InterfaceName, *Blueprint));
Result->SetArrayField(TEXT("implementedInterfaces"), IfaceList); Result->SetArrayField(TEXT("implementedInterfaces"), IfaceList);

View File

@@ -1,4 +1,5 @@
#include "BlueprintMCPServer.h" #include "BlueprintMCPServer.h"
#include "MCPUtils.h"
#include "Materials/Material.h" #include "Materials/Material.h"
#include "Materials/MaterialInterface.h" #include "Materials/MaterialInterface.h"
#include "Materials/MaterialInstanceConstant.h" #include "Materials/MaterialInstanceConstant.h"
@@ -28,20 +29,20 @@ void FBlueprintMCPServer::HandleCreateMaterialInstance(const FJsonObject* Json,
if (Name.IsEmpty() || PackagePath.IsEmpty() || ParentMaterialName.IsEmpty()) 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 // Validate packagePath starts with /Game
if (!PackagePath.StartsWith(TEXT("/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 // Check if asset already exists
FString FullAssetPath = PackagePath / Name; FString FullAssetPath = PackagePath / Name;
if (FindMaterialInstanceAsset(Name) || FindMaterialInstanceAsset(FullAssetPath)) 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."), TEXT("Material Instance '%s' already exists. Use a different name or delete the existing asset first."),
*Name)); *Name));
} }
@@ -74,7 +75,7 @@ void FBlueprintMCPServer::HandleCreateMaterialInstance(const FJsonObject* Json,
if (!ParentMaterial) 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."), TEXT("Parent material '%s' not found. Provide a Material or Material Instance name/path."),
*ParentMaterialName)); *ParentMaterialName));
} }
@@ -89,13 +90,13 @@ void FBlueprintMCPServer::HandleCreateMaterialInstance(const FJsonObject* Json,
UObject* NewAsset = AssetTools.CreateAsset(Name, PackagePath, UMaterialInstanceConstant::StaticClass(), Factory); UObject* NewAsset = AssetTools.CreateAsset(Name, PackagePath, UMaterialInstanceConstant::StaticClass(), Factory);
if (!NewAsset) 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<UMaterialInstanceConstant>(NewAsset); UMaterialInstanceConstant* MI = Cast<UMaterialInstanceConstant>(NewAsset);
if (!MI) 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 // Set parent
@@ -104,7 +105,7 @@ void FBlueprintMCPServer::HandleCreateMaterialInstance(const FJsonObject* Json,
MI->PostEditChange(); MI->PostEditChange();
// Save // Save
bool bSaved = SaveGenericPackage(MI); bool bSaved = MCPUtils::SaveGenericPackage(MI);
// Refresh asset cache // Refresh asset cache
FAssetRegistryModule& ARM = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry"); FAssetRegistryModule& ARM = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
@@ -132,12 +133,12 @@ void FBlueprintMCPServer::HandleSetMaterialInstanceParameter(const FJsonObject*
if (MIName.IsEmpty() || ParamName.IsEmpty()) 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"))) 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; bool bDryRun = false;
@@ -151,7 +152,7 @@ void FBlueprintMCPServer::HandleSetMaterialInstanceParameter(const FJsonObject*
UMaterialInstanceConstant* MI = LoadMaterialInstanceByName(MIName, LoadError); UMaterialInstanceConstant* MI = LoadMaterialInstanceByName(MIName, LoadError);
if (!MI) if (!MI)
{ {
return MakeErrorJson(Result, LoadError); return MCPUtils::MakeErrorJson(Result, LoadError);
} }
// Determine the parameter type — explicit or auto-detect from parent // Determine the parameter type — explicit or auto-detect from parent
@@ -223,7 +224,7 @@ void FBlueprintMCPServer::HandleSetMaterialInstanceParameter(const FJsonObject*
if (TypeStr.IsEmpty()) 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)."), TEXT("Could not determine parameter type for '%s'. Specify the 'type' field explicitly (scalar, vector, texture, staticSwitch)."),
*ParamName)); *ParamName));
} }
@@ -252,7 +253,7 @@ void FBlueprintMCPServer::HandleSetMaterialInstanceParameter(const FJsonObject*
const TSharedPtr<FJsonObject>* ValueObj = nullptr; const TSharedPtr<FJsonObject>* ValueObj = nullptr;
if (!Json->TryGetObjectField(TEXT("value"), ValueObj) || !ValueObj || !(*ValueObj).IsValid()) 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")); double R = (*ValueObj)->GetNumberField(TEXT("r"));
@@ -274,13 +275,13 @@ void FBlueprintMCPServer::HandleSetMaterialInstanceParameter(const FJsonObject*
FString TexturePath = Json->GetStringField(TEXT("value")); FString TexturePath = Json->GetStringField(TEXT("value"));
if (TexturePath.IsEmpty()) 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<UTexture>(nullptr, *TexturePath); UTexture* TextureObj = LoadObject<UTexture>(nullptr, *TexturePath);
if (!TextureObj) 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) if (!bDryRun)
@@ -328,7 +329,7 @@ void FBlueprintMCPServer::HandleSetMaterialInstanceParameter(const FJsonObject*
} }
else else
{ {
return MakeErrorJson(Result, FString::Printf( return MCPUtils::MakeErrorJson(Result, FString::Printf(
TEXT("Unknown parameter type '%s'. Valid types: scalar, vector, texture, staticSwitch"), TEXT("Unknown parameter type '%s'. Valid types: scalar, vector, texture, staticSwitch"),
*TypeStr)); *TypeStr));
} }
@@ -338,7 +339,7 @@ void FBlueprintMCPServer::HandleSetMaterialInstanceParameter(const FJsonObject*
MI->PreEditChange(nullptr); MI->PreEditChange(nullptr);
MI->PostEditChange(); MI->PostEditChange();
MI->MarkPackageDirty(); MI->MarkPackageDirty();
SaveGenericPackage(MI); MCPUtils::SaveGenericPackage(MI);
} }
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: %s parameter '%s' = %s on '%s'"), 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")); FString NameParam = Json->GetStringField(TEXT("name"));
if (NameParam.IsEmpty()) if (NameParam.IsEmpty())
{ {
return MakeErrorJson(Result, TEXT("Missing required query parameter: name")); return MCPUtils::MakeErrorJson(Result, TEXT("Missing required query parameter: name"));
} }
FString LoadError; FString LoadError;
UMaterialInstanceConstant* MI = LoadMaterialInstanceByName(NameParam, LoadError); UMaterialInstanceConstant* MI = LoadMaterialInstanceByName(NameParam, LoadError);
if (!MI) if (!MI)
{ {
return MakeErrorJson(Result, LoadError); return MCPUtils::MakeErrorJson(Result, LoadError);
} }
Result->SetStringField(TEXT("name"), MI->GetName()); Result->SetStringField(TEXT("name"), MI->GetName());
@@ -606,7 +607,7 @@ void FBlueprintMCPServer::HandleReparentMaterialInstance(const FJsonObject* Json
if (MIName.IsEmpty() || NewParentName.IsEmpty()) 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; bool bDryRun = false;
@@ -620,7 +621,7 @@ void FBlueprintMCPServer::HandleReparentMaterialInstance(const FJsonObject* Json
UMaterialInstanceConstant* MI = LoadMaterialInstanceByName(MIName, LoadError); UMaterialInstanceConstant* MI = LoadMaterialInstanceByName(MIName, LoadError);
if (!MI) if (!MI)
{ {
return MakeErrorJson(Result, LoadError); return MCPUtils::MakeErrorJson(Result, LoadError);
} }
// Capture old parent // Capture old parent
@@ -654,7 +655,7 @@ void FBlueprintMCPServer::HandleReparentMaterialInstance(const FJsonObject* Json
if (!NewParent) 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."), TEXT("New parent material '%s' not found. Provide a Material or Material Instance name/path."),
*NewParentName)); *NewParentName));
} }
@@ -666,7 +667,7 @@ void FBlueprintMCPServer::HandleReparentMaterialInstance(const FJsonObject* Json
{ {
if (Check == MI) 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."), TEXT("Cannot reparent '%s' to '%s' — this would create a circular parent chain."),
*MIName, *NewParentName)); *MIName, *NewParentName));
} }
@@ -692,7 +693,7 @@ void FBlueprintMCPServer::HandleReparentMaterialInstance(const FJsonObject* Json
MI->Parent = NewParent; MI->Parent = NewParent;
MI->PostEditChange(); MI->PostEditChange();
bool bSaved = SaveGenericPackage(MI); bool bSaved = MCPUtils::SaveGenericPackage(MI);
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Reparented Material Instance '%s' (saved: %s)"), UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Reparented Material Instance '%s' (saved: %s)"),
*MIName, bSaved ? TEXT("true") : TEXT("false")); *MIName, bSaved ? TEXT("true") : TEXT("false"));

View File

@@ -1,4 +1,5 @@
#include "BlueprintMCPServer.h" #include "BlueprintMCPServer.h"
#include "MCPUtils.h"
#include "Materials/Material.h" #include "Materials/Material.h"
#include "MaterialDomain.h" #include "MaterialDomain.h"
#include "Materials/MaterialInstanceConstant.h" #include "Materials/MaterialInstanceConstant.h"
@@ -63,19 +64,19 @@ void FBlueprintMCPServer::HandleCreateMaterial(const FJsonObject* Json, FJsonObj
if (Name.IsEmpty() || PackagePath.IsEmpty()) 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"))) 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 // Check if asset already exists
FString FullAssetPath = PackagePath / Name; FString FullAssetPath = PackagePath / Name;
if (FindMaterialAsset(Name) || FindMaterialAsset(FullAssetPath)) 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."), TEXT("Material '%s' already exists. Use a different name or delete the existing asset first."),
*Name)); *Name));
} }
@@ -89,13 +90,13 @@ void FBlueprintMCPServer::HandleCreateMaterial(const FJsonObject* Json, FJsonObj
if (!NewAsset) 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<UMaterial>(NewAsset); UMaterial* Material = Cast<UMaterial>(NewAsset);
if (!Material) 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 // Apply optional properties
@@ -150,7 +151,7 @@ void FBlueprintMCPServer::HandleCreateMaterial(const FJsonObject* Json, FJsonObj
Material->PostEditChange(); Material->PostEditChange();
// Save // Save
bool bSaved = SaveMaterialPackage(Material); bool bSaved = MCPUtils::SaveMaterialPackage(Material);
// Refresh asset cache // Refresh asset cache
FAssetRegistryModule& ARM = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry"); FAssetRegistryModule& ARM = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
@@ -208,12 +209,12 @@ void FBlueprintMCPServer::HandleSetMaterialProperty(const FJsonObject* Json, FJs
if (MaterialName.IsEmpty() || Property.IsEmpty()) 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"))) 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; bool bDryRun = false;
@@ -224,7 +225,7 @@ void FBlueprintMCPServer::HandleSetMaterialProperty(const FJsonObject* Json, FJs
UMaterial* Material = LoadMaterialByName(MaterialName, LoadError); UMaterial* Material = LoadMaterialByName(MaterialName, LoadError);
if (!Material) if (!Material)
{ {
return MakeErrorJson(Result, LoadError); return MCPUtils::MakeErrorJson(Result, LoadError);
} }
FString OldValue; FString OldValue;
@@ -290,7 +291,7 @@ void FBlueprintMCPServer::HandleSetMaterialProperty(const FJsonObject* Json, FJs
else if (ValueStr == TEXT("UI")) NewDomain = MD_UI; else if (ValueStr == TEXT("UI")) NewDomain = MD_UI;
else else
{ {
return MakeErrorJson(Result, FString::Printf( return MCPUtils::MakeErrorJson(Result, FString::Printf(
TEXT("Invalid domain '%s'. Valid values: Surface, DeferredDecal, LightFunction, Volume, PostProcess, UI"), TEXT("Invalid domain '%s'. Valid values: Surface, DeferredDecal, LightFunction, Volume, PostProcess, UI"),
*ValueStr)); *ValueStr));
} }
@@ -317,7 +318,7 @@ void FBlueprintMCPServer::HandleSetMaterialProperty(const FJsonObject* Json, FJs
else if (ValueStr == TEXT("Modulate")) NewBlend = BLEND_Modulate; else if (ValueStr == TEXT("Modulate")) NewBlend = BLEND_Modulate;
else else
{ {
return MakeErrorJson(Result, FString::Printf( return MCPUtils::MakeErrorJson(Result, FString::Printf(
TEXT("Invalid blendMode '%s'. Valid values: Opaque, Masked, Translucent, Additive, Modulate"), TEXT("Invalid blendMode '%s'. Valid values: Opaque, Masked, Translucent, Additive, Modulate"),
*ValueStr)); *ValueStr));
} }
@@ -362,7 +363,7 @@ void FBlueprintMCPServer::HandleSetMaterialProperty(const FJsonObject* Json, FJs
else if (ValueStr == TEXT("Eye")) NewModel = MSM_Eye; else if (ValueStr == TEXT("Eye")) NewModel = MSM_Eye;
else 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"), TEXT("Invalid shadingModel '%s'. Valid values: Unlit, DefaultLit, Subsurface, PreintegratedSkin, ClearCoat, SubsurfaceProfile, TwoSidedFoliage, Hair, Cloth, Eye"),
*ValueStr)); *ValueStr));
} }
@@ -456,7 +457,7 @@ void FBlueprintMCPServer::HandleSetMaterialProperty(const FJsonObject* Json, FJs
} }
else else
{ {
return MakeErrorJson(Result, FString::Printf( return MCPUtils::MakeErrorJson(Result, FString::Printf(
TEXT("Unknown property '%s'. Valid properties: domain, blendMode, twoSided, shadingModel, opacity, " TEXT("Unknown property '%s'. Valid properties: domain, blendMode, twoSided, shadingModel, opacity, "
"opacityMaskClipValue, bUsedWithSkeletalMesh, bUsedWithMorphTargets, bUsedWithNiagaraSprites, " "opacityMaskClipValue, bUsedWithSkeletalMesh, bUsedWithMorphTargets, bUsedWithNiagaraSprites, "
"ditheredLODTransition, bAllowNegativeEmissiveColor"), "ditheredLODTransition, bAllowNegativeEmissiveColor"),
@@ -467,7 +468,7 @@ void FBlueprintMCPServer::HandleSetMaterialProperty(const FJsonObject* Json, FJs
bool bSaved = false; bool bSaved = false;
if (!bDryRun) if (!bDryRun)
{ {
bSaved = SaveMaterialPackage(Material); bSaved = MCPUtils::SaveMaterialPackage(Material);
} }
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: %sSet material property '%s' on '%s': '%s' -> '%s'"), 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"))) 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()) 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; int32 PosX = 0, PosY = 0;
@@ -540,14 +541,14 @@ void FBlueprintMCPServer::HandleAddMaterialExpression(const FJsonObject* Json, F
if (!ExprClass) 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 " 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.)"), "(e.g. 'Constant', 'ScalarParameter', 'Add', 'Multiply', 'Lerp', 'Subtract', 'Fresnel', 'Comment', etc.)"),
*ExpressionClassName)); *ExpressionClassName));
} }
if (ExprClass->HasAnyClassFlags(CLASS_Abstract)) 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)); 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()) 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; FString LoadError;
MatFunc = LoadMaterialFunctionByName(MaterialFunctionName, LoadError); MatFunc = LoadMaterialFunctionByName(MaterialFunctionName, LoadError);
if (!MatFunc) { MakeErrorJson(Result, LoadError); return; } if (!MatFunc) { MCPUtils::MakeErrorJson(Result, LoadError); return; }
Owner = MatFunc; Owner = MatFunc;
AssetDisplayName = MatFunc->GetName(); AssetDisplayName = MatFunc->GetName();
} }
@@ -574,7 +575,7 @@ void FBlueprintMCPServer::HandleAddMaterialExpression(const FJsonObject* Json, F
{ {
FString LoadError; FString LoadError;
Material = LoadMaterialByName(MaterialName, LoadError); Material = LoadMaterialByName(MaterialName, LoadError);
if (!Material) { MakeErrorJson(Result, LoadError); return; } if (!Material) { MCPUtils::MakeErrorJson(Result, LoadError); return; }
Owner = Material; Owner = Material;
AssetDisplayName = Material->GetName(); 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) // 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 // Create, register, and PostEditChange the expression — all inside an SEH wrapper because
// some classes (e.g. UMaterialExpressionParameter) lack CLASS_Abstract but crash during // 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); int32 CreateResult = TryAddMaterialExpressionSEH(Owner, ExprClass, Material, MatFunc, PosX, PosY, &NewExpr);
if (CreateResult != 0 || !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)."), TEXT("Expression class '%s' cannot be instantiated (may be abstract or have internal errors)."),
*ExpressionClassName)); *ExpressionClassName));
} }
@@ -612,7 +613,7 @@ void FBlueprintMCPServer::HandleAddMaterialExpression(const FJsonObject* Json, F
NewExpr = NewObject<UMaterialExpression>(Owner, ExprClass); NewExpr = NewObject<UMaterialExpression>(Owner, ExprClass);
if (!NewExpr) 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->MaterialExpressionEditorX = PosX;
NewExpr->MaterialExpressionEditorY = PosY; NewExpr->MaterialExpressionEditorY = PosY;
@@ -637,7 +638,7 @@ void FBlueprintMCPServer::HandleAddMaterialExpression(const FJsonObject* Json, F
#endif #endif
// Save // 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) // Find the node GUID from the material graph (only for materials)
FString NodeGuid; FString NodeGuid;
@@ -658,7 +659,7 @@ void FBlueprintMCPServer::HandleAddMaterialExpression(const FJsonObject* Json, F
*ExpressionClassName, *AssetDisplayName, *NodeGuid, bSaved ? TEXT("true") : TEXT("false")); *ExpressionClassName, *AssetDisplayName, *NodeGuid, bSaved ? TEXT("true") : TEXT("false"));
// Serialize the expression details // Serialize the expression details
TSharedPtr<FJsonObject> ExprDetails = SerializeMaterialExpression(NewExpr); TSharedPtr<FJsonObject> ExprDetails = MCPUtils::SerializeMaterialExpression(NewExpr);
Result->SetBoolField(TEXT("success"), true); Result->SetBoolField(TEXT("success"), true);
Result->SetStringField(TEXT("material"), AssetDisplayName); Result->SetStringField(TEXT("material"), AssetDisplayName);
@@ -685,11 +686,11 @@ void FBlueprintMCPServer::HandleDeleteMaterialExpression(const FJsonObject* Json
if (MaterialName.IsEmpty() && MaterialFunctionName.IsEmpty()) 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()) if (NodeId.IsEmpty())
{ {
return MakeErrorJson(Result, TEXT("Missing required field: nodeId")); return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: nodeId"));
} }
bool bDryRun = false; bool bDryRun = false;
@@ -704,23 +705,23 @@ void FBlueprintMCPServer::HandleDeleteMaterialExpression(const FJsonObject* Json
{ {
FString LoadError; FString LoadError;
MatFunc = LoadMaterialFunctionByName(MaterialFunctionName, LoadError); MatFunc = LoadMaterialFunctionByName(MaterialFunctionName, LoadError);
if (!MatFunc) { MakeErrorJson(Result, LoadError); return; } if (!MatFunc) { MCPUtils::MakeErrorJson(Result, LoadError); return; }
AssetDisplayName = MatFunc->GetName(); AssetDisplayName = MatFunc->GetName();
} }
else else
{ {
FString LoadError; FString LoadError;
Material = LoadMaterialByName(MaterialName, LoadError); Material = LoadMaterialByName(MaterialName, LoadError);
if (!Material) { MakeErrorJson(Result, LoadError); return; } if (!Material) { MCPUtils::MakeErrorJson(Result, LoadError); return; }
AssetDisplayName = Material->GetName(); AssetDisplayName = Material->GetName();
} }
// For materials, we need the graph to find nodes by GUID // 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); UEdGraph* Graph = Material ? (UEdGraph*)Material->MaterialGraph : (MatFunc ? MatFunc->MaterialGraph : nullptr);
if (!Graph) 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 // Find the node by GUID
@@ -737,12 +738,12 @@ void FBlueprintMCPServer::HandleDeleteMaterialExpression(const FJsonObject* Json
if (!TargetMatNode) 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) 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 // Capture info before deletion
@@ -784,7 +785,7 @@ void FBlueprintMCPServer::HandleDeleteMaterialExpression(const FJsonObject* Json
Asset->MarkPackageDirty(); Asset->MarkPackageDirty();
// Save // 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)"), UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Deleted expression '%s' (nodeId: %s) from '%s' (saved: %s)"),
*DeletedExprClass, *NodeId, *AssetDisplayName, bSaved ? TEXT("true") : TEXT("false")); *DeletedExprClass, *NodeId, *AssetDisplayName, bSaved ? TEXT("true") : TEXT("false"));
@@ -812,11 +813,11 @@ void FBlueprintMCPServer::HandleConnectMaterialPins(const FJsonObject* Json, FJs
if (MaterialName.IsEmpty() && MaterialFunctionName.IsEmpty()) 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()) 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; bool bDryRun = false;
@@ -831,22 +832,22 @@ void FBlueprintMCPServer::HandleConnectMaterialPins(const FJsonObject* Json, FJs
{ {
FString LoadError; FString LoadError;
MatFunc = LoadMaterialFunctionByName(MaterialFunctionName, LoadError); MatFunc = LoadMaterialFunctionByName(MaterialFunctionName, LoadError);
if (!MatFunc) { MakeErrorJson(Result, LoadError); return; } if (!MatFunc) { MCPUtils::MakeErrorJson(Result, LoadError); return; }
AssetDisplayName = MatFunc->GetName(); AssetDisplayName = MatFunc->GetName();
} }
else else
{ {
FString LoadError; FString LoadError;
Material = LoadMaterialByName(MaterialName, LoadError); Material = LoadMaterialByName(MaterialName, LoadError);
if (!Material) { MakeErrorJson(Result, LoadError); return; } if (!Material) { MCPUtils::MakeErrorJson(Result, LoadError); return; }
AssetDisplayName = Material->GetName(); AssetDisplayName = Material->GetName();
} }
if (Material) EnsureMaterialGraph(Material); if (Material) MCPUtils::EnsureMaterialGraph(Material);
UEdGraph* Graph = Material ? (UEdGraph*)Material->MaterialGraph : (MatFunc ? MatFunc->MaterialGraph : nullptr); UEdGraph* Graph = Material ? (UEdGraph*)Material->MaterialGraph : (MatFunc ? MatFunc->MaterialGraph : nullptr);
if (!Graph) 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 // Find source and target nodes by GUID
@@ -866,11 +867,11 @@ void FBlueprintMCPServer::HandleConnectMaterialPins(const FJsonObject* Json, FJs
if (!SourceNode) 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) 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 // Find pins
@@ -885,7 +886,7 @@ void FBlueprintMCPServer::HandleConnectMaterialPins(const FJsonObject* Json, FJs
FString::Printf(TEXT("%s (%s)"), *P->PinName.ToString(), FString::Printf(TEXT("%s (%s)"), *P->PinName.ToString(),
P->Direction == EGPD_Input ? TEXT("Input") : TEXT("Output")))); 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)); *SourcePinName, *SourceNodeId));
Result->SetArrayField(TEXT("availablePins"), PinNames); Result->SetArrayField(TEXT("availablePins"), PinNames);
return; return;
@@ -901,7 +902,7 @@ void FBlueprintMCPServer::HandleConnectMaterialPins(const FJsonObject* Json, FJs
FString::Printf(TEXT("%s (%s)"), *P->PinName.ToString(), FString::Printf(TEXT("%s (%s)"), *P->PinName.ToString(),
P->Direction == EGPD_Input ? TEXT("Input") : TEXT("Output")))); 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)); *TargetPinName, *TargetNodeId));
Result->SetArrayField(TEXT("availablePins"), PinNames); Result->SetArrayField(TEXT("availablePins"), PinNames);
return; return;
@@ -926,7 +927,7 @@ void FBlueprintMCPServer::HandleConnectMaterialPins(const FJsonObject* Json, FJs
const UEdGraphSchema* Schema = Graph->GetSchema(); const UEdGraphSchema* Schema = Graph->GetSchema();
if (!Schema) 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); bool bConnected = Schema->TryCreateConnection(SourcePin, TargetPin);
@@ -937,7 +938,7 @@ void FBlueprintMCPServer::HandleConnectMaterialPins(const FJsonObject* Json, FJs
if (!bConnected) 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"), TEXT("Cannot connect %s.%s to %s.%s — types may be incompatible"),
*SourceNodeId, *SourcePinName, *TargetNodeId, *TargetPinName)); *SourceNodeId, *SourcePinName, *TargetNodeId, *TargetPinName));
} }
@@ -946,7 +947,7 @@ void FBlueprintMCPServer::HandleConnectMaterialPins(const FJsonObject* Json, FJs
UObject* Asset = Material ? (UObject*)Material : (UObject*)MatFunc; UObject* Asset = Material ? (UObject*)Material : (UObject*)MatFunc;
Asset->PreEditChange(nullptr); Asset->PreEditChange(nullptr);
Asset->PostEditChange(); Asset->PostEditChange();
bool bSaved = Material ? SaveMaterialPackage(Material) : SaveGenericPackage(MatFunc); bool bSaved = Material ? MCPUtils::SaveMaterialPackage(Material) : MCPUtils::SaveGenericPackage(MatFunc);
Result->SetBoolField(TEXT("saved"), bSaved); Result->SetBoolField(TEXT("saved"), bSaved);
} }
@@ -963,11 +964,11 @@ void FBlueprintMCPServer::HandleDisconnectMaterialPin(const FJsonObject* Json, F
if (MaterialName.IsEmpty() && MaterialFunctionName.IsEmpty()) 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()) 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; bool bDryRun = false;
@@ -982,22 +983,22 @@ void FBlueprintMCPServer::HandleDisconnectMaterialPin(const FJsonObject* Json, F
{ {
FString LoadError; FString LoadError;
MatFunc = LoadMaterialFunctionByName(MaterialFunctionName, LoadError); MatFunc = LoadMaterialFunctionByName(MaterialFunctionName, LoadError);
if (!MatFunc) { MakeErrorJson(Result, LoadError); return; } if (!MatFunc) { MCPUtils::MakeErrorJson(Result, LoadError); return; }
AssetDisplayName = MatFunc->GetName(); AssetDisplayName = MatFunc->GetName();
} }
else else
{ {
FString LoadError; FString LoadError;
Material = LoadMaterialByName(MaterialName, LoadError); Material = LoadMaterialByName(MaterialName, LoadError);
if (!Material) { MakeErrorJson(Result, LoadError); return; } if (!Material) { MCPUtils::MakeErrorJson(Result, LoadError); return; }
AssetDisplayName = Material->GetName(); AssetDisplayName = Material->GetName();
} }
if (Material) EnsureMaterialGraph(Material); if (Material) MCPUtils::EnsureMaterialGraph(Material);
UEdGraph* Graph = Material ? (UEdGraph*)Material->MaterialGraph : (MatFunc ? MatFunc->MaterialGraph : nullptr); UEdGraph* Graph = Material ? (UEdGraph*)Material->MaterialGraph : (MatFunc ? MatFunc->MaterialGraph : nullptr);
if (!Graph) 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 // Find node by GUID
@@ -1014,7 +1015,7 @@ void FBlueprintMCPServer::HandleDisconnectMaterialPin(const FJsonObject* Json, F
if (!TargetNode) 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 // Find pin
@@ -1028,7 +1029,7 @@ void FBlueprintMCPServer::HandleDisconnectMaterialPin(const FJsonObject* Json, F
FString::Printf(TEXT("%s (%s)"), *P->PinName.ToString(), FString::Printf(TEXT("%s (%s)"), *P->PinName.ToString(),
P->Direction == EGPD_Input ? TEXT("Input") : TEXT("Output")))); 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)); *PinName, *NodeId));
Result->SetArrayField(TEXT("availablePins"), PinNames); Result->SetArrayField(TEXT("availablePins"), PinNames);
return; return;
@@ -1061,7 +1062,7 @@ void FBlueprintMCPServer::HandleDisconnectMaterialPin(const FJsonObject* Json, F
Asset->PostEditChange(); Asset->PostEditChange();
// Save // Save
bool bSaved = Material ? SaveMaterialPackage(Material) : SaveGenericPackage(MatFunc); bool bSaved = Material ? MCPUtils::SaveMaterialPackage(Material) : MCPUtils::SaveGenericPackage(MatFunc);
Result->SetBoolField(TEXT("success"), true); Result->SetBoolField(TEXT("success"), true);
Result->SetStringField(TEXT("material"), AssetDisplayName); Result->SetStringField(TEXT("material"), AssetDisplayName);
@@ -1083,16 +1084,16 @@ void FBlueprintMCPServer::HandleSetExpressionValue(const FJsonObject* Json, FJso
if (MaterialName.IsEmpty() && MaterialFunctionName.IsEmpty()) 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()) 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"))) 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 // Load material or material function
@@ -1104,22 +1105,22 @@ void FBlueprintMCPServer::HandleSetExpressionValue(const FJsonObject* Json, FJso
{ {
FString LoadError; FString LoadError;
MatFunc = LoadMaterialFunctionByName(MaterialFunctionName, LoadError); MatFunc = LoadMaterialFunctionByName(MaterialFunctionName, LoadError);
if (!MatFunc) { MakeErrorJson(Result, LoadError); return; } if (!MatFunc) { MCPUtils::MakeErrorJson(Result, LoadError); return; }
AssetDisplayName = MatFunc->GetName(); AssetDisplayName = MatFunc->GetName();
} }
else else
{ {
FString LoadError; FString LoadError;
Material = LoadMaterialByName(MaterialName, LoadError); Material = LoadMaterialByName(MaterialName, LoadError);
if (!Material) { MakeErrorJson(Result, LoadError); return; } if (!Material) { MCPUtils::MakeErrorJson(Result, LoadError); return; }
AssetDisplayName = Material->GetName(); AssetDisplayName = Material->GetName();
} }
if (Material) EnsureMaterialGraph(Material); if (Material) MCPUtils::EnsureMaterialGraph(Material);
UEdGraph* Graph = Material ? (UEdGraph*)Material->MaterialGraph : (MatFunc ? MatFunc->MaterialGraph : nullptr); UEdGraph* Graph = Material ? (UEdGraph*)Material->MaterialGraph : (MatFunc ? MatFunc->MaterialGraph : nullptr);
if (!Graph) 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 // Find the node by GUID
@@ -1136,13 +1137,13 @@ void FBlueprintMCPServer::HandleSetExpressionValue(const FJsonObject* Json, FJso
if (!TargetMatNode) 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; UMaterialExpression* Expr = TargetMatNode->MaterialExpression;
if (!Expr) 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; FString ExprType;
@@ -1175,7 +1176,7 @@ void FBlueprintMCPServer::HandleSetExpressionValue(const FJsonObject* Json, FJso
else else
{ {
Asset->PostEditChange(); 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<UMaterialExpressionConstant4Vector>(Expr)) else if (UMaterialExpressionConstant4Vector* C4Expr = Cast<UMaterialExpressionConstant4Vector>(Expr))
@@ -1195,7 +1196,7 @@ void FBlueprintMCPServer::HandleSetExpressionValue(const FJsonObject* Json, FJso
else else
{ {
Asset->PostEditChange(); 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<UMaterialExpressionScalarParameter>(Expr)) else if (UMaterialExpressionScalarParameter* SPExpr = Cast<UMaterialExpressionScalarParameter>(Expr))
@@ -1228,7 +1229,7 @@ void FBlueprintMCPServer::HandleSetExpressionValue(const FJsonObject* Json, FJso
else else
{ {
Asset->PostEditChange(); 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; FString ParamName;
@@ -1255,7 +1256,7 @@ void FBlueprintMCPServer::HandleSetExpressionValue(const FJsonObject* Json, FJso
else else
{ {
Asset->PostEditChange(); 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<UMaterialExpressionCustom>(Expr)) else if (UMaterialExpressionCustom* CustomExpr = Cast<UMaterialExpressionCustom>(Expr))
@@ -1312,13 +1313,13 @@ void FBlueprintMCPServer::HandleSetExpressionValue(const FJsonObject* Json, FJso
else else
{ {
Asset->PostEditChange(); 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 else
{ {
Asset->PostEditChange(); 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, " TEXT("Expression type '%s' does not support direct value setting. Supported types: Constant, "
"Constant3Vector, Constant4Vector, ScalarParameter, VectorParameter, TextureCoordinate, " "Constant3Vector, Constant4Vector, ScalarParameter, VectorParameter, TextureCoordinate, "
"Custom, ComponentMask"), "Custom, ComponentMask"),
@@ -1329,7 +1330,7 @@ void FBlueprintMCPServer::HandleSetExpressionValue(const FJsonObject* Json, FJso
Asset->MarkPackageDirty(); Asset->MarkPackageDirty();
// Save // 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"), UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Set expression value on node '%s' (%s) in '%s': %s"),
*NodeId, *ExprType, *AssetDisplayName, *NewValueStr); *NodeId, *ExprType, *AssetDisplayName, *NewValueStr);
@@ -1354,16 +1355,16 @@ void FBlueprintMCPServer::HandleMoveMaterialExpression(const FJsonObject* Json,
if (MaterialName.IsEmpty() && MaterialFunctionName.IsEmpty()) 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()) 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"))) 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")); int32 PosX = (int32)Json->GetNumberField(TEXT("posX"));
@@ -1381,22 +1382,22 @@ void FBlueprintMCPServer::HandleMoveMaterialExpression(const FJsonObject* Json,
{ {
FString LoadError; FString LoadError;
MatFunc = LoadMaterialFunctionByName(MaterialFunctionName, LoadError); MatFunc = LoadMaterialFunctionByName(MaterialFunctionName, LoadError);
if (!MatFunc) { MakeErrorJson(Result, LoadError); return; } if (!MatFunc) { MCPUtils::MakeErrorJson(Result, LoadError); return; }
AssetDisplayName = MatFunc->GetName(); AssetDisplayName = MatFunc->GetName();
} }
else else
{ {
FString LoadError; FString LoadError;
Material = LoadMaterialByName(MaterialName, LoadError); Material = LoadMaterialByName(MaterialName, LoadError);
if (!Material) { MakeErrorJson(Result, LoadError); return; } if (!Material) { MCPUtils::MakeErrorJson(Result, LoadError); return; }
AssetDisplayName = Material->GetName(); AssetDisplayName = Material->GetName();
} }
if (Material) EnsureMaterialGraph(Material); if (Material) MCPUtils::EnsureMaterialGraph(Material);
UEdGraph* Graph = Material ? (UEdGraph*)Material->MaterialGraph : (MatFunc ? MatFunc->MaterialGraph : nullptr); UEdGraph* Graph = Material ? (UEdGraph*)Material->MaterialGraph : (MatFunc ? MatFunc->MaterialGraph : nullptr);
if (!Graph) 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 // Find node by GUID
@@ -1413,7 +1414,7 @@ void FBlueprintMCPServer::HandleMoveMaterialExpression(const FJsonObject* Json,
if (!TargetMatNode) 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) if (bDryRun)
@@ -1446,7 +1447,7 @@ void FBlueprintMCPServer::HandleMoveMaterialExpression(const FJsonObject* Json,
Asset->PostEditChange(); Asset->PostEditChange();
// Save // 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)"), 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")); *NodeId, PosX, PosY, *AssetDisplayName, bSaved ? TEXT("true") : TEXT("false"));
@@ -1474,19 +1475,19 @@ void FBlueprintMCPServer::HandleCreateMaterialFunction(const FJsonObject* Json,
if (Name.IsEmpty() || PackagePath.IsEmpty()) 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"))) 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 // Check if asset already exists
FString FullAssetPath = PackagePath / Name; FString FullAssetPath = PackagePath / Name;
if (FindMaterialFunctionAsset(Name) || FindMaterialFunctionAsset(FullAssetPath)) 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."), TEXT("Material Function '%s' already exists. Use a different name or delete the existing asset first."),
*Name)); *Name));
} }
@@ -1503,13 +1504,13 @@ void FBlueprintMCPServer::HandleCreateMaterialFunction(const FJsonObject* Json,
if (!NewAsset) 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<UMaterialFunction>(NewAsset); UMaterialFunction* MF = Cast<UMaterialFunction>(NewAsset);
if (!MF) 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 // Set optional description
@@ -1519,7 +1520,7 @@ void FBlueprintMCPServer::HandleCreateMaterialFunction(const FJsonObject* Json,
} }
// Save // Save
bool bSaved = SaveGenericPackage(MF); bool bSaved = MCPUtils::SaveGenericPackage(MF);
// Refresh asset cache // Refresh asset cache
FAssetRegistryModule& ARM = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry"); FAssetRegistryModule& ARM = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
@@ -1552,7 +1553,7 @@ void FBlueprintMCPServer::HandleSnapshotMaterialGraph(const FJsonObject* Json, F
FString MaterialName = Json->GetStringField(TEXT("material")); FString MaterialName = Json->GetStringField(TEXT("material"));
if (MaterialName.IsEmpty()) if (MaterialName.IsEmpty())
{ {
return MakeErrorJson(Result, TEXT("Missing required field: material")); return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: material"));
} }
// Load material // Load material
@@ -1560,13 +1561,13 @@ void FBlueprintMCPServer::HandleSnapshotMaterialGraph(const FJsonObject* Json, F
UMaterial* Material = LoadMaterialByName(MaterialName, LoadError); UMaterial* Material = LoadMaterialByName(MaterialName, LoadError);
if (!Material) if (!Material)
{ {
return MakeErrorJson(Result, LoadError); return MCPUtils::MakeErrorJson(Result, LoadError);
} }
EnsureMaterialGraph(Material); MCPUtils::EnsureMaterialGraph(Material);
if (!Material->MaterialGraph) 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); 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()) 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) // Load snapshot from material snapshots (memory or disk)
@@ -1649,7 +1650,7 @@ void FBlueprintMCPServer::HandleDiffMaterialGraph(const FJsonObject* Json, FJson
{ {
if (!LoadSnapshotFromDisk(SnapshotId, LoadedSnapshot)) 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; SnapshotPtr = &LoadedSnapshot;
} }
@@ -1659,13 +1660,13 @@ void FBlueprintMCPServer::HandleDiffMaterialGraph(const FJsonObject* Json, FJson
UMaterial* Material = LoadMaterialByName(MaterialName, LoadError); UMaterial* Material = LoadMaterialByName(MaterialName, LoadError);
if (!Material) if (!Material)
{ {
return MakeErrorJson(Result, LoadError); return MCPUtils::MakeErrorJson(Result, LoadError);
} }
EnsureMaterialGraph(Material); MCPUtils::EnsureMaterialGraph(Material);
if (!Material->MaterialGraph) 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); 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")); const FGraphSnapshotData* SnapDataPtr = SnapshotPtr->Graphs.Find(TEXT("MaterialGraph"));
if (!SnapDataPtr) 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; const FGraphSnapshotData& SnapData = *SnapDataPtr;
@@ -1802,7 +1803,7 @@ void FBlueprintMCPServer::HandleRestoreMaterialGraph(const FJsonObject* Json, FJ
if (MaterialName.IsEmpty() || SnapshotId.IsEmpty()) 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; bool bDryRun = false;
@@ -1815,7 +1816,7 @@ void FBlueprintMCPServer::HandleRestoreMaterialGraph(const FJsonObject* Json, FJ
{ {
if (!LoadSnapshotFromDisk(SnapshotId, LoadedSnapshot)) 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; SnapshotPtr = &LoadedSnapshot;
} }
@@ -1825,13 +1826,13 @@ void FBlueprintMCPServer::HandleRestoreMaterialGraph(const FJsonObject* Json, FJ
UMaterial* Material = LoadMaterialByName(MaterialName, LoadError); UMaterial* Material = LoadMaterialByName(MaterialName, LoadError);
if (!Material) if (!Material)
{ {
return MakeErrorJson(Result, LoadError); return MCPUtils::MakeErrorJson(Result, LoadError);
} }
EnsureMaterialGraph(Material); MCPUtils::EnsureMaterialGraph(Material);
if (!Material->MaterialGraph) 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)"), 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")); const FGraphSnapshotData* SnapDataPtr = SnapshotPtr->Graphs.Find(TEXT("MaterialGraph"));
if (!SnapDataPtr) 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; int32 Reconnected = 0;
@@ -1971,7 +1972,7 @@ void FBlueprintMCPServer::HandleRestoreMaterialGraph(const FJsonObject* Json, FJ
{ {
Material->PreEditChange(nullptr); Material->PreEditChange(nullptr);
Material->PostEditChange(); Material->PostEditChange();
bSaved = SaveMaterialPackage(Material); bSaved = MCPUtils::SaveMaterialPackage(Material);
} }
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Material restore complete — %d reconnected, %d failed, saved=%s"), UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Material restore complete — %d reconnected, %d failed, saved=%s"),

View File

@@ -1,4 +1,5 @@
#include "BlueprintMCPServer.h" #include "BlueprintMCPServer.h"
#include "MCPUtils.h"
#include "Materials/Material.h" #include "Materials/Material.h"
#include "Materials/MaterialInstanceConstant.h" #include "Materials/MaterialInstanceConstant.h"
#include "Materials/MaterialFunction.h" #include "Materials/MaterialFunction.h"
@@ -108,10 +109,10 @@ void FBlueprintMCPServer::HandleGetMaterial(const FJsonObject* Json, FJsonObject
FString Name = Json->GetStringField(TEXT("name")); FString Name = Json->GetStringField(TEXT("name"));
if (Name.IsEmpty()) 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 // Try loading as UMaterial first
FString LoadError; FString LoadError;
@@ -342,7 +343,7 @@ void FBlueprintMCPServer::HandleGetMaterial(const FJsonObject* Json, FJsonObject
return; 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")); FString Name = Json->GetStringField(TEXT("name"));
if (Name.IsEmpty()) 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; FString LoadError;
UMaterial* Material = LoadMaterialByName(DecodedName, LoadError); UMaterial* Material = LoadMaterialByName(DecodedName, LoadError);
if (!Material) if (!Material)
{ {
return MakeErrorJson(Result, LoadError); return MCPUtils::MakeErrorJson(Result, LoadError);
} }
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: GetMaterialGraph — material '%s'"), *Material->GetName()); 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) 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<FJsonObject> GraphJson = SerializeGraph(Material->MaterialGraph); TSharedPtr<FJsonObject> GraphJson = MCPUtils::SerializeGraph(Material->MaterialGraph);
if (!GraphJson.IsValid()) 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 // Add material name context
Result->SetStringField(TEXT("material"), Material->GetName()); Result->SetStringField(TEXT("material"), Material->GetName());
@@ -406,14 +407,14 @@ void FBlueprintMCPServer::HandleDescribeMaterial(const FJsonObject* Json, FJsonO
FString MaterialName = Json->GetStringField(TEXT("material")); FString MaterialName = Json->GetStringField(TEXT("material"));
if (MaterialName.IsEmpty()) if (MaterialName.IsEmpty())
{ {
return MakeErrorJson(Result, TEXT("Missing required field: material")); return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: material"));
} }
FString LoadError; FString LoadError;
UMaterial* Material = LoadMaterialByName(MaterialName, LoadError); UMaterial* Material = LoadMaterialByName(MaterialName, LoadError);
if (!Material) if (!Material)
{ {
return MakeErrorJson(Result, LoadError); return MCPUtils::MakeErrorJson(Result, LoadError);
} }
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: DescribeMaterial — '%s'"), *Material->GetName()); UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: DescribeMaterial — '%s'"), *Material->GetName());
@@ -429,7 +430,7 @@ void FBlueprintMCPServer::HandleDescribeMaterial(const FJsonObject* Json, FJsonO
if (!Material->MaterialGraph) 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 // 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) 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) for (UEdGraphPin* Pin : RootNode->Pins)
@@ -609,10 +610,10 @@ void FBlueprintMCPServer::HandleSearchMaterials(const FJsonObject* Json, FJsonOb
FString Query = Json->GetStringField(TEXT("query")); FString Query = Json->GetStringField(TEXT("query"));
if (Query.IsEmpty()) 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; int32 MaxResults = 50;
if (Json->HasField(TEXT("maxResults"))) if (Json->HasField(TEXT("maxResults")))
@@ -699,7 +700,7 @@ void FBlueprintMCPServer::HandleFindMaterialReferences(const FJsonObject* Json,
FString MaterialName = Json->GetStringField(TEXT("material")); FString MaterialName = Json->GetStringField(TEXT("material"));
if (MaterialName.IsEmpty()) 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 // Try to find the material's package path
@@ -721,7 +722,7 @@ void FBlueprintMCPServer::HandleFindMaterialReferences(const FJsonObject* Json,
if (PackagePath.IsEmpty()) 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); 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")); FString Name = Json->GetStringField(TEXT("name"));
if (Name.IsEmpty()) 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; FString LoadError;
UMaterialFunction* MF = LoadMaterialFunctionByName(DecodedName, LoadError); UMaterialFunction* MF = LoadMaterialFunctionByName(DecodedName, LoadError);
if (!MF) if (!MF)
{ {
return MakeErrorJson(Result, LoadError); return MCPUtils::MakeErrorJson(Result, LoadError);
} }
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: GetMaterialFunction — '%s'"), *MF->GetName()); UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: GetMaterialFunction — '%s'"), *MF->GetName());
@@ -843,7 +844,7 @@ void FBlueprintMCPServer::HandleGetMaterialFunction(const FJsonObject* Json, FJs
} }
// Serialize every expression // Serialize every expression
TSharedPtr<FJsonObject> ExprJson = SerializeMaterialExpression(Expr); TSharedPtr<FJsonObject> ExprJson = MCPUtils::SerializeMaterialExpression(Expr);
if (ExprJson.IsValid()) if (ExprJson.IsValid())
{ {
ExpressionList.Add(MakeShared<FJsonValueObject>(ExprJson.ToSharedRef())); ExpressionList.Add(MakeShared<FJsonValueObject>(ExprJson.ToSharedRef()));
@@ -859,7 +860,7 @@ void FBlueprintMCPServer::HandleGetMaterialFunction(const FJsonObject* Json, FJs
UEdGraph* FuncGraph = MF->MaterialGraph; UEdGraph* FuncGraph = MF->MaterialGraph;
if (FuncGraph) if (FuncGraph)
{ {
TSharedPtr<FJsonObject> GraphJson = SerializeGraph(FuncGraph); TSharedPtr<FJsonObject> GraphJson = MCPUtils::SerializeGraph(FuncGraph);
if (GraphJson.IsValid()) if (GraphJson.IsValid())
{ {
Result->SetObjectField(TEXT("graph"), GraphJson); Result->SetObjectField(TEXT("graph"), GraphJson);
@@ -876,7 +877,7 @@ void FBlueprintMCPServer::HandleValidateMaterial(const FJsonObject* Json, FJsonO
FString MaterialName = Json->GetStringField(TEXT("material")); FString MaterialName = Json->GetStringField(TEXT("material"));
if (MaterialName.IsEmpty()) if (MaterialName.IsEmpty())
{ {
return MakeErrorJson(Result, TEXT("Missing required field: material")); return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: material"));
} }
// Load material // Load material
@@ -884,7 +885,7 @@ void FBlueprintMCPServer::HandleValidateMaterial(const FJsonObject* Json, FJsonO
UMaterial* Material = LoadMaterialByName(MaterialName, LoadError); UMaterial* Material = LoadMaterialByName(MaterialName, LoadError);
if (!Material) if (!Material)
{ {
return MakeErrorJson(Result, LoadError); return MCPUtils::MakeErrorJson(Result, LoadError);
} }
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Validating material '%s'"), *Material->GetName()); UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Validating material '%s'"), *Material->GetName());

View File

@@ -1,5 +1,6 @@
#include "BlueprintMCPHandlers_Mutation.h" #include "BlueprintMCPHandlers_Mutation.h"
#include "BlueprintMCPServer.h" #include "BlueprintMCPServer.h"
#include "MCPUtils.h"
#include "Engine/Blueprint.h" #include "Engine/Blueprint.h"
#include "Materials/Material.h" #include "Materials/Material.h"
#include "Materials/MaterialInstanceConstant.h" #include "Materials/MaterialInstanceConstant.h"
@@ -42,7 +43,6 @@
#include "AssetRegistry/IAssetRegistry.h" #include "AssetRegistry/IAssetRegistry.h"
#include "AssetToolsModule.h" #include "AssetToolsModule.h"
#include "IAssetTools.h" #include "IAssetTools.h"
#include "BlueprintActionDatabase.h"
#include "BlueprintNodeSpawner.h" #include "BlueprintNodeSpawner.h"
@@ -59,7 +59,7 @@ void UMCPHandler_ReplaceFunctionCalls::Handle(const FJsonObject* Json, FJsonObje
UBlueprint* BP = Helper->LoadBlueprintByName(Blueprint, LoadError); UBlueprint* BP = Helper->LoadBlueprintByName(Blueprint, LoadError);
if (!BP) if (!BP)
{ {
return Helper->MakeErrorJson(Result, LoadError); return MCPUtils::MakeErrorJson(Result, LoadError);
} }
// Find the new class — try several search strategies // Find the new class — try several search strategies
@@ -93,7 +93,7 @@ void UMCPHandler_ReplaceFunctionCalls::Handle(const FJsonObject* Json, FJsonObje
if (!NewClassPtr) 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)"), 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)) 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 // 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->SetStringField(TEXT("assetPath"), AssetPath);
Result->SetNumberField(TEXT("referencerCount"), Referencers.Num()); Result->SetNumberField(TEXT("referencerCount"), Referencers.Num());
Result->SetNumberField(TEXT("liveReferencerCount"), LiveRefs.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); Result->SetBoolField(TEXT("forced"), Force);
if (!bDeleted) 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) if (RefWarnings.Num() > 0)
{ {
@@ -437,7 +437,7 @@ void UMCPHandler_ConnectPins::Handle(const FJsonObject* Json, FJsonObject* Resul
UBlueprint* BP = Helper->LoadBlueprintByName(Blueprint, LoadError); UBlueprint* BP = Helper->LoadBlueprintByName(Blueprint, LoadError);
if (!BP) if (!BP)
{ {
return Helper->MakeErrorJson(Result, LoadError); return MCPUtils::MakeErrorJson(Result, LoadError);
} }
TArray<TSharedPtr<FJsonValue>> Results; TArray<TSharedPtr<FJsonValue>> Results;
@@ -449,7 +449,7 @@ void UMCPHandler_ConnectPins::Handle(const FJsonObject* Json, FJsonObject* Resul
Results.Add(MakeShared<FJsonValueObject>(EntryResult)); Results.Add(MakeShared<FJsonValueObject>(EntryResult));
FConnectPinsEntry Entry; FConnectPinsEntry Entry;
FString PopulateError = Helper->PopulateFromJson(FConnectPinsEntry::StaticStruct(), &Entry, ConnVal); FString PopulateError = MCPUtils::PopulateFromJson(FConnectPinsEntry::StaticStruct(), &Entry, ConnVal);
if (!PopulateError.IsEmpty()) if (!PopulateError.IsEmpty())
{ {
EntryResult->SetStringField(TEXT("error"), PopulateError); EntryResult->SetStringField(TEXT("error"), PopulateError);
@@ -462,14 +462,14 @@ void UMCPHandler_ConnectPins::Handle(const FJsonObject* Json, FJsonObject* Resul
EntryResult->SetStringField(TEXT("targetPinName"), Entry.TargetPinName); EntryResult->SetStringField(TEXT("targetPinName"), Entry.TargetPinName);
UEdGraph* SourceGraph = nullptr; UEdGraph* SourceGraph = nullptr;
UEdGraphNode* SourceNode = Helper->FindNodeByGuid(BP, Entry.SourceNodeId, &SourceGraph); UEdGraphNode* SourceNode = MCPUtils::FindNodeByGuid(BP, Entry.SourceNodeId, &SourceGraph);
if (!SourceNode) if (!SourceNode)
{ {
EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Source node '%s' not found"), *Entry.SourceNodeId)); EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Source node '%s' not found"), *Entry.SourceNodeId));
continue; continue;
} }
UEdGraphNode* TargetNode = Helper->FindNodeByGuid(BP, Entry.TargetNodeId); UEdGraphNode* TargetNode = MCPUtils::FindNodeByGuid(BP, Entry.TargetNodeId);
if (!TargetNode) if (!TargetNode)
{ {
EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Target node '%s' not found"), *Entry.TargetNodeId)); 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); UBlueprint* BP = Helper->LoadBlueprintByName(Blueprint, LoadError);
if (!BP) if (!BP)
{ {
return Helper->MakeErrorJson(Result, LoadError); return MCPUtils::MakeErrorJson(Result, LoadError);
} }
TArray<TSharedPtr<FJsonValue>> Results; TArray<TSharedPtr<FJsonValue>> Results;
@@ -553,7 +553,7 @@ void UMCPHandler_DisconnectPin::Handle(const FJsonObject* Json, FJsonObject* Res
Results.Add(MakeShared<FJsonValueObject>(EntryResult)); Results.Add(MakeShared<FJsonValueObject>(EntryResult));
FDisconnectPinEntry Entry; FDisconnectPinEntry Entry;
FString PopulateError = Helper->PopulateFromJson(FDisconnectPinEntry::StaticStruct(), &Entry, DiscVal); FString PopulateError = MCPUtils::PopulateFromJson(FDisconnectPinEntry::StaticStruct(), &Entry, DiscVal);
if (!PopulateError.IsEmpty()) if (!PopulateError.IsEmpty())
{ {
EntryResult->SetStringField(TEXT("error"), PopulateError); 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("nodeId"), Entry.NodeId);
EntryResult->SetStringField(TEXT("pinName"), Entry.PinName); EntryResult->SetStringField(TEXT("pinName"), Entry.PinName);
UEdGraphNode* Node = Helper->FindNodeByGuid(BP, Entry.NodeId); UEdGraphNode* Node = MCPUtils::FindNodeByGuid(BP, Entry.NodeId);
if (!Node) if (!Node)
{ {
EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Node '%s' not found"), *Entry.NodeId)); 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()) if (!Entry.TargetNodeId.IsEmpty() && !Entry.TargetPinName.IsEmpty())
{ {
UEdGraphNode* TargetNode = Helper->FindNodeByGuid(BP, Entry.TargetNodeId); UEdGraphNode* TargetNode = MCPUtils::FindNodeByGuid(BP, Entry.TargetNodeId);
if (!TargetNode) if (!TargetNode)
{ {
EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Target node '%s' not found"), *Entry.TargetNodeId)); 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); UBlueprint* BP = Helper->LoadBlueprintByName(Blueprint, LoadError);
if (!BP) if (!BP)
{ {
return Helper->MakeErrorJson(Result, LoadError); return MCPUtils::MakeErrorJson(Result, LoadError);
} }
// Count graphs and nodes before refresh // Count graphs and nodes before refresh
@@ -761,7 +761,7 @@ void UMCPHandler_SetPinDefault::Handle(const FJsonObject* Json, FJsonObject* Res
Results.Add(MakeShared<FJsonValueObject>(EntryResult)); Results.Add(MakeShared<FJsonValueObject>(EntryResult));
FSetPinDefaultEntry Entry; FSetPinDefaultEntry Entry;
FString PopulateError = Helper->PopulateFromJson(FSetPinDefaultEntry::StaticStruct(), &Entry, PinVal); FString PopulateError = MCPUtils::PopulateFromJson(FSetPinDefaultEntry::StaticStruct(), &Entry, PinVal);
if (!PopulateError.IsEmpty()) if (!PopulateError.IsEmpty())
{ {
EntryResult->SetStringField(TEXT("error"), PopulateError); EntryResult->SetStringField(TEXT("error"), PopulateError);
@@ -781,7 +781,7 @@ void UMCPHandler_SetPinDefault::Handle(const FJsonObject* Json, FJsonObject* Res
} }
UEdGraph* Graph = nullptr; UEdGraph* Graph = nullptr;
UEdGraphNode* Node = Helper->FindNodeByGuid(BP, Entry.NodeId, &Graph); UEdGraphNode* Node = MCPUtils::FindNodeByGuid(BP, Entry.NodeId, &Graph);
if (!Node) if (!Node)
{ {
EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Node '%s' not found"), *Entry.NodeId)); 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); UBlueprint* BP = Helper->LoadBlueprintByName(Blueprint, LoadError);
if (!BP) if (!BP)
{ {
return Helper->MakeErrorJson(Result, LoadError); return MCPUtils::MakeErrorJson(Result, LoadError);
} }
// Find node // Find node
UEdGraph* Graph = nullptr; UEdGraph* Graph = nullptr;
UEdGraphNode* Node = Helper->FindNodeByGuid(BP, NodeId, &Graph); UEdGraphNode* Node = MCPUtils::FindNodeByGuid(BP, NodeId, &Graph);
if (!Node) 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 // Determine what kind of struct node this is
@@ -873,7 +873,7 @@ void UMCPHandler_ChangeStructNodeType::Handle(const FJsonObject* Json, FJsonObje
if (!BreakNode && !MakeNode) 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())); *NodeId, *Node->GetClass()->GetName()));
} }
@@ -892,7 +892,7 @@ void UMCPHandler_ChangeStructNodeType::Handle(const FJsonObject* Json, FJsonObje
} }
if (!NewStruct) 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'"), 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(); const UEdGraphSchema* Schema = Graph->GetSchema();
if (!Schema) 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) // 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); FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
// Return updated node state // Return updated node state
TSharedPtr<FJsonObject> UpdatedNodeState = Helper->SerializeNode(Node); TSharedPtr<FJsonObject> UpdatedNodeState = MCPUtils::SerializeNode(Node);
Result->SetBoolField(TEXT("success"), true); Result->SetBoolField(TEXT("success"), true);
Result->SetStringField(TEXT("blueprint"), Blueprint); Result->SetStringField(TEXT("blueprint"), Blueprint);
@@ -1069,18 +1069,18 @@ void UMCPHandler_DeleteNode::Handle(const FJsonObject* Json, FJsonObject* Result
UBlueprint* BP = Helper->LoadBlueprintByName(Blueprint, LoadError); UBlueprint* BP = Helper->LoadBlueprintByName(Blueprint, LoadError);
if (!BP) if (!BP)
{ {
return Helper->MakeErrorJson(Result, LoadError); return MCPUtils::MakeErrorJson(Result, LoadError);
} }
UEdGraph* Graph = nullptr; UEdGraph* Graph = nullptr;
UEdGraphNode* Node = Helper->FindNodeByGuid(BP, NodeId, &Graph); UEdGraphNode* Node = MCPUtils::FindNodeByGuid(BP, NodeId, &Graph);
if (!Node) 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) 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(); FString NodeClass = Node->GetClass()->GetName();
@@ -1092,7 +1092,7 @@ void UMCPHandler_DeleteNode::Handle(const FJsonObject* Json, FJsonObject* Result
// without recreating the entire function/event. // without recreating the entire function/event.
if (Cast<UK2Node_FunctionEntry>(Node)) if (Cast<UK2Node_FunctionEntry>(Node))
{ {
return Helper->MakeErrorJson(Result, FString::Printf( return MCPUtils::MakeErrorJson(Result, FString::Printf(
TEXT("Cannot delete FunctionEntry node '%s' in graph '%s'. ") 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("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."), 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<UK2Node_Event>(Node)) if (Cast<UK2Node_Event>(Node))
{ {
return Helper->MakeErrorJson(Result, FString::Printf( return MCPUtils::MakeErrorJson(Result, FString::Printf(
TEXT("Cannot delete event entry node '%s' in graph '%s'. ") 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."), TEXT("This is the root node of the event handler — removing it would leave an empty, uncompilable graph."),
*NodeTitle, *GraphName)); *NodeTitle, *GraphName));
} }
if (Cast<UK2Node_CustomEvent>(Node)) if (Cast<UK2Node_CustomEvent>(Node))
{ {
return Helper->MakeErrorJson(Result, FString::Printf( return MCPUtils::MakeErrorJson(Result, FString::Printf(
TEXT("Cannot delete CustomEvent entry node '%s' in graph '%s'. ") 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."), TEXT("This is the root node of the custom event — removing it would leave an empty, uncompilable graph."),
*NodeTitle, *GraphName)); *NodeTitle, *GraphName));
@@ -1154,13 +1154,13 @@ void UMCPHandler_RenameAsset::Handle(const FJsonObject* Json, FJsonObject* Resul
FAssetData* FoundAsset = Helper->FindAnyAsset(AssetPath); FAssetData* FoundAsset = Helper->FindAnyAsset(AssetPath);
if (!FoundAsset) 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(); UObject* AssetObj = FoundAsset->GetAsset();
if (!AssetObj) 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 // 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); Result->SetStringField(TEXT("newAssetName"), NewAssetName);
if (!bSuccess) 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); UBlueprint* BP = Helper->LoadBlueprintByName(Blueprint, LoadError);
if (!BP) if (!BP)
{ {
return Helper->MakeErrorJson(Result, LoadError); return MCPUtils::MakeErrorJson(Result, LoadError);
} }
if (!BP->GeneratedClass) 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(); UObject* CDO = BP->GeneratedClass->GetDefaultObject();
if (!CDO) 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); FProperty* Prop = BP->GeneratedClass->FindPropertyByName(*Property);
if (!Prop) 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; FString OldValue;
@@ -1286,7 +1286,7 @@ void UMCPHandler_SetBlueprintDefault::Handle(const FJsonObject* Json, FJsonObjec
if (!ResolvedClass) 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 // Validate meta class compatibility
@@ -1295,7 +1295,7 @@ void UMCPHandler_SetBlueprintDefault::Handle(const FJsonObject* Json, FJsonObjec
UClass* MetaClass = ClassProp->MetaClass; UClass* MetaClass = ClassProp->MetaClass;
if (MetaClass && !ResolvedClass->IsChildOf(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')"), TEXT("'%s' is not a subclass of '%s' (required by property '%s')"),
*ResolvedClass->GetName(), *MetaClass->GetName(), *Property)); *ResolvedClass->GetName(), *MetaClass->GetName(), *Property));
} }
@@ -1325,7 +1325,7 @@ void UMCPHandler_SetBlueprintDefault::Handle(const FJsonObject* Json, FJsonObjec
if (!ResolvedObj) 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); ObjProp->SetPropertyValue_InContainer(CDO, ResolvedObj);
@@ -1343,7 +1343,7 @@ void UMCPHandler_SetBlueprintDefault::Handle(const FJsonObject* Json, FJsonObjec
} }
else 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'"), TEXT("Failed to set property '%s' to '%s' — value could not be parsed for type '%s'"),
*Property, *Value, *Prop->GetCPPType())); *Property, *Value, *Prop->GetCPPType()));
} }
@@ -1351,7 +1351,7 @@ void UMCPHandler_SetBlueprintDefault::Handle(const FJsonObject* Json, FJsonObjec
if (!bSuccess) 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 // Mark modified and save
@@ -1359,7 +1359,7 @@ void UMCPHandler_SetBlueprintDefault::Handle(const FJsonObject* Json, FJsonObjec
BP->Modify(); BP->Modify();
FKismetEditorUtilities::CompileBlueprint(BP); 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)"), UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Set '%s.%s' from '%s' to '%s' (saved: %s)"),
*Blueprint, *Property, *OldValue, *ActualNewValue, bSaved ? TEXT("true") : TEXT("false")); *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); UBlueprint* BP = Helper->LoadBlueprintByName(Blueprint, LoadError);
if (!BP) if (!BP)
{ {
return Helper->MakeErrorJson(Result, LoadError); return MCPUtils::MakeErrorJson(Result, LoadError);
} }
TArray<TSharedPtr<FJsonValue>> Results; TArray<TSharedPtr<FJsonValue>> Results;
@@ -1397,7 +1397,7 @@ void UMCPHandler_MoveNode::Handle(const FJsonObject* Json, FJsonObject* Result)
Results.Add(MakeShared<FJsonValueObject>(EntryResult)); Results.Add(MakeShared<FJsonValueObject>(EntryResult));
FMoveNodeEntry Entry; FMoveNodeEntry Entry;
FString PopulateError = Helper->PopulateFromJson(FMoveNodeEntry::StaticStruct(), &Entry, NodeVal); FString PopulateError = MCPUtils::PopulateFromJson(FMoveNodeEntry::StaticStruct(), &Entry, NodeVal);
if (!PopulateError.IsEmpty()) if (!PopulateError.IsEmpty())
{ {
EntryResult->SetStringField(TEXT("error"), PopulateError); EntryResult->SetStringField(TEXT("error"), PopulateError);
@@ -1406,7 +1406,7 @@ void UMCPHandler_MoveNode::Handle(const FJsonObject* Json, FJsonObject* Result)
EntryResult->SetStringField(TEXT("nodeId"), Entry.NodeId); EntryResult->SetStringField(TEXT("nodeId"), Entry.NodeId);
UEdGraphNode* Node = Helper->FindNodeByGuid(BP, Entry.NodeId); UEdGraphNode* Node = MCPUtils::FindNodeByGuid(BP, Entry.NodeId);
if (!Node) if (!Node)
{ {
EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Node '%s' not found"), *Entry.NodeId)); 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); UBlueprint* BP = Helper->LoadBlueprintByName(Blueprint, LoadError);
if (!BP) if (!BP)
{ {
return Helper->MakeErrorJson(Result, LoadError); return MCPUtils::MakeErrorJson(Result, LoadError);
} }
// Find the target graph // Find the target graph
FString DecodedGraphName = MCPHelper::UrlDecode(Graph); FString DecodedGraphName = MCPUtils::UrlDecode(Graph);
UEdGraph* TargetGraph = nullptr; UEdGraph* TargetGraph = nullptr;
TArray<UEdGraph*> AllGraphs; TArray<UEdGraph*> AllGraphs;
BP->GetAllGraphs(AllGraphs); BP->GetAllGraphs(AllGraphs);
@@ -1469,12 +1469,12 @@ void UMCPHandler_DuplicateNodes::Handle(const FJsonObject* Json, FJsonObject* Re
if (!TargetGraph) 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) 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 // Find all source nodes
@@ -1484,7 +1484,7 @@ void UMCPHandler_DuplicateNodes::Handle(const FJsonObject* Json, FJsonObject* Re
for (const TSharedPtr<FJsonValue>& IdVal : NodeIds.Array) for (const TSharedPtr<FJsonValue>& IdVal : NodeIds.Array)
{ {
FString NodeId = IdVal->AsString(); FString NodeId = IdVal->AsString();
UEdGraphNode* Node = Helper->FindNodeByGuid(BP, NodeId); UEdGraphNode* Node = MCPUtils::FindNodeByGuid(BP, NodeId);
if (Node) if (Node)
{ {
if (Node->GetGraph() == TargetGraph) if (Node->GetGraph() == TargetGraph)
@@ -1504,7 +1504,7 @@ void UMCPHandler_DuplicateNodes::Handle(const FJsonObject* Json, FJsonObject* Re
if (SourceNodes.Num() == 0) 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'"), 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); UBlueprint* BP = Helper->LoadBlueprintByName(Blueprint, LoadError);
if (!BP) 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) 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); Result->SetBoolField(TEXT("success"), true);
@@ -1618,13 +1618,13 @@ void UMCPHandler_SetNodeComment::Handle(const FJsonObject* Json, FJsonObject* Re
UBlueprint* BP = Helper->LoadBlueprintByName(Blueprint, LoadError); UBlueprint* BP = Helper->LoadBlueprintByName(Blueprint, LoadError);
if (!BP) 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) 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; FString OldComment = Node->NodeComment;
@@ -1651,81 +1651,6 @@ void UMCPHandler_SetNodeComment::Handle(const FJsonObject* Json, FJsonObject* Re
// ============================================================ // ============================================================
// Shared helper: iterate the blueprint action database. // 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<UBlueprintNodeSpawner*> AllSpawners()
{
TArray<UBlueprintNodeSpawner*> 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<FString>& 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<UBlueprintNodeSpawner*> FindSpawner(const FString& FullName)
{
FString FullNameLower = FullName.ToLower();
TArray<UBlueprintNodeSpawner*> Result;
for (UBlueprintNodeSpawner* Spawner : AllSpawners())
{
if (MakeFullName(Spawner).ToLower() == FullNameLower)
{
Result.Add(Spawner);
}
}
return Result;
}
};
// ============================================================ // ============================================================
// SearchNodeTypes — search the blueprint action database // SearchNodeTypes — search the blueprint action database
// for spawners matching a query string (same pool as the right-click menu) // 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); int32 ClampedMax = FMath::Clamp(MaxResults, 1, 500);
TArray<FString> FullNames; TArray<UBlueprintNodeSpawner*> Spawners = MCPUtils::SearchNodeSpawners(Query, ClampedMax);
FNodeActionSearch::Find(Query, ClampedMax, FullNames);
TArray<TSharedPtr<FJsonValue>> ResultArray; TArray<TSharedPtr<FJsonValue>> ResultArray;
for (const FString& Name : FullNames) for (UBlueprintNodeSpawner* Spawner : Spawners)
{ {
ResultArray.Add(MakeShared<FJsonValueString>(Name)); ResultArray.Add(MakeShared<FJsonValueString>(MCPUtils::NodeSpawnerFullName(Spawner)));
} }
Result->SetBoolField(TEXT("success"), true); Result->SetBoolField(TEXT("success"), true);
@@ -1763,11 +1687,11 @@ void UMCPHandler_SpawnNode::Handle(const FJsonObject* Json, FJsonObject* Result)
UBlueprint* BP = Helper->LoadBlueprintByName(Blueprint, LoadError); UBlueprint* BP = Helper->LoadBlueprintByName(Blueprint, LoadError);
if (!BP) if (!BP)
{ {
return Helper->MakeErrorJson(Result, LoadError); return MCPUtils::MakeErrorJson(Result, LoadError);
} }
// Find the target graph // Find the target graph
FString DecodedGraphName = MCPHelper::UrlDecode(Graph); FString DecodedGraphName = MCPUtils::UrlDecode(Graph);
UEdGraph* TargetGraph = nullptr; UEdGraph* TargetGraph = nullptr;
TArray<UEdGraph*> AllGraphs; TArray<UEdGraph*> AllGraphs;
BP->GetAllGraphs(AllGraphs); BP->GetAllGraphs(AllGraphs);
@@ -1788,7 +1712,7 @@ void UMCPHandler_SpawnNode::Handle(const FJsonObject* Json, FJsonObject* Result)
{ {
if (G) GraphNames.Add(MakeShared<FJsonValueString>(G->GetName())); if (G) GraphNames.Add(MakeShared<FJsonValueString>(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); Result->SetArrayField(TEXT("availableGraphs"), GraphNames);
return; return;
} }
@@ -1802,7 +1726,7 @@ void UMCPHandler_SpawnNode::Handle(const FJsonObject* Json, FJsonObject* Result)
Results.Add(MakeShared<FJsonValueObject>(EntryResult)); Results.Add(MakeShared<FJsonValueObject>(EntryResult));
FSpawnNodeEntry Entry; FSpawnNodeEntry Entry;
FString PopulateError = Helper->PopulateFromJson(FSpawnNodeEntry::StaticStruct(), &Entry, NodeVal); FString PopulateError = MCPUtils::PopulateFromJson(FSpawnNodeEntry::StaticStruct(), &Entry, NodeVal);
if (!PopulateError.IsEmpty()) if (!PopulateError.IsEmpty())
{ {
EntryResult->SetStringField(TEXT("error"), PopulateError); EntryResult->SetStringField(TEXT("error"), PopulateError);
@@ -1812,7 +1736,7 @@ void UMCPHandler_SpawnNode::Handle(const FJsonObject* Json, FJsonObject* Result)
EntryResult->SetStringField(TEXT("actionName"), Entry.ActionName); EntryResult->SetStringField(TEXT("actionName"), Entry.ActionName);
// Find the spawner by exact full name // Find the spawner by exact full name
TArray<UBlueprintNodeSpawner*> Matches = FNodeActionSearch::FindSpawner(Entry.ActionName); TArray<UBlueprintNodeSpawner*> Matches = MCPUtils::SearchNodeSpawners(Entry.ActionName, 0, /*ExactMatch=*/true);
if (Matches.Num() == 0) if (Matches.Num() == 0)
{ {
EntryResult->SetStringField(TEXT("error"), FString::Printf( EntryResult->SetStringField(TEXT("error"), FString::Printf(
@@ -1854,7 +1778,7 @@ void UMCPHandler_SpawnNode::Handle(const FJsonObject* Json, FJsonObject* Result)
*Blueprint); *Blueprint);
// Serialize result // Serialize result
TSharedPtr<FJsonObject> NodeState = Helper->SerializeNode(NewNode); TSharedPtr<FJsonObject> NodeState = MCPUtils::SerializeNode(NewNode);
EntryResult->SetBoolField(TEXT("success"), true); EntryResult->SetBoolField(TEXT("success"), true);
EntryResult->SetStringField(TEXT("nodeId"), NewNode->NodeGuid.ToString()); EntryResult->SetStringField(TEXT("nodeId"), NewNode->NodeGuid.ToString());

View File

@@ -1,4 +1,5 @@
#include "BlueprintMCPServer.h" #include "BlueprintMCPServer.h"
#include "MCPUtils.h"
#include "Engine/Blueprint.h" #include "Engine/Blueprint.h"
#include "EdGraph/EdGraph.h" #include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphPin.h" #include "EdGraph/EdGraphPin.h"
@@ -22,7 +23,7 @@ void FBlueprintMCPServer::HandleChangeFunctionParamType(const FJsonObject* Json,
if (BlueprintName.IsEmpty() || FunctionName.IsEmpty() || ParamName.IsEmpty() || NewTypeName.IsEmpty()) 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 // Load Blueprint
@@ -30,15 +31,15 @@ void FBlueprintMCPServer::HandleChangeFunctionParamType(const FJsonObject* Json,
UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError); UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError);
if (!BP) 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) // Resolve the new type using the shared resolver (supports primitives, structs, enums, and object references)
FEdGraphPinType NewPinType; FEdGraphPinType NewPinType;
FString TypeError; 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, // 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'"), TEXT("Function or custom event '%s' not found in Blueprint '%s'"),
*FunctionName, *BlueprintName)); *FunctionName, *BlueprintName));
Result->SetArrayField(TEXT("availableFunctionsAndEvents"), Available); 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'"), TEXT("Parameter '%s' not found in %s '%s'"),
*ParamName, *FoundNodeType, *FunctionName)); *ParamName, *FoundNodeType, *FunctionName));
Result->SetArrayField(TEXT("availableParams"), ParamNames); Result->SetArrayField(TEXT("availableParams"), ParamNames);
@@ -206,13 +207,13 @@ void FBlueprintMCPServer::HandleChangeFunctionParamType(const FJsonObject* Json,
} }
// Save // Save
bool bSaved = SaveBlueprintPackage(BP); bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Parameter type changed, save %s"), UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Parameter type changed, save %s"),
bSaved ? TEXT("succeeded") : TEXT("failed")); bSaved ? TEXT("succeeded") : TEXT("failed"));
// Serialize the updated entry node state // Serialize the updated entry node state
TSharedPtr<FJsonObject> UpdatedNodeState = SerializeNode(EntryNode); TSharedPtr<FJsonObject> UpdatedNodeState = MCPUtils::SerializeNode(EntryNode);
Result->SetBoolField(TEXT("success"), true); Result->SetBoolField(TEXT("success"), true);
Result->SetStringField(TEXT("blueprint"), BlueprintName); Result->SetStringField(TEXT("blueprint"), BlueprintName);
@@ -240,7 +241,7 @@ void FBlueprintMCPServer::HandleRemoveFunctionParameter(const FJsonObject* Json,
if (BlueprintName.IsEmpty() || FunctionName.IsEmpty() || ParamName.IsEmpty()) 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 // Load Blueprint
@@ -248,7 +249,7 @@ void FBlueprintMCPServer::HandleRemoveFunctionParameter(const FJsonObject* Json,
UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError); UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError);
if (!BP) if (!BP)
{ {
return MakeErrorJson(Result, LoadError); return MCPUtils::MakeErrorJson(Result, LoadError);
} }
// Find the entry node // 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'"), TEXT("Function or custom event '%s' not found in Blueprint '%s'"),
*FunctionName, *BlueprintName)); *FunctionName, *BlueprintName));
Result->SetArrayField(TEXT("availableFunctionsAndEvents"), Available); 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'"), TEXT("Parameter '%s' not found in %s '%s'"),
*ParamName, *FoundNodeType, *FunctionName)); *ParamName, *FoundNodeType, *FunctionName));
Result->SetArrayField(TEXT("availableParams"), ParamNames); Result->SetArrayField(TEXT("availableParams"), ParamNames);
@@ -374,7 +375,7 @@ void FBlueprintMCPServer::HandleRemoveFunctionParameter(const FJsonObject* Json,
} }
// Save // Save
bool bSaved = SaveBlueprintPackage(BP); bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Parameter removed, save %s"), UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Parameter removed, save %s"),
bSaved ? TEXT("succeeded") : TEXT("failed")); 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()) 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 // Load Blueprint
@@ -409,15 +410,15 @@ void FBlueprintMCPServer::HandleAddFunctionParameter(const FJsonObject* Json, FJ
UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError); UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError);
if (!BP) if (!BP)
{ {
return MakeErrorJson(Result, LoadError); return MCPUtils::MakeErrorJson(Result, LoadError);
} }
// Resolve param type // Resolve param type
FEdGraphPinType PinType; FEdGraphPinType PinType;
FString TypeError; 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 // 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()))); 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'"), TEXT("Function, custom event, or event dispatcher '%s' not found in Blueprint '%s'"),
*FunctionName, *BlueprintName)); *FunctionName, *BlueprintName));
Result->SetArrayField(TEXT("availableFunctions"), AvailFuncs); 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)) 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)); 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); EntryNode->CreateUserDefinedPin(FName(*ParamName), PinType, EGPD_Output);
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP); 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)"), UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Added parameter '%s' to '%s' in '%s' (saved: %s)"),
*ParamName, *FunctionName, *BlueprintName, bSaved ? TEXT("true") : TEXT("false")); *ParamName, *FunctionName, *BlueprintName, bSaved ? TEXT("true") : TEXT("false"));

View File

@@ -1,4 +1,5 @@
#include "BlueprintMCPServer.h" #include "BlueprintMCPServer.h"
#include "MCPUtils.h"
#include "Engine/Blueprint.h" #include "Engine/Blueprint.h"
#include "Engine/World.h" #include "Engine/World.h"
#include "Engine/Level.h" #include "Engine/Level.h"
@@ -116,18 +117,18 @@ void FBlueprintMCPServer::HandleGetBlueprint(const FJsonObject* Json, FJsonObjec
FString Name = Json->GetStringField(TEXT("name")); FString Name = Json->GetStringField(TEXT("name"));
if (Name.IsEmpty()) if (Name.IsEmpty())
{ {
return MakeErrorJson(Result, TEXT("Missing 'name' parameter")); return MCPUtils::MakeErrorJson(Result, TEXT("Missing 'name' parameter"));
} }
FString LoadError; FString LoadError;
UBlueprint* BP = LoadBlueprintByName(Name, LoadError); UBlueprint* BP = LoadBlueprintByName(Name, LoadError);
if (!BP) if (!BP)
{ {
return MakeErrorJson(Result, LoadError); return MCPUtils::MakeErrorJson(Result, LoadError);
} }
TSharedRef<FJsonObject> Tmp = SerializeBlueprint(BP); TSharedRef<FJsonObject> Tmp = MCPUtils::SerializeBlueprint(BP);
CopyJsonFields(&*Tmp, Result); MCPUtils::CopyJsonFields(&*Tmp, Result);
} }
void FBlueprintMCPServer::HandleGetGraph(const FJsonObject* Json, FJsonObject* 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")); FString GraphName = Json->GetStringField(TEXT("graph"));
if (Name.IsEmpty() || GraphName.IsEmpty()) 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' // URL-decode graph name to handle spaces encoded as '+' or '%20'
FString DecodedGraphName = UrlDecode(GraphName); FString DecodedGraphName = MCPUtils::UrlDecode(GraphName);
FString LoadError; FString LoadError;
UBlueprint* BP = LoadBlueprintByName(Name, LoadError); UBlueprint* BP = LoadBlueprintByName(Name, LoadError);
if (!BP) if (!BP)
{ {
return MakeErrorJson(Result, LoadError); return MCPUtils::MakeErrorJson(Result, LoadError);
} }
TArray<UEdGraph*> AllGraphs; TArray<UEdGraph*> AllGraphs;
@@ -156,10 +157,10 @@ void FBlueprintMCPServer::HandleGetGraph(const FJsonObject* Json, FJsonObject* R
{ {
if (Graph && Graph->GetName().Equals(DecodedGraphName, ESearchCase::IgnoreCase)) if (Graph && Graph->GetName().Equals(DecodedGraphName, ESearchCase::IgnoreCase))
{ {
TSharedPtr<FJsonObject> GraphJson = SerializeGraph(Graph); TSharedPtr<FJsonObject> GraphJson = MCPUtils::SerializeGraph(Graph);
if (GraphJson.IsValid()) if (GraphJson.IsValid())
{ {
CopyJsonFields(GraphJson.Get(), Result); MCPUtils::CopyJsonFields(GraphJson.Get(), Result);
return; return;
} }
} }
@@ -174,7 +175,7 @@ void FBlueprintMCPServer::HandleGetGraph(const FJsonObject* Json, FJsonObject* R
GraphNames.Add(MakeShared<FJsonValueString>(Graph->GetName())); GraphNames.Add(MakeShared<FJsonValueString>(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); Result->SetArrayField(TEXT("availableGraphs"), GraphNames);
} }
@@ -183,7 +184,7 @@ void FBlueprintMCPServer::HandleSearch(const FJsonObject* Json, FJsonObject* Res
FString Query = Json->GetStringField(TEXT("query")); FString Query = Json->GetStringField(TEXT("query"));
if (Query.IsEmpty()) if (Query.IsEmpty())
{ {
return MakeErrorJson(Result, TEXT("Missing 'query' parameter")); return MCPUtils::MakeErrorJson(Result, TEXT("Missing 'query' parameter"));
} }
FString PathFilter = Json->GetStringField(TEXT("path")); FString PathFilter = Json->GetStringField(TEXT("path"));
@@ -310,7 +311,7 @@ void FBlueprintMCPServer::HandleTestSave(const FJsonObject* Json, FJsonObject* R
FString Name = Json->GetStringField(TEXT("name")); FString Name = Json->GetStringField(TEXT("name"));
if (Name.IsEmpty()) 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); 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); UBlueprint* BP = LoadBlueprintByName(Name, LoadError);
if (!BP) if (!BP)
{ {
return MakeErrorJson(Result, LoadError); return MCPUtils::MakeErrorJson(Result, LoadError);
} }
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: test-save — loaded '%s', GeneratedClass=%s"), 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")); BP->GeneratedClass ? *BP->GeneratedClass->GetName() : TEXT("null"));
// Attempt save with NO modifications // Attempt save with NO modifications
bool bSaved = SaveBlueprintPackage(BP); bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
Result->SetStringField(TEXT("blueprint"), Name); Result->SetStringField(TEXT("blueprint"), Name);
Result->SetStringField(TEXT("packagePath"), BP->GetPackage()->GetName()); Result->SetStringField(TEXT("packagePath"), BP->GetPackage()->GetName());
@@ -344,7 +345,7 @@ void FBlueprintMCPServer::HandleFindReferences(const FJsonObject* Json, FJsonObj
FString AssetPath = Json->GetStringField(TEXT("assetPath")); FString AssetPath = Json->GetStringField(TEXT("assetPath"));
if (AssetPath.IsEmpty()) if (AssetPath.IsEmpty())
{ {
return MakeErrorJson(Result, TEXT("Missing 'assetPath' query parameter")); return MCPUtils::MakeErrorJson(Result, TEXT("Missing 'assetPath' query parameter"));
} }
IAssetRegistry& Registry = *IAssetRegistry::Get(); IAssetRegistry& Registry = *IAssetRegistry::Get();
@@ -391,12 +392,12 @@ void FBlueprintMCPServer::HandleSearchByType(const FJsonObject* Json, FJsonObjec
FString TypeNameRaw = Json->GetStringField(TEXT("typeName")); FString TypeNameRaw = Json->GetStringField(TEXT("typeName"));
if (TypeNameRaw.IsEmpty()) 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 FilterRaw = Json->GetStringField(TEXT("filter"));
FString FilterStr = FilterRaw.IsEmpty() ? FString() : UrlDecode(FilterRaw); FString FilterStr = FilterRaw.IsEmpty() ? FString() : MCPUtils::UrlDecode(FilterRaw);
int32 MaxResults = 200; int32 MaxResults = 200;
if (Json->HasField(TEXT("maxResults"))) if (Json->HasField(TEXT("maxResults")))

View File

@@ -1,4 +1,5 @@
#include "BlueprintMCPServer.h" #include "BlueprintMCPServer.h"
#include "MCPUtils.h"
#include "Engine/Blueprint.h" #include "Engine/Blueprint.h"
#include "EdGraph/EdGraph.h" #include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphNode.h" #include "EdGraph/EdGraphNode.h"
@@ -161,7 +162,7 @@ bool FBlueprintMCPServer::SaveSnapshotToDisk(const FString& SnapshotId, const FG
} }
Root->SetObjectField(TEXT("graphs"), GraphsObj); Root->SetObjectField(TEXT("graphs"), GraphsObj);
FString JsonString = JsonToString(Root); FString JsonString = MCPUtils::JsonToString(Root);
bool bSuccess = FFileHelper::SaveStringToFile(JsonString, *FilePath, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM); bool bSuccess = FFileHelper::SaveStringToFile(JsonString, *FilePath, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM);
if (bSuccess) if (bSuccess)
{ {
@@ -259,7 +260,7 @@ void FBlueprintMCPServer::HandleSnapshotGraph(const FJsonObject* Json, FJsonObje
FString BlueprintName = Json->GetStringField(TEXT("blueprint")); FString BlueprintName = Json->GetStringField(TEXT("blueprint"));
if (BlueprintName.IsEmpty()) if (BlueprintName.IsEmpty())
{ {
return MakeErrorJson(Result, TEXT("Missing required field: blueprint")); return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: blueprint"));
} }
FString GraphFilter; FString GraphFilter;
@@ -270,7 +271,7 @@ void FBlueprintMCPServer::HandleSnapshotGraph(const FJsonObject* Json, FJsonObje
UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError); UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError);
if (!BP) if (!BP)
{ {
return MakeErrorJson(Result, LoadError); return MCPUtils::MakeErrorJson(Result, LoadError);
} }
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Creating snapshot for blueprint '%s'"), *BlueprintName); 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()) 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; int32 TotalConnections = 0;
@@ -347,7 +348,7 @@ void FBlueprintMCPServer::HandleDiffGraph(const FJsonObject* Json, FJsonObject*
FString SnapshotId = Json->GetStringField(TEXT("snapshotId")); FString SnapshotId = Json->GetStringField(TEXT("snapshotId"));
if (BlueprintName.IsEmpty() || SnapshotId.IsEmpty()) 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; FString GraphFilter;
@@ -360,7 +361,7 @@ void FBlueprintMCPServer::HandleDiffGraph(const FJsonObject* Json, FJsonObject*
{ {
if (!LoadSnapshotFromDisk(SnapshotId, LoadedSnapshot)) 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; SnapshotPtr = &LoadedSnapshot;
} }
@@ -370,7 +371,7 @@ void FBlueprintMCPServer::HandleDiffGraph(const FJsonObject* Json, FJsonObject*
UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError); UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError);
if (!BP) 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); 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")); FString SnapshotId = Json->GetStringField(TEXT("snapshotId"));
if (BlueprintName.IsEmpty() || SnapshotId.IsEmpty()) 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; FString GraphFilter;
@@ -579,7 +580,7 @@ void FBlueprintMCPServer::HandleRestoreGraph(const FJsonObject* Json, FJsonObjec
{ {
if (!LoadSnapshotFromDisk(SnapshotId, LoadedSnapshot)) 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; SnapshotPtr = &LoadedSnapshot;
} }
@@ -589,7 +590,7 @@ void FBlueprintMCPServer::HandleRestoreGraph(const FJsonObject* Json, FJsonObjec
UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError); UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError);
if (!BP) 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)"), 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 // Find source and target nodes
UEdGraph* SourceGraph = nullptr; UEdGraph* SourceGraph = nullptr;
UEdGraphNode* SourceNode = FindNodeByGuid(BP, Conn.SourceNodeGuid, &SourceGraph); UEdGraphNode* SourceNode = MCPUtils::FindNodeByGuid(BP, Conn.SourceNodeGuid, &SourceGraph);
UEdGraphNode* TargetNode = FindNodeByGuid(BP, Conn.TargetNodeGuid); UEdGraphNode* TargetNode = MCPUtils::FindNodeByGuid(BP, Conn.TargetNodeGuid);
if (!SourceNode) if (!SourceNode)
{ {
@@ -746,7 +747,7 @@ void FBlueprintMCPServer::HandleRestoreGraph(const FJsonObject* Json, FJsonObjec
bool bSaved = false; bool bSaved = false;
if (!bDryRun && Reconnected > 0) 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"), 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()) 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 // Optionally load snapshot for definite-break detection
@@ -1074,7 +1075,7 @@ void FBlueprintMCPServer::HandleAnalyzeRebuildImpact(const FJsonObject* Json, FJ
FString ModuleName = Json->GetStringField(TEXT("moduleName")); FString ModuleName = Json->GetStringField(TEXT("moduleName"));
if (ModuleName.IsEmpty()) if (ModuleName.IsEmpty())
{ {
return MakeErrorJson(Result, TEXT("Missing required field: moduleName")); return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: moduleName"));
} }
// Optional struct name filter // Optional struct name filter

View File

@@ -1,4 +1,5 @@
#include "BlueprintMCPServer.h" #include "BlueprintMCPServer.h"
#include "MCPUtils.h"
#include "Engine/UserDefinedStruct.h" #include "Engine/UserDefinedStruct.h"
#include "Engine/UserDefinedEnum.h" #include "Engine/UserDefinedEnum.h"
#include "Kismet2/BlueprintEditorUtils.h" #include "Kismet2/BlueprintEditorUtils.h"
@@ -25,7 +26,7 @@ void FBlueprintMCPServer::HandleCreateStruct(const FJsonObject* Json, FJsonObjec
FString AssetPath = Json->GetStringField(TEXT("assetPath")); FString AssetPath = Json->GetStringField(TEXT("assetPath"));
if (AssetPath.IsEmpty()) 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 // Split path into package path and asset name
@@ -38,12 +39,12 @@ void FBlueprintMCPServer::HandleCreateStruct(const FJsonObject* Json, FJsonObjec
} }
else 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()) 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 // 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)); FAssetData ExistingAsset = ARM.Get().GetAssetByObjectPath(FSoftObjectPath(AssetPath + TEXT(".") + AssetName));
if (ExistingAsset.IsValid()) 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 // Create the struct using the AssetTools factory
@@ -63,13 +64,13 @@ void FBlueprintMCPServer::HandleCreateStruct(const FJsonObject* Json, FJsonObjec
if (!NewAsset) if (!NewAsset)
{ {
return MakeErrorJson(Result, TEXT("Failed to create UserDefinedStruct asset")); return MCPUtils::MakeErrorJson(Result, TEXT("Failed to create UserDefinedStruct asset"));
} }
UUserDefinedStruct* NewStruct = Cast<UUserDefinedStruct>(NewAsset); UUserDefinedStruct* NewStruct = Cast<UUserDefinedStruct>(NewAsset);
if (!NewStruct) 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 // Add properties if specified
@@ -88,7 +89,7 @@ void FBlueprintMCPServer::HandleCreateStruct(const FJsonObject* Json, FJsonObjec
FEdGraphPinType PinType; FEdGraphPinType PinType;
FString TypeError; 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); UE_LOG(LogTemp, Warning, TEXT("BlueprintMCP: Could not resolve type '%s' for property '%s': %s"), *PropType, *PropName, *TypeError);
continue; continue;
@@ -149,7 +150,7 @@ void FBlueprintMCPServer::HandleCreateEnum(const FJsonObject* Json, FJsonObject*
FString AssetPath = Json->GetStringField(TEXT("assetPath")); FString AssetPath = Json->GetStringField(TEXT("assetPath"));
if (AssetPath.IsEmpty()) 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 // Split path
@@ -162,19 +163,19 @@ void FBlueprintMCPServer::HandleCreateEnum(const FJsonObject* Json, FJsonObject*
} }
else 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()) if (AssetName.IsEmpty())
{ {
return MakeErrorJson(Result, TEXT("Invalid asset name in assetPath")); return MCPUtils::MakeErrorJson(Result, TEXT("Invalid asset name in assetPath"));
} }
// Get values // Get values
const TArray<TSharedPtr<FJsonValue>>* ValuesArray = nullptr; const TArray<TSharedPtr<FJsonValue>>* ValuesArray = nullptr;
if (!Json->TryGetArrayField(TEXT("values"), ValuesArray) || !ValuesArray || ValuesArray->Num() == 0) 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<FString> EnumValues; TArray<FString> EnumValues;
@@ -185,7 +186,7 @@ void FBlueprintMCPServer::HandleCreateEnum(const FJsonObject* Json, FJsonObject*
} }
if (EnumValues.Num() == 0) 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 // Create the enum using AssetTools
@@ -197,13 +198,13 @@ void FBlueprintMCPServer::HandleCreateEnum(const FJsonObject* Json, FJsonObject*
if (!NewAsset) if (!NewAsset)
{ {
return MakeErrorJson(Result, TEXT("Failed to create UserDefinedEnum asset")); return MCPUtils::MakeErrorJson(Result, TEXT("Failed to create UserDefinedEnum asset"));
} }
UUserDefinedEnum* NewEnum = Cast<UUserDefinedEnum>(NewAsset); UUserDefinedEnum* NewEnum = Cast<UUserDefinedEnum>(NewAsset);
if (!NewEnum) 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. // 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()) 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 // Find the struct
@@ -259,15 +260,15 @@ void FBlueprintMCPServer::HandleAddStructProperty(const FJsonObject* Json, FJson
} }
if (!Struct) 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 // Resolve type
FEdGraphPinType PinType; FEdGraphPinType PinType;
FString TypeError; 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 // 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); bool bAdded = FStructureEditorUtils::AddVariable(Struct, PinType);
if (!bAdded) 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 // 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()) 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 // Find the struct
@@ -333,7 +334,7 @@ void FBlueprintMCPServer::HandleRemoveStructProperty(const FJsonObject* Json, FJ
} }
if (!Struct) 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 // Find the property GUID by name
@@ -357,7 +358,7 @@ void FBlueprintMCPServer::HandleRemoveStructProperty(const FJsonObject* Json, FJ
{ {
AvailProps.Add(MakeShared<FJsonValueString>(Var.FriendlyName)); AvailProps.Add(MakeShared<FJsonValueString>(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); Result->SetArrayField(TEXT("availableProperties"), AvailProps);
return; return;
} }
@@ -365,7 +366,7 @@ void FBlueprintMCPServer::HandleRemoveStructProperty(const FJsonObject* Json, FJ
bool bRemoved = FStructureEditorUtils::RemoveVariable(Struct, TargetGuid); bool bRemoved = FStructureEditorUtils::RemoveVariable(Struct, TargetGuid);
if (!bRemoved) 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 // Save

View File

@@ -1,4 +1,5 @@
#include "BlueprintMCPServer.h" #include "BlueprintMCPServer.h"
#include "MCPUtils.h"
#include "Engine/Blueprint.h" #include "Engine/Blueprint.h"
#include "EdGraph/EdGraph.h" #include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphNode.h" #include "EdGraph/EdGraphNode.h"
@@ -180,7 +181,7 @@ void FBlueprintMCPServer::HandleValidateBlueprint(const FJsonObject* Json, FJson
FString BlueprintName = Json->GetStringField(TEXT("blueprint")); FString BlueprintName = Json->GetStringField(TEXT("blueprint"));
if (BlueprintName.IsEmpty()) if (BlueprintName.IsEmpty())
{ {
return MakeErrorJson(Result, TEXT("Missing required field: blueprint")); return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: blueprint"));
} }
// Load Blueprint // Load Blueprint
@@ -188,13 +189,13 @@ void FBlueprintMCPServer::HandleValidateBlueprint(const FJsonObject* Json, FJson
UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError); UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError);
if (!BP) if (!BP)
{ {
return MakeErrorJson(Result, LoadError); return MCPUtils::MakeErrorJson(Result, LoadError);
} }
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Validating blueprint '%s'"), *BlueprintName); UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Validating blueprint '%s'"), *BlueprintName);
TSharedRef<FJsonObject> ValidationResult = ValidateSingleBlueprint(BP, BlueprintName); TSharedRef<FJsonObject> ValidationResult = ValidateSingleBlueprint(BP, BlueprintName);
CopyJsonFields(&*ValidationResult, Result); MCPUtils::CopyJsonFields(&*ValidationResult, Result);
} }
// ============================================================ // ============================================================

View File

@@ -1,4 +1,5 @@
#include "BlueprintMCPServer.h" #include "BlueprintMCPServer.h"
#include "MCPUtils.h"
#include "Engine/Blueprint.h" #include "Engine/Blueprint.h"
#include "EdGraph/EdGraph.h" #include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphPin.h" #include "EdGraph/EdGraphPin.h"
@@ -28,7 +29,7 @@ void FBlueprintMCPServer::HandleChangeVariableType(const FJsonObject* Json, FJso
if (BlueprintName.IsEmpty() || VariableName.IsEmpty() || NewTypeName.IsEmpty()) 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 // Load Blueprint
@@ -36,7 +37,7 @@ void FBlueprintMCPServer::HandleChangeVariableType(const FJsonObject* Json, FJso
UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError); UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError);
if (!BP) if (!BP)
{ {
return MakeErrorJson(Result, LoadError); return MCPUtils::MakeErrorJson(Result, LoadError);
} }
// Verify variable exists // Verify variable exists
@@ -51,7 +52,7 @@ void FBlueprintMCPServer::HandleChangeVariableType(const FJsonObject* Json, FJso
} }
if (!bVarFound) 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 // Build the new pin type using shared resolver
@@ -67,9 +68,9 @@ void FBlueprintMCPServer::HandleChangeVariableType(const FJsonObject* Json, FJso
} }
FString TypeError; 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 // Derive typeCategory from the resolved pin type for the response
@@ -185,7 +186,7 @@ void FBlueprintMCPServer::HandleChangeVariableType(const FJsonObject* Json, FJso
} }
// Save // Save
bool bSaved = SaveBlueprintPackage(BP); bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Variable type changed, save %s"), UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Variable type changed, save %s"),
bSaved ? TEXT("succeeded") : TEXT("failed")); bSaved ? TEXT("succeeded") : TEXT("failed"));
@@ -226,7 +227,7 @@ void FBlueprintMCPServer::HandleAddVariable(const FJsonObject* Json, FJsonObject
if (BlueprintName.IsEmpty() || VariableName.IsEmpty() || VariableType.IsEmpty()) 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; FString Category;
@@ -252,7 +253,7 @@ void FBlueprintMCPServer::HandleAddVariable(const FJsonObject* Json, FJsonObject
UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError); UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError);
if (!BP) if (!BP)
{ {
return MakeErrorJson(Result, LoadError); return MCPUtils::MakeErrorJson(Result, LoadError);
} }
// Check for duplicate variable name // Check for duplicate variable name
@@ -261,7 +262,7 @@ void FBlueprintMCPServer::HandleAddVariable(const FJsonObject* Json, FJsonObject
{ {
if (Var.VarName == VarFName) 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)); 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 // Resolve the type using the shared helper
FEdGraphPinType PinType; FEdGraphPinType PinType;
FString TypeError; 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 // Set container type for arrays
@@ -287,7 +288,7 @@ void FBlueprintMCPServer::HandleAddVariable(const FJsonObject* Json, FJsonObject
bool bSuccess = FBlueprintEditorUtils::AddMemberVariable(BP, VarFName, PinType, DefaultValue); bool bSuccess = FBlueprintEditorUtils::AddMemberVariable(BP, VarFName, PinType, DefaultValue);
if (!bSuccess) if (!bSuccess)
{ {
return MakeErrorJson(Result, FString::Printf( return MCPUtils::MakeErrorJson(Result, FString::Printf(
TEXT("FBlueprintEditorUtils::AddMemberVariable failed for '%s'"), *VariableName)); TEXT("FBlueprintEditorUtils::AddMemberVariable failed for '%s'"), *VariableName));
} }
@@ -298,7 +299,7 @@ void FBlueprintMCPServer::HandleAddVariable(const FJsonObject* Json, FJsonObject
} }
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP); 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)"), UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Added variable '%s' to '%s' (saved: %s)"),
*VariableName, *BlueprintName, bSaved ? TEXT("true") : TEXT("false")); *VariableName, *BlueprintName, bSaved ? TEXT("true") : TEXT("false"));
@@ -326,7 +327,7 @@ void FBlueprintMCPServer::HandleRemoveVariable(const FJsonObject* Json, FJsonObj
if (BlueprintName.IsEmpty() || VariableName.IsEmpty()) 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 // Load Blueprint
@@ -334,7 +335,7 @@ void FBlueprintMCPServer::HandleRemoveVariable(const FJsonObject* Json, FJsonObj
UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError); UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError);
if (!BP) if (!BP)
{ {
return MakeErrorJson(Result, LoadError); return MCPUtils::MakeErrorJson(Result, LoadError);
} }
// Find variable by name (case-insensitive) // Find variable by name (case-insensitive)
@@ -359,7 +360,7 @@ void FBlueprintMCPServer::HandleRemoveVariable(const FJsonObject* Json, FJsonObj
AvailVars.Add(MakeShared<FJsonValueString>(Var.VarName.ToString())); AvailVars.Add(MakeShared<FJsonValueString>(Var.VarName.ToString()));
} }
MakeErrorJson(Result, FString::Printf( MCPUtils::MakeErrorJson(Result, FString::Printf(
TEXT("Variable '%s' not found in Blueprint '%s'"), *VariableName, *BlueprintName)); TEXT("Variable '%s' not found in Blueprint '%s'"), *VariableName, *BlueprintName));
Result->SetArrayField(TEXT("availableVariables"), AvailVars); Result->SetArrayField(TEXT("availableVariables"), AvailVars);
return; return;
@@ -372,7 +373,7 @@ void FBlueprintMCPServer::HandleRemoveVariable(const FJsonObject* Json, FJsonObj
FBlueprintEditorUtils::RemoveMemberVariable(BP, VarFName); FBlueprintEditorUtils::RemoveMemberVariable(BP, VarFName);
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP); 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)"), UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Removed variable '%s' from '%s' (saved: %s)"),
*VariableName, *BlueprintName, bSaved ? TEXT("true") : TEXT("false")); *VariableName, *BlueprintName, bSaved ? TEXT("true") : TEXT("false"));
@@ -394,7 +395,7 @@ void FBlueprintMCPServer::HandleSetVariableMetadata(const FJsonObject* Json, FJs
if (BlueprintName.IsEmpty() || VariableName.IsEmpty()) 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 // Load Blueprint
@@ -402,7 +403,7 @@ void FBlueprintMCPServer::HandleSetVariableMetadata(const FJsonObject* Json, FJs
UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError); UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError);
if (!BP) if (!BP)
{ {
return MakeErrorJson(Result, LoadError); return MCPUtils::MakeErrorJson(Result, LoadError);
} }
// Find the variable // Find the variable
@@ -424,7 +425,7 @@ void FBlueprintMCPServer::HandleSetVariableMetadata(const FJsonObject* Json, FJs
{ {
AvailableVars.Add(MakeShared<FJsonValueString>(Var.VarName.ToString())); AvailableVars.Add(MakeShared<FJsonValueString>(Var.VarName.ToString()));
} }
MakeErrorJson(Result, FString::Printf( MCPUtils::MakeErrorJson(Result, FString::Printf(
TEXT("Variable '%s' not found in Blueprint '%s'"), *VariableName, *BlueprintName)); TEXT("Variable '%s' not found in Blueprint '%s'"), *VariableName, *BlueprintName));
Result->SetArrayField(TEXT("availableVariables"), AvailableVars); Result->SetArrayField(TEXT("availableVariables"), AvailableVars);
return; return;
@@ -488,7 +489,7 @@ void FBlueprintMCPServer::HandleSetVariableMetadata(const FJsonObject* Json, FJs
} }
else else
{ {
return MakeErrorJson(Result, FString::Printf( return MCPUtils::MakeErrorJson(Result, FString::Printf(
TEXT("Invalid replication value '%s'. Valid: none, replicated, repNotify"), *ReplicationStr)); TEXT("Invalid replication value '%s'. Valid: none, replicated, repNotify"), *ReplicationStr));
} }
@@ -557,7 +558,7 @@ void FBlueprintMCPServer::HandleSetVariableMetadata(const FJsonObject* Json, FJs
} }
else else
{ {
return MakeErrorJson(Result, FString::Printf( return MCPUtils::MakeErrorJson(Result, FString::Printf(
TEXT("Invalid editability value '%s'. Valid: editAnywhere, editDefaultsOnly, editInstanceOnly, none"), TEXT("Invalid editability value '%s'. Valid: editAnywhere, editDefaultsOnly, editInstanceOnly, none"),
*Editability)); *Editability));
} }
@@ -570,14 +571,14 @@ void FBlueprintMCPServer::HandleSetVariableMetadata(const FJsonObject* Json, FJs
if (Changes.Num() == 0) 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"), UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: SetVariableMetadata on '%s.%s' — %d field(s) changed"),
*BlueprintName, *VariableName, Changes.Num()); *BlueprintName, *VariableName, Changes.Num());
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP); FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
bool bSaved = SaveBlueprintPackage(BP); bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
Result->SetBoolField(TEXT("success"), true); Result->SetBoolField(TEXT("success"), true);
Result->SetStringField(TEXT("blueprint"), BlueprintName); Result->SetStringField(TEXT("blueprint"), BlueprintName);

View File

@@ -1,5 +1,5 @@
#include "MCPHandler.h" #include "MCPHandler.h"
#include "BlueprintMCPServer.h" #include "MCPUtils.h"
#include "Dom/JsonObject.h" #include "Dom/JsonObject.h"
#include "UObject/UnrealType.h" #include "UObject/UnrealType.h"
#include "UObject/EnumProperty.h" #include "UObject/EnumProperty.h"
@@ -164,7 +164,7 @@ FString PropertyNameToJsonKey(const FString& PropName)
} // namespace MCPPopulate } // namespace MCPPopulate
FString FBlueprintMCPServer::PopulateFromJson( FString MCPUtils::PopulateFromJson(
UStruct* StructType, UStruct* StructType,
void* Container, void* Container,
const TSharedPtr<FJsonValue>& JsonValue) const TSharedPtr<FJsonValue>& JsonValue)
@@ -176,7 +176,7 @@ FString FBlueprintMCPServer::PopulateFromJson(
return PopulateFromJson(StructType, Container, JsonValue->AsObject().Get()); return PopulateFromJson(StructType, Container, JsonValue->AsObject().Get());
} }
FString FBlueprintMCPServer::PopulateFromJson( FString MCPUtils::PopulateFromJson(
UStruct* StructType, UStruct* StructType,
void* Container, void* Container,
const FJsonObject* Json) const FJsonObject* Json)

File diff suppressed because it is too large Load Diff

View File

@@ -254,47 +254,19 @@ private:
void HandleSetStateBlendSpace(const FJsonObject* Json, FJsonObject* Result); void HandleSetStateBlendSpace(const FJsonObject* Json, FJsonObject* Result);
public: public:
// ----- Serialization ----- // ----- Helpers (stateful — use cached asset lists) -----
TSharedRef<FJsonObject> SerializeBlueprint(UBlueprint* BP);
TSharedPtr<FJsonObject> SerializeGraph(UEdGraph* Graph);
TSharedPtr<FJsonObject> SerializeNode(UEdGraphNode* Node);
TSharedPtr<FJsonObject> SerializePin(UEdGraphPin* Pin);
TSharedPtr<FJsonObject> SerializeMaterialExpression(UMaterialExpression* Expression);
FString JsonToString(TSharedRef<FJsonObject> JsonObj);
// ----- Helpers -----
FAssetData* FindAnyAsset(const FString& NameOrPath); FAssetData* FindAnyAsset(const FString& NameOrPath);
FAssetData* FindBlueprintAsset(const FString& NameOrPath); FAssetData* FindBlueprintAsset(const FString& NameOrPath);
FAssetData* FindMapAsset(const FString& NameOrPath); FAssetData* FindMapAsset(const FString& NameOrPath);
UBlueprint* LoadBlueprintByName(const FString& NameOrPath, FString& OutError); UBlueprint* LoadBlueprintByName(const FString& NameOrPath, FString& OutError);
UEdGraphNode* FindNodeByGuid(UBlueprint* BP, const FString& GuidString, UEdGraph** OutGraph = nullptr);
TSharedPtr<FJsonObject> 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. // ----- Material helpers (stateful — use cached asset lists) -----
// Returns empty string on success, or an error message on failure.
FString PopulateFromJson(UStruct* StructType, void* Container, const TSharedPtr<FJsonValue>& 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);
FAssetData* FindMaterialAsset(const FString& NameOrPath); FAssetData* FindMaterialAsset(const FString& NameOrPath);
UMaterial* LoadMaterialByName(const FString& NameOrPath, FString& OutError); UMaterial* LoadMaterialByName(const FString& NameOrPath, FString& OutError);
FAssetData* FindMaterialInstanceAsset(const FString& NameOrPath); FAssetData* FindMaterialInstanceAsset(const FString& NameOrPath);
UMaterialInstanceConstant* LoadMaterialInstanceByName(const FString& NameOrPath, FString& OutError); UMaterialInstanceConstant* LoadMaterialInstanceByName(const FString& NameOrPath, FString& OutError);
FAssetData* FindMaterialFunctionAsset(const FString& NameOrPath); FAssetData* FindMaterialFunctionAsset(const FString& NameOrPath);
UMaterialFunction* LoadMaterialFunctionByName(const FString& NameOrPath, FString& OutError); 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 ----- // ----- Snapshot storage -----
TMap<FString, FGraphSnapshot> Snapshots; TMap<FString, FGraphSnapshot> Snapshots;
@@ -309,5 +281,5 @@ public:
bool LoadSnapshotFromDisk(const FString& SnapshotId, FGraphSnapshot& OutSnapshot); 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; using MCPHelper = FBlueprintMCPServer;

View File

@@ -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<FJsonObject> JsonObj);
static TSharedPtr<FJsonObject> 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<FJsonObject> SerializeBlueprint(UBlueprint* BP);
static TSharedPtr<FJsonObject> SerializeGraph(UEdGraph* Graph);
static TSharedPtr<FJsonObject> SerializeNode(UEdGraphNode* Node);
static TSharedPtr<FJsonObject> SerializePin(UEdGraphPin* Pin);
static TSharedPtr<FJsonObject> 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<UBlueprintNodeSpawner*> AllNodeSpawners();
static TArray<UBlueprintNodeSpawner*> SearchNodeSpawners(const FString& Query, int32 MaxResults = 0, bool ExactMatch = false);
// ----- Property population -----
static FString PopulateFromJson(UStruct* StructType, void* Container, const TSharedPtr<FJsonValue>& JsonValue);
static FString PopulateFromJson(UStruct* StructType, void* Container, const FJsonObject* Json);
};