From a72b65641e5f34e11c857554baf1ac90a18f721f Mon Sep 17 00:00:00 2001 From: jyelon Date: Sun, 8 Mar 2026 03:44:27 -0400 Subject: [PATCH] More MCP work --- .../BlueprintMCP/Private/MCPAssetFinder.cpp | 255 +------------- .../Private/MCPHandlers_AnimMutation.h | 56 +-- .../Private/MCPHandlers_Dispatchers.h | 27 +- .../BlueprintMCP/Private/MCPHandlers_Graphs.h | 46 +-- .../Private/MCPHandlers_Mutation.h | 112 ++---- .../BlueprintMCP/Private/MCPHandlers_Params.h | 208 ++++-------- .../BlueprintMCP/Private/MCPHandlers_Read.h | 318 ++++++++---------- .../Private/MCPHandlers_Validation.h | 44 +-- .../Private/MCPHandlers_Variables.h | 83 ++--- .../Source/BlueprintMCP/Private/MCPServer.cpp | 1 - .../Source/BlueprintMCP/Private/MCPUtils.cpp | 32 ++ .../BlueprintMCP/Public/MCPAssetFinder.h | 100 +----- .../Source/BlueprintMCP/Public/MCPUtils.h | 21 ++ 13 files changed, 381 insertions(+), 922 deletions(-) diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPAssetFinder.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPAssetFinder.cpp index fdc11a21..349d0f43 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPAssetFinder.cpp +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPAssetFinder.cpp @@ -1,248 +1,12 @@ #include "MCPAssetFinder.h" -#include "Engine/Engine.h" #include "Engine/Blueprint.h" #include "Engine/World.h" #include "Engine/Level.h" #include "Engine/LevelScriptBlueprint.h" -#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" -const TArray UMCPAssetFinder::EmptyAssetArray; - -// ============================================================ -// Initialize / Deinitialize — subscribe to asset registry events -// ============================================================ - -void UMCPAssetFinder::Initialize(FSubsystemCollectionBase& Collection) -{ - Super::Initialize(Collection); - - IAssetRegistry& AR = FModuleManager::LoadModuleChecked("AssetRegistry").Get(); - AR.OnAssetAdded().AddUObject(this, &UMCPAssetFinder::OnAssetEvent); - AR.OnAssetRemoved().AddUObject(this, &UMCPAssetFinder::OnAssetEvent); - AR.OnAssetUpdated().AddUObject(this, &UMCPAssetFinder::OnAssetEvent); - AR.OnAssetRenamed().AddUObject(this, &UMCPAssetFinder::OnAssetRenamed); -} - -void UMCPAssetFinder::Deinitialize() -{ - IAssetRegistry* AR = IAssetRegistry::Get(); - if (AR) - { - AR->OnAssetAdded().RemoveAll(this); - AR->OnAssetRemoved().RemoveAll(this); - AR->OnAssetUpdated().RemoveAll(this); - AR->OnAssetRenamed().RemoveAll(this); - } - Super::Deinitialize(); -} - -// ============================================================ -// Get / Refresh -// ============================================================ - -UMCPAssetFinder* UMCPAssetFinder::Get() -{ - UMCPAssetFinder* Self = GEngine ? GEngine->GetEngineSubsystem() : nullptr; - checkf(Self, TEXT("MCPAssetFinder::Get() called before engine initialization")); - return Self; -} - -void UMCPAssetFinder::CacheAssets(IAssetRegistry &Registry, UClass *Class, bool IncludeSubclasses) -{ - FName Key = Class->GetFName(); - TArray& List = AssetCache.Add(Key); - Registry.GetAssetsByClass(Class->GetClassPathName(), List, IncludeSubclasses); -} - -void UMCPAssetFinder::Refresh() -{ - checkf(IsInGameThread(), TEXT("MCPAssetFinder must only be accessed from the game thread")); - UMCPAssetFinder* Self = Get(); - - IAssetRegistry& AR = FModuleManager::LoadModuleChecked("AssetRegistry").Get(); - - while (AR.IsLoadingAssets()) FPlatformProcess::Sleep(0.1f); - - if (!Self->bDirty) return; - - Self->AssetCache.Empty(); - - // Cache all asset classes. - Self->CacheAssets(AR, UBlueprint::StaticClass(), true); - Self->CacheAssets(AR, UWorld::StaticClass(), false); - Self->CacheAssets(AR, UMaterial::StaticClass(), false); - Self->CacheAssets(AR, UMaterialInstanceConstant::StaticClass(), false); - 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& Combined = Self->AssetCache.Add(BlueprintsAndMaps); - Combined = Self->AssetCache[UBlueprint::StaticClass()->GetFName()]; - Combined.Append(Self->AssetCache[UWorld::StaticClass()->GetFName()]); - - Self->bDirty = false; - - UE_LOG(LogTemp, Display, TEXT("MCPAssetFinder: Refreshed — %d asset types cached"), - Self->AssetCache.Num()); -} - -// ============================================================ -// Static asset list accessors -// ============================================================ - -const TArray& UMCPAssetFinder::GetAssets(FName Key) -{ - TArray* Found = Get()->AssetCache.Find(Key); - return Found ? *Found : EmptyAssetArray; -} - -const TArray& UMCPAssetFinder::GetAssets(UClass* Class) -{ - return GetAssets(Class->GetFName()); -} - -FName UMCPAssetFinder::BlueprintsAndMaps = FName(TEXT("BlueprintsAndMaps")); - -// ============================================================ -// Find / Search helpers -// ============================================================ - -FAssetData* UMCPAssetFinder::FindAsset(FName Class, const FString& NameOrPath, FString* OutError) -{ - const TArray& List = GetAssets(Class); - bool IsPath = NameOrPath.Contains(TEXT("/")); - - FAssetData* Found = nullptr; - for (const FAssetData& Asset : List) - { - FName Name = IsPath ? Asset.PackageName : Asset.AssetName; - if (!Name.ToString().Equals(NameOrPath, ESearchCase::IgnoreCase)) continue; - if (!Found) - { - Found = const_cast(&Asset); - continue; - } - if (OutError) - { - *OutError = FString::Printf( - TEXT("Ambiguous asset name '%s' — matches both '%s' and '%s'. Use the full package path to disambiguate."), - *NameOrPath, *Found->PackageName.ToString(), *Asset.PackageName.ToString()); - } - return nullptr; - } - return Found; -} - -FAssetData* UMCPAssetFinder::FindAsset(UClass* Class, const FString& NameOrPath, FString* OutError) -{ - return FindAsset(Class->GetFName(), NameOrPath, OutError); -} - -TArray UMCPAssetFinder::SearchAssets(FName Class, const FString& Filter, FString* OutError) -{ - const TArray& List = GetAssets(Class); - TArray Results; - for (const FAssetData& Asset : List) - { - FString AssetName = Asset.AssetName.ToString(); - FString PackagePath = Asset.PackageName.ToString(); - if (AssetName.Contains(Filter, ESearchCase::IgnoreCase) || - PackagePath.Contains(Filter, ESearchCase::IgnoreCase)) - { - Results.Add(const_cast(&Asset)); - } - } - return Results; -} - -TArray UMCPAssetFinder::SearchAssets(UClass* Class, const FString& Filter, FString* OutError) -{ - return SearchAssets(Class->GetFName(), Filter, OutError); -} - -FAssetData* UMCPAssetFinder::FindAnyAsset(const FString& NameOrPath, FString* OutError) -{ - FAssetData* Found = nullptr; - - for (auto& Pair : Get()->AssetCache) - { - if (Pair.Key == BlueprintsAndMaps) continue; - - FString LocalError; - FAssetData* Asset = FindAsset(Pair.Key, NameOrPath, &LocalError); - if (!LocalError.IsEmpty()) - { - if (OutError) *OutError = LocalError; - return nullptr; - } - if (!Asset) continue; - if (Found) - { - if (OutError) - { - *OutError = FString::Printf( - TEXT("Ambiguous asset name '%s' — matches '%s' and '%s'. Use the full package path to disambiguate."), - *NameOrPath, *Found->PackageName.ToString(), *Asset->PackageName.ToString()); - } - return nullptr; - } - Found = Asset; - } - return Found; -} - -// ============================================================ -// Load helpers -// ============================================================ - -UBlueprint* UMCPAssetFinder::LoadBlueprintOrLevelBlueprint(FAssetData& Asset, MCPErrorCallback Error) -{ - // Regular blueprint asset - UBlueprint* BP = Cast(Asset.GetAsset()); - if (BP) return BP; - - // Map asset — extract the level blueprint - UWorld* World = Cast(Asset.GetAsset()); - if (World && World->PersistentLevel) - { - ULevelScriptBlueprint* LevelBP = World->PersistentLevel->GetLevelScriptBlueprint(true); - if (LevelBP) return LevelBP; - } - - Error.SetError(FString::Printf(TEXT("Asset '%s' loaded but its level blueprint could not be retrieved."), - *Asset.AssetName.ToString())); - return nullptr; -} - -UBlueprint* UMCPAssetFinder::LoadBlueprintOrLevelBlueprint(const FString& NameOrPath, MCPErrorCallback Error) -{ - FString FindError; - FAssetData* Asset = FindAsset(BlueprintsAndMaps, NameOrPath, &FindError); - if (!Asset) - { - if (FindError.IsEmpty()) - FindError = FString::Printf(TEXT("Blueprint '%s' not found."), *NameOrPath); - Error.SetError(FindError); - return nullptr; - } - return LoadBlueprintOrLevelBlueprint(*Asset, Error); -} - // ============================================================ // MCPAssetsBase // ============================================================ @@ -263,7 +27,7 @@ MCPAssetsBase& MCPAssetsBase::Exact(const FString& InName) MCPAssetsBase& MCPAssetsBase::Substring(const FString& InFilter) { MatchName = InFilter; - bExactMatch = false; + bExactMatch = false; bPatternHasSlash = MatchName.Contains(TEXT("/")); return *this; } @@ -411,20 +175,3 @@ void MCPAssetsBase::SetError(const FString &Msg) UObjectResults.Empty(); ErrorCB.SetError(Msg); } - -// ============================================================ -// Load helpers -// ============================================================ - -UAnimationStateMachineGraph* UMCPAssetFinder::LoadAnimStateMachineGraph( - const FString& BlueprintName, const FString& GraphName, MCPErrorCallback Error) -{ - UAnimBlueprint* AnimBP = LoadAsset(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; -} - diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_AnimMutation.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_AnimMutation.h index 7233198b..e8035b9e 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_AnimMutation.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_AnimMutation.h @@ -118,17 +118,7 @@ public: bool bSaved = MCPUtils::SaveBlueprintPackage(NewAnimBP); - // Collect graph names - TArray> GraphNames; - TArray AllGraphs; - NewAnimBP->GetAllGraphs(AllGraphs); - for (UEdGraph* Graph : AllGraphs) - { - if (Graph) - { - GraphNames.Add(MakeShared(Graph->GetName())); - } - } + TArray> GraphNames = MCPUtils::AllGraphNamesJson(NewAnimBP); UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Created AnimBlueprint '%s' with %d graphs (saved: %s)"), *Name, GraphNames.Num(), bSaved ? TEXT("true") : TEXT("false")); @@ -172,25 +162,17 @@ public: // Walk all anim nodes to collect slot names TSet SlotNames; - TArray AllGraphs; - AnimBP->GetAllGraphs(AllGraphs); - for (UEdGraph* Graph : AllGraphs) + for (UAnimGraphNode_Base* AnimNode : MCPUtils::AllNodes(AnimBP)) { - for (UEdGraphNode* Node : Graph->Nodes) + // Check for SlotName property via reflection + for (TFieldIterator PropIt(AnimNode->GetClass()); PropIt; ++PropIt) { - if (UAnimGraphNode_Base* AnimNode = Cast(Node)) + if (PropIt->GetName().Contains(TEXT("SlotName")) || PropIt->GetName().Contains(TEXT("Slot"))) { - // Check for SlotName property via reflection - for (TFieldIterator PropIt(AnimNode->GetClass()); PropIt; ++PropIt) + FName SlotValue = PropIt->GetPropertyValue_InContainer(AnimNode); + if (!SlotValue.IsNone()) { - if (PropIt->GetName().Contains(TEXT("SlotName")) || PropIt->GetName().Contains(TEXT("Slot"))) - { - FName SlotValue = PropIt->GetPropertyValue_InContainer(AnimNode); - if (!SlotValue.IsNone()) - { - SlotNames.Add(SlotValue.ToString()); - } - } + SlotNames.Add(SlotValue.ToString()); } } } @@ -238,25 +220,17 @@ public: // Walk all anim nodes to collect sync group names TSet SyncGroupNames; - TArray AllGraphs; - AnimBP->GetAllGraphs(AllGraphs); - for (UEdGraph* Graph : AllGraphs) + for (UAnimGraphNode_Base* AnimNode : MCPUtils::AllNodes(AnimBP)) { - for (UEdGraphNode* Node : Graph->Nodes) + // Check for SyncGroup/GroupName property via reflection + for (TFieldIterator PropIt(AnimNode->GetClass()); PropIt; ++PropIt) { - if (UAnimGraphNode_Base* AnimNode = Cast(Node)) + if (PropIt->GetName().Contains(TEXT("SyncGroup")) || PropIt->GetName().Contains(TEXT("GroupName"))) { - // Check for SyncGroup/GroupName property via reflection - for (TFieldIterator PropIt(AnimNode->GetClass()); PropIt; ++PropIt) + FName GroupValue = PropIt->GetPropertyValue_InContainer(AnimNode); + if (!GroupValue.IsNone()) { - if (PropIt->GetName().Contains(TEXT("SyncGroup")) || PropIt->GetName().Contains(TEXT("GroupName"))) - { - FName GroupValue = PropIt->GetPropertyValue_InContainer(AnimNode); - if (!GroupValue.IsNone()) - { - SyncGroupNames.Add(GroupValue.ToString()); - } - } + SyncGroupNames.Add(GroupValue.ToString()); } } } diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_Dispatchers.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_Dispatchers.h index e6e2da2d..2de1c8d8 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_Dispatchers.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_Dispatchers.h @@ -57,16 +57,11 @@ public: } // Check against existing graphs (functions, macros, etc.) - TArray AllGraphs; - BP->GetAllGraphs(AllGraphs); - for (UEdGraph* Existing : AllGraphs) + if (!MCPUtils::AllGraphsNamed(BP, DispatcherName).IsEmpty()) { - if (Existing && Existing->GetName().Equals(DispatcherName, ESearchCase::IgnoreCase)) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf( - TEXT("A graph named '%s' already exists in Blueprint '%s'"), - *DispatcherName, *Blueprint)); - } + return MCPUtils::MakeErrorJson(Result, FString::Printf( + TEXT("A graph named '%s' already exists in Blueprint '%s'"), + *DispatcherName, *Blueprint)); } UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Adding event dispatcher '%s' to Blueprint '%s'"), @@ -106,13 +101,10 @@ public: { // Find the entry node in the signature graph UK2Node_EditablePinBase* EntryNode = nullptr; - for (UEdGraphNode* Node : SigGraph->Nodes) + for (UK2Node_FunctionEntry* FE : MCPUtils::AllNodes(SigGraph)) { - if (UK2Node_FunctionEntry* FE = Cast(Node)) - { - EntryNode = FE; - break; - } + EntryNode = FE; + break; } if (!EntryNode) @@ -197,11 +189,8 @@ public: UEdGraph* SigGraph = FBlueprintEditorUtils::GetDelegateSignatureGraphByName(BP, DelegateName); if (SigGraph) { - for (UEdGraphNode* Node : SigGraph->Nodes) + for (UK2Node_FunctionEntry* FE : MCPUtils::AllNodes(SigGraph)) { - UK2Node_FunctionEntry* FE = Cast(Node); - if (!FE) continue; - for (const TSharedPtr& PinInfo : FE->UserDefinedPins) { if (!PinInfo.IsValid()) continue; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_Graphs.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_Graphs.h index b42f9794..14216922 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_Graphs.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_Graphs.h @@ -248,19 +248,7 @@ public: // Collect graph names - TArray> GraphNames; - for (UEdGraph* Graph : NewBP->UbergraphPages) - { - GraphNames.Add(MakeShared(Graph->GetName())); - } - for (UEdGraph* Graph : NewBP->FunctionGraphs) - { - GraphNames.Add(MakeShared(Graph->GetName())); - } - for (UEdGraph* Graph : NewBP->MacroGraphs) - { - GraphNames.Add(MakeShared(Graph->GetName())); - } + TArray> GraphNames = MCPUtils::AllGraphNamesJson(NewBP); UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Created Blueprint '%s' with %d graphs (saved: %s)"), *Blueprint, GraphNames.Num(), bSaved ? TEXT("true") : TEXT("false")); @@ -310,33 +298,21 @@ public: UBlueprint* BP = Assets.Object(); // Check graph name uniqueness - TArray AllGraphs; - BP->GetAllGraphs(AllGraphs); - for (UEdGraph* Existing : AllGraphs) + if (!MCPUtils::AllGraphsNamed(BP, Graph).IsEmpty()) { - if (Existing && Existing->GetName().Equals(Graph, ESearchCase::IgnoreCase)) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf( - TEXT("A graph named '%s' already exists in Blueprint '%s'"), *Graph, *Blueprint)); - } + return MCPUtils::MakeErrorJson(Result, FString::Printf( + TEXT("A graph named '%s' already exists in Blueprint '%s'"), *Graph, *Blueprint)); } // Also check for existing custom events with the same name if (GraphType == TEXT("customEvent")) { - for (UEdGraph* ExistingGraph : AllGraphs) + for (UK2Node_CustomEvent* CE : MCPUtils::AllNodes(BP)) { - if (!ExistingGraph) continue; - for (UEdGraphNode* Node : ExistingGraph->Nodes) + if (CE->CustomFunctionName == FName(*Graph)) { - if (UK2Node_CustomEvent* CE = Cast(Node)) - { - if (CE->CustomFunctionName == FName(*Graph)) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf( - TEXT("A custom event named '%s' already exists in Blueprint '%s'"), *Graph, *Blueprint)); - } - } + return MCPUtils::MakeErrorJson(Result, FString::Printf( + TEXT("A custom event named '%s' already exists in Blueprint '%s'"), *Graph, *Blueprint)); } } } @@ -566,11 +542,9 @@ public: } // Check for name collision - TArray AllGraphs; - BP->GetAllGraphs(AllGraphs); - for (UEdGraph* Existing : AllGraphs) + for (UEdGraph* Existing : MCPUtils::AllGraphsNamed(BP, NewName)) { - if (Existing && Existing != TargetGraph && Existing->GetName().Equals(NewName, ESearchCase::IgnoreCase)) + if (Existing != TargetGraph) { return MCPUtils::MakeErrorJson(Result, FString::Printf( TEXT("A graph named '%s' already exists in Blueprint '%s'"), *NewName, *Blueprint)); diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_Mutation.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_Mutation.h index 6e532a81..cb5fb559 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_Mutation.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_Mutation.h @@ -180,23 +180,12 @@ public: // Find the target graph FString DecodedGraphName = MCPUtils::UrlDecode(Graph); - UEdGraph* TargetGraph = nullptr; - TArray AllGraphs; - BP->GetAllGraphs(AllGraphs); - - for (UEdGraph* G : AllGraphs) - { - if (G && G->GetName().Equals(DecodedGraphName, ESearchCase::IgnoreCase)) - { - TargetGraph = G; - break; - } - } - - if (!TargetGraph) + TArray MatchingGraphs = MCPUtils::AllGraphsNamed(BP, DecodedGraphName); + if (MatchingGraphs.Num() == 0) { return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Graph '%s' not found"), *DecodedGraphName)); } + UEdGraph* TargetGraph = MatchingGraphs[0]; if (Nodes.Array.Num() == 0) { @@ -350,30 +339,19 @@ public: // Find the target graph FString DecodedGraphName = MCPUtils::UrlDecode(Graph); - UEdGraph* TargetGraph = nullptr; - TArray AllGraphs; - BP->GetAllGraphs(AllGraphs); - - for (UEdGraph* G : AllGraphs) - { - if (G && G->GetName().Equals(DecodedGraphName, ESearchCase::IgnoreCase)) - { - TargetGraph = G; - break; - } - } - - if (!TargetGraph) + TArray MatchingGraphs = MCPUtils::AllGraphsNamed(BP, DecodedGraphName); + if (MatchingGraphs.Num() == 0) { TArray> GraphNames; - for (UEdGraph* G : AllGraphs) + for (UEdGraph* G : MCPUtils::AllGraphs(BP)) { - if (G) GraphNames.Add(MakeShared(G->GetName())); + GraphNames.Add(MakeShared(G->GetName())); } MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Graph '%s' not found"), *DecodedGraphName)); Result->SetArrayField(TEXT("availableGraphs"), GraphNames); return; } + UEdGraph* TargetGraph = MatchingGraphs[0]; TArray> Results; int32 SuccessCount = 0; @@ -713,13 +691,10 @@ public: *Blueprint, *OldClass, *NewClass, *NewClassPtr->GetPathName()); // Find all CallFunction nodes - TArray AllCallNodes; - FBlueprintEditorUtils::GetAllNodesOfClass(BP, AllCallNodes); - int32 ReplacedCount = 0; TArray> BrokenConnections; - for (UK2Node_CallFunction* CallNode : AllCallNodes) + for (UK2Node_CallFunction* CallNode : MCPUtils::AllNodes(BP)) { UClass* ParentClass = CallNode->FunctionReference.GetMemberParentClass(); if (!ParentClass) @@ -920,14 +895,8 @@ public: UBlueprint* BP = Assets.Object(); // Count graphs and nodes before refresh - TArray AllGraphs; - BP->GetAllGraphs(AllGraphs); - int32 GraphCount = AllGraphs.Num(); - int32 NodeCount = 0; - for (UEdGraph* G : AllGraphs) - { - if (G) NodeCount += G->Nodes.Num(); - } + int32 GraphCount = MCPUtils::AllGraphs(BP).Num(); + int32 NodeCount = MCPUtils::AllNodes(BP).Num(); UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Refreshing all nodes in '%s' (%d graphs, %d nodes)"), *Blueprint, GraphCount, NodeCount); @@ -937,21 +906,16 @@ public: // Remove orphaned pins from all nodes int32 OrphanedPinsRemoved = 0; - for (UEdGraph* G : AllGraphs) + for (UEdGraphNode* Node : MCPUtils::AllNodes(BP)) { - if (!G) continue; - for (UEdGraphNode* Node : G->Nodes) + for (int32 i = Node->Pins.Num() - 1; i >= 0; --i) { - if (!Node) continue; - for (int32 i = Node->Pins.Num() - 1; i >= 0; --i) + UEdGraphPin* Pin = Node->Pins[i]; + if (Pin && Pin->bOrphanedPin) { - UEdGraphPin* Pin = Node->Pins[i]; - if (Pin && Pin->bOrphanedPin) - { - Pin->BreakAllPinLinks(); - Node->Pins.RemoveAt(i); - OrphanedPinsRemoved++; - } + Pin->BreakAllPinLinks(); + Node->Pins.RemoveAt(i); + OrphanedPinsRemoved++; } } } @@ -976,27 +940,20 @@ public: } // Check each graph for nodes with error/warning status - AllGraphs.Empty(); - BP->GetAllGraphs(AllGraphs); - for (UEdGraph* G : AllGraphs) + for (UEdGraphNode* Node : MCPUtils::AllNodes(BP)) { - if (!G) continue; - for (UEdGraphNode* Node : G->Nodes) + if (Node->bHasCompilerMessage) { - if (!Node) continue; - if (Node->bHasCompilerMessage) + FString NodeTitle = Node->GetNodeTitle(ENodeTitleType::FullTitle).ToString(); + FString NodeMsg = FString::Printf(TEXT("[%s] %s: %s"), + *Node->GetGraph()->GetName(), *NodeTitle, *Node->ErrorMsg); + if (Node->ErrorType == EMessageSeverity::Error) { - FString NodeTitle = Node->GetNodeTitle(ENodeTitleType::FullTitle).ToString(); - FString NodeMsg = FString::Printf(TEXT("[%s] %s: %s"), - *G->GetName(), *NodeTitle, *Node->ErrorMsg); - if (Node->ErrorType == EMessageSeverity::Error) - { - ErrorsArr.Add(MakeShared(NodeMsg)); - } - else - { - WarningsArr.Add(MakeShared(NodeMsg)); - } + ErrorsArr.Add(MakeShared(NodeMsg)); + } + else + { + WarningsArr.Add(MakeShared(NodeMsg)); } } } @@ -1445,15 +1402,10 @@ public: UBlueprint* BP = BPAssets.Object(); FString DecodedGraphName = MCPUtils::UrlDecode(Graph); - TArray AllGraphs; - BP->GetAllGraphs(AllGraphs); - for (UEdGraph* G : AllGraphs) + TArray MatchingGraphs = MCPUtils::AllGraphsNamed(BP, DecodedGraphName); + if (MatchingGraphs.Num() > 0) { - if (G && G->GetName().Equals(DecodedGraphName, ESearchCase::IgnoreCase)) - { - GraphFilter = G; - break; - } + GraphFilter = MatchingGraphs[0]; } if (!GraphFilter) { diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_Params.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_Params.h index 8ec6551f..4ea0aae7 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_Params.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_Params.h @@ -59,46 +59,28 @@ public: UK2Node_EditablePinBase* EntryNode = nullptr; FString FoundNodeType; - TArray AllGraphs; - BP->GetAllGraphs(AllGraphs); - - // Strategy 1: Look for a function graph matching the name - for (UEdGraph* Graph : AllGraphs) + // Strategy 1: Look for a K2Node_FunctionEntry in a function graph matching the name + for (UK2Node_FunctionEntry* FuncEntry : MCPUtils::AllNodes(BP)) { - if (Graph && Graph->GetName().Equals(FunctionName, ESearchCase::IgnoreCase)) + if (FuncEntry->GetGraph()->GetName().Equals(FunctionName, ESearchCase::IgnoreCase)) { - for (UEdGraphNode* Node : Graph->Nodes) - { - if (UK2Node_FunctionEntry* FuncEntry = Cast(Node)) - { - EntryNode = FuncEntry; - FoundNodeType = TEXT("FunctionEntry"); - break; - } - } - if (EntryNode) break; + EntryNode = FuncEntry; + FoundNodeType = TEXT("FunctionEntry"); + break; } } // Strategy 2: Search for a K2Node_CustomEvent with matching CustomFunctionName if (!EntryNode) { - for (UEdGraph* Graph : AllGraphs) + for (UK2Node_CustomEvent* CustomEvent : MCPUtils::AllNodes(BP)) { - if (!Graph) continue; - for (UEdGraphNode* Node : Graph->Nodes) + if (CustomEvent->CustomFunctionName.ToString().Equals(FunctionName, ESearchCase::IgnoreCase)) { - if (UK2Node_CustomEvent* CustomEvent = Cast(Node)) - { - if (CustomEvent->CustomFunctionName.ToString().Equals(FunctionName, ESearchCase::IgnoreCase)) - { - EntryNode = CustomEvent; - FoundNodeType = TEXT("CustomEvent"); - break; - } - } + EntryNode = CustomEvent; + FoundNodeType = TEXT("CustomEvent"); + break; } - if (EntryNode) break; } } @@ -106,22 +88,15 @@ public: { // List available functions/events for debugging TArray> Available; - for (UEdGraph* Graph : AllGraphs) + for (UK2Node_FunctionEntry* FE : MCPUtils::AllNodes(BP)) { - if (!Graph) continue; - for (UEdGraphNode* Node : Graph->Nodes) - { - if (UK2Node_FunctionEntry* FE = Cast(Node)) - { - Available.Add(MakeShared( - FString::Printf(TEXT("function:%s"), *Graph->GetName()))); - } - else if (UK2Node_CustomEvent* CE = Cast(Node)) - { - Available.Add(MakeShared( - FString::Printf(TEXT("event:%s"), *CE->CustomFunctionName.ToString()))); - } - } + Available.Add(MakeShared( + FString::Printf(TEXT("function:%s"), *FE->GetGraph()->GetName()))); + } + for (UK2Node_CustomEvent* CE : MCPUtils::AllNodes(BP)) + { + Available.Add(MakeShared( + FString::Printf(TEXT("event:%s"), *CE->CustomFunctionName.ToString()))); } MCPUtils::MakeErrorJson(Result, FString::Printf( @@ -261,46 +236,28 @@ public: UK2Node_EditablePinBase* EntryNode = nullptr; FString FoundNodeType; - TArray AllGraphs; - BP->GetAllGraphs(AllGraphs); - - // Strategy 1: Look for a function graph matching the name - for (UEdGraph* Graph : AllGraphs) + // Strategy 1: Look for a K2Node_FunctionEntry in a function graph matching the name + for (UK2Node_FunctionEntry* FuncEntry : MCPUtils::AllNodes(BP)) { - if (Graph && Graph->GetName().Equals(FunctionName, ESearchCase::IgnoreCase)) + if (FuncEntry->GetGraph()->GetName().Equals(FunctionName, ESearchCase::IgnoreCase)) { - for (UEdGraphNode* Node : Graph->Nodes) - { - if (UK2Node_FunctionEntry* FuncEntry = Cast(Node)) - { - EntryNode = FuncEntry; - FoundNodeType = TEXT("FunctionEntry"); - break; - } - } - if (EntryNode) break; + EntryNode = FuncEntry; + FoundNodeType = TEXT("FunctionEntry"); + break; } } // Strategy 2: Search for a K2Node_CustomEvent with matching CustomFunctionName if (!EntryNode) { - for (UEdGraph* Graph : AllGraphs) + for (UK2Node_CustomEvent* CustomEvent : MCPUtils::AllNodes(BP)) { - if (!Graph) continue; - for (UEdGraphNode* Node : Graph->Nodes) + if (CustomEvent->CustomFunctionName.ToString().Equals(FunctionName, ESearchCase::IgnoreCase)) { - if (UK2Node_CustomEvent* CustomEvent = Cast(Node)) - { - if (CustomEvent->CustomFunctionName.ToString().Equals(FunctionName, ESearchCase::IgnoreCase)) - { - EntryNode = CustomEvent; - FoundNodeType = TEXT("CustomEvent"); - break; - } - } + EntryNode = CustomEvent; + FoundNodeType = TEXT("CustomEvent"); + break; } - if (EntryNode) break; } } @@ -308,22 +265,15 @@ public: { // List available functions/events for debugging TArray> Available; - for (UEdGraph* Graph : AllGraphs) + for (UK2Node_FunctionEntry* FE : MCPUtils::AllNodes(BP)) { - if (!Graph) continue; - for (UEdGraphNode* Node : Graph->Nodes) - { - if (UK2Node_FunctionEntry* FE = Cast(Node)) - { - Available.Add(MakeShared( - FString::Printf(TEXT("function:%s"), *Graph->GetName()))); - } - else if (UK2Node_CustomEvent* CE = Cast(Node)) - { - Available.Add(MakeShared( - FString::Printf(TEXT("event:%s"), *CE->CustomFunctionName.ToString()))); - } - } + Available.Add(MakeShared( + FString::Printf(TEXT("function:%s"), *FE->GetGraph()->GetName()))); + } + for (UK2Node_CustomEvent* CE : MCPUtils::AllNodes(BP)) + { + Available.Add(MakeShared( + FString::Printf(TEXT("event:%s"), *CE->CustomFunctionName.ToString()))); } MCPUtils::MakeErrorJson(Result, FString::Printf( @@ -436,75 +386,44 @@ public: FName FuncFName(*FunctionName); // Strategy 1: K2Node_FunctionEntry in function graphs - TArray AllGraphs; - BP->GetAllGraphs(AllGraphs); - for (UEdGraph* Graph : AllGraphs) + for (UK2Node_FunctionEntry* FE : MCPUtils::AllNodes(BP)) { - if (!Graph || !Graph->GetName().Equals(FunctionName, ESearchCase::IgnoreCase)) - { - continue; - } - + UEdGraph* FEGraph = FE->GetGraph(); + if (!FEGraph->GetName().Equals(FunctionName, ESearchCase::IgnoreCase)) continue; // Skip delegate signature graphs (handled in Strategy 3) - if (BP->DelegateSignatureGraphs.Contains(Graph)) - { - continue; - } + if (BP->DelegateSignatureGraphs.Contains(FEGraph)) continue; - for (UEdGraphNode* Node : Graph->Nodes) - { - if (UK2Node_FunctionEntry* FE = Cast(Node)) - { - EntryNode = FE; - NodeType = TEXT("FunctionEntry"); - break; - } - } - if (EntryNode) break; + EntryNode = FE; + NodeType = TEXT("FunctionEntry"); + break; } // Strategy 2: K2Node_CustomEvent with matching CustomFunctionName if (!EntryNode) { - for (UEdGraph* Graph : AllGraphs) + for (UK2Node_CustomEvent* CE : MCPUtils::AllNodes(BP)) { - if (!Graph) continue; - for (UEdGraphNode* Node : Graph->Nodes) + if (CE->CustomFunctionName.ToString().Equals(FunctionName, ESearchCase::IgnoreCase)) { - if (UK2Node_CustomEvent* CE = Cast(Node)) - { - if (CE->CustomFunctionName.ToString().Equals(FunctionName, ESearchCase::IgnoreCase)) - { - EntryNode = CE; - NodeType = TEXT("CustomEvent"); - break; - } - } + EntryNode = CE; + NodeType = TEXT("CustomEvent"); + break; } - if (EntryNode) break; } } // Strategy 3: K2Node_FunctionEntry in DelegateSignatureGraphs if (!EntryNode) { - for (UEdGraph* SigGraph : BP->DelegateSignatureGraphs) + for (UK2Node_FunctionEntry* FE : MCPUtils::AllNodes(BP)) { - if (!SigGraph || !SigGraph->GetName().Equals(FunctionName, ESearchCase::IgnoreCase)) - { - continue; - } + UEdGraph* FEGraph = FE->GetGraph(); + if (!FEGraph->GetName().Equals(FunctionName, ESearchCase::IgnoreCase)) continue; + if (!BP->DelegateSignatureGraphs.Contains(FEGraph)) continue; - for (UEdGraphNode* Node : SigGraph->Nodes) - { - if (UK2Node_FunctionEntry* FE = Cast(Node)) - { - EntryNode = FE; - NodeType = TEXT("EventDispatcher"); - break; - } - } - if (EntryNode) break; + EntryNode = FE; + NodeType = TEXT("EventDispatcher"); + break; } } @@ -519,17 +438,10 @@ public: } // Custom events - for (UEdGraph* Graph : AllGraphs) + for (UK2Node_CustomEvent* CE : MCPUtils::AllNodes(BP)) { - if (!Graph) continue; - for (UEdGraphNode* Node : Graph->Nodes) - { - if (UK2Node_CustomEvent* CE = Cast(Node)) - { - AvailFuncs.Add(MakeShared( - FString::Printf(TEXT("%s (custom event)"), *CE->CustomFunctionName.ToString()))); - } - } + AvailFuncs.Add(MakeShared( + FString::Printf(TEXT("%s (custom event)"), *CE->CustomFunctionName.ToString()))); } // Dispatchers diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_Read.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_Read.h index 5048a51c..226a8252 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_Read.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_Read.h @@ -194,12 +194,11 @@ public: if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; UBlueprint* BP = Assets.Object(); - TArray AllGraphs; - BP->GetAllGraphs(AllGraphs); + TArray AllGraphs = MCPUtils::AllGraphs(BP); for (UEdGraph* GraphObj : AllGraphs) { - if (GraphObj && GraphObj->GetName().Equals(DecodedGraphName, ESearchCase::IgnoreCase)) + if (GraphObj->GetName().Equals(DecodedGraphName, ESearchCase::IgnoreCase)) { TSharedPtr GraphJson = MCPUtils::SerializeGraph(GraphObj); if (GraphJson.IsValid()) @@ -214,10 +213,7 @@ public: TArray> GraphNames; for (UEdGraph* GraphObj : AllGraphs) { - if (GraphObj) - { - GraphNames.Add(MakeShared(GraphObj->GetName())); - } + GraphNames.Add(MakeShared(GraphObj->GetName())); } MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Graph '%s' not found"), *DecodedGraphName)); Result->SetArrayField(TEXT("availableGraphs"), GraphNames); @@ -255,59 +251,51 @@ public: // Build a combined list of all searchable blueprints (regular + level) auto SearchBlueprint = [&](const FString& AssetName, const FString& AssetPath, UBlueprint* BP, TArray>& OutResults) { - TArray Graphs; - BP->GetAllGraphs(Graphs); - - for (UEdGraph* GraphObj : Graphs) + for (UEdGraphNode* Node : MCPUtils::AllNodes(BP)) { - if (!GraphObj || OutResults.Num() >= EffectiveMaxResults) break; + if (OutResults.Num() >= EffectiveMaxResults) break; - for (UEdGraphNode* Node : GraphObj->Nodes) + FString Title = Node->GetNodeTitle(ENodeTitleType::FullTitle).ToString(); + + FString FuncName, EventName, VarName; + if (auto* CF = Cast(Node)) { - if (!Node || OutResults.Num() >= EffectiveMaxResults) break; + FuncName = CF->FunctionReference.GetMemberName().ToString(); + } + else if (auto* Ev = Cast(Node)) + { + EventName = Ev->EventReference.GetMemberName().ToString(); + } + else if (auto* CE = Cast(Node)) + { + EventName = CE->CustomFunctionName.ToString(); + } + else if (auto* VG = Cast(Node)) + { + VarName = VG->GetVarName().ToString(); + } + else if (auto* VS = Cast(Node)) + { + VarName = VS->GetVarName().ToString(); + } - FString Title = Node->GetNodeTitle(ENodeTitleType::FullTitle).ToString(); + bool bMatch = Title.Contains(Query, ESearchCase::IgnoreCase) || + (!FuncName.IsEmpty() && FuncName.Contains(Query, ESearchCase::IgnoreCase)) || + (!EventName.IsEmpty() && EventName.Contains(Query, ESearchCase::IgnoreCase)) || + (!VarName.IsEmpty() && VarName.Contains(Query, ESearchCase::IgnoreCase)); - FString FuncName, EventName, VarName; - if (auto* CF = Cast(Node)) - { - FuncName = CF->FunctionReference.GetMemberName().ToString(); - } - else if (auto* Ev = Cast(Node)) - { - EventName = Ev->EventReference.GetMemberName().ToString(); - } - else if (auto* CE = Cast(Node)) - { - EventName = CE->CustomFunctionName.ToString(); - } - else if (auto* VG = Cast(Node)) - { - VarName = VG->GetVarName().ToString(); - } - else if (auto* VS = Cast(Node)) - { - VarName = VS->GetVarName().ToString(); - } - - bool bMatch = Title.Contains(Query, ESearchCase::IgnoreCase) || - (!FuncName.IsEmpty() && FuncName.Contains(Query, ESearchCase::IgnoreCase)) || - (!EventName.IsEmpty() && EventName.Contains(Query, ESearchCase::IgnoreCase)) || - (!VarName.IsEmpty() && VarName.Contains(Query, ESearchCase::IgnoreCase)); - - if (bMatch) - { - TSharedRef R = MakeShared(); - R->SetStringField(TEXT("blueprint"), AssetName); - R->SetStringField(TEXT("blueprintPath"), AssetPath); - R->SetStringField(TEXT("graph"), GraphObj->GetName()); - R->SetStringField(TEXT("nodeTitle"), Title); - R->SetStringField(TEXT("nodeClass"), Node->GetClass()->GetName()); - if (!FuncName.IsEmpty()) R->SetStringField(TEXT("functionName"), FuncName); - if (!EventName.IsEmpty()) R->SetStringField(TEXT("eventName"), EventName); - if (!VarName.IsEmpty()) R->SetStringField(TEXT("variableName"), VarName); - OutResults.Add(MakeShared(R)); - } + if (bMatch) + { + TSharedRef R = MakeShared(); + R->SetStringField(TEXT("blueprint"), AssetName); + R->SetStringField(TEXT("blueprintPath"), AssetPath); + R->SetStringField(TEXT("graph"), Node->GetGraph()->GetName()); + R->SetStringField(TEXT("nodeTitle"), Title); + R->SetStringField(TEXT("nodeClass"), Node->GetClass()->GetName()); + if (!FuncName.IsEmpty()) R->SetStringField(TEXT("functionName"), FuncName); + if (!EventName.IsEmpty()) R->SetStringField(TEXT("eventName"), EventName); + if (!VarName.IsEmpty()) R->SetStringField(TEXT("variableName"), VarName); + OutResults.Add(MakeShared(R)); } } }; @@ -549,137 +537,129 @@ public: } // Check graphs for function/event params, struct nodes, and pin connections - TArray AllGraphs; - BP->GetAllGraphs(AllGraphs); - - for (UEdGraph* GraphObj : AllGraphs) + for (UEdGraphNode* Node : MCPUtils::AllNodes(BP)) { - if (!GraphObj || Results.Num() >= EffectiveMaxResults) break; + if (Results.Num() >= EffectiveMaxResults) break; - for (UEdGraphNode* Node : GraphObj->Nodes) + // Check FunctionEntry/CustomEvent parameters + if (auto* FuncEntry = Cast(Node)) { - if (!Node || Results.Num() >= EffectiveMaxResults) break; - - // Check FunctionEntry/CustomEvent parameters - if (auto* FuncEntry = Cast(Node)) + for (const TSharedPtr& PinInfo : FuncEntry->UserDefinedPins) { - for (const TSharedPtr& PinInfo : FuncEntry->UserDefinedPins) - { - if (!PinInfo.IsValid()) continue; - FString ParamSubtype; - if (PinInfo->PinType.PinSubCategoryObject.IsValid()) - ParamSubtype = PinInfo->PinType.PinSubCategoryObject->GetName(); + if (!PinInfo.IsValid()) continue; + FString ParamSubtype; + if (PinInfo->PinType.PinSubCategoryObject.IsValid()) + ParamSubtype = PinInfo->PinType.PinSubCategoryObject->GetName(); - if (MatchesType(ParamSubtype) || MatchesType(PinInfo->PinType.PinCategory.ToString())) - { - TSharedRef R = MakeShared(); - R->SetStringField(TEXT("blueprint"), BPName); - R->SetStringField(TEXT("blueprintPath"), BPPath); - R->SetStringField(TEXT("usage"), TEXT("functionParameter")); - R->SetStringField(TEXT("location"), FString::Printf(TEXT("%s.%s"), - *GraphObj->GetName(), *PinInfo->PinName.ToString())); - R->SetStringField(TEXT("nodeId"), Node->NodeGuid.ToString()); - R->SetStringField(TEXT("currentType"), PinInfo->PinType.PinCategory.ToString()); - if (!ParamSubtype.IsEmpty()) - R->SetStringField(TEXT("currentSubtype"), ParamSubtype); - if (bIsLevel) - R->SetBoolField(TEXT("isLevelBlueprint"), true); - Results.Add(MakeShared(R)); - } - } - } - else if (auto* CustomEvent = Cast(Node)) - { - for (const TSharedPtr& PinInfo : CustomEvent->UserDefinedPins) - { - if (!PinInfo.IsValid()) continue; - FString ParamSubtype; - if (PinInfo->PinType.PinSubCategoryObject.IsValid()) - ParamSubtype = PinInfo->PinType.PinSubCategoryObject->GetName(); - - if (MatchesType(ParamSubtype) || MatchesType(PinInfo->PinType.PinCategory.ToString())) - { - TSharedRef R = MakeShared(); - R->SetStringField(TEXT("blueprint"), BPName); - R->SetStringField(TEXT("blueprintPath"), BPPath); - R->SetStringField(TEXT("usage"), TEXT("eventParameter")); - R->SetStringField(TEXT("location"), FString::Printf(TEXT("%s.%s"), - *CustomEvent->CustomFunctionName.ToString(), *PinInfo->PinName.ToString())); - R->SetStringField(TEXT("nodeId"), Node->NodeGuid.ToString()); - R->SetStringField(TEXT("currentType"), PinInfo->PinType.PinCategory.ToString()); - if (!ParamSubtype.IsEmpty()) - R->SetStringField(TEXT("currentSubtype"), ParamSubtype); - if (bIsLevel) - R->SetBoolField(TEXT("isLevelBlueprint"), true); - Results.Add(MakeShared(R)); - } - } - } - // Check Break/Make struct nodes - else if (auto* BreakNode = Cast(Node)) - { - if (BreakNode->StructType && MatchesType(BreakNode->StructType->GetName())) + if (MatchesType(ParamSubtype) || MatchesType(PinInfo->PinType.PinCategory.ToString())) { TSharedRef R = MakeShared(); R->SetStringField(TEXT("blueprint"), BPName); R->SetStringField(TEXT("blueprintPath"), BPPath); - R->SetStringField(TEXT("usage"), TEXT("breakStruct")); - R->SetStringField(TEXT("location"), GraphObj->GetName()); - R->SetStringField(TEXT("nodeId"), Node->NodeGuid.ToString()); - R->SetStringField(TEXT("structType"), BreakNode->StructType->GetName()); - if (bIsLevel) - R->SetBoolField(TEXT("isLevelBlueprint"), true); - Results.Add(MakeShared(R)); - } - } - else if (auto* MakeNode = Cast(Node)) - { - if (MakeNode->StructType && MatchesType(MakeNode->StructType->GetName())) - { - TSharedRef R = MakeShared(); - R->SetStringField(TEXT("blueprint"), BPName); - R->SetStringField(TEXT("blueprintPath"), BPPath); - R->SetStringField(TEXT("usage"), TEXT("makeStruct")); - R->SetStringField(TEXT("location"), GraphObj->GetName()); - R->SetStringField(TEXT("nodeId"), Node->NodeGuid.ToString()); - R->SetStringField(TEXT("structType"), MakeNode->StructType->GetName()); - if (bIsLevel) - R->SetBoolField(TEXT("isLevelBlueprint"), true); - Results.Add(MakeShared(R)); - } - } - - // Check pin connections carrying the type - for (UEdGraphPin* Pin : Node->Pins) - { - if (!Pin || Pin->bHidden || Results.Num() >= EffectiveMaxResults) continue; - - FString PinSubtype; - if (Pin->PinType.PinSubCategoryObject.IsValid()) - PinSubtype = Pin->PinType.PinSubCategoryObject->GetName(); - - if (Pin->LinkedTo.Num() > 0 && - (MatchesType(PinSubtype) || MatchesType(Pin->PinType.PinCategory.ToString()))) - { - TSharedRef R = MakeShared(); - R->SetStringField(TEXT("blueprint"), BPName); - R->SetStringField(TEXT("blueprintPath"), BPPath); - R->SetStringField(TEXT("usage"), TEXT("pinConnection")); + R->SetStringField(TEXT("usage"), TEXT("functionParameter")); R->SetStringField(TEXT("location"), FString::Printf(TEXT("%s.%s"), - *Node->GetNodeTitle(ENodeTitleType::FullTitle).ToString(), - *Pin->PinName.ToString())); + *Node->GetGraph()->GetName(), *PinInfo->PinName.ToString())); R->SetStringField(TEXT("nodeId"), Node->NodeGuid.ToString()); - R->SetStringField(TEXT("graph"), GraphObj->GetName()); - R->SetStringField(TEXT("pinType"), Pin->PinType.PinCategory.ToString()); - if (!PinSubtype.IsEmpty()) - R->SetStringField(TEXT("pinSubtype"), PinSubtype); - R->SetNumberField(TEXT("connectionCount"), Pin->LinkedTo.Num()); + R->SetStringField(TEXT("currentType"), PinInfo->PinType.PinCategory.ToString()); + if (!ParamSubtype.IsEmpty()) + R->SetStringField(TEXT("currentSubtype"), ParamSubtype); if (bIsLevel) R->SetBoolField(TEXT("isLevelBlueprint"), true); Results.Add(MakeShared(R)); } } } + else if (auto* CustomEvent = Cast(Node)) + { + for (const TSharedPtr& PinInfo : CustomEvent->UserDefinedPins) + { + if (!PinInfo.IsValid()) continue; + FString ParamSubtype; + if (PinInfo->PinType.PinSubCategoryObject.IsValid()) + ParamSubtype = PinInfo->PinType.PinSubCategoryObject->GetName(); + + if (MatchesType(ParamSubtype) || MatchesType(PinInfo->PinType.PinCategory.ToString())) + { + TSharedRef R = MakeShared(); + R->SetStringField(TEXT("blueprint"), BPName); + R->SetStringField(TEXT("blueprintPath"), BPPath); + R->SetStringField(TEXT("usage"), TEXT("eventParameter")); + R->SetStringField(TEXT("location"), FString::Printf(TEXT("%s.%s"), + *CustomEvent->CustomFunctionName.ToString(), *PinInfo->PinName.ToString())); + R->SetStringField(TEXT("nodeId"), Node->NodeGuid.ToString()); + R->SetStringField(TEXT("currentType"), PinInfo->PinType.PinCategory.ToString()); + if (!ParamSubtype.IsEmpty()) + R->SetStringField(TEXT("currentSubtype"), ParamSubtype); + if (bIsLevel) + R->SetBoolField(TEXT("isLevelBlueprint"), true); + Results.Add(MakeShared(R)); + } + } + } + // Check Break/Make struct nodes + else if (auto* BreakNode = Cast(Node)) + { + if (BreakNode->StructType && MatchesType(BreakNode->StructType->GetName())) + { + TSharedRef R = MakeShared(); + R->SetStringField(TEXT("blueprint"), BPName); + R->SetStringField(TEXT("blueprintPath"), BPPath); + R->SetStringField(TEXT("usage"), TEXT("breakStruct")); + R->SetStringField(TEXT("location"), Node->GetGraph()->GetName()); + R->SetStringField(TEXT("nodeId"), Node->NodeGuid.ToString()); + R->SetStringField(TEXT("structType"), BreakNode->StructType->GetName()); + if (bIsLevel) + R->SetBoolField(TEXT("isLevelBlueprint"), true); + Results.Add(MakeShared(R)); + } + } + else if (auto* MakeNode = Cast(Node)) + { + if (MakeNode->StructType && MatchesType(MakeNode->StructType->GetName())) + { + TSharedRef R = MakeShared(); + R->SetStringField(TEXT("blueprint"), BPName); + R->SetStringField(TEXT("blueprintPath"), BPPath); + R->SetStringField(TEXT("usage"), TEXT("makeStruct")); + R->SetStringField(TEXT("location"), Node->GetGraph()->GetName()); + R->SetStringField(TEXT("nodeId"), Node->NodeGuid.ToString()); + R->SetStringField(TEXT("structType"), MakeNode->StructType->GetName()); + if (bIsLevel) + R->SetBoolField(TEXT("isLevelBlueprint"), true); + Results.Add(MakeShared(R)); + } + } + + // Check pin connections carrying the type + for (UEdGraphPin* Pin : Node->Pins) + { + if (!Pin || Pin->bHidden || Results.Num() >= EffectiveMaxResults) continue; + + FString PinSubtype; + if (Pin->PinType.PinSubCategoryObject.IsValid()) + PinSubtype = Pin->PinType.PinSubCategoryObject->GetName(); + + if ((Pin->LinkedTo.Num() > 0) && + (MatchesType(PinSubtype) || MatchesType(Pin->PinType.PinCategory.ToString()))) + { + TSharedRef R = MakeShared(); + R->SetStringField(TEXT("blueprint"), BPName); + R->SetStringField(TEXT("blueprintPath"), BPPath); + R->SetStringField(TEXT("usage"), TEXT("pinConnection")); + R->SetStringField(TEXT("location"), FString::Printf(TEXT("%s.%s"), + *Node->GetNodeTitle(ENodeTitleType::FullTitle).ToString(), + *Pin->PinName.ToString())); + R->SetStringField(TEXT("nodeId"), Node->NodeGuid.ToString()); + R->SetStringField(TEXT("graph"), Node->GetGraph()->GetName()); + R->SetStringField(TEXT("pinType"), Pin->PinType.PinCategory.ToString()); + if (!PinSubtype.IsEmpty()) + R->SetStringField(TEXT("pinSubtype"), PinSubtype); + R->SetNumberField(TEXT("connectionCount"), Pin->LinkedTo.Num()); + if (bIsLevel) + R->SetBoolField(TEXT("isLevelBlueprint"), true); + Results.Add(MakeShared(R)); + } + } } }; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_Validation.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_Validation.h index 7c581cf6..33f45328 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_Validation.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_Validation.h @@ -61,34 +61,26 @@ public: TArray> ErrorsArr; TArray> WarningsArr; - TArray AllGraphs; - BP->GetAllGraphs(AllGraphs); - - for (UEdGraph* Graph : AllGraphs) + for (UEdGraphNode* Node : MCPUtils::AllNodes(BP)) { - if (!Graph) continue; - for (UEdGraphNode* Node : Graph->Nodes) + if (Node->bHasCompilerMessage) { - if (!Node) continue; - if (Node->bHasCompilerMessage) - { - TSharedRef Msg = MakeShared(); - Msg->SetStringField(TEXT("graph"), Graph->GetName()); - Msg->SetStringField(TEXT("nodeId"), Node->NodeGuid.ToString()); - Msg->SetStringField(TEXT("nodeTitle"), Node->GetNodeTitle(ENodeTitleType::FullTitle).ToString()); - Msg->SetStringField(TEXT("nodeClass"), Node->GetClass()->GetName()); - Msg->SetStringField(TEXT("message"), Node->ErrorMsg); + TSharedRef Msg = MakeShared(); + Msg->SetStringField(TEXT("graph"), Node->GetGraph()->GetName()); + Msg->SetStringField(TEXT("nodeId"), Node->NodeGuid.ToString()); + Msg->SetStringField(TEXT("nodeTitle"), Node->GetNodeTitle(ENodeTitleType::FullTitle).ToString()); + Msg->SetStringField(TEXT("nodeClass"), Node->GetClass()->GetName()); + Msg->SetStringField(TEXT("message"), Node->ErrorMsg); - if (Node->ErrorType == EMessageSeverity::Error) - { - Msg->SetStringField(TEXT("severity"), TEXT("error")); - ErrorsArr.Add(MakeShared(Msg)); - } - else - { - Msg->SetStringField(TEXT("severity"), TEXT("warning")); - WarningsArr.Add(MakeShared(Msg)); - } + if (Node->ErrorType == EMessageSeverity::Error) + { + Msg->SetStringField(TEXT("severity"), TEXT("error")); + ErrorsArr.Add(MakeShared(Msg)); + } + else + { + Msg->SetStringField(TEXT("severity"), TEXT("warning")); + WarningsArr.Add(MakeShared(Msg)); } } } @@ -121,7 +113,7 @@ public: default: StatusStr = FString::Printf(TEXT("Status_%d"), (int32)BP->Status); break; } - bool bIsValid = (BP->Status == BS_UpToDate) && ErrorsArr.Num() == 0; + bool bIsValid = (BP->Status == BS_UpToDate) && (ErrorsArr.Num() == 0); TSharedRef Result = MakeShared(); Result->SetStringField(TEXT("blueprint"), BlueprintName); diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_Variables.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_Variables.h index 10711784..0de731cc 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_Variables.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_Variables.h @@ -107,60 +107,45 @@ public: // Analyze affected nodes (get/set nodes for this variable) TArray> AffectedNodes; - TArray AllGraphs; - BP->GetAllGraphs(AllGraphs); - for (UEdGraph* Graph : AllGraphs) + for (UK2Node_VariableGet* VG : MCPUtils::AllNodes(BP)) { - if (!Graph) continue; - for (UEdGraphNode* Node : Graph->Nodes) + if (VG->GetVarName().ToString() != Variable) continue; + TSharedRef AffNode = MakeShared(); + AffNode->SetStringField(TEXT("nodeId"), VG->NodeGuid.ToString()); + AffNode->SetStringField(TEXT("nodeType"), TEXT("VariableGet")); + AffNode->SetStringField(TEXT("graph"), VG->GetGraph()->GetName()); + TArray> AffPins; + for (UEdGraphPin* Pin : VG->Pins) { - if (!Node) continue; - if (auto* VG = Cast(Node)) + if (Pin && (Pin->LinkedTo.Num() > 0) && (Pin->Direction == EGPD_Output)) { - if (VG->GetVarName().ToString() == Variable) - { - TSharedRef AffNode = MakeShared(); - AffNode->SetStringField(TEXT("nodeId"), VG->NodeGuid.ToString()); - AffNode->SetStringField(TEXT("nodeType"), TEXT("VariableGet")); - AffNode->SetStringField(TEXT("graph"), Graph->GetName()); - // Check which pins would be affected - TArray> AffPins; - for (UEdGraphPin* Pin : VG->Pins) - { - if (Pin && Pin->LinkedTo.Num() > 0 && Pin->Direction == EGPD_Output) - { - AffPins.Add(MakeShared( - FString::Printf(TEXT("%s (connected to %d pin(s))"), - *Pin->PinName.ToString(), Pin->LinkedTo.Num()))); - } - } - AffNode->SetArrayField(TEXT("affectedPins"), AffPins); - AffectedNodes.Add(MakeShared(AffNode)); - } - } - else if (auto* VS = Cast(Node)) - { - if (VS->GetVarName().ToString() == Variable) - { - TSharedRef AffNode = MakeShared(); - AffNode->SetStringField(TEXT("nodeId"), VS->NodeGuid.ToString()); - AffNode->SetStringField(TEXT("nodeType"), TEXT("VariableSet")); - AffNode->SetStringField(TEXT("graph"), Graph->GetName()); - TArray> AffPins; - for (UEdGraphPin* Pin : VS->Pins) - { - if (Pin && Pin->LinkedTo.Num() > 0) - { - AffPins.Add(MakeShared( - FString::Printf(TEXT("%s (connected to %d pin(s))"), - *Pin->PinName.ToString(), Pin->LinkedTo.Num()))); - } - } - AffNode->SetArrayField(TEXT("affectedPins"), AffPins); - AffectedNodes.Add(MakeShared(AffNode)); - } + AffPins.Add(MakeShared( + FString::Printf(TEXT("%s (connected to %d pin(s))"), + *Pin->PinName.ToString(), Pin->LinkedTo.Num()))); } } + AffNode->SetArrayField(TEXT("affectedPins"), AffPins); + AffectedNodes.Add(MakeShared(AffNode)); + } + for (UK2Node_VariableSet* VS : MCPUtils::AllNodes(BP)) + { + if (VS->GetVarName().ToString() != Variable) continue; + TSharedRef AffNode = MakeShared(); + AffNode->SetStringField(TEXT("nodeId"), VS->NodeGuid.ToString()); + AffNode->SetStringField(TEXT("nodeType"), TEXT("VariableSet")); + AffNode->SetStringField(TEXT("graph"), VS->GetGraph()->GetName()); + TArray> AffPins; + for (UEdGraphPin* Pin : VS->Pins) + { + if (Pin && Pin->LinkedTo.Num() > 0) + { + AffPins.Add(MakeShared( + FString::Printf(TEXT("%s (connected to %d pin(s))"), + *Pin->PinName.ToString(), Pin->LinkedTo.Num()))); + } + } + AffNode->SetArrayField(TEXT("affectedPins"), AffPins); + AffectedNodes.Add(MakeShared(AffNode)); } if (DryRun) diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPServer.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPServer.cpp index e57f40f5..80f76d89 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPServer.cpp +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPServer.cpp @@ -251,7 +251,6 @@ int32 TryAddMaterialExpressionSEH( void FMCPServer::DispatchToolCall(const FString& ToolName, const FJsonObject* Params, FJsonObject* Result) { - UMCPAssetFinder::Refresh(); if (UClass** HandlerClass = MCPHandlerRegistry.Find(ToolName)) { const bool bIsMutation = MutationEndpoints.Contains(ToolName); diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPUtils.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPUtils.cpp index f02c236d..4a1dd0ba 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPUtils.cpp +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPUtils.cpp @@ -189,6 +189,38 @@ FString MCPUtils::UrlDecode(const FString& EncodedString) // Blueprint helpers // ============================================================ +TArray MCPUtils::AllGraphs(UBlueprint* BP) +{ + TArray Graphs; + BP->GetAllGraphs(Graphs); + return Graphs; +} + +TArray MCPUtils::AllGraphsNamed(UBlueprint* BP, const FString& Name) +{ + TArray Result; + for (UEdGraph* Graph : AllGraphs(BP)) + if (Graph->GetName().Equals(Name, ESearchCase::IgnoreCase)) + Result.Add(Graph); + return Result; +} + +TArray MCPUtils::AllNodes(UBlueprint* BP) +{ + TArray Nodes; + for (UEdGraph* Graph : AllGraphs(BP)) + Nodes.Append(Graph->Nodes); + return Nodes; +} + +TArray> MCPUtils::AllGraphNamesJson(UBlueprint* BP) +{ + TArray> Result; + for (UEdGraph* Graph : AllGraphs(BP)) + Result.Add(MakeShared(Graph->GetName())); + return Result; +} + UEdGraphNode* MCPUtils::FindNodeByGuid( UBlueprint* BP, const FString& GuidString, UEdGraph** OutGraph) { diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPAssetFinder.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPAssetFinder.h index 3af9773b..1f8dea01 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPAssetFinder.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPAssetFinder.h @@ -1,112 +1,14 @@ #pragma once #include "CoreMinimal.h" -#include "Subsystems/EngineSubsystem.h" #include "AssetRegistry/AssetData.h" -#include "StructUtils/UserDefinedStruct.h" -#include "Engine/UserDefinedEnum.h" #include "MCPUtils.h" #include "Engine/Blueprint.h" #include "Engine/LevelScriptBlueprint.h" #include "Engine/World.h" -#include "MCPAssetFinder.generated.h" -class UMaterial; -class UMaterialInstanceConstant; -class UMaterialFunction; -class UAnimationStateMachineGraph; -class IAssetRegistry; + struct FARFilter; -/** - * Engine subsystem that caches asset registry data for the BlueprintMCP server. - * All public API is static — callers never need to fetch the subsystem directly. - * Asset lists are auto-refreshed when the asset registry signals changes. - */ -UCLASS() -class UMCPAssetFinder : public UEngineSubsystem -{ - GENERATED_BODY() - -public: - virtual void Initialize(FSubsystemCollectionBase& Collection) override; - virtual void Deinitialize() override; - - static FName BlueprintsAndMaps; - - // Call once before processing a request. Rebuilds caches if the asset registry has changed. - // After this call, all query methods read from a stable snapshot until the next Refresh(). - static void Refresh(); - - // --- Static API: asset lists --- - static const TArray& GetAssets(FName Class); - static const TArray& GetAssets(UClass *Class); - - // --- Static API: find/load helpers --- - static FAssetData* FindAsset(FName Class, const FString& NameOrPath, FString* OutError = nullptr); - static FAssetData* FindAsset(UClass *Class, const FString& NameOrPath, FString* OutError = nullptr); - - // --- Static API: find/load helpers --- - static TArray SearchAssets(FName Class, const FString& NameOrPath, FString* OutError = nullptr); - static TArray SearchAssets(UClass *Class, const FString& NameOrPath, FString* OutError = nullptr); - - // Load an asset from an FAssetData. Returns nullptr and reports error on failure. - template - static T* LoadAsset(FAssetData& Asset, MCPErrorCallback Error) - { - T* Result = Cast(Asset.GetAsset()); - if (!Result) - Error.SetError(FString::Printf(TEXT("Asset '%s' found but could not be loaded as %s."), - *Asset.AssetName.ToString(), *T::StaticClass()->GetName())); - return Result; - } - - // Load an asset by name or path. Returns nullptr and reports error on failure. - template - static T* LoadAsset(const FString& NameOrPath, MCPErrorCallback Error) - { - FString FindError; - FAssetData* Asset = FindAsset(T::StaticClass(), NameOrPath, &FindError); - if (!Asset) - { - if (FindError.IsEmpty()) - FindError = FString::Printf(TEXT("'%s' not found."), *NameOrPath); - Error.SetError(FindError); - return nullptr; - } - return LoadAsset(*Asset, Error); - } - - static FAssetData* FindAnyAsset(const FString& NameOrPath, FString* OutError = nullptr); - - // Load a blueprint or level blueprint from an asset (handles UWorld → level blueprint extraction). - 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(). - 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); - - // Returns the subsystem instance, or nullptr if the engine is not initialized. - static UMCPAssetFinder* Get(); - - // All cached asset lists, keyed by UClass::GetName() (e.g. "Blueprint", "World", "Material"). - // The special key "BlueprintsAndMaps" combines Blueprint and World assets. - TMap> AssetCache; - - // Change detection — set true by asset registry delegates - bool bDirty = true; - void OnAssetEvent(const FAssetData&) { bDirty = true; } - void OnAssetRenamed(const FAssetData&, const FString&) { bDirty = true; } - - // Empty array returned when subsystem is unavailable - static const TArray EmptyAssetArray; -}; - // ============================================================ // MCPAssetsBase — non-template base for MCPAssets // ============================================================ diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPUtils.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPUtils.h index cceff657..196d9b8a 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPUtils.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPUtils.h @@ -98,6 +98,27 @@ public: static FString UrlDecode(const FString& EncodedString); // ----- Blueprint helpers ----- + static TArray AllGraphs(UBlueprint* BP); + static TArray AllGraphsNamed(UBlueprint* BP, const FString& Name); + static TArray AllNodes(UBlueprint* BP); + template static TArray AllNodes(UBlueprint* BP) + { + TArray Result; + for (UEdGraph* Graph : AllGraphs(BP)) + for (UEdGraphNode* Node : Graph->Nodes) + if (T* Typed = Cast(Node)) + Result.Add(Typed); + return Result; + } + template static TArray AllNodes(UEdGraph* Graph) + { + TArray Result; + for (UEdGraphNode* Node : Graph->Nodes) + if (T* Typed = Cast(Node)) + Result.Add(Typed); + return Result; + } + static TArray> AllGraphNamesJson(UBlueprint* BP); static UEdGraphNode* FindNodeByGuid(UBlueprint* BP, const FString& GuidString, UEdGraph** OutGraph = nullptr); static bool SaveBlueprintPackage(UBlueprint* BP);