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

View File

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

View File

@@ -1,5 +1,6 @@
#include "BlueprintMCPHandlers_DiffBlueprints.h"
#include "BlueprintMCPServer.h"
#include "MCPUtils.h"
#include "Engine/Blueprint.h"
#include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphNode.h"
@@ -12,10 +13,10 @@ void UMCPHandler_DiffBlueprints::Handle(const FJsonObject* Json, FJsonObject* Re
// Load both blueprints
FString LoadErrorA, LoadErrorB;
UBlueprint* BPA = Helper->LoadBlueprintByName(BlueprintA, LoadErrorA);
if (!BPA) { Helper->MakeErrorJson(Result, FString::Printf(TEXT("blueprintA: %s"), *LoadErrorA)); return; }
if (!BPA) { MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("blueprintA: %s"), *LoadErrorA)); return; }
UBlueprint* BPB = Helper->LoadBlueprintByName(BlueprintB, LoadErrorB);
if (!BPB) { Helper->MakeErrorJson(Result, FString::Printf(TEXT("blueprintB: %s"), *LoadErrorB)); return; }
if (!BPB) { MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("blueprintB: %s"), *LoadErrorB)); return; }
// Helper to gather graphs from a Blueprint
auto GatherGraphs = [this](UBlueprint* BP) -> TArray<UEdGraph*>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,5 @@
#include "BlueprintMCPServer.h"
#include "MCPUtils.h"
#include "Materials/Material.h"
#include "Materials/MaterialInstanceConstant.h"
#include "Materials/MaterialFunction.h"
@@ -108,10 +109,10 @@ void FBlueprintMCPServer::HandleGetMaterial(const FJsonObject* Json, FJsonObject
FString Name = Json->GetStringField(TEXT("name"));
if (Name.IsEmpty())
{
return MakeErrorJson(Result, TEXT("Missing 'name' parameter"));
return MCPUtils::MakeErrorJson(Result, TEXT("Missing 'name' parameter"));
}
FString DecodedName = UrlDecode(Name);
FString DecodedName = MCPUtils::UrlDecode(Name);
// Try loading as UMaterial first
FString LoadError;
@@ -342,7 +343,7 @@ void FBlueprintMCPServer::HandleGetMaterial(const FJsonObject* Json, FJsonObject
return;
}
MakeErrorJson(Result, FString::Printf(TEXT("Material or MaterialInstance '%s' not found. Use list_materials to see available assets."), *DecodedName));
MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Material or MaterialInstance '%s' not found. Use list_materials to see available assets."), *DecodedName));
}
// ============================================================
@@ -354,16 +355,16 @@ void FBlueprintMCPServer::HandleGetMaterialGraph(const FJsonObject* Json, FJsonO
FString Name = Json->GetStringField(TEXT("name"));
if (Name.IsEmpty())
{
return MakeErrorJson(Result, TEXT("Missing 'name' parameter"));
return MCPUtils::MakeErrorJson(Result, TEXT("Missing 'name' parameter"));
}
FString DecodedName = UrlDecode(Name);
FString DecodedName = MCPUtils::UrlDecode(Name);
FString LoadError;
UMaterial* Material = LoadMaterialByName(DecodedName, LoadError);
if (!Material)
{
return MakeErrorJson(Result, LoadError);
return MCPUtils::MakeErrorJson(Result, LoadError);
}
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: GetMaterialGraph — material '%s'"), *Material->GetName());
@@ -381,16 +382,16 @@ void FBlueprintMCPServer::HandleGetMaterialGraph(const FJsonObject* Json, FJsonO
if (!Material->MaterialGraph)
{
return MakeErrorJson(Result, TEXT("Could not build MaterialGraph for this material"));
return MCPUtils::MakeErrorJson(Result, TEXT("Could not build MaterialGraph for this material"));
}
TSharedPtr<FJsonObject> GraphJson = SerializeGraph(Material->MaterialGraph);
TSharedPtr<FJsonObject> GraphJson = MCPUtils::SerializeGraph(Material->MaterialGraph);
if (!GraphJson.IsValid())
{
return MakeErrorJson(Result, TEXT("Failed to serialize material graph"));
return MCPUtils::MakeErrorJson(Result, TEXT("Failed to serialize material graph"));
}
CopyJsonFields(GraphJson.Get(), Result);
MCPUtils::CopyJsonFields(GraphJson.Get(), Result);
// Add material name context
Result->SetStringField(TEXT("material"), Material->GetName());
@@ -406,14 +407,14 @@ void FBlueprintMCPServer::HandleDescribeMaterial(const FJsonObject* Json, FJsonO
FString MaterialName = Json->GetStringField(TEXT("material"));
if (MaterialName.IsEmpty())
{
return MakeErrorJson(Result, TEXT("Missing required field: material"));
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: material"));
}
FString LoadError;
UMaterial* Material = LoadMaterialByName(MaterialName, LoadError);
if (!Material)
{
return MakeErrorJson(Result, LoadError);
return MCPUtils::MakeErrorJson(Result, LoadError);
}
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: DescribeMaterial — '%s'"), *Material->GetName());
@@ -429,7 +430,7 @@ void FBlueprintMCPServer::HandleDescribeMaterial(const FJsonObject* Json, FJsonO
if (!Material->MaterialGraph)
{
return MakeErrorJson(Result, TEXT("Could not build MaterialGraph for this material"));
return MCPUtils::MakeErrorJson(Result, TEXT("Could not build MaterialGraph for this material"));
}
// Recursive helper: trace backwards from a pin and build a description string
@@ -549,7 +550,7 @@ void FBlueprintMCPServer::HandleDescribeMaterial(const FJsonObject* Json, FJsonO
if (!RootNode)
{
return MakeErrorJson(Result, TEXT("Could not find root node in material graph"));
return MCPUtils::MakeErrorJson(Result, TEXT("Could not find root node in material graph"));
}
for (UEdGraphPin* Pin : RootNode->Pins)
@@ -609,10 +610,10 @@ void FBlueprintMCPServer::HandleSearchMaterials(const FJsonObject* Json, FJsonOb
FString Query = Json->GetStringField(TEXT("query"));
if (Query.IsEmpty())
{
return MakeErrorJson(Result, TEXT("Missing 'query' parameter"));
return MCPUtils::MakeErrorJson(Result, TEXT("Missing 'query' parameter"));
}
FString DecodedQuery = UrlDecode(Query);
FString DecodedQuery = MCPUtils::UrlDecode(Query);
int32 MaxResults = 50;
if (Json->HasField(TEXT("maxResults")))
@@ -699,7 +700,7 @@ void FBlueprintMCPServer::HandleFindMaterialReferences(const FJsonObject* Json,
FString MaterialName = Json->GetStringField(TEXT("material"));
if (MaterialName.IsEmpty())
{
return MakeErrorJson(Result, TEXT("Missing required field: material"));
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: material"));
}
// Try to find the material's package path
@@ -721,7 +722,7 @@ void FBlueprintMCPServer::HandleFindMaterialReferences(const FJsonObject* Json,
if (PackagePath.IsEmpty())
{
return MakeErrorJson(Result, FString::Printf(TEXT("Material '%s' not found. Use list_materials to see available assets."), *MaterialName));
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Material '%s' not found. Use list_materials to see available assets."), *MaterialName));
}
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: FindMaterialReferences — '%s' (package: %s)"), *MaterialName, *PackagePath);
@@ -791,16 +792,16 @@ void FBlueprintMCPServer::HandleGetMaterialFunction(const FJsonObject* Json, FJs
FString Name = Json->GetStringField(TEXT("name"));
if (Name.IsEmpty())
{
return MakeErrorJson(Result, TEXT("Missing 'name' parameter"));
return MCPUtils::MakeErrorJson(Result, TEXT("Missing 'name' parameter"));
}
FString DecodedName = UrlDecode(Name);
FString DecodedName = MCPUtils::UrlDecode(Name);
FString LoadError;
UMaterialFunction* MF = LoadMaterialFunctionByName(DecodedName, LoadError);
if (!MF)
{
return MakeErrorJson(Result, LoadError);
return MCPUtils::MakeErrorJson(Result, LoadError);
}
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: GetMaterialFunction — '%s'"), *MF->GetName());
@@ -843,7 +844,7 @@ void FBlueprintMCPServer::HandleGetMaterialFunction(const FJsonObject* Json, FJs
}
// Serialize every expression
TSharedPtr<FJsonObject> ExprJson = SerializeMaterialExpression(Expr);
TSharedPtr<FJsonObject> ExprJson = MCPUtils::SerializeMaterialExpression(Expr);
if (ExprJson.IsValid())
{
ExpressionList.Add(MakeShared<FJsonValueObject>(ExprJson.ToSharedRef()));
@@ -859,7 +860,7 @@ void FBlueprintMCPServer::HandleGetMaterialFunction(const FJsonObject* Json, FJs
UEdGraph* FuncGraph = MF->MaterialGraph;
if (FuncGraph)
{
TSharedPtr<FJsonObject> GraphJson = SerializeGraph(FuncGraph);
TSharedPtr<FJsonObject> GraphJson = MCPUtils::SerializeGraph(FuncGraph);
if (GraphJson.IsValid())
{
Result->SetObjectField(TEXT("graph"), GraphJson);
@@ -876,7 +877,7 @@ void FBlueprintMCPServer::HandleValidateMaterial(const FJsonObject* Json, FJsonO
FString MaterialName = Json->GetStringField(TEXT("material"));
if (MaterialName.IsEmpty())
{
return MakeErrorJson(Result, TEXT("Missing required field: material"));
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: material"));
}
// Load material
@@ -884,7 +885,7 @@ void FBlueprintMCPServer::HandleValidateMaterial(const FJsonObject* Json, FJsonO
UMaterial* Material = LoadMaterialByName(MaterialName, LoadError);
if (!Material)
{
return MakeErrorJson(Result, LoadError);
return MCPUtils::MakeErrorJson(Result, LoadError);
}
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Validating material '%s'"), *Material->GetName());

View File

@@ -1,5 +1,6 @@
#include "BlueprintMCPHandlers_Mutation.h"
#include "BlueprintMCPServer.h"
#include "MCPUtils.h"
#include "Engine/Blueprint.h"
#include "Materials/Material.h"
#include "Materials/MaterialInstanceConstant.h"
@@ -42,7 +43,6 @@
#include "AssetRegistry/IAssetRegistry.h"
#include "AssetToolsModule.h"
#include "IAssetTools.h"
#include "BlueprintActionDatabase.h"
#include "BlueprintNodeSpawner.h"
@@ -59,7 +59,7 @@ void UMCPHandler_ReplaceFunctionCalls::Handle(const FJsonObject* Json, FJsonObje
UBlueprint* BP = Helper->LoadBlueprintByName(Blueprint, LoadError);
if (!BP)
{
return Helper->MakeErrorJson(Result, LoadError);
return MCPUtils::MakeErrorJson(Result, LoadError);
}
// Find the new class — try several search strategies
@@ -93,7 +93,7 @@ void UMCPHandler_ReplaceFunctionCalls::Handle(const FJsonObject* Json, FJsonObje
if (!NewClassPtr)
{
return Helper->MakeErrorJson(Result, FString::Printf(TEXT("Could not find class '%s'"), *NewClass));
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Could not find class '%s'"), *NewClass));
}
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: %s function calls in '%s': %s -> %s (%s)"),
@@ -298,7 +298,7 @@ void UMCPHandler_DeleteAsset::Handle(const FJsonObject* Json, FJsonObject* Resul
if (!IFileManager::Get().FileExists(*PackageFilename))
{
return Helper->MakeErrorJson(Result, FString::Printf(TEXT("Asset file not found on disk: %s"), *PackageFilename));
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Asset file not found on disk: %s"), *PackageFilename));
}
// Check references
@@ -330,7 +330,7 @@ void UMCPHandler_DeleteAsset::Handle(const FJsonObject* Json, FJsonObject* Resul
}
}
Helper->MakeErrorJson(Result, TEXT("Asset is still referenced. Remove all references first."));
MCPUtils::MakeErrorJson(Result, TEXT("Asset is still referenced. Remove all references first."));
Result->SetStringField(TEXT("assetPath"), AssetPath);
Result->SetNumberField(TEXT("referencerCount"), Referencers.Num());
Result->SetNumberField(TEXT("liveReferencerCount"), LiveRefs.Num());
@@ -415,7 +415,7 @@ void UMCPHandler_DeleteAsset::Handle(const FJsonObject* Json, FJsonObject* Resul
Result->SetBoolField(TEXT("forced"), Force);
if (!bDeleted)
{
Helper->MakeErrorJson(Result, TEXT("Failed to delete file from disk"));
MCPUtils::MakeErrorJson(Result, TEXT("Failed to delete file from disk"));
}
if (RefWarnings.Num() > 0)
{
@@ -437,7 +437,7 @@ void UMCPHandler_ConnectPins::Handle(const FJsonObject* Json, FJsonObject* Resul
UBlueprint* BP = Helper->LoadBlueprintByName(Blueprint, LoadError);
if (!BP)
{
return Helper->MakeErrorJson(Result, LoadError);
return MCPUtils::MakeErrorJson(Result, LoadError);
}
TArray<TSharedPtr<FJsonValue>> Results;
@@ -449,7 +449,7 @@ void UMCPHandler_ConnectPins::Handle(const FJsonObject* Json, FJsonObject* Resul
Results.Add(MakeShared<FJsonValueObject>(EntryResult));
FConnectPinsEntry Entry;
FString PopulateError = Helper->PopulateFromJson(FConnectPinsEntry::StaticStruct(), &Entry, ConnVal);
FString PopulateError = MCPUtils::PopulateFromJson(FConnectPinsEntry::StaticStruct(), &Entry, ConnVal);
if (!PopulateError.IsEmpty())
{
EntryResult->SetStringField(TEXT("error"), PopulateError);
@@ -462,14 +462,14 @@ void UMCPHandler_ConnectPins::Handle(const FJsonObject* Json, FJsonObject* Resul
EntryResult->SetStringField(TEXT("targetPinName"), Entry.TargetPinName);
UEdGraph* SourceGraph = nullptr;
UEdGraphNode* SourceNode = Helper->FindNodeByGuid(BP, Entry.SourceNodeId, &SourceGraph);
UEdGraphNode* SourceNode = MCPUtils::FindNodeByGuid(BP, Entry.SourceNodeId, &SourceGraph);
if (!SourceNode)
{
EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Source node '%s' not found"), *Entry.SourceNodeId));
continue;
}
UEdGraphNode* TargetNode = Helper->FindNodeByGuid(BP, Entry.TargetNodeId);
UEdGraphNode* TargetNode = MCPUtils::FindNodeByGuid(BP, Entry.TargetNodeId);
if (!TargetNode)
{
EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Target node '%s' not found"), *Entry.TargetNodeId));
@@ -540,7 +540,7 @@ void UMCPHandler_DisconnectPin::Handle(const FJsonObject* Json, FJsonObject* Res
UBlueprint* BP = Helper->LoadBlueprintByName(Blueprint, LoadError);
if (!BP)
{
return Helper->MakeErrorJson(Result, LoadError);
return MCPUtils::MakeErrorJson(Result, LoadError);
}
TArray<TSharedPtr<FJsonValue>> Results;
@@ -553,7 +553,7 @@ void UMCPHandler_DisconnectPin::Handle(const FJsonObject* Json, FJsonObject* Res
Results.Add(MakeShared<FJsonValueObject>(EntryResult));
FDisconnectPinEntry Entry;
FString PopulateError = Helper->PopulateFromJson(FDisconnectPinEntry::StaticStruct(), &Entry, DiscVal);
FString PopulateError = MCPUtils::PopulateFromJson(FDisconnectPinEntry::StaticStruct(), &Entry, DiscVal);
if (!PopulateError.IsEmpty())
{
EntryResult->SetStringField(TEXT("error"), PopulateError);
@@ -563,7 +563,7 @@ void UMCPHandler_DisconnectPin::Handle(const FJsonObject* Json, FJsonObject* Res
EntryResult->SetStringField(TEXT("nodeId"), Entry.NodeId);
EntryResult->SetStringField(TEXT("pinName"), Entry.PinName);
UEdGraphNode* Node = Helper->FindNodeByGuid(BP, Entry.NodeId);
UEdGraphNode* Node = MCPUtils::FindNodeByGuid(BP, Entry.NodeId);
if (!Node)
{
EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Node '%s' not found"), *Entry.NodeId));
@@ -581,7 +581,7 @@ void UMCPHandler_DisconnectPin::Handle(const FJsonObject* Json, FJsonObject* Res
if (!Entry.TargetNodeId.IsEmpty() && !Entry.TargetPinName.IsEmpty())
{
UEdGraphNode* TargetNode = Helper->FindNodeByGuid(BP, Entry.TargetNodeId);
UEdGraphNode* TargetNode = MCPUtils::FindNodeByGuid(BP, Entry.TargetNodeId);
if (!TargetNode)
{
EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Target node '%s' not found"), *Entry.TargetNodeId));
@@ -648,7 +648,7 @@ void UMCPHandler_RefreshAllNodes::Handle(const FJsonObject* Json, FJsonObject* R
UBlueprint* BP = Helper->LoadBlueprintByName(Blueprint, LoadError);
if (!BP)
{
return Helper->MakeErrorJson(Result, LoadError);
return MCPUtils::MakeErrorJson(Result, LoadError);
}
// Count graphs and nodes before refresh
@@ -761,7 +761,7 @@ void UMCPHandler_SetPinDefault::Handle(const FJsonObject* Json, FJsonObject* Res
Results.Add(MakeShared<FJsonValueObject>(EntryResult));
FSetPinDefaultEntry Entry;
FString PopulateError = Helper->PopulateFromJson(FSetPinDefaultEntry::StaticStruct(), &Entry, PinVal);
FString PopulateError = MCPUtils::PopulateFromJson(FSetPinDefaultEntry::StaticStruct(), &Entry, PinVal);
if (!PopulateError.IsEmpty())
{
EntryResult->SetStringField(TEXT("error"), PopulateError);
@@ -781,7 +781,7 @@ void UMCPHandler_SetPinDefault::Handle(const FJsonObject* Json, FJsonObject* Res
}
UEdGraph* Graph = nullptr;
UEdGraphNode* Node = Helper->FindNodeByGuid(BP, Entry.NodeId, &Graph);
UEdGraphNode* Node = MCPUtils::FindNodeByGuid(BP, Entry.NodeId, &Graph);
if (!Node)
{
EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Node '%s' not found"), *Entry.NodeId));
@@ -856,15 +856,15 @@ void UMCPHandler_ChangeStructNodeType::Handle(const FJsonObject* Json, FJsonObje
UBlueprint* BP = Helper->LoadBlueprintByName(Blueprint, LoadError);
if (!BP)
{
return Helper->MakeErrorJson(Result, LoadError);
return MCPUtils::MakeErrorJson(Result, LoadError);
}
// Find node
UEdGraph* Graph = nullptr;
UEdGraphNode* Node = Helper->FindNodeByGuid(BP, NodeId, &Graph);
UEdGraphNode* Node = MCPUtils::FindNodeByGuid(BP, NodeId, &Graph);
if (!Node)
{
return Helper->MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found"), *NodeId));
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found"), *NodeId));
}
// Determine what kind of struct node this is
@@ -873,7 +873,7 @@ void UMCPHandler_ChangeStructNodeType::Handle(const FJsonObject* Json, FJsonObje
if (!BreakNode && !MakeNode)
{
return Helper->MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' is not a BreakStruct or MakeStruct node (class: %s)"),
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' is not a BreakStruct or MakeStruct node (class: %s)"),
*NodeId, *Node->GetClass()->GetName()));
}
@@ -892,7 +892,7 @@ void UMCPHandler_ChangeStructNodeType::Handle(const FJsonObject* Json, FJsonObje
}
if (!NewStruct)
{
return Helper->MakeErrorJson(Result, FString::Printf(TEXT("Struct type '%s' not found"), *NewType));
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Struct type '%s' not found"), *NewType));
}
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Changing struct node '%s' to type '%s'"),
@@ -962,7 +962,7 @@ void UMCPHandler_ChangeStructNodeType::Handle(const FJsonObject* Json, FJsonObje
const UEdGraphSchema* Schema = Graph->GetSchema();
if (!Schema)
{
return Helper->MakeErrorJson(Result, TEXT("Graph schema not found"));
return MCPUtils::MakeErrorJson(Result, TEXT("Graph schema not found"));
}
// Reconstruct to rebuild pins for the new struct type (use schema version for MinimalAPI compat)
@@ -1039,7 +1039,7 @@ void UMCPHandler_ChangeStructNodeType::Handle(const FJsonObject* Json, FJsonObje
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
// Return updated node state
TSharedPtr<FJsonObject> UpdatedNodeState = Helper->SerializeNode(Node);
TSharedPtr<FJsonObject> UpdatedNodeState = MCPUtils::SerializeNode(Node);
Result->SetBoolField(TEXT("success"), true);
Result->SetStringField(TEXT("blueprint"), Blueprint);
@@ -1069,18 +1069,18 @@ void UMCPHandler_DeleteNode::Handle(const FJsonObject* Json, FJsonObject* Result
UBlueprint* BP = Helper->LoadBlueprintByName(Blueprint, LoadError);
if (!BP)
{
return Helper->MakeErrorJson(Result, LoadError);
return MCPUtils::MakeErrorJson(Result, LoadError);
}
UEdGraph* Graph = nullptr;
UEdGraphNode* Node = Helper->FindNodeByGuid(BP, NodeId, &Graph);
UEdGraphNode* Node = MCPUtils::FindNodeByGuid(BP, NodeId, &Graph);
if (!Node)
{
return Helper->MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found"), *NodeId));
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found"), *NodeId));
}
if (!Graph)
{
return Helper->MakeErrorJson(Result, FString::Printf(TEXT("Graph not found for node '%s'"), *NodeId));
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Graph not found for node '%s'"), *NodeId));
}
FString NodeClass = Node->GetClass()->GetName();
@@ -1092,7 +1092,7 @@ void UMCPHandler_DeleteNode::Handle(const FJsonObject* Json, FJsonObject* Result
// without recreating the entire function/event.
if (Cast<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("This is the root node of the function — removing it would leave an empty, uncompilable graph. ")
TEXT("To remove the entire function, delete it from the Blueprint editor."),
@@ -1100,14 +1100,14 @@ void UMCPHandler_DeleteNode::Handle(const FJsonObject* Json, FJsonObject* Result
}
if (Cast<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("This is the root node of the event handler — removing it would leave an empty, uncompilable graph."),
*NodeTitle, *GraphName));
}
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("This is the root node of the custom event — removing it would leave an empty, uncompilable graph."),
*NodeTitle, *GraphName));
@@ -1154,13 +1154,13 @@ void UMCPHandler_RenameAsset::Handle(const FJsonObject* Json, FJsonObject* Resul
FAssetData* FoundAsset = Helper->FindAnyAsset(AssetPath);
if (!FoundAsset)
{
return Helper->MakeErrorJson(Result, FString::Printf(TEXT("Asset '%s' not found. Checked Blueprints, Materials, Material Instances, and Material Functions."), *AssetPath));
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Asset '%s' not found. Checked Blueprints, Materials, Material Instances, and Material Functions."), *AssetPath));
}
UObject* AssetObj = FoundAsset->GetAsset();
if (!AssetObj)
{
return Helper->MakeErrorJson(Result, FString::Printf(TEXT("Failed to load asset '%s'"), *AssetPath));
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Failed to load asset '%s'"), *AssetPath));
}
// Parse new path into package path and asset name
@@ -1211,7 +1211,7 @@ void UMCPHandler_RenameAsset::Handle(const FJsonObject* Json, FJsonObject* Resul
Result->SetStringField(TEXT("newAssetName"), NewAssetName);
if (!bSuccess)
{
Helper->MakeErrorJson(Result, TEXT("Asset rename failed. The target path may be invalid or a conflicting asset may exist."));
MCPUtils::MakeErrorJson(Result, TEXT("Asset rename failed. The target path may be invalid or a conflicting asset may exist."));
}
}
@@ -1228,24 +1228,24 @@ void UMCPHandler_SetBlueprintDefault::Handle(const FJsonObject* Json, FJsonObjec
UBlueprint* BP = Helper->LoadBlueprintByName(Blueprint, LoadError);
if (!BP)
{
return Helper->MakeErrorJson(Result, LoadError);
return MCPUtils::MakeErrorJson(Result, LoadError);
}
if (!BP->GeneratedClass)
{
return Helper->MakeErrorJson(Result, TEXT("Blueprint has no GeneratedClass"));
return MCPUtils::MakeErrorJson(Result, TEXT("Blueprint has no GeneratedClass"));
}
UObject* CDO = BP->GeneratedClass->GetDefaultObject();
if (!CDO)
{
return Helper->MakeErrorJson(Result, TEXT("Could not get Class Default Object"));
return MCPUtils::MakeErrorJson(Result, TEXT("Could not get Class Default Object"));
}
FProperty* Prop = BP->GeneratedClass->FindPropertyByName(*Property);
if (!Prop)
{
return Helper->MakeErrorJson(Result, FString::Printf(TEXT("Property '%s' not found on '%s'"), *Property, *Blueprint));
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Property '%s' not found on '%s'"), *Property, *Blueprint));
}
FString OldValue;
@@ -1286,7 +1286,7 @@ void UMCPHandler_SetBlueprintDefault::Handle(const FJsonObject* Json, FJsonObjec
if (!ResolvedClass)
{
return Helper->MakeErrorJson(Result, FString::Printf(TEXT("Could not resolve '%s' to a class"), *Value));
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Could not resolve '%s' to a class"), *Value));
}
// Validate meta class compatibility
@@ -1295,7 +1295,7 @@ void UMCPHandler_SetBlueprintDefault::Handle(const FJsonObject* Json, FJsonObjec
UClass* MetaClass = ClassProp->MetaClass;
if (MetaClass && !ResolvedClass->IsChildOf(MetaClass))
{
return Helper->MakeErrorJson(Result, FString::Printf(
return MCPUtils::MakeErrorJson(Result, FString::Printf(
TEXT("'%s' is not a subclass of '%s' (required by property '%s')"),
*ResolvedClass->GetName(), *MetaClass->GetName(), *Property));
}
@@ -1325,7 +1325,7 @@ void UMCPHandler_SetBlueprintDefault::Handle(const FJsonObject* Json, FJsonObjec
if (!ResolvedObj)
{
return Helper->MakeErrorJson(Result, FString::Printf(TEXT("Could not resolve '%s' to an object"), *Value));
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Could not resolve '%s' to an object"), *Value));
}
ObjProp->SetPropertyValue_InContainer(CDO, ResolvedObj);
@@ -1343,7 +1343,7 @@ void UMCPHandler_SetBlueprintDefault::Handle(const FJsonObject* Json, FJsonObjec
}
else
{
return Helper->MakeErrorJson(Result, FString::Printf(
return MCPUtils::MakeErrorJson(Result, FString::Printf(
TEXT("Failed to set property '%s' to '%s' — value could not be parsed for type '%s'"),
*Property, *Value, *Prop->GetCPPType()));
}
@@ -1351,7 +1351,7 @@ void UMCPHandler_SetBlueprintDefault::Handle(const FJsonObject* Json, FJsonObjec
if (!bSuccess)
{
return Helper->MakeErrorJson(Result, TEXT("Failed to set property value"));
return MCPUtils::MakeErrorJson(Result, TEXT("Failed to set property value"));
}
// Mark modified and save
@@ -1359,7 +1359,7 @@ void UMCPHandler_SetBlueprintDefault::Handle(const FJsonObject* Json, FJsonObjec
BP->Modify();
FKismetEditorUtilities::CompileBlueprint(BP);
bool bSaved = Helper->SaveBlueprintPackage(BP);
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Set '%s.%s' from '%s' to '%s' (saved: %s)"),
*Blueprint, *Property, *OldValue, *ActualNewValue, bSaved ? TEXT("true") : TEXT("false"));
@@ -1385,7 +1385,7 @@ void UMCPHandler_MoveNode::Handle(const FJsonObject* Json, FJsonObject* Result)
UBlueprint* BP = Helper->LoadBlueprintByName(Blueprint, LoadError);
if (!BP)
{
return Helper->MakeErrorJson(Result, LoadError);
return MCPUtils::MakeErrorJson(Result, LoadError);
}
TArray<TSharedPtr<FJsonValue>> Results;
@@ -1397,7 +1397,7 @@ void UMCPHandler_MoveNode::Handle(const FJsonObject* Json, FJsonObject* Result)
Results.Add(MakeShared<FJsonValueObject>(EntryResult));
FMoveNodeEntry Entry;
FString PopulateError = Helper->PopulateFromJson(FMoveNodeEntry::StaticStruct(), &Entry, NodeVal);
FString PopulateError = MCPUtils::PopulateFromJson(FMoveNodeEntry::StaticStruct(), &Entry, NodeVal);
if (!PopulateError.IsEmpty())
{
EntryResult->SetStringField(TEXT("error"), PopulateError);
@@ -1406,7 +1406,7 @@ void UMCPHandler_MoveNode::Handle(const FJsonObject* Json, FJsonObject* Result)
EntryResult->SetStringField(TEXT("nodeId"), Entry.NodeId);
UEdGraphNode* Node = Helper->FindNodeByGuid(BP, Entry.NodeId);
UEdGraphNode* Node = MCPUtils::FindNodeByGuid(BP, Entry.NodeId);
if (!Node)
{
EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Node '%s' not found"), *Entry.NodeId));
@@ -1449,11 +1449,11 @@ void UMCPHandler_DuplicateNodes::Handle(const FJsonObject* Json, FJsonObject* Re
UBlueprint* BP = Helper->LoadBlueprintByName(Blueprint, LoadError);
if (!BP)
{
return Helper->MakeErrorJson(Result, LoadError);
return MCPUtils::MakeErrorJson(Result, LoadError);
}
// Find the target graph
FString DecodedGraphName = MCPHelper::UrlDecode(Graph);
FString DecodedGraphName = MCPUtils::UrlDecode(Graph);
UEdGraph* TargetGraph = nullptr;
TArray<UEdGraph*> AllGraphs;
BP->GetAllGraphs(AllGraphs);
@@ -1469,12 +1469,12 @@ void UMCPHandler_DuplicateNodes::Handle(const FJsonObject* Json, FJsonObject* Re
if (!TargetGraph)
{
return Helper->MakeErrorJson(Result, FString::Printf(TEXT("Graph '%s' not found"), *DecodedGraphName));
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Graph '%s' not found"), *DecodedGraphName));
}
if (NodeIds.Array.Num() == 0)
{
return Helper->MakeErrorJson(Result, TEXT("nodeIds array is empty"));
return MCPUtils::MakeErrorJson(Result, TEXT("nodeIds array is empty"));
}
// Find all source nodes
@@ -1484,7 +1484,7 @@ void UMCPHandler_DuplicateNodes::Handle(const FJsonObject* Json, FJsonObject* Re
for (const TSharedPtr<FJsonValue>& IdVal : NodeIds.Array)
{
FString NodeId = IdVal->AsString();
UEdGraphNode* Node = Helper->FindNodeByGuid(BP, NodeId);
UEdGraphNode* Node = MCPUtils::FindNodeByGuid(BP, NodeId);
if (Node)
{
if (Node->GetGraph() == TargetGraph)
@@ -1504,7 +1504,7 @@ void UMCPHandler_DuplicateNodes::Handle(const FJsonObject* Json, FJsonObject* Re
if (SourceNodes.Num() == 0)
{
return Helper->MakeErrorJson(Result, TEXT("No valid nodes found to duplicate"));
return MCPUtils::MakeErrorJson(Result, TEXT("No valid nodes found to duplicate"));
}
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Duplicating %d node(s) in graph '%s' of '%s'"),
@@ -1588,13 +1588,13 @@ void UMCPHandler_GetNodeComment::Handle(const FJsonObject* Json, FJsonObject* Re
UBlueprint* BP = Helper->LoadBlueprintByName(Blueprint, LoadError);
if (!BP)
{
return Helper->MakeErrorJson(Result, LoadError);
return MCPUtils::MakeErrorJson(Result, LoadError);
}
UEdGraphNode* Node = Helper->FindNodeByGuid(BP, NodeId);
UEdGraphNode* Node = MCPUtils::FindNodeByGuid(BP, NodeId);
if (!Node)
{
return Helper->MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found"), *NodeId));
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found"), *NodeId));
}
Result->SetBoolField(TEXT("success"), true);
@@ -1618,13 +1618,13 @@ void UMCPHandler_SetNodeComment::Handle(const FJsonObject* Json, FJsonObject* Re
UBlueprint* BP = Helper->LoadBlueprintByName(Blueprint, LoadError);
if (!BP)
{
return Helper->MakeErrorJson(Result, LoadError);
return MCPUtils::MakeErrorJson(Result, LoadError);
}
UEdGraphNode* Node = Helper->FindNodeByGuid(BP, NodeId);
UEdGraphNode* Node = MCPUtils::FindNodeByGuid(BP, NodeId);
if (!Node)
{
return Helper->MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found"), *NodeId));
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found"), *NodeId));
}
FString OldComment = Node->NodeComment;
@@ -1651,81 +1651,6 @@ void UMCPHandler_SetNodeComment::Handle(const FJsonObject* Json, FJsonObject* Re
// ============================================================
// Shared helper: iterate the blueprint action database.
// Full name = "Category|MenuName", e.g. "Luprex|Lua|Read Lua Values".
// Substring search matches against full name and keywords.
// Exact search matches full name exactly (case-insensitive).
// Returns full names only. FindSpawner returns the spawner for an exact match.
// ============================================================
struct FNodeActionSearch
{
static FString MakeFullName(UBlueprintNodeSpawner* Spawner)
{
const FBlueprintActionUiSpec& UiSpec = Spawner->PrimeDefaultUiSpec();
FString Category = UiSpec.Category.ToString();
FString MenuName = UiSpec.MenuName.ToString();
if (Category.IsEmpty())
{
return MenuName;
}
return Category + TEXT("|") + MenuName;
}
static TArray<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
// for spawners matching a query string (same pool as the right-click menu)
@@ -1735,13 +1660,12 @@ void UMCPHandler_SearchNodeTypes::Handle(const FJsonObject* Json, FJsonObject* R
{
int32 ClampedMax = FMath::Clamp(MaxResults, 1, 500);
TArray<FString> FullNames;
FNodeActionSearch::Find(Query, ClampedMax, FullNames);
TArray<UBlueprintNodeSpawner*> Spawners = MCPUtils::SearchNodeSpawners(Query, ClampedMax);
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);
@@ -1763,11 +1687,11 @@ void UMCPHandler_SpawnNode::Handle(const FJsonObject* Json, FJsonObject* Result)
UBlueprint* BP = Helper->LoadBlueprintByName(Blueprint, LoadError);
if (!BP)
{
return Helper->MakeErrorJson(Result, LoadError);
return MCPUtils::MakeErrorJson(Result, LoadError);
}
// Find the target graph
FString DecodedGraphName = MCPHelper::UrlDecode(Graph);
FString DecodedGraphName = MCPUtils::UrlDecode(Graph);
UEdGraph* TargetGraph = nullptr;
TArray<UEdGraph*> 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()));
}
Helper->MakeErrorJson(Result, FString::Printf(TEXT("Graph '%s' not found"), *DecodedGraphName));
MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Graph '%s' not found"), *DecodedGraphName));
Result->SetArrayField(TEXT("availableGraphs"), GraphNames);
return;
}
@@ -1802,7 +1726,7 @@ void UMCPHandler_SpawnNode::Handle(const FJsonObject* Json, FJsonObject* Result)
Results.Add(MakeShared<FJsonValueObject>(EntryResult));
FSpawnNodeEntry Entry;
FString PopulateError = Helper->PopulateFromJson(FSpawnNodeEntry::StaticStruct(), &Entry, NodeVal);
FString PopulateError = MCPUtils::PopulateFromJson(FSpawnNodeEntry::StaticStruct(), &Entry, NodeVal);
if (!PopulateError.IsEmpty())
{
EntryResult->SetStringField(TEXT("error"), PopulateError);
@@ -1812,7 +1736,7 @@ void UMCPHandler_SpawnNode::Handle(const FJsonObject* Json, FJsonObject* Result)
EntryResult->SetStringField(TEXT("actionName"), Entry.ActionName);
// Find the spawner by exact full name
TArray<UBlueprintNodeSpawner*> Matches = FNodeActionSearch::FindSpawner(Entry.ActionName);
TArray<UBlueprintNodeSpawner*> Matches = MCPUtils::SearchNodeSpawners(Entry.ActionName, 0, /*ExactMatch=*/true);
if (Matches.Num() == 0)
{
EntryResult->SetStringField(TEXT("error"), FString::Printf(
@@ -1854,7 +1778,7 @@ void UMCPHandler_SpawnNode::Handle(const FJsonObject* Json, FJsonObject* Result)
*Blueprint);
// Serialize result
TSharedPtr<FJsonObject> NodeState = Helper->SerializeNode(NewNode);
TSharedPtr<FJsonObject> NodeState = MCPUtils::SerializeNode(NewNode);
EntryResult->SetBoolField(TEXT("success"), true);
EntryResult->SetStringField(TEXT("nodeId"), NewNode->NodeGuid.ToString());

View File

@@ -1,4 +1,5 @@
#include "BlueprintMCPServer.h"
#include "MCPUtils.h"
#include "Engine/Blueprint.h"
#include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphPin.h"
@@ -22,7 +23,7 @@ void FBlueprintMCPServer::HandleChangeFunctionParamType(const FJsonObject* Json,
if (BlueprintName.IsEmpty() || FunctionName.IsEmpty() || ParamName.IsEmpty() || NewTypeName.IsEmpty())
{
return MakeErrorJson(Result, TEXT("Missing required fields: blueprint, functionName, paramName, newType"));
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: blueprint, functionName, paramName, newType"));
}
// Load Blueprint
@@ -30,15 +31,15 @@ void FBlueprintMCPServer::HandleChangeFunctionParamType(const FJsonObject* Json,
UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError);
if (!BP)
{
return MakeErrorJson(Result, LoadError);
return MCPUtils::MakeErrorJson(Result, LoadError);
}
// Resolve the new type using the shared resolver (supports primitives, structs, enums, and object references)
FEdGraphPinType NewPinType;
FString TypeError;
if (!ResolveTypeFromString(NewTypeName, NewPinType, TypeError))
if (!MCPUtils::ResolveTypeFromString(NewTypeName, NewPinType, TypeError))
{
return MakeErrorJson(Result, TypeError);
return MCPUtils::MakeErrorJson(Result, TypeError);
}
// Find the entry node: K2Node_FunctionEntry in a function graph,
@@ -111,7 +112,7 @@ void FBlueprintMCPServer::HandleChangeFunctionParamType(const FJsonObject* Json,
}
}
MakeErrorJson(Result, FString::Printf(
MCPUtils::MakeErrorJson(Result, FString::Printf(
TEXT("Function or custom event '%s' not found in Blueprint '%s'"),
*FunctionName, *BlueprintName));
Result->SetArrayField(TEXT("availableFunctionsAndEvents"), Available);
@@ -142,7 +143,7 @@ void FBlueprintMCPServer::HandleChangeFunctionParamType(const FJsonObject* Json,
}
}
MakeErrorJson(Result, FString::Printf(
MCPUtils::MakeErrorJson(Result, FString::Printf(
TEXT("Parameter '%s' not found in %s '%s'"),
*ParamName, *FoundNodeType, *FunctionName));
Result->SetArrayField(TEXT("availableParams"), ParamNames);
@@ -206,13 +207,13 @@ void FBlueprintMCPServer::HandleChangeFunctionParamType(const FJsonObject* Json,
}
// Save
bool bSaved = SaveBlueprintPackage(BP);
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Parameter type changed, save %s"),
bSaved ? TEXT("succeeded") : TEXT("failed"));
// Serialize the updated entry node state
TSharedPtr<FJsonObject> UpdatedNodeState = SerializeNode(EntryNode);
TSharedPtr<FJsonObject> UpdatedNodeState = MCPUtils::SerializeNode(EntryNode);
Result->SetBoolField(TEXT("success"), true);
Result->SetStringField(TEXT("blueprint"), BlueprintName);
@@ -240,7 +241,7 @@ void FBlueprintMCPServer::HandleRemoveFunctionParameter(const FJsonObject* Json,
if (BlueprintName.IsEmpty() || FunctionName.IsEmpty() || ParamName.IsEmpty())
{
return MakeErrorJson(Result, TEXT("Missing required fields: blueprint, functionName, paramName"));
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: blueprint, functionName, paramName"));
}
// Load Blueprint
@@ -248,7 +249,7 @@ void FBlueprintMCPServer::HandleRemoveFunctionParameter(const FJsonObject* Json,
UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError);
if (!BP)
{
return MakeErrorJson(Result, LoadError);
return MCPUtils::MakeErrorJson(Result, LoadError);
}
// Find the entry node
@@ -320,7 +321,7 @@ void FBlueprintMCPServer::HandleRemoveFunctionParameter(const FJsonObject* Json,
}
}
MakeErrorJson(Result, FString::Printf(
MCPUtils::MakeErrorJson(Result, FString::Printf(
TEXT("Function or custom event '%s' not found in Blueprint '%s'"),
*FunctionName, *BlueprintName));
Result->SetArrayField(TEXT("availableFunctionsAndEvents"), Available);
@@ -351,7 +352,7 @@ void FBlueprintMCPServer::HandleRemoveFunctionParameter(const FJsonObject* Json,
}
}
MakeErrorJson(Result, FString::Printf(
MCPUtils::MakeErrorJson(Result, FString::Printf(
TEXT("Parameter '%s' not found in %s '%s'"),
*ParamName, *FoundNodeType, *FunctionName));
Result->SetArrayField(TEXT("availableParams"), ParamNames);
@@ -374,7 +375,7 @@ void FBlueprintMCPServer::HandleRemoveFunctionParameter(const FJsonObject* Json,
}
// Save
bool bSaved = SaveBlueprintPackage(BP);
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Parameter removed, save %s"),
bSaved ? TEXT("succeeded") : TEXT("failed"));
@@ -401,7 +402,7 @@ void FBlueprintMCPServer::HandleAddFunctionParameter(const FJsonObject* Json, FJ
if (BlueprintName.IsEmpty() || FunctionName.IsEmpty() || ParamName.IsEmpty() || ParamType.IsEmpty())
{
return MakeErrorJson(Result, TEXT("Missing required fields: blueprint, functionName, paramName, paramType"));
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: blueprint, functionName, paramName, paramType"));
}
// Load Blueprint
@@ -409,15 +410,15 @@ void FBlueprintMCPServer::HandleAddFunctionParameter(const FJsonObject* Json, FJ
UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError);
if (!BP)
{
return MakeErrorJson(Result, LoadError);
return MCPUtils::MakeErrorJson(Result, LoadError);
}
// Resolve param type
FEdGraphPinType PinType;
FString TypeError;
if (!ResolveTypeFromString(ParamType, PinType, TypeError))
if (!MCPUtils::ResolveTypeFromString(ParamType, PinType, TypeError))
{
return MakeErrorJson(Result, TypeError);
return MCPUtils::MakeErrorJson(Result, TypeError);
}
// Find the entry node using 3 strategies
@@ -532,7 +533,7 @@ void FBlueprintMCPServer::HandleAddFunctionParameter(const FJsonObject* Json, FJ
FString::Printf(TEXT("%s (event dispatcher)"), *DN.ToString())));
}
MakeErrorJson(Result, FString::Printf(
MCPUtils::MakeErrorJson(Result, FString::Printf(
TEXT("Function, custom event, or event dispatcher '%s' not found in Blueprint '%s'"),
*FunctionName, *BlueprintName));
Result->SetArrayField(TEXT("availableFunctions"), AvailFuncs);
@@ -544,7 +545,7 @@ void FBlueprintMCPServer::HandleAddFunctionParameter(const FJsonObject* Json, FJ
{
if (Existing.IsValid() && Existing->PinName.ToString().Equals(ParamName, ESearchCase::IgnoreCase))
{
return MakeErrorJson(Result, FString::Printf(
return MCPUtils::MakeErrorJson(Result, FString::Printf(
TEXT("Parameter '%s' already exists on '%s'"), *ParamName, *FunctionName));
}
}
@@ -556,7 +557,7 @@ void FBlueprintMCPServer::HandleAddFunctionParameter(const FJsonObject* Json, FJ
EntryNode->CreateUserDefinedPin(FName(*ParamName), PinType, EGPD_Output);
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
bool bSaved = SaveBlueprintPackage(BP);
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Added parameter '%s' to '%s' in '%s' (saved: %s)"),
*ParamName, *FunctionName, *BlueprintName, bSaved ? TEXT("true") : TEXT("false"));

View File

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

View File

@@ -1,4 +1,5 @@
#include "BlueprintMCPServer.h"
#include "MCPUtils.h"
#include "Engine/Blueprint.h"
#include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphNode.h"
@@ -161,7 +162,7 @@ bool FBlueprintMCPServer::SaveSnapshotToDisk(const FString& SnapshotId, const FG
}
Root->SetObjectField(TEXT("graphs"), GraphsObj);
FString JsonString = JsonToString(Root);
FString JsonString = MCPUtils::JsonToString(Root);
bool bSuccess = FFileHelper::SaveStringToFile(JsonString, *FilePath, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM);
if (bSuccess)
{
@@ -259,7 +260,7 @@ void FBlueprintMCPServer::HandleSnapshotGraph(const FJsonObject* Json, FJsonObje
FString BlueprintName = Json->GetStringField(TEXT("blueprint"));
if (BlueprintName.IsEmpty())
{
return MakeErrorJson(Result, TEXT("Missing required field: blueprint"));
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: blueprint"));
}
FString GraphFilter;
@@ -270,7 +271,7 @@ void FBlueprintMCPServer::HandleSnapshotGraph(const FJsonObject* Json, FJsonObje
UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError);
if (!BP)
{
return MakeErrorJson(Result, LoadError);
return MCPUtils::MakeErrorJson(Result, LoadError);
}
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Creating snapshot for blueprint '%s'"), *BlueprintName);
@@ -299,7 +300,7 @@ void FBlueprintMCPServer::HandleSnapshotGraph(const FJsonObject* Json, FJsonObje
if (GraphsToCapture.Num() == 0 && !GraphFilter.IsEmpty())
{
return MakeErrorJson(Result, FString::Printf(TEXT("Graph '%s' not found in blueprint '%s'"), *GraphFilter, *BlueprintName));
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Graph '%s' not found in blueprint '%s'"), *GraphFilter, *BlueprintName));
}
int32 TotalConnections = 0;
@@ -347,7 +348,7 @@ void FBlueprintMCPServer::HandleDiffGraph(const FJsonObject* Json, FJsonObject*
FString SnapshotId = Json->GetStringField(TEXT("snapshotId"));
if (BlueprintName.IsEmpty() || SnapshotId.IsEmpty())
{
return MakeErrorJson(Result, TEXT("Missing required fields: blueprint, snapshotId"));
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: blueprint, snapshotId"));
}
FString GraphFilter;
@@ -360,7 +361,7 @@ void FBlueprintMCPServer::HandleDiffGraph(const FJsonObject* Json, FJsonObject*
{
if (!LoadSnapshotFromDisk(SnapshotId, LoadedSnapshot))
{
return MakeErrorJson(Result, FString::Printf(TEXT("Snapshot '%s' not found in memory or on disk"), *SnapshotId));
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Snapshot '%s' not found in memory or on disk"), *SnapshotId));
}
SnapshotPtr = &LoadedSnapshot;
}
@@ -370,7 +371,7 @@ void FBlueprintMCPServer::HandleDiffGraph(const FJsonObject* Json, FJsonObject*
UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError);
if (!BP)
{
return MakeErrorJson(Result, LoadError);
return MCPUtils::MakeErrorJson(Result, LoadError);
}
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Diffing blueprint '%s' against snapshot '%s'"), *BlueprintName, *SnapshotId);
@@ -560,7 +561,7 @@ void FBlueprintMCPServer::HandleRestoreGraph(const FJsonObject* Json, FJsonObjec
FString SnapshotId = Json->GetStringField(TEXT("snapshotId"));
if (BlueprintName.IsEmpty() || SnapshotId.IsEmpty())
{
return MakeErrorJson(Result, TEXT("Missing required fields: blueprint, snapshotId"));
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: blueprint, snapshotId"));
}
FString GraphFilter;
@@ -579,7 +580,7 @@ void FBlueprintMCPServer::HandleRestoreGraph(const FJsonObject* Json, FJsonObjec
{
if (!LoadSnapshotFromDisk(SnapshotId, LoadedSnapshot))
{
return MakeErrorJson(Result, FString::Printf(TEXT("Snapshot '%s' not found in memory or on disk"), *SnapshotId));
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Snapshot '%s' not found in memory or on disk"), *SnapshotId));
}
SnapshotPtr = &LoadedSnapshot;
}
@@ -589,7 +590,7 @@ void FBlueprintMCPServer::HandleRestoreGraph(const FJsonObject* Json, FJsonObjec
UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError);
if (!BP)
{
return MakeErrorJson(Result, LoadError);
return MCPUtils::MakeErrorJson(Result, LoadError);
}
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Restoring connections from snapshot '%s' for blueprint '%s' (dryRun=%s)"),
@@ -659,8 +660,8 @@ void FBlueprintMCPServer::HandleRestoreGraph(const FJsonObject* Json, FJsonObjec
// Find source and target nodes
UEdGraph* SourceGraph = nullptr;
UEdGraphNode* SourceNode = FindNodeByGuid(BP, Conn.SourceNodeGuid, &SourceGraph);
UEdGraphNode* TargetNode = FindNodeByGuid(BP, Conn.TargetNodeGuid);
UEdGraphNode* SourceNode = MCPUtils::FindNodeByGuid(BP, Conn.SourceNodeGuid, &SourceGraph);
UEdGraphNode* TargetNode = MCPUtils::FindNodeByGuid(BP, Conn.TargetNodeGuid);
if (!SourceNode)
{
@@ -746,7 +747,7 @@ void FBlueprintMCPServer::HandleRestoreGraph(const FJsonObject* Json, FJsonObjec
bool bSaved = false;
if (!bDryRun && Reconnected > 0)
{
bSaved = SaveBlueprintPackage(BP);
bSaved = MCPUtils::SaveBlueprintPackage(BP);
}
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Restore complete — %d reconnected, %d failed, saved=%s"),
@@ -777,7 +778,7 @@ void FBlueprintMCPServer::HandleFindDisconnectedPins(const FJsonObject* Json, FJ
if (BlueprintName.IsEmpty() && PathFilter.IsEmpty() && SnapshotId.IsEmpty())
{
return MakeErrorJson(Result, TEXT("Provide at least one of: blueprint, filter, or snapshotId"));
return MCPUtils::MakeErrorJson(Result, TEXT("Provide at least one of: blueprint, filter, or snapshotId"));
}
// Optionally load snapshot for definite-break detection
@@ -1074,7 +1075,7 @@ void FBlueprintMCPServer::HandleAnalyzeRebuildImpact(const FJsonObject* Json, FJ
FString ModuleName = Json->GetStringField(TEXT("moduleName"));
if (ModuleName.IsEmpty())
{
return MakeErrorJson(Result, TEXT("Missing required field: moduleName"));
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: moduleName"));
}
// Optional struct name filter

View File

@@ -1,4 +1,5 @@
#include "BlueprintMCPServer.h"
#include "MCPUtils.h"
#include "Engine/UserDefinedStruct.h"
#include "Engine/UserDefinedEnum.h"
#include "Kismet2/BlueprintEditorUtils.h"
@@ -25,7 +26,7 @@ void FBlueprintMCPServer::HandleCreateStruct(const FJsonObject* Json, FJsonObjec
FString AssetPath = Json->GetStringField(TEXT("assetPath"));
if (AssetPath.IsEmpty())
{
return MakeErrorJson(Result, TEXT("Missing required field: assetPath (e.g. '/Game/DataTypes/S_MyStruct')"));
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: assetPath (e.g. '/Game/DataTypes/S_MyStruct')"));
}
// Split path into package path and asset name
@@ -38,12 +39,12 @@ void FBlueprintMCPServer::HandleCreateStruct(const FJsonObject* Json, FJsonObjec
}
else
{
return MakeErrorJson(Result, TEXT("assetPath must be a full path (e.g. '/Game/DataTypes/S_MyStruct')"));
return MCPUtils::MakeErrorJson(Result, TEXT("assetPath must be a full path (e.g. '/Game/DataTypes/S_MyStruct')"));
}
if (AssetName.IsEmpty())
{
return MakeErrorJson(Result, TEXT("Invalid asset name in assetPath"));
return MCPUtils::MakeErrorJson(Result, TEXT("Invalid asset name in assetPath"));
}
// Check if asset already exists
@@ -51,7 +52,7 @@ void FBlueprintMCPServer::HandleCreateStruct(const FJsonObject* Json, FJsonObjec
FAssetData ExistingAsset = ARM.Get().GetAssetByObjectPath(FSoftObjectPath(AssetPath + TEXT(".") + AssetName));
if (ExistingAsset.IsValid())
{
return MakeErrorJson(Result, FString::Printf(TEXT("Asset already exists at '%s'"), *AssetPath));
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Asset already exists at '%s'"), *AssetPath));
}
// Create the struct using the AssetTools factory
@@ -63,13 +64,13 @@ void FBlueprintMCPServer::HandleCreateStruct(const FJsonObject* Json, FJsonObjec
if (!NewAsset)
{
return MakeErrorJson(Result, TEXT("Failed to create UserDefinedStruct asset"));
return MCPUtils::MakeErrorJson(Result, TEXT("Failed to create UserDefinedStruct asset"));
}
UUserDefinedStruct* NewStruct = Cast<UUserDefinedStruct>(NewAsset);
if (!NewStruct)
{
return MakeErrorJson(Result, TEXT("Created asset is not a UserDefinedStruct"));
return MCPUtils::MakeErrorJson(Result, TEXT("Created asset is not a UserDefinedStruct"));
}
// Add properties if specified
@@ -88,7 +89,7 @@ void FBlueprintMCPServer::HandleCreateStruct(const FJsonObject* Json, FJsonObjec
FEdGraphPinType PinType;
FString TypeError;
if (!ResolveTypeFromString(PropType, PinType, TypeError))
if (!MCPUtils::ResolveTypeFromString(PropType, PinType, TypeError))
{
UE_LOG(LogTemp, Warning, TEXT("BlueprintMCP: Could not resolve type '%s' for property '%s': %s"), *PropType, *PropName, *TypeError);
continue;
@@ -149,7 +150,7 @@ void FBlueprintMCPServer::HandleCreateEnum(const FJsonObject* Json, FJsonObject*
FString AssetPath = Json->GetStringField(TEXT("assetPath"));
if (AssetPath.IsEmpty())
{
return MakeErrorJson(Result, TEXT("Missing required field: assetPath (e.g. '/Game/DataTypes/E_MyEnum')"));
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: assetPath (e.g. '/Game/DataTypes/E_MyEnum')"));
}
// Split path
@@ -162,19 +163,19 @@ void FBlueprintMCPServer::HandleCreateEnum(const FJsonObject* Json, FJsonObject*
}
else
{
return MakeErrorJson(Result, TEXT("assetPath must be a full path (e.g. '/Game/DataTypes/E_MyEnum')"));
return MCPUtils::MakeErrorJson(Result, TEXT("assetPath must be a full path (e.g. '/Game/DataTypes/E_MyEnum')"));
}
if (AssetName.IsEmpty())
{
return MakeErrorJson(Result, TEXT("Invalid asset name in assetPath"));
return MCPUtils::MakeErrorJson(Result, TEXT("Invalid asset name in assetPath"));
}
// Get values
const TArray<TSharedPtr<FJsonValue>>* ValuesArray = nullptr;
if (!Json->TryGetArrayField(TEXT("values"), ValuesArray) || !ValuesArray || ValuesArray->Num() == 0)
{
return MakeErrorJson(Result, TEXT("Missing or empty required field: values (array of strings)"));
return MCPUtils::MakeErrorJson(Result, TEXT("Missing or empty required field: values (array of strings)"));
}
TArray<FString> EnumValues;
@@ -185,7 +186,7 @@ void FBlueprintMCPServer::HandleCreateEnum(const FJsonObject* Json, FJsonObject*
}
if (EnumValues.Num() == 0)
{
return MakeErrorJson(Result, TEXT("No valid enum values provided"));
return MCPUtils::MakeErrorJson(Result, TEXT("No valid enum values provided"));
}
// Create the enum using AssetTools
@@ -197,13 +198,13 @@ void FBlueprintMCPServer::HandleCreateEnum(const FJsonObject* Json, FJsonObject*
if (!NewAsset)
{
return MakeErrorJson(Result, TEXT("Failed to create UserDefinedEnum asset"));
return MCPUtils::MakeErrorJson(Result, TEXT("Failed to create UserDefinedEnum asset"));
}
UUserDefinedEnum* NewEnum = Cast<UUserDefinedEnum>(NewAsset);
if (!NewEnum)
{
return MakeErrorJson(Result, TEXT("Created asset is not a UserDefinedEnum"));
return MCPUtils::MakeErrorJson(Result, TEXT("Created asset is not a UserDefinedEnum"));
}
// Add enum values — UUserDefinedEnum starts with a MAX value.
@@ -246,7 +247,7 @@ void FBlueprintMCPServer::HandleAddStructProperty(const FJsonObject* Json, FJson
if (AssetPath.IsEmpty() || PropName.IsEmpty() || PropType.IsEmpty())
{
return MakeErrorJson(Result, TEXT("Missing required fields: assetPath, name, type"));
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: assetPath, name, type"));
}
// Find the struct
@@ -259,15 +260,15 @@ void FBlueprintMCPServer::HandleAddStructProperty(const FJsonObject* Json, FJson
}
if (!Struct)
{
return MakeErrorJson(Result, FString::Printf(TEXT("UserDefinedStruct not found at '%s'"), *AssetPath));
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("UserDefinedStruct not found at '%s'"), *AssetPath));
}
// Resolve type
FEdGraphPinType PinType;
FString TypeError;
if (!ResolveTypeFromString(PropType, PinType, TypeError))
if (!MCPUtils::ResolveTypeFromString(PropType, PinType, TypeError))
{
return MakeErrorJson(Result, FString::Printf(TEXT("Cannot resolve type '%s': %s"), *PropType, *TypeError));
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Cannot resolve type '%s': %s"), *PropType, *TypeError));
}
// Snapshot existing GUIDs so we can find the newly added one
@@ -280,7 +281,7 @@ void FBlueprintMCPServer::HandleAddStructProperty(const FJsonObject* Json, FJson
bool bAdded = FStructureEditorUtils::AddVariable(Struct, PinType);
if (!bAdded)
{
return MakeErrorJson(Result, TEXT("Failed to add property to struct"));
return MCPUtils::MakeErrorJson(Result, TEXT("Failed to add property to struct"));
}
// Find the new variable by diffing GUID sets and rename it
@@ -321,7 +322,7 @@ void FBlueprintMCPServer::HandleRemoveStructProperty(const FJsonObject* Json, FJ
if (AssetPath.IsEmpty() || PropName.IsEmpty())
{
return MakeErrorJson(Result, TEXT("Missing required fields: assetPath, name"));
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: assetPath, name"));
}
// Find the struct
@@ -333,7 +334,7 @@ void FBlueprintMCPServer::HandleRemoveStructProperty(const FJsonObject* Json, FJ
}
if (!Struct)
{
return MakeErrorJson(Result, FString::Printf(TEXT("UserDefinedStruct not found at '%s'"), *AssetPath));
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("UserDefinedStruct not found at '%s'"), *AssetPath));
}
// Find the property GUID by name
@@ -357,7 +358,7 @@ void FBlueprintMCPServer::HandleRemoveStructProperty(const FJsonObject* Json, FJ
{
AvailProps.Add(MakeShared<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);
return;
}
@@ -365,7 +366,7 @@ void FBlueprintMCPServer::HandleRemoveStructProperty(const FJsonObject* Json, FJ
bool bRemoved = FStructureEditorUtils::RemoveVariable(Struct, TargetGuid);
if (!bRemoved)
{
return MakeErrorJson(Result, FString::Printf(TEXT("Failed to remove property '%s'"), *PropName));
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Failed to remove property '%s'"), *PropName));
}
// Save

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -254,47 +254,19 @@ private:
void HandleSetStateBlendSpace(const FJsonObject* Json, FJsonObject* Result);
public:
// ----- Serialization -----
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 -----
// ----- Helpers (stateful — use cached asset lists) -----
FAssetData* FindAnyAsset(const FString& NameOrPath);
FAssetData* FindBlueprintAsset(const FString& NameOrPath);
FAssetData* FindMapAsset(const FString& NameOrPath);
UBlueprint* LoadBlueprintByName(const FString& NameOrPath, FString& OutError);
UEdGraphNode* FindNodeByGuid(UBlueprint* BP, const FString& GuidString, UEdGraph** OutGraph = nullptr);
TSharedPtr<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.
// 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);
// ----- Material helpers (stateful — use cached asset lists) -----
FAssetData* FindMaterialAsset(const FString& NameOrPath);
UMaterial* LoadMaterialByName(const FString& NameOrPath, FString& OutError);
FAssetData* FindMaterialInstanceAsset(const FString& NameOrPath);
UMaterialInstanceConstant* LoadMaterialInstanceByName(const FString& NameOrPath, FString& OutError);
FAssetData* FindMaterialFunctionAsset(const FString& NameOrPath);
UMaterialFunction* LoadMaterialFunctionByName(const FString& NameOrPath, FString& OutError);
bool SaveMaterialPackage(UMaterial* Material);
bool SaveGenericPackage(UObject* Asset);
// ----- Type resolution -----
bool ResolveTypeFromString(const FString& TypeName, FEdGraphPinType& OutPinType, FString& OutError);
static UClass* FindClassByName(const FString& ClassName);
// ----- Snapshot storage -----
TMap<FString, FGraphSnapshot> Snapshots;
@@ -309,5 +281,5 @@ public:
bool LoadSnapshotFromDisk(const FString& SnapshotId, FGraphSnapshot& OutSnapshot);
};
// Transitional alias — eventually MCPHelper will be its own class.
// Transitional alias — old-style handlers use this to access the server instance.
using MCPHelper = FBlueprintMCPServer;

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