More MCP refactoring.

This commit is contained in:
2026-03-07 02:36:43 -05:00
parent 282ee3ef33
commit 114464dbfe
6 changed files with 103 additions and 444 deletions

View File

@@ -7,6 +7,12 @@
#include "Materials/Material.h"
#include "Materials/MaterialInstanceConstant.h"
#include "Materials/MaterialFunction.h"
#include "Animation/Skeleton.h"
#include "Animation/AnimSequence.h"
#include "Animation/BlendSpace.h"
#include "Animation/AnimBlueprint.h"
#include "AnimationStateMachineGraph.h"
#include "MCPUtils.h"
#include "AssetRegistry/AssetRegistryModule.h"
#include "AssetRegistry/IAssetRegistry.h"
@@ -79,6 +85,10 @@ void UMCPAssetFinder::Refresh()
Self->CacheAssets(AR, UMaterialFunction::StaticClass(), false);
Self->CacheAssets(AR, UUserDefinedStruct::StaticClass(), false);
Self->CacheAssets(AR, UUserDefinedEnum::StaticClass(), false);
Self->CacheAssets(AR, USkeleton::StaticClass(), false);
Self->CacheAssets(AR, UAnimSequence::StaticClass(), false);
Self->CacheAssets(AR, UBlendSpace::StaticClass(), false);
Self->CacheAssets(AR, UAnimBlueprint::StaticClass(), false);
// Combined list: blueprints + maps
TArray<FAssetData>& Combined = Self->AssetCache.Add(BlueprintsAndMaps);
@@ -233,3 +243,15 @@ UBlueprint* UMCPAssetFinder::LoadBlueprintOrLevelBlueprint(const FString& NameOr
return LoadBlueprintOrLevelBlueprint(*Asset, Error);
}
UAnimationStateMachineGraph* UMCPAssetFinder::LoadAnimStateMachineGraph(
const FString& BlueprintName, const FString& GraphName, MCPErrorCallback Error)
{
UAnimBlueprint* AnimBP = LoadAsset<UAnimBlueprint>(BlueprintName, Error);
if (!AnimBP) return nullptr;
UAnimationStateMachineGraph* SMGraph = MCPUtils::FindStateMachineGraph(AnimBP, GraphName);
if (!SMGraph)
Error.SetError(FString::Printf(TEXT("State machine graph '%s' not found in '%s'"), *GraphName, *BlueprintName));
return SMGraph;
}

View File

@@ -59,7 +59,7 @@ void FBlueprintMCPServer::HandleCreateAnimBlueprint(const FJsonObject* Json, FJs
// Check if asset already exists
FString FullAssetPath = PackagePath / Name;
if (UMCPAssetFinder::FindAsset(UBlueprint::StaticClass(), Name) || UMCPAssetFinder::FindAsset(UBlueprint::StaticClass(), FullAssetPath))
if (UMCPAssetFinder::FindAsset(UBlueprint::StaticClass(), Name))
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(
TEXT("Blueprint '%s' already exists. Use a different name or delete the existing asset first."),
@@ -67,59 +67,8 @@ void FBlueprintMCPServer::HandleCreateAnimBlueprint(const FJsonObject* Json, FJs
}
// Resolve skeleton
USkeleton* Skeleton = nullptr;
if (SkeletonName == TEXT("__create_test_skeleton__"))
{
// Create a minimal in-memory skeleton for tests
FString TestSkeletonPath = PackagePath / (Name + TEXT("_TestSkeleton"));
UPackage* SkelPackage = CreatePackage(*TestSkeletonPath);
Skeleton = NewObject<USkeleton>(SkelPackage, FName(*(Name + TEXT("_TestSkeleton"))), RF_Public | RF_Standalone);
if (Skeleton)
{
Skeleton->MarkPackageDirty();
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Created test skeleton '%s'"), *Skeleton->GetName());
}
}
else
{
// Search asset registry for the skeleton
FAssetRegistryModule& ARM = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
TArray<FAssetData> SkeletonAssets;
ARM.Get().GetAssetsByClass(USkeleton::StaticClass()->GetClassPathName(), SkeletonAssets, false);
for (const FAssetData& Asset : SkeletonAssets)
{
if (Asset.AssetName.ToString() == SkeletonName ||
Asset.PackageName.ToString() == SkeletonName ||
Asset.GetObjectPathString() == SkeletonName)
{
Skeleton = Cast<USkeleton>(Asset.GetAsset());
break;
}
}
// Case-insensitive fallback
if (!Skeleton)
{
for (const FAssetData& Asset : SkeletonAssets)
{
if (Asset.AssetName.ToString().Equals(SkeletonName, ESearchCase::IgnoreCase) ||
Asset.PackageName.ToString().Equals(SkeletonName, ESearchCase::IgnoreCase))
{
Skeleton = Cast<USkeleton>(Asset.GetAsset());
break;
}
}
}
}
if (!Skeleton)
{
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));
}
USkeleton* Skeleton = UMCPAssetFinder::LoadAsset<USkeleton>(SkeletonName, Result);
if (!Skeleton) return;
// Resolve parent class (default: UAnimInstance)
UClass* ParentClass = UAnimInstance::StaticClass();
@@ -213,27 +162,12 @@ void FBlueprintMCPServer::HandleAddAnimState(const FJsonObject* Json, FJsonObjec
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: blueprint, graph, stateName"));
}
FString LoadError;
UBlueprint* BP = UMCPAssetFinder::LoadBlueprintOrLevelBlueprint(BlueprintName, LoadError);
if (!BP)
{
return MCPUtils::MakeErrorJson(Result, LoadError);
}
UAnimBlueprint* AnimBP = Cast<UAnimBlueprint>(BP);
if (!AnimBP)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("'%s' is not an Animation Blueprint"), *BlueprintName));
}
UAnimationStateMachineGraph* SMGraph = MCPUtils::FindStateMachineGraph(BP, GraphName);
if (!SMGraph)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("State machine graph '%s' not found"), *GraphName));
}
UAnimationStateMachineGraph* SMGraph = UMCPAssetFinder::LoadAnimStateMachineGraph(BlueprintName, GraphName, Result);
if (!SMGraph) return;
UAnimBlueprint* AnimBP = SMGraph->GetTypedOuter<UAnimBlueprint>();
// Check for duplicate state name
if (MCPUtils::FindStateByName(SMGraph, StateName))
if (MCPUtils::FindStateByName(SMGraph, StateName, nullptr))
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("State '%s' already exists in graph '%s'"), *StateName, *GraphName));
}
@@ -266,19 +200,8 @@ void FBlueprintMCPServer::HandleAddAnimState(const FJsonObject* Json, FJsonObjec
if (!AnimAssetName.IsEmpty() && NewState->GetBoundGraph())
{
// Try to find the animation asset and create a sequence player in the state's inner graph
UAnimSequence* AnimSeq = nullptr;
FAssetRegistryModule& ARM2 = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
TArray<FAssetData> AnimAssets;
ARM2.Get().GetAssetsByClass(UAnimSequence::StaticClass()->GetClassPathName(), AnimAssets, false);
for (const FAssetData& Asset : AnimAssets)
{
if (Asset.AssetName.ToString() == AnimAssetName ||
Asset.PackageName.ToString() == AnimAssetName)
{
AnimSeq = Cast<UAnimSequence>(Asset.GetAsset());
break;
}
}
FAssetData* FoundAnimAsset = UMCPAssetFinder::FindAsset(UAnimSequence::StaticClass(), AnimAssetName);
UAnimSequence* AnimSeq = FoundAnimAsset ? Cast<UAnimSequence>(FoundAnimAsset->GetAsset()) : nullptr;
if (AnimSeq)
{
@@ -315,30 +238,12 @@ void FBlueprintMCPServer::HandleRemoveAnimState(const FJsonObject* Json, FJsonOb
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: blueprint, graph, stateName"));
}
FString LoadError;
UBlueprint* BP = UMCPAssetFinder::LoadBlueprintOrLevelBlueprint(BlueprintName, LoadError);
if (!BP)
{
return MCPUtils::MakeErrorJson(Result, LoadError);
}
UAnimationStateMachineGraph* SMGraph = UMCPAssetFinder::LoadAnimStateMachineGraph(BlueprintName, GraphName, Result);
if (!SMGraph) return;
UAnimBlueprint* AnimBP = SMGraph->GetTypedOuter<UAnimBlueprint>();
UAnimBlueprint* AnimBP = Cast<UAnimBlueprint>(BP);
if (!AnimBP)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("'%s' is not an Animation Blueprint"), *BlueprintName));
}
UAnimationStateMachineGraph* SMGraph = MCPUtils::FindStateMachineGraph(BP, GraphName);
if (!SMGraph)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("State machine graph '%s' not found"), *GraphName));
}
UAnimStateNode* StateNode = MCPUtils::FindStateByName(SMGraph, StateName);
if (!StateNode)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("State '%s' not found in graph '%s'"), *StateName, *GraphName));
}
UAnimStateNode* StateNode = MCPUtils::FindStateByName(SMGraph, StateName, Result);
if (!StateNode) return;
// Collect and remove transitions connected to this state
TArray<UAnimStateTransitionNode*> TransitionsToRemove;
@@ -386,36 +291,15 @@ void FBlueprintMCPServer::HandleAddAnimTransition(const FJsonObject* Json, FJson
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: blueprint, graph, fromState, toState"));
}
FString LoadError;
UBlueprint* BP = UMCPAssetFinder::LoadBlueprintOrLevelBlueprint(BlueprintName, LoadError);
if (!BP)
{
return MCPUtils::MakeErrorJson(Result, LoadError);
}
UAnimationStateMachineGraph* SMGraph = UMCPAssetFinder::LoadAnimStateMachineGraph(BlueprintName, GraphName, Result);
if (!SMGraph) return;
UAnimBlueprint* AnimBP = SMGraph->GetTypedOuter<UAnimBlueprint>();
UAnimBlueprint* AnimBP = Cast<UAnimBlueprint>(BP);
if (!AnimBP)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("'%s' is not an Animation Blueprint"), *BlueprintName));
}
UAnimStateNode* FromState = MCPUtils::FindStateByName(SMGraph, FromStateName, Result);
if (!FromState) return;
UAnimationStateMachineGraph* SMGraph = MCPUtils::FindStateMachineGraph(BP, GraphName);
if (!SMGraph)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("State machine graph '%s' not found"), *GraphName));
}
UAnimStateNode* FromState = MCPUtils::FindStateByName(SMGraph, FromStateName);
if (!FromState)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("From state '%s' not found"), *FromStateName));
}
UAnimStateNode* ToState = MCPUtils::FindStateByName(SMGraph, ToStateName);
if (!ToState)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("To state '%s' not found"), *ToStateName));
}
UAnimStateNode* ToState = MCPUtils::FindStateByName(SMGraph, ToStateName, Result);
if (!ToState) return;
// Create transition node
UAnimStateTransitionNode* TransNode = NewObject<UAnimStateTransitionNode>(SMGraph);
@@ -473,24 +357,9 @@ void FBlueprintMCPServer::HandleSetTransitionRule(const FJsonObject* Json, FJson
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: blueprint, graph, fromState, toState"));
}
FString LoadError;
UBlueprint* BP = UMCPAssetFinder::LoadBlueprintOrLevelBlueprint(BlueprintName, LoadError);
if (!BP)
{
return MCPUtils::MakeErrorJson(Result, LoadError);
}
UAnimBlueprint* AnimBP = Cast<UAnimBlueprint>(BP);
if (!AnimBP)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("'%s' is not an Animation Blueprint"), *BlueprintName));
}
UAnimationStateMachineGraph* SMGraph = MCPUtils::FindStateMachineGraph(BP, GraphName);
if (!SMGraph)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("State machine graph '%s' not found"), *GraphName));
}
UAnimationStateMachineGraph* SMGraph = UMCPAssetFinder::LoadAnimStateMachineGraph(BlueprintName, GraphName, Result);
if (!SMGraph) return;
UAnimBlueprint* AnimBP = SMGraph->GetTypedOuter<UAnimBlueprint>();
UAnimStateTransitionNode* TransNode = MCPUtils::FindTransition(SMGraph, FromStateName, ToStateName);
if (!TransNode)
@@ -565,18 +434,8 @@ void FBlueprintMCPServer::HandleAddAnimNode(const FJsonObject* Json, FJsonObject
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: blueprint, nodeType"));
}
FString LoadError;
UBlueprint* BP = UMCPAssetFinder::LoadBlueprintOrLevelBlueprint(BlueprintName, LoadError);
if (!BP)
{
return MCPUtils::MakeErrorJson(Result, LoadError);
}
UAnimBlueprint* AnimBP = Cast<UAnimBlueprint>(BP);
if (!AnimBP)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("'%s' is not an Animation Blueprint"), *BlueprintName));
}
UAnimBlueprint* AnimBP = UMCPAssetFinder::LoadAsset<UAnimBlueprint>(BlueprintName, Result);
if (!AnimBP) return;
// Find target graph (default to AnimGraph if not specified)
UEdGraph* TargetGraph = nullptr;
@@ -586,7 +445,7 @@ void FBlueprintMCPServer::HandleAddAnimNode(const FJsonObject* Json, FJsonObject
}
TArray<UEdGraph*> AllGraphs;
BP->GetAllGraphs(AllGraphs);
AnimBP->GetAllGraphs(AllGraphs);
for (UEdGraph* Graph : AllGraphs)
{
if (Graph->GetName() == GraphName)
@@ -617,20 +476,13 @@ void FBlueprintMCPServer::HandleAddAnimNode(const FJsonObject* Json, FJsonObject
FString AnimAssetName = Json->GetStringField(TEXT("animationAsset"));
if (!AnimAssetName.IsEmpty())
{
FAssetRegistryModule& ARM = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
TArray<FAssetData> AnimAssets;
ARM.Get().GetAssetsByClass(UAnimSequence::StaticClass()->GetClassPathName(), AnimAssets, false);
for (const FAssetData& Asset : AnimAssets)
FAssetData* FoundAnimAsset = UMCPAssetFinder::FindAsset(UAnimSequence::StaticClass(), AnimAssetName);
if (FoundAnimAsset)
{
if (Asset.AssetName.ToString() == AnimAssetName ||
Asset.PackageName.ToString() == AnimAssetName)
UAnimSequence* AnimSeq = Cast<UAnimSequence>(FoundAnimAsset->GetAsset());
if (AnimSeq)
{
UAnimSequence* AnimSeq = Cast<UAnimSequence>(Asset.GetAsset());
if (AnimSeq)
{
SeqNode->SetAnimationAsset(AnimSeq);
}
break;
SeqNode->SetAnimationAsset(AnimSeq);
}
}
}
@@ -648,20 +500,13 @@ void FBlueprintMCPServer::HandleAddAnimNode(const FJsonObject* Json, FJsonObject
FString BSAssetName = Json->GetStringField(TEXT("animationAsset"));
if (!BSAssetName.IsEmpty())
{
FAssetRegistryModule& ARM = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
TArray<FAssetData> BSAssets;
ARM.Get().GetAssetsByClass(UBlendSpace::StaticClass()->GetClassPathName(), BSAssets, true);
for (const FAssetData& Asset : BSAssets)
FAssetData* FoundBSAsset = UMCPAssetFinder::FindAsset(UBlendSpace::StaticClass(), BSAssetName);
if (FoundBSAsset)
{
if (Asset.AssetName.ToString() == BSAssetName ||
Asset.PackageName.ToString() == BSAssetName)
UBlendSpace* BS = Cast<UBlendSpace>(FoundBSAsset->GetAsset());
if (BS)
{
UBlendSpace* BS = Cast<UBlendSpace>(Asset.GetAsset());
if (BS)
{
BSNode->SetAnimationAsset(BS);
}
break;
BSNode->SetAnimationAsset(BS);
}
}
}
@@ -761,30 +606,12 @@ void FBlueprintMCPServer::HandleSetStateAnimation(const FJsonObject* Json, FJson
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: blueprint, graph, stateName, animationAsset"));
}
FString LoadError;
UBlueprint* BP = UMCPAssetFinder::LoadBlueprintOrLevelBlueprint(BlueprintName, LoadError);
if (!BP)
{
return MCPUtils::MakeErrorJson(Result, LoadError);
}
UAnimationStateMachineGraph* SMGraph = UMCPAssetFinder::LoadAnimStateMachineGraph(BlueprintName, GraphName, Result);
if (!SMGraph) return;
UAnimBlueprint* AnimBP = SMGraph->GetTypedOuter<UAnimBlueprint>();
UAnimBlueprint* AnimBP = Cast<UAnimBlueprint>(BP);
if (!AnimBP)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("'%s' is not an Animation Blueprint"), *BlueprintName));
}
UAnimationStateMachineGraph* SMGraph = MCPUtils::FindStateMachineGraph(BP, GraphName);
if (!SMGraph)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("State machine graph '%s' not found"), *GraphName));
}
UAnimStateNode* StateNode = MCPUtils::FindStateByName(SMGraph, StateName);
if (!StateNode)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("State '%s' not found in graph '%s'"), *StateName, *GraphName));
}
UAnimStateNode* StateNode = MCPUtils::FindStateByName(SMGraph, StateName, Result);
if (!StateNode) return;
UEdGraph* InnerGraph = StateNode->GetBoundGraph();
if (!InnerGraph)
@@ -793,24 +620,8 @@ void FBlueprintMCPServer::HandleSetStateAnimation(const FJsonObject* Json, FJson
}
// Find the animation asset
UAnimSequence* AnimSeq = nullptr;
FAssetRegistryModule& ARM = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
TArray<FAssetData> AnimAssets;
ARM.Get().GetAssetsByClass(UAnimSequence::StaticClass()->GetClassPathName(), AnimAssets, false);
for (const FAssetData& Asset : AnimAssets)
{
if (Asset.AssetName.ToString() == AnimAssetName ||
Asset.PackageName.ToString() == AnimAssetName)
{
AnimSeq = Cast<UAnimSequence>(Asset.GetAsset());
break;
}
}
if (!AnimSeq)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Animation asset '%s' not found"), *AnimAssetName));
}
UAnimSequence* AnimSeq = UMCPAssetFinder::LoadAsset<UAnimSequence>(AnimAssetName, Result);
if (!AnimSeq) return;
// Find existing SequencePlayer or create one
UAnimGraphNode_SequencePlayer* SeqNode = nullptr;
@@ -854,23 +665,13 @@ void FBlueprintMCPServer::HandleListAnimSlots(const FJsonObject* Json, FJsonObje
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: blueprint"));
}
FString LoadError;
UBlueprint* BP = UMCPAssetFinder::LoadBlueprintOrLevelBlueprint(BlueprintName, LoadError);
if (!BP)
{
return MCPUtils::MakeErrorJson(Result, LoadError);
}
UAnimBlueprint* AnimBP = Cast<UAnimBlueprint>(BP);
if (!AnimBP)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("'%s' is not an Animation Blueprint"), *BlueprintName));
}
UAnimBlueprint* AnimBP = UMCPAssetFinder::LoadAsset<UAnimBlueprint>(BlueprintName, Result);
if (!AnimBP) return;
// Walk all anim nodes to collect slot names
TSet<FString> SlotNames;
TArray<UEdGraph*> AllGraphs;
BP->GetAllGraphs(AllGraphs);
AnimBP->GetAllGraphs(AllGraphs);
for (UEdGraph* Graph : AllGraphs)
{
for (UEdGraphNode* Node : Graph->Nodes)
@@ -913,23 +714,13 @@ void FBlueprintMCPServer::HandleListSyncGroups(const FJsonObject* Json, FJsonObj
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: blueprint"));
}
FString LoadError;
UBlueprint* BP = UMCPAssetFinder::LoadBlueprintOrLevelBlueprint(BlueprintName, LoadError);
if (!BP)
{
return MCPUtils::MakeErrorJson(Result, LoadError);
}
UAnimBlueprint* AnimBP = Cast<UAnimBlueprint>(BP);
if (!AnimBP)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("'%s' is not an Animation Blueprint"), *BlueprintName));
}
UAnimBlueprint* AnimBP = UMCPAssetFinder::LoadAsset<UAnimBlueprint>(BlueprintName, Result);
if (!AnimBP) return;
// Walk all anim nodes to collect sync group names
TSet<FString> SyncGroupNames;
TArray<UEdGraph*> AllGraphs;
BP->GetAllGraphs(AllGraphs);
AnimBP->GetAllGraphs(AllGraphs);
for (UEdGraph* Graph : AllGraphs)
{
for (UEdGraphNode* Node : Graph->Nodes)
@@ -986,73 +777,16 @@ void FBlueprintMCPServer::HandleCreateBlendSpace(const FJsonObject* Json, FJsonO
// Check if asset already exists
FString FullAssetPath = PackagePath / Name;
if (UMCPAssetFinder::FindAsset(UBlendSpace::StaticClass(), Name))
{
FAssetRegistryModule& ARM = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
TArray<FAssetData> ExistingAssets;
ARM.Get().GetAssetsByClass(UBlendSpace::StaticClass()->GetClassPathName(), ExistingAssets, true);
for (const FAssetData& Asset : ExistingAssets)
{
if (Asset.AssetName.ToString() == Name || Asset.GetObjectPathString() == FullAssetPath)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(
TEXT("Blend Space '%s' already exists. Use a different name or delete the existing asset first."),
*Name));
}
}
return MCPUtils::MakeErrorJson(Result, FString::Printf(
TEXT("Blend Space '%s' already exists. Use a different name or delete the existing asset first."),
*Name));
}
// Resolve skeleton
USkeleton* Skeleton = nullptr;
if (SkeletonName == TEXT("__create_test_skeleton__"))
{
FString TestSkeletonPath = PackagePath / (Name + TEXT("_TestSkeleton"));
UPackage* SkelPackage = CreatePackage(*TestSkeletonPath);
Skeleton = NewObject<USkeleton>(SkelPackage, FName(*(Name + TEXT("_TestSkeleton"))), RF_Public | RF_Standalone);
if (Skeleton)
{
Skeleton->MarkPackageDirty();
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Created test skeleton '%s'"), *Skeleton->GetName());
}
}
else
{
FAssetRegistryModule& ARM = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
TArray<FAssetData> SkeletonAssets;
ARM.Get().GetAssetsByClass(USkeleton::StaticClass()->GetClassPathName(), SkeletonAssets, false);
for (const FAssetData& Asset : SkeletonAssets)
{
if (Asset.AssetName.ToString() == SkeletonName ||
Asset.PackageName.ToString() == SkeletonName ||
Asset.GetObjectPathString() == SkeletonName)
{
Skeleton = Cast<USkeleton>(Asset.GetAsset());
break;
}
}
// Case-insensitive fallback
if (!Skeleton)
{
for (const FAssetData& Asset : SkeletonAssets)
{
if (Asset.AssetName.ToString().Equals(SkeletonName, ESearchCase::IgnoreCase) ||
Asset.PackageName.ToString().Equals(SkeletonName, ESearchCase::IgnoreCase))
{
Skeleton = Cast<USkeleton>(Asset.GetAsset());
break;
}
}
}
}
if (!Skeleton)
{
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));
}
USkeleton* Skeleton = UMCPAssetFinder::LoadAsset<USkeleton>(SkeletonName, Result);
if (!Skeleton) return;
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Creating Blend Space '%s' in '%s' with skeleton '%s'"),
*Name, *PackagePath, *Skeleton->GetName());
@@ -1101,40 +835,8 @@ void FBlueprintMCPServer::HandleSetBlendSpaceSamples(const FJsonObject* Json, FJ
}
// Load the blend space
UBlendSpace* BS = nullptr;
{
FAssetRegistryModule& ARM = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
TArray<FAssetData> BSAssets;
ARM.Get().GetAssetsByClass(UBlendSpace::StaticClass()->GetClassPathName(), BSAssets, true);
for (const FAssetData& Asset : BSAssets)
{
if (Asset.AssetName.ToString() == BlendSpaceName ||
Asset.PackageName.ToString() == BlendSpaceName ||
Asset.GetObjectPathString() == BlendSpaceName)
{
BS = Cast<UBlendSpace>(Asset.GetAsset());
break;
}
}
// Try FName-based path match (e.g. /Game/Folder/BS_Name)
if (!BS)
{
FString LeafName = FPaths::GetCleanFilename(BlendSpaceName);
for (const FAssetData& Asset : BSAssets)
{
if (Asset.AssetName.ToString() == LeafName)
{
BS = Cast<UBlendSpace>(Asset.GetAsset());
break;
}
}
}
}
if (!BS)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Blend Space '%s' not found"), *BlendSpaceName));
}
UBlendSpace* BS = UMCPAssetFinder::LoadAsset<UBlendSpace>(BlendSpaceName, Result);
if (!BS) return;
// Set axis parameters
BS->PreEditChange(nullptr);
@@ -1170,11 +872,6 @@ void FBlueprintMCPServer::HandleSetBlendSpaceSamples(const FJsonObject* Json, FJ
if (Json->TryGetArrayField(TEXT("samples"), SamplesArray) && SamplesArray)
{
// Pre-load all animation sequences for lookup
FAssetRegistryModule& ARM = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
TArray<FAssetData> AnimAssets;
ARM.Get().GetAssetsByClass(UAnimSequence::StaticClass()->GetClassPathName(), AnimAssets, false);
for (const TSharedPtr<FJsonValue>& SampleVal : *SamplesArray)
{
const TSharedPtr<FJsonObject>& SampleObj = SampleVal->AsObject();
@@ -1187,29 +884,10 @@ void FBlueprintMCPServer::HandleSetBlendSpaceSamples(const FJsonObject* Json, FJ
UAnimSequence* AnimSeq = nullptr;
if (!AnimAssetName.IsEmpty())
{
for (const FAssetData& Asset : AnimAssets)
FAssetData* FoundAnimAsset = UMCPAssetFinder::FindAsset(UAnimSequence::StaticClass(), AnimAssetName);
if (FoundAnimAsset)
{
if (Asset.AssetName.ToString() == AnimAssetName ||
Asset.PackageName.ToString() == AnimAssetName ||
Asset.GetObjectPathString() == AnimAssetName)
{
AnimSeq = Cast<UAnimSequence>(Asset.GetAsset());
break;
}
}
// Also try extracting leaf name from path
if (!AnimSeq)
{
FString LeafName = FPaths::GetCleanFilename(AnimAssetName);
for (const FAssetData& Asset : AnimAssets)
{
if (Asset.AssetName.ToString() == LeafName)
{
AnimSeq = Cast<UAnimSequence>(Asset.GetAsset());
break;
}
}
AnimSeq = Cast<UAnimSequence>(FoundAnimAsset->GetAsset());
}
}
@@ -1260,30 +938,12 @@ void FBlueprintMCPServer::HandleSetStateBlendSpace(const FJsonObject* Json, FJso
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: blueprint, graph, stateName, blendSpace"));
}
FString LoadError;
UBlueprint* BP = UMCPAssetFinder::LoadBlueprintOrLevelBlueprint(BlueprintName, LoadError);
if (!BP)
{
return MCPUtils::MakeErrorJson(Result, LoadError);
}
UAnimationStateMachineGraph* SMGraph = UMCPAssetFinder::LoadAnimStateMachineGraph(BlueprintName, GraphName, Result);
if (!SMGraph) return;
UAnimBlueprint* AnimBP = SMGraph->GetTypedOuter<UAnimBlueprint>();
UAnimBlueprint* AnimBP = Cast<UAnimBlueprint>(BP);
if (!AnimBP)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("'%s' is not an Animation Blueprint"), *BlueprintName));
}
UAnimationStateMachineGraph* SMGraph = MCPUtils::FindStateMachineGraph(BP, GraphName);
if (!SMGraph)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("State machine graph '%s' not found"), *GraphName));
}
UAnimStateNode* StateNode = MCPUtils::FindStateByName(SMGraph, StateName);
if (!StateNode)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("State '%s' not found in graph '%s'"), *StateName, *GraphName));
}
UAnimStateNode* StateNode = MCPUtils::FindStateByName(SMGraph, StateName, Result);
if (!StateNode) return;
UEdGraph* InnerGraph = StateNode->GetBoundGraph();
if (!InnerGraph)
@@ -1292,40 +952,8 @@ void FBlueprintMCPServer::HandleSetStateBlendSpace(const FJsonObject* Json, FJso
}
// Find the blend space asset
UBlendSpace* BlendSpaceAsset = nullptr;
{
FAssetRegistryModule& ARM = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
TArray<FAssetData> BSAssets;
ARM.Get().GetAssetsByClass(UBlendSpace::StaticClass()->GetClassPathName(), BSAssets, true);
for (const FAssetData& Asset : BSAssets)
{
if (Asset.AssetName.ToString() == BlendSpaceName ||
Asset.PackageName.ToString() == BlendSpaceName ||
Asset.GetObjectPathString() == BlendSpaceName)
{
BlendSpaceAsset = Cast<UBlendSpace>(Asset.GetAsset());
break;
}
}
// Leaf name fallback
if (!BlendSpaceAsset)
{
FString LeafName = FPaths::GetCleanFilename(BlendSpaceName);
for (const FAssetData& Asset : BSAssets)
{
if (Asset.AssetName.ToString() == LeafName)
{
BlendSpaceAsset = Cast<UBlendSpace>(Asset.GetAsset());
break;
}
}
}
}
if (!BlendSpaceAsset)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Blend Space '%s' not found"), *BlendSpaceName));
}
UBlendSpace* BlendSpaceAsset = UMCPAssetFinder::LoadAsset<UBlendSpace>(BlendSpaceName, Result);
if (!BlendSpaceAsset) return;
// Find existing BlendSpacePlayer or create one
UAnimGraphNode_BlendSpacePlayer* BSNode = nullptr;
@@ -1406,7 +1034,7 @@ void FBlueprintMCPServer::HandleSetStateBlendSpace(const FJsonObject* Json, FJso
// Verify the variable exists in the blueprint
FName VarFName(*VarName);
bool bVarFound = false;
for (FBPVariableDescription& Var : BP->NewVariables)
for (FBPVariableDescription& Var : AnimBP->NewVariables)
{
if (Var.VarName == VarFName)
{
@@ -1417,7 +1045,7 @@ void FBlueprintMCPServer::HandleSetStateBlendSpace(const FJsonObject* Json, FJso
if (!bVarFound)
{
// Also check parent class properties
if (UClass* GenClass = BP->SkeletonGeneratedClass)
if (UClass* GenClass = AnimBP->SkeletonGeneratedClass)
{
if (FProperty* Prop = GenClass->FindPropertyByName(VarFName))
{

View File

@@ -1136,7 +1136,7 @@ UAnimationStateMachineGraph* MCPUtils::FindStateMachineGraph(UBlueprint* BP, con
return nullptr;
}
UAnimStateNode* MCPUtils::FindStateByName(UAnimationStateMachineGraph* SMGraph, const FString& StateName)
UAnimStateNode* MCPUtils::FindStateByName(UAnimationStateMachineGraph* SMGraph, const FString& StateName, MCPErrorCallback Error)
{
for (UEdGraphNode* Node : SMGraph->Nodes)
{
@@ -1148,6 +1148,7 @@ UAnimStateNode* MCPUtils::FindStateByName(UAnimationStateMachineGraph* SMGraph,
}
}
}
Error.SetError(FString::Printf(TEXT("State '%s' not found in graph '%s'"), *StateName, *SMGraph->GetName()));
return nullptr;
}

View File

@@ -3,3 +3,5 @@
- When compiling blueprints, always check for errors properly. Many handlers call `FKismetEditorUtilities::CompileBlueprint` and assume success without checking. The validation handler (`MCPHandlers_Validation.h`) already has proper error collection logic — all compile sites should follow that pattern.
- Add `FindBlueprintGraph` to `UMCPAssetFinder`: load blueprint, URL-decode graph name, search all graphs by name, return `UEdGraph*` or nullptr with error. Currently ~10 handlers duplicate this 25-line pattern (see `MCPHandlers_Mutation.h:186-210` for a typical example).
- Look for all uses of `GetAllGraphs`. Every place this is used is terrible — replace with a proper graph lookup helper.

View File

@@ -12,6 +12,7 @@ class UBlueprint;
class UMaterial;
class UMaterialInstanceConstant;
class UMaterialFunction;
class UAnimationStateMachineGraph;
class IAssetRegistry;
/**
@@ -79,6 +80,11 @@ public:
static UBlueprint* LoadBlueprintOrLevelBlueprint(FAssetData& Asset, MCPErrorCallback Error);
static UBlueprint* LoadBlueprintOrLevelBlueprint(const FString& NameOrPath, MCPErrorCallback Error);
// Load an anim blueprint and find a state machine graph within it.
// Callers can recover the AnimBP via SMGraph->GetTypedOuter<UAnimBlueprint>().
static UAnimationStateMachineGraph* LoadAnimStateMachineGraph(
const FString& BlueprintName, const FString& GraphName, MCPErrorCallback Error);
private:
// Fetch assets from the Unreal asset registry and store them locally.
void CacheAssets(IAssetRegistry &Registry, UClass *Class, bool IncludeSubclasses);

View File

@@ -152,7 +152,7 @@ public:
// ----- Anim blueprint helpers -----
static UAnimationStateMachineGraph* FindStateMachineGraph(UBlueprint* BP, const FString& GraphName);
static UAnimStateNode* FindStateByName(UAnimationStateMachineGraph* SMGraph, const FString& StateName);
static UAnimStateNode* FindStateByName(UAnimationStateMachineGraph* SMGraph, const FString& StateName, MCPErrorCallback Error);
static UAnimStateTransitionNode* FindTransition(UAnimationStateMachineGraph* SMGraph, const FString& FromStateName, const FString& ToStateName);
// ----- Node spawners -----