More MCP work

This commit is contained in:
2026-03-08 03:44:27 -04:00
parent 0fe0cfa1c2
commit a72b65641e
13 changed files with 381 additions and 922 deletions

View File

@@ -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<FAssetData> UMCPAssetFinder::EmptyAssetArray;
// ============================================================
// Initialize / Deinitialize — subscribe to asset registry events
// ============================================================
void UMCPAssetFinder::Initialize(FSubsystemCollectionBase& Collection)
{
Super::Initialize(Collection);
IAssetRegistry& AR = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("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<UMCPAssetFinder>() : 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<FAssetData>& 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<FAssetRegistryModule>("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<FAssetData>& 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<FAssetData>& UMCPAssetFinder::GetAssets(FName Key)
{
TArray<FAssetData>* Found = Get()->AssetCache.Find(Key);
return Found ? *Found : EmptyAssetArray;
}
const TArray<FAssetData>& 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<FAssetData>& 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<FAssetData*>(&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<FAssetData*> UMCPAssetFinder::SearchAssets(FName Class, const FString& Filter, FString* OutError)
{
const TArray<FAssetData>& List = GetAssets(Class);
TArray<FAssetData*> 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<FAssetData*>(&Asset));
}
}
return Results;
}
TArray<FAssetData*> 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<UBlueprint>(Asset.GetAsset());
if (BP) return BP;
// Map asset — extract the level blueprint
UWorld* World = Cast<UWorld>(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<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

@@ -118,17 +118,7 @@ public:
bool bSaved = MCPUtils::SaveBlueprintPackage(NewAnimBP);
// Collect graph names
TArray<TSharedPtr<FJsonValue>> GraphNames;
TArray<UEdGraph*> AllGraphs;
NewAnimBP->GetAllGraphs(AllGraphs);
for (UEdGraph* Graph : AllGraphs)
{
if (Graph)
{
GraphNames.Add(MakeShared<FJsonValueString>(Graph->GetName()));
}
}
TArray<TSharedPtr<FJsonValue>> 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<FString> SlotNames;
TArray<UEdGraph*> AllGraphs;
AnimBP->GetAllGraphs(AllGraphs);
for (UEdGraph* Graph : AllGraphs)
for (UAnimGraphNode_Base* AnimNode : MCPUtils::AllNodes<UAnimGraphNode_Base>(AnimBP))
{
for (UEdGraphNode* Node : Graph->Nodes)
// Check for SlotName property via reflection
for (TFieldIterator<FNameProperty> PropIt(AnimNode->GetClass()); PropIt; ++PropIt)
{
if (UAnimGraphNode_Base* AnimNode = Cast<UAnimGraphNode_Base>(Node))
if (PropIt->GetName().Contains(TEXT("SlotName")) || PropIt->GetName().Contains(TEXT("Slot")))
{
// Check for SlotName property via reflection
for (TFieldIterator<FNameProperty> 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<FString> SyncGroupNames;
TArray<UEdGraph*> AllGraphs;
AnimBP->GetAllGraphs(AllGraphs);
for (UEdGraph* Graph : AllGraphs)
for (UAnimGraphNode_Base* AnimNode : MCPUtils::AllNodes<UAnimGraphNode_Base>(AnimBP))
{
for (UEdGraphNode* Node : Graph->Nodes)
// Check for SyncGroup/GroupName property via reflection
for (TFieldIterator<FNameProperty> PropIt(AnimNode->GetClass()); PropIt; ++PropIt)
{
if (UAnimGraphNode_Base* AnimNode = Cast<UAnimGraphNode_Base>(Node))
if (PropIt->GetName().Contains(TEXT("SyncGroup")) || PropIt->GetName().Contains(TEXT("GroupName")))
{
// Check for SyncGroup/GroupName property via reflection
for (TFieldIterator<FNameProperty> 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());
}
}
}

View File

@@ -57,16 +57,11 @@ public:
}
// Check against existing graphs (functions, macros, etc.)
TArray<UEdGraph*> 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<UK2Node_FunctionEntry>(SigGraph))
{
if (UK2Node_FunctionEntry* FE = Cast<UK2Node_FunctionEntry>(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<UK2Node_FunctionEntry>(SigGraph))
{
UK2Node_FunctionEntry* FE = Cast<UK2Node_FunctionEntry>(Node);
if (!FE) continue;
for (const TSharedPtr<FUserPinInfo>& PinInfo : FE->UserDefinedPins)
{
if (!PinInfo.IsValid()) continue;

View File

@@ -248,19 +248,7 @@ public:
// Collect graph names
TArray<TSharedPtr<FJsonValue>> GraphNames;
for (UEdGraph* Graph : NewBP->UbergraphPages)
{
GraphNames.Add(MakeShared<FJsonValueString>(Graph->GetName()));
}
for (UEdGraph* Graph : NewBP->FunctionGraphs)
{
GraphNames.Add(MakeShared<FJsonValueString>(Graph->GetName()));
}
for (UEdGraph* Graph : NewBP->MacroGraphs)
{
GraphNames.Add(MakeShared<FJsonValueString>(Graph->GetName()));
}
TArray<TSharedPtr<FJsonValue>> 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<UEdGraph*> 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<UK2Node_CustomEvent>(BP))
{
if (!ExistingGraph) continue;
for (UEdGraphNode* Node : ExistingGraph->Nodes)
if (CE->CustomFunctionName == FName(*Graph))
{
if (UK2Node_CustomEvent* CE = Cast<UK2Node_CustomEvent>(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<UEdGraph*> 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));

View File

@@ -180,23 +180,12 @@ public:
// Find the target graph
FString DecodedGraphName = MCPUtils::UrlDecode(Graph);
UEdGraph* TargetGraph = nullptr;
TArray<UEdGraph*> AllGraphs;
BP->GetAllGraphs(AllGraphs);
for (UEdGraph* G : AllGraphs)
{
if (G && G->GetName().Equals(DecodedGraphName, ESearchCase::IgnoreCase))
{
TargetGraph = G;
break;
}
}
if (!TargetGraph)
TArray<UEdGraph*> 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<UEdGraph*> AllGraphs;
BP->GetAllGraphs(AllGraphs);
for (UEdGraph* G : AllGraphs)
{
if (G && G->GetName().Equals(DecodedGraphName, ESearchCase::IgnoreCase))
{
TargetGraph = G;
break;
}
}
if (!TargetGraph)
TArray<UEdGraph*> MatchingGraphs = MCPUtils::AllGraphsNamed(BP, DecodedGraphName);
if (MatchingGraphs.Num() == 0)
{
TArray<TSharedPtr<FJsonValue>> GraphNames;
for (UEdGraph* G : AllGraphs)
for (UEdGraph* G : MCPUtils::AllGraphs(BP))
{
if (G) GraphNames.Add(MakeShared<FJsonValueString>(G->GetName()));
GraphNames.Add(MakeShared<FJsonValueString>(G->GetName()));
}
MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Graph '%s' not found"), *DecodedGraphName));
Result->SetArrayField(TEXT("availableGraphs"), GraphNames);
return;
}
UEdGraph* TargetGraph = MatchingGraphs[0];
TArray<TSharedPtr<FJsonValue>> Results;
int32 SuccessCount = 0;
@@ -713,13 +691,10 @@ public:
*Blueprint, *OldClass, *NewClass, *NewClassPtr->GetPathName());
// Find all CallFunction nodes
TArray<UK2Node_CallFunction*> AllCallNodes;
FBlueprintEditorUtils::GetAllNodesOfClass<UK2Node_CallFunction>(BP, AllCallNodes);
int32 ReplacedCount = 0;
TArray<TSharedPtr<FJsonValue>> BrokenConnections;
for (UK2Node_CallFunction* CallNode : AllCallNodes)
for (UK2Node_CallFunction* CallNode : MCPUtils::AllNodes<UK2Node_CallFunction>(BP))
{
UClass* ParentClass = CallNode->FunctionReference.GetMemberParentClass();
if (!ParentClass)
@@ -920,14 +895,8 @@ public:
UBlueprint* BP = Assets.Object();
// Count graphs and nodes before refresh
TArray<UEdGraph*> 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<FJsonValueString>(NodeMsg));
}
else
{
WarningsArr.Add(MakeShared<FJsonValueString>(NodeMsg));
}
ErrorsArr.Add(MakeShared<FJsonValueString>(NodeMsg));
}
else
{
WarningsArr.Add(MakeShared<FJsonValueString>(NodeMsg));
}
}
}
@@ -1445,15 +1402,10 @@ public:
UBlueprint* BP = BPAssets.Object();
FString DecodedGraphName = MCPUtils::UrlDecode(Graph);
TArray<UEdGraph*> AllGraphs;
BP->GetAllGraphs(AllGraphs);
for (UEdGraph* G : AllGraphs)
TArray<UEdGraph*> 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)
{

View File

@@ -59,46 +59,28 @@ public:
UK2Node_EditablePinBase* EntryNode = nullptr;
FString FoundNodeType;
TArray<UEdGraph*> 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<UK2Node_FunctionEntry>(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<UK2Node_FunctionEntry>(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<UK2Node_CustomEvent>(BP))
{
if (!Graph) continue;
for (UEdGraphNode* Node : Graph->Nodes)
if (CustomEvent->CustomFunctionName.ToString().Equals(FunctionName, ESearchCase::IgnoreCase))
{
if (UK2Node_CustomEvent* CustomEvent = Cast<UK2Node_CustomEvent>(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<TSharedPtr<FJsonValue>> Available;
for (UEdGraph* Graph : AllGraphs)
for (UK2Node_FunctionEntry* FE : MCPUtils::AllNodes<UK2Node_FunctionEntry>(BP))
{
if (!Graph) continue;
for (UEdGraphNode* Node : Graph->Nodes)
{
if (UK2Node_FunctionEntry* FE = Cast<UK2Node_FunctionEntry>(Node))
{
Available.Add(MakeShared<FJsonValueString>(
FString::Printf(TEXT("function:%s"), *Graph->GetName())));
}
else if (UK2Node_CustomEvent* CE = Cast<UK2Node_CustomEvent>(Node))
{
Available.Add(MakeShared<FJsonValueString>(
FString::Printf(TEXT("event:%s"), *CE->CustomFunctionName.ToString())));
}
}
Available.Add(MakeShared<FJsonValueString>(
FString::Printf(TEXT("function:%s"), *FE->GetGraph()->GetName())));
}
for (UK2Node_CustomEvent* CE : MCPUtils::AllNodes<UK2Node_CustomEvent>(BP))
{
Available.Add(MakeShared<FJsonValueString>(
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<UEdGraph*> 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<UK2Node_FunctionEntry>(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<UK2Node_FunctionEntry>(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<UK2Node_CustomEvent>(BP))
{
if (!Graph) continue;
for (UEdGraphNode* Node : Graph->Nodes)
if (CustomEvent->CustomFunctionName.ToString().Equals(FunctionName, ESearchCase::IgnoreCase))
{
if (UK2Node_CustomEvent* CustomEvent = Cast<UK2Node_CustomEvent>(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<TSharedPtr<FJsonValue>> Available;
for (UEdGraph* Graph : AllGraphs)
for (UK2Node_FunctionEntry* FE : MCPUtils::AllNodes<UK2Node_FunctionEntry>(BP))
{
if (!Graph) continue;
for (UEdGraphNode* Node : Graph->Nodes)
{
if (UK2Node_FunctionEntry* FE = Cast<UK2Node_FunctionEntry>(Node))
{
Available.Add(MakeShared<FJsonValueString>(
FString::Printf(TEXT("function:%s"), *Graph->GetName())));
}
else if (UK2Node_CustomEvent* CE = Cast<UK2Node_CustomEvent>(Node))
{
Available.Add(MakeShared<FJsonValueString>(
FString::Printf(TEXT("event:%s"), *CE->CustomFunctionName.ToString())));
}
}
Available.Add(MakeShared<FJsonValueString>(
FString::Printf(TEXT("function:%s"), *FE->GetGraph()->GetName())));
}
for (UK2Node_CustomEvent* CE : MCPUtils::AllNodes<UK2Node_CustomEvent>(BP))
{
Available.Add(MakeShared<FJsonValueString>(
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<UEdGraph*> AllGraphs;
BP->GetAllGraphs(AllGraphs);
for (UEdGraph* Graph : AllGraphs)
for (UK2Node_FunctionEntry* FE : MCPUtils::AllNodes<UK2Node_FunctionEntry>(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<UK2Node_FunctionEntry>(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<UK2Node_CustomEvent>(BP))
{
if (!Graph) continue;
for (UEdGraphNode* Node : Graph->Nodes)
if (CE->CustomFunctionName.ToString().Equals(FunctionName, ESearchCase::IgnoreCase))
{
if (UK2Node_CustomEvent* CE = Cast<UK2Node_CustomEvent>(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<UK2Node_FunctionEntry>(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<UK2Node_FunctionEntry>(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<UK2Node_CustomEvent>(BP))
{
if (!Graph) continue;
for (UEdGraphNode* Node : Graph->Nodes)
{
if (UK2Node_CustomEvent* CE = Cast<UK2Node_CustomEvent>(Node))
{
AvailFuncs.Add(MakeShared<FJsonValueString>(
FString::Printf(TEXT("%s (custom event)"), *CE->CustomFunctionName.ToString())));
}
}
AvailFuncs.Add(MakeShared<FJsonValueString>(
FString::Printf(TEXT("%s (custom event)"), *CE->CustomFunctionName.ToString())));
}
// Dispatchers

View File

@@ -194,12 +194,11 @@ public:
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
UBlueprint* BP = Assets.Object();
TArray<UEdGraph*> AllGraphs;
BP->GetAllGraphs(AllGraphs);
TArray<UEdGraph*> AllGraphs = MCPUtils::AllGraphs(BP);
for (UEdGraph* GraphObj : AllGraphs)
{
if (GraphObj && GraphObj->GetName().Equals(DecodedGraphName, ESearchCase::IgnoreCase))
if (GraphObj->GetName().Equals(DecodedGraphName, ESearchCase::IgnoreCase))
{
TSharedPtr<FJsonObject> GraphJson = MCPUtils::SerializeGraph(GraphObj);
if (GraphJson.IsValid())
@@ -214,10 +213,7 @@ public:
TArray<TSharedPtr<FJsonValue>> GraphNames;
for (UEdGraph* GraphObj : AllGraphs)
{
if (GraphObj)
{
GraphNames.Add(MakeShared<FJsonValueString>(GraphObj->GetName()));
}
GraphNames.Add(MakeShared<FJsonValueString>(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<TSharedPtr<FJsonValue>>& OutResults)
{
TArray<UEdGraph*> 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<UK2Node_CallFunction>(Node))
{
if (!Node || OutResults.Num() >= EffectiveMaxResults) break;
FuncName = CF->FunctionReference.GetMemberName().ToString();
}
else if (auto* Ev = Cast<UK2Node_Event>(Node))
{
EventName = Ev->EventReference.GetMemberName().ToString();
}
else if (auto* CE = Cast<UK2Node_CustomEvent>(Node))
{
EventName = CE->CustomFunctionName.ToString();
}
else if (auto* VG = Cast<UK2Node_VariableGet>(Node))
{
VarName = VG->GetVarName().ToString();
}
else if (auto* VS = Cast<UK2Node_VariableSet>(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<UK2Node_CallFunction>(Node))
{
FuncName = CF->FunctionReference.GetMemberName().ToString();
}
else if (auto* Ev = Cast<UK2Node_Event>(Node))
{
EventName = Ev->EventReference.GetMemberName().ToString();
}
else if (auto* CE = Cast<UK2Node_CustomEvent>(Node))
{
EventName = CE->CustomFunctionName.ToString();
}
else if (auto* VG = Cast<UK2Node_VariableGet>(Node))
{
VarName = VG->GetVarName().ToString();
}
else if (auto* VS = Cast<UK2Node_VariableSet>(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<FJsonObject> R = MakeShared<FJsonObject>();
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<FJsonValueObject>(R));
}
if (bMatch)
{
TSharedRef<FJsonObject> R = MakeShared<FJsonObject>();
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<FJsonValueObject>(R));
}
}
};
@@ -549,137 +537,129 @@ public:
}
// Check graphs for function/event params, struct nodes, and pin connections
TArray<UEdGraph*> 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<UK2Node_FunctionEntry>(Node))
{
if (!Node || Results.Num() >= EffectiveMaxResults) break;
// Check FunctionEntry/CustomEvent parameters
if (auto* FuncEntry = Cast<UK2Node_FunctionEntry>(Node))
for (const TSharedPtr<FUserPinInfo>& PinInfo : FuncEntry->UserDefinedPins)
{
for (const TSharedPtr<FUserPinInfo>& 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<FJsonObject> R = MakeShared<FJsonObject>();
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<FJsonValueObject>(R));
}
}
}
else if (auto* CustomEvent = Cast<UK2Node_CustomEvent>(Node))
{
for (const TSharedPtr<FUserPinInfo>& 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<FJsonObject> R = MakeShared<FJsonObject>();
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<FJsonValueObject>(R));
}
}
}
// Check Break/Make struct nodes
else if (auto* BreakNode = Cast<UK2Node_BreakStruct>(Node))
{
if (BreakNode->StructType && MatchesType(BreakNode->StructType->GetName()))
if (MatchesType(ParamSubtype) || MatchesType(PinInfo->PinType.PinCategory.ToString()))
{
TSharedRef<FJsonObject> R = MakeShared<FJsonObject>();
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<FJsonValueObject>(R));
}
}
else if (auto* MakeNode = Cast<UK2Node_MakeStruct>(Node))
{
if (MakeNode->StructType && MatchesType(MakeNode->StructType->GetName()))
{
TSharedRef<FJsonObject> R = MakeShared<FJsonObject>();
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<FJsonValueObject>(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<FJsonObject> R = MakeShared<FJsonObject>();
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<FJsonValueObject>(R));
}
}
}
else if (auto* CustomEvent = Cast<UK2Node_CustomEvent>(Node))
{
for (const TSharedPtr<FUserPinInfo>& 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<FJsonObject> R = MakeShared<FJsonObject>();
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<FJsonValueObject>(R));
}
}
}
// Check Break/Make struct nodes
else if (auto* BreakNode = Cast<UK2Node_BreakStruct>(Node))
{
if (BreakNode->StructType && MatchesType(BreakNode->StructType->GetName()))
{
TSharedRef<FJsonObject> R = MakeShared<FJsonObject>();
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<FJsonValueObject>(R));
}
}
else if (auto* MakeNode = Cast<UK2Node_MakeStruct>(Node))
{
if (MakeNode->StructType && MatchesType(MakeNode->StructType->GetName()))
{
TSharedRef<FJsonObject> R = MakeShared<FJsonObject>();
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<FJsonValueObject>(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<FJsonObject> R = MakeShared<FJsonObject>();
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<FJsonValueObject>(R));
}
}
}
};

View File

@@ -61,34 +61,26 @@ public:
TArray<TSharedPtr<FJsonValue>> ErrorsArr;
TArray<TSharedPtr<FJsonValue>> WarningsArr;
TArray<UEdGraph*> 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<FJsonObject> Msg = MakeShared<FJsonObject>();
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<FJsonObject> Msg = MakeShared<FJsonObject>();
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<FJsonValueObject>(Msg));
}
else
{
Msg->SetStringField(TEXT("severity"), TEXT("warning"));
WarningsArr.Add(MakeShared<FJsonValueObject>(Msg));
}
if (Node->ErrorType == EMessageSeverity::Error)
{
Msg->SetStringField(TEXT("severity"), TEXT("error"));
ErrorsArr.Add(MakeShared<FJsonValueObject>(Msg));
}
else
{
Msg->SetStringField(TEXT("severity"), TEXT("warning"));
WarningsArr.Add(MakeShared<FJsonValueObject>(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<FJsonObject> Result = MakeShared<FJsonObject>();
Result->SetStringField(TEXT("blueprint"), BlueprintName);

View File

@@ -107,60 +107,45 @@ public:
// Analyze affected nodes (get/set nodes for this variable)
TArray<TSharedPtr<FJsonValue>> AffectedNodes;
TArray<UEdGraph*> AllGraphs;
BP->GetAllGraphs(AllGraphs);
for (UEdGraph* Graph : AllGraphs)
for (UK2Node_VariableGet* VG : MCPUtils::AllNodes<UK2Node_VariableGet>(BP))
{
if (!Graph) continue;
for (UEdGraphNode* Node : Graph->Nodes)
if (VG->GetVarName().ToString() != Variable) continue;
TSharedRef<FJsonObject> AffNode = MakeShared<FJsonObject>();
AffNode->SetStringField(TEXT("nodeId"), VG->NodeGuid.ToString());
AffNode->SetStringField(TEXT("nodeType"), TEXT("VariableGet"));
AffNode->SetStringField(TEXT("graph"), VG->GetGraph()->GetName());
TArray<TSharedPtr<FJsonValue>> AffPins;
for (UEdGraphPin* Pin : VG->Pins)
{
if (!Node) continue;
if (auto* VG = Cast<UK2Node_VariableGet>(Node))
if (Pin && (Pin->LinkedTo.Num() > 0) && (Pin->Direction == EGPD_Output))
{
if (VG->GetVarName().ToString() == Variable)
{
TSharedRef<FJsonObject> AffNode = MakeShared<FJsonObject>();
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<TSharedPtr<FJsonValue>> AffPins;
for (UEdGraphPin* Pin : VG->Pins)
{
if (Pin && Pin->LinkedTo.Num() > 0 && Pin->Direction == EGPD_Output)
{
AffPins.Add(MakeShared<FJsonValueString>(
FString::Printf(TEXT("%s (connected to %d pin(s))"),
*Pin->PinName.ToString(), Pin->LinkedTo.Num())));
}
}
AffNode->SetArrayField(TEXT("affectedPins"), AffPins);
AffectedNodes.Add(MakeShared<FJsonValueObject>(AffNode));
}
}
else if (auto* VS = Cast<UK2Node_VariableSet>(Node))
{
if (VS->GetVarName().ToString() == Variable)
{
TSharedRef<FJsonObject> AffNode = MakeShared<FJsonObject>();
AffNode->SetStringField(TEXT("nodeId"), VS->NodeGuid.ToString());
AffNode->SetStringField(TEXT("nodeType"), TEXT("VariableSet"));
AffNode->SetStringField(TEXT("graph"), Graph->GetName());
TArray<TSharedPtr<FJsonValue>> AffPins;
for (UEdGraphPin* Pin : VS->Pins)
{
if (Pin && Pin->LinkedTo.Num() > 0)
{
AffPins.Add(MakeShared<FJsonValueString>(
FString::Printf(TEXT("%s (connected to %d pin(s))"),
*Pin->PinName.ToString(), Pin->LinkedTo.Num())));
}
}
AffNode->SetArrayField(TEXT("affectedPins"), AffPins);
AffectedNodes.Add(MakeShared<FJsonValueObject>(AffNode));
}
AffPins.Add(MakeShared<FJsonValueString>(
FString::Printf(TEXT("%s (connected to %d pin(s))"),
*Pin->PinName.ToString(), Pin->LinkedTo.Num())));
}
}
AffNode->SetArrayField(TEXT("affectedPins"), AffPins);
AffectedNodes.Add(MakeShared<FJsonValueObject>(AffNode));
}
for (UK2Node_VariableSet* VS : MCPUtils::AllNodes<UK2Node_VariableSet>(BP))
{
if (VS->GetVarName().ToString() != Variable) continue;
TSharedRef<FJsonObject> AffNode = MakeShared<FJsonObject>();
AffNode->SetStringField(TEXT("nodeId"), VS->NodeGuid.ToString());
AffNode->SetStringField(TEXT("nodeType"), TEXT("VariableSet"));
AffNode->SetStringField(TEXT("graph"), VS->GetGraph()->GetName());
TArray<TSharedPtr<FJsonValue>> AffPins;
for (UEdGraphPin* Pin : VS->Pins)
{
if (Pin && Pin->LinkedTo.Num() > 0)
{
AffPins.Add(MakeShared<FJsonValueString>(
FString::Printf(TEXT("%s (connected to %d pin(s))"),
*Pin->PinName.ToString(), Pin->LinkedTo.Num())));
}
}
AffNode->SetArrayField(TEXT("affectedPins"), AffPins);
AffectedNodes.Add(MakeShared<FJsonValueObject>(AffNode));
}
if (DryRun)

View File

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

View File

@@ -189,6 +189,38 @@ FString MCPUtils::UrlDecode(const FString& EncodedString)
// Blueprint helpers
// ============================================================
TArray<UEdGraph*> MCPUtils::AllGraphs(UBlueprint* BP)
{
TArray<UEdGraph*> Graphs;
BP->GetAllGraphs(Graphs);
return Graphs;
}
TArray<UEdGraph*> MCPUtils::AllGraphsNamed(UBlueprint* BP, const FString& Name)
{
TArray<UEdGraph*> Result;
for (UEdGraph* Graph : AllGraphs(BP))
if (Graph->GetName().Equals(Name, ESearchCase::IgnoreCase))
Result.Add(Graph);
return Result;
}
TArray<UEdGraphNode*> MCPUtils::AllNodes(UBlueprint* BP)
{
TArray<UEdGraphNode*> Nodes;
for (UEdGraph* Graph : AllGraphs(BP))
Nodes.Append(Graph->Nodes);
return Nodes;
}
TArray<TSharedPtr<FJsonValue>> MCPUtils::AllGraphNamesJson(UBlueprint* BP)
{
TArray<TSharedPtr<FJsonValue>> Result;
for (UEdGraph* Graph : AllGraphs(BP))
Result.Add(MakeShared<FJsonValueString>(Graph->GetName()));
return Result;
}
UEdGraphNode* MCPUtils::FindNodeByGuid(
UBlueprint* BP, const FString& GuidString, UEdGraph** OutGraph)
{

View File

@@ -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<FAssetData>& GetAssets(FName Class);
static const TArray<FAssetData>& 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<FAssetData*> SearchAssets(FName Class, const FString& NameOrPath, FString* OutError = nullptr);
static TArray<FAssetData*> SearchAssets(UClass *Class, const FString& NameOrPath, FString* OutError = nullptr);
// Load an asset from an FAssetData. Returns nullptr and reports error on failure.
template<typename T>
static T* LoadAsset(FAssetData& Asset, MCPErrorCallback Error)
{
T* Result = Cast<T>(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<typename T>
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<T>(*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<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);
// 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<FName, TArray<FAssetData>> 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<FAssetData> EmptyAssetArray;
};
// ============================================================
// MCPAssetsBase — non-template base for MCPAssets<T>
// ============================================================

View File

@@ -98,6 +98,27 @@ public:
static FString UrlDecode(const FString& EncodedString);
// ----- Blueprint helpers -----
static TArray<UEdGraph*> AllGraphs(UBlueprint* BP);
static TArray<UEdGraph*> AllGraphsNamed(UBlueprint* BP, const FString& Name);
static TArray<UEdGraphNode*> AllNodes(UBlueprint* BP);
template<class T> static TArray<T*> AllNodes(UBlueprint* BP)
{
TArray<T*> Result;
for (UEdGraph* Graph : AllGraphs(BP))
for (UEdGraphNode* Node : Graph->Nodes)
if (T* Typed = Cast<T>(Node))
Result.Add(Typed);
return Result;
}
template<class T> static TArray<T*> AllNodes(UEdGraph* Graph)
{
TArray<T*> Result;
for (UEdGraphNode* Node : Graph->Nodes)
if (T* Typed = Cast<T>(Node))
Result.Add(Typed);
return Result;
}
static TArray<TSharedPtr<FJsonValue>> AllGraphNamesJson(UBlueprint* BP);
static UEdGraphNode* FindNodeByGuid(UBlueprint* BP, const FString& GuidString, UEdGraph** OutGraph = nullptr);
static bool SaveBlueprintPackage(UBlueprint* BP);